Repository: jamesramsay/hercule (js)

Issue:
Library example throws error
In the Readme (https://github.com/jamesramsay/hercule#usage), there is an example describing how to use this library. This example does not currently work as described in node 4.2.1.

It specifically errors at the very start of `transcludeStringSync` in lib/hercule.js

```js
function transcludeStringSync(input, _ref2, log) {
  var relativePath = _ref2.relativePath;
```
The example in the Readme does not provide a second parameter, so in the above function _ref2 does not exist, and accessing relativePath on _ref2 throws an error.

This is fixed by providing an object as the second parameter, with a relativePath property.

```js
var hercule = require('hercule');

var output = hercule.transcludeStringSync("# Title\n\n:[abstract](abstract.md)", {
    relativePath: __dirname
});

console.log(output);
```
Which side of this problem do you think needs to be fixed? Is relativePath a required option? Or should the function declare that field optional?

Pull request description:
Fix: sync API incorrectly required `relativePath` option (fixes #178)
**BREAKING:** CLI argument `relative` renamed to `relativePath`

Patch:
diff --git a/package.json b/package.json
index e605189..cbcab7e 100644
--- a/package.json
+++ b/package.json
@@ -66,7 +66,7 @@
     "through2-get": "^0.0.2"
   },
   "devDependencies": {
-    "ava": "^0.11.0",
+    "ava": "^0.10.0",
     "babel-cli": "^6.4.0",
     "babel-core": "^6.4.0",
     "babel-preset-es2015": "^6.3.13",
diff --git a/src/hercule.js b/src/hercule.js
index a563444..5bfa8b1 100644
--- a/src/hercule.js
+++ b/src/hercule.js
@@ -14,9 +14,12 @@ const LOG_LEVELS = {
   50: 'error',
 };
 
+function relog(log, message) {
+  const msg = message.msg;
+  const level = message.level;
+  const body = _.omit(message, LOG_OMIT);
 
-function relog(log, level, body, message) {
-  log[LOG_LEVELS[level]](body, message);
+  log[LOG_LEVELS[level]](body, msg);
 }
 
 export const TranscludeStream = Transcluder;
@@ -65,44 +68,43 @@ export function transcludeFile(...args) {
 }
 
 
-export function transcludeFileSync(input, { relativePath }, log) {
-  const options = {
-    cwd: __dirname,
-    timeout: SYNC_TIMEOUT,
-  };
-  const args = [input, '--reporter', 'json-err'];
-  const result = childProcess.spawnSync('../bin/hercule', args, options);
+export function transcludeFileSync(...args) {
+  const input = args.shift();
+  const [options, log] = args;
+
+  const syncOptions = { cwd: __dirname, timeout: SYNC_TIMEOUT };
+  const syncArgs = [input, '--reporter', 'json-err'];
+
+  _.each(options, (optionValue, optionName) => {
+    syncArgs.push(`--${optionName}`, `${optionValue}`);
+  });
+
+  const result = childProcess.spawnSync('../bin/hercule', syncArgs, syncOptions);
   const outputContent = result.stdout.toString();
   const outputLogs = result.stderr.toString().split('\n');
 
-  _.compact(outputLogs).map(JSON.parse).forEach((message) => {
-    const msg = message.msg;
-    const level = message.level;
-    const body = _.omit(message, LOG_OMIT);
-    relog(log, level, body, msg);
-  });
+  _.compact(outputLogs).map(JSON.parse).forEach((message) => relog(log, message));
 
   return outputContent;
 }
 
 
-export function transcludeStringSync(input, { relativePath }, log) {
-  const options = {
-    input,
-    cwd: __dirname,
-    timeout: SYNC_TIMEOUT,
-  };
-  const args = ['--relative', relativePath, '--reporter', 'json-err'];
-  const result = childProcess.spawnSync('../bin/hercule', args, options);
+export function transcludeStringSync(...args) {
+  const input = args.shift();
+  const [options, log] = args;
+
+  const syncOptions = { input, cwd: __dirname, timeout: SYNC_TIMEOUT };
+  const syncArgs = ['--reporter', 'json-err'];
+
+  _.each(options, (optionValue, optionName) => {
+    syncArgs.push(`--${optionName}`, `${optionValue}`);
+  });
+
+  const result = childProcess.spawnSync('../bin/hercule', syncArgs, syncOptions);
   const outputContent = result.stdout.toString();
   const outputLogs = result.stderr.toString().split('\n');
 
-  _.compact(outputLogs).map(JSON.parse).forEach((message) => {
-    const msg = message.msg;
-    const level = message.level;
-    const body = _.omit(message, LOG_OMIT);
-    relog(log, level, body, msg);
-  });
+  _.compact(outputLogs).map(JSON.parse).forEach((message) => relog(log, message));
 
   return outputContent;
 }
diff --git a/src/main.js b/src/main.js
index 641e2e8..5b43c7e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -29,7 +29,7 @@ const parser = dashdash.createParser({
       helpArg: 'FILE',
     },
     {
-      names: ['relative', 'r'],
+      names: ['relativePath', 'r'],
       type: 'string',
       help: 'Relative path. stdin will be parsed relative to this path.',
     },
@@ -73,9 +73,10 @@ function main() {
   if (opts._args.length === 0) {
     // Reading input from stdin
     inputStream = process.stdin;
-    options.relativePath = opts.relative;
+    options.relativePath = opts.relativePath;
   } else {
     // Reading input from file
+    // TODO: handle file error!
     inputStream = fs.createReadStream(opts._args[0], { encoding: 'utf8' });
     options.relativePath = path.dirname(opts._args[0]);
   }


Test patch:
diff --git a/test/bats b/test/bats
index 8ff6937..74a627a 100755
--- a/test/bats
+++ b/test/bats
@@ -13,7 +13,7 @@
 }
 
 @test "transcluding stdin" {
-  result="$(cat ./test/fixtures/local-link/index.md | ./bin/hercule --relative ./test/fixtures/local-link)"
+  result="$(cat ./test/fixtures/local-link/index.md | ./bin/hercule --relativePath ./test/fixtures/local-link)"
   expected="Jackdaws love my big sphinx of quartz."
   [ "$result" == "$expected" ]
 }
diff --git a/test/integration/transcludeFileSync.js b/test/integration/transcludeFileSync.js
index 1fdf6d8..92060b4 100644
--- a/test/integration/transcludeFileSync.js
+++ b/test/integration/transcludeFileSync.js
@@ -13,45 +13,45 @@ let minor;
 [major, minor] = process.versions.node.split('.');
 
 if (major < 1 && minor < 12) {
-  test(`synchronous support not available < 0.12`, (t) => {
+  test.only(`synchronous support not available < 0.12`, (t) => {
     t.pass();
   });
-} else {
-  test.beforeEach((t) => {
-    t.context.logOutput = [];
-    t.context.logStream = through2.obj();
-
-    t.context.log = bunyan.createLogger({
-      name: 'hercule',
-      streams: [{
-        stream: t.context.logStream,
-      }],
-    });
-
-    t.context.logStream.on('readable', function read() {
-      let message = null;
-      while ((message = this.read()) !== null) {
-        message = _.pick(JSON.parse(message), 'name', 'msg', 'link', 'level');
-
-        // Make paths relatice to process for testing purposes
-        if (message.link && message.link.href) {
-          message.link.href = path.relative(process.cwd(), message.link.href);
-        }
-
-        t.context.logOutput.push(message);
+}
+
+test.beforeEach((t) => {
+  t.context.logOutput = [];
+  t.context.logStream = through2.obj();
+
+  t.context.log = bunyan.createLogger({
+    name: 'hercule',
+    streams: [{
+      stream: t.context.logStream,
+    }],
+  });
+
+  t.context.logStream.on('readable', function read() {
+    let message = null;
+    while ((message = this.read()) !== null) {
+      message = _.pick(JSON.parse(message), 'name', 'msg', 'link', 'level');
+
+      // Make paths relatice to process for testing purposes
+      if (message.link && message.link.href) {
+        message.link.href = path.relative(process.cwd(), message.link.href);
       }
-    });
+
+      t.context.logOutput.push(message);
+    }
   });
+});
 
 
-  _.forEach((fixtures.fixtures), (fixture) => {
-    // Exclude http tests because mocking won't cover sync sub-process
-    if (_.includes(['http-link', 'http-deep-nesting'], fixture.name)) return;
+_.forEach((fixtures.fixtures), (fixture) => {
+  // Exclude http tests because mocking won't cover sync sub-process
+  if (_.includes(['http-link', 'http-deep-nesting'], fixture.name)) return;
 
-    test(`should transclude ${fixture.name}`, (t) => {
-      const output = transcludeFileSync(fixture.inputFile, {}, t.context.log);
-      t.same(output, fixture.expectedOutput);
-      t.same(t.context.logOutput, fixture.expectedLogOutput);
-    });
+  test(`should transclude ${fixture.name}`, (t) => {
+    const output = transcludeFileSync(fixture.inputFile, {}, t.context.log);
+    t.same(output, fixture.expectedOutput);
+    t.same(t.context.logOutput, fixture.expectedLogOutput);
   });
-}
+});
diff --git a/test/integration/transcludeStringSync.js b/test/integration/transcludeStringSync.js
index 61da204..412e60b 100644
--- a/test/integration/transcludeStringSync.js
+++ b/test/integration/transcludeStringSync.js
@@ -13,48 +13,48 @@ let minor;
 [major, minor] = process.versions.node.split('.');
 
 if (major < 1 && minor < 12) {
-  test(`synchronous support not available < 0.12`, (t) => {
+  test.only(`synchronous support not available < 0.12`, (t) => {
     t.pass();
   });
-} else {
-  test.beforeEach((t) => {
-    t.context.logOutput = [];
-    t.context.logStream = through2.obj();
-
-    t.context.log = bunyan.createLogger({
-      name: 'hercule',
-      streams: [{
-        stream: t.context.logStream,
-      }],
-    });
-
-    t.context.logStream.on('readable', function read() {
-      let message = null;
-      while ((message = this.read()) !== null) {
-        message = _.pick(JSON.parse(message), 'name', 'msg', 'link', 'level');
-
-        // Make paths relatice to process for testing purposes
-        if (message.link && message.link.href) {
-          message.link.href = path.relative(process.cwd(), message.link.href);
-        }
-
-        t.context.logOutput.push(message);
+}
+
+test.beforeEach((t) => {
+  t.context.logOutput = [];
+  t.context.logStream = through2.obj();
+
+  t.context.log = bunyan.createLogger({
+    name: 'hercule',
+    streams: [{
+      stream: t.context.logStream,
+    }],
+  });
+
+  t.context.logStream.on('readable', function read() {
+    let message = null;
+    while ((message = this.read()) !== null) {
+      message = _.pick(JSON.parse(message), 'name', 'msg', 'link', 'level');
+
+      // Make paths relatice to process for testing purposes
+      if (message.link && message.link.href) {
+        message.link.href = path.relative(process.cwd(), message.link.href);
       }
-    });
+
+      t.context.logOutput.push(message);
+    }
   });
+});
 
 
-  _.forEach((fixtures.fixtures), (fixture) => {
-    // Exclude http tests because mocking won't cover sub-process
-    if (_.includes(['http-link', 'http-deep-nesting'], fixture.name)) return;
+_.forEach((fixtures.fixtures), (fixture) => {
+  // Exclude http tests because mocking won't cover sub-process
+  if (_.includes(['http-link', 'http-deep-nesting'], fixture.name)) return;
 
-    test(`should transclude ${fixture.name}`, (t) => {
-      const options = {
-        relativePath: path.resolve(__dirname, '../fixtures', fixture.name),
-      };
-      const output = transcludeStringSync(fixture.input, options, t.context.log);
-      t.same(output, fixture.expectedOutput);
-      t.same(t.context.logOutput, fixture.expectedLogOutput);
-    });
+  test(`should transclude ${fixture.name}`, (t) => {
+    const options = {
+      relativePath: path.resolve(__dirname, '../fixtures', fixture.name),
+    };
+    const output = transcludeStringSync(fixture.input, options, t.context.log);
+    t.same(output, fixture.expectedOutput);
+    t.same(t.context.logOutput, fixture.expectedLogOutput);
   });
-}
+});
diff --git a/test/units/transcludeFile.js b/test/units/transcludeFile.js
new file mode 100644
index 0000000..1a3d019
--- /dev/null
+++ b/test/units/transcludeFile.js
@@ -0,0 +1,35 @@
+import test from 'ava';
+import path from 'path';
+
+import { transcludeFile } from '../../lib/hercule';
+
+test.cb(`should transclude with only required arguments`, (t) => {
+  const input = path.join(__dirname, '../fixtures/no-link/index.md');
+  const expected = 'The quick brown fox jumps over the lazy dog.\n';
+  transcludeFile(input, (output) => {
+    t.same(output, expected);
+    t.end();
+  });
+});
+
+test.cb(`should transclude with optional relativePath argument`, (t) => {
+  const input = path.join(__dirname, '../fixtures/no-link/index.md');
+  const expected = 'The quick brown fox jumps over the lazy dog.\n';
+  transcludeFile(input, { relativePath: 'test' }, (output) => {
+    t.same(output, expected);
+    t.end();
+  });
+});
+
+test.cb(`should transclude with optional log handler`, (t) => {
+  const input = path.join(__dirname, '../fixtures/no-link/index.md');
+  const expected = 'The quick brown fox jumps over the lazy dog.\n';
+  const logger = {
+    error: () => t.fail(),
+    warn: () => t.fail(),
+  };
+  transcludeFile(input, null, logger, (output) => {
+    t.same(output, expected);
+    t.end();
+  });
+});
diff --git a/test/units/transcludeFileSync.js b/test/units/transcludeFileSync.js
new file mode 100644
index 0000000..c544b59
--- /dev/null
+++ b/test/units/transcludeFileSync.js
@@ -0,0 +1,40 @@
+import test from 'ava';
+import path from 'path';
+
+import { transcludeFileSync } from '../../lib/hercule';
+
+let major;
+let minor;
+
+[major, minor] = process.versions.node.split('.');
+
+if (major < 1 && minor < 12) {
+  test.only(`synchronous support not available < 0.12`, (t) => {
+    t.pass();
+  });
+}
+
+test(`should transclude with only required arguments`, (t) => {
+  const input = path.join(__dirname, '../fixtures/no-link/index.md');
+  const expected = 'The quick brown fox jumps over the lazy dog.\n';
+  const output = transcludeFileSync(input);
+  t.same(output, expected);
+});
+
+test(`should transclude with optional relativePath argument`, (t) => {
+  const input = path.join(__dirname, '../fixtures/no-link/index.md');
+  const expected = 'The quick brown fox jumps over the lazy dog.\n';
+  const output = transcludeFileSync(input, { relativePath: 'test' });
+  t.same(output, expected);
+});
+
+test(`should transclude with optional log handler`, (t) => {
+  const input = path.join(__dirname, '../fixtures/no-link/index.md');
+  const expected = 'The quick brown fox jumps over the lazy dog.\n';
+  const logger = {
+    error: () => t.fail(),
+    warn: () => t.fail(),
+  };
+  const output = transcludeFileSync(input, null, logger);
+  t.same(output, expected);
+});
diff --git a/test/units/transcludeString.js b/test/units/transcludeString.js
new file mode 100644
index 0000000..3592d0e
--- /dev/null
+++ b/test/units/transcludeString.js
@@ -0,0 +1,34 @@
+import test from 'ava';
+
+import { transcludeString } from '../../lib/hercule';
+
+test.cb(`should transclude with only required arguments`, (t) => {
+  const input = 'The quick brown fox jumps over the lazy dog.';
+  const expected = 'The quick brown fox jumps over the lazy dog.';
+  transcludeString(input, (output) => {
+    t.same(output, expected);
+    t.end();
+  });
+});
+
+test.cb(`should transclude with optional relativePath argument`, (t) => {
+  const input = 'The quick brown fox jumps over the lazy dog.';
+  const expected = 'The quick brown fox jumps over the lazy dog.';
+  transcludeString(input, { relativePath: 'test' }, (output) => {
+    t.same(output, expected);
+    t.end();
+  });
+});
+
+test.cb(`should transclude with optional log handler`, (t) => {
+  const input = 'The quick brown fox jumps over the lazy dog.';
+  const expected = 'The quick brown fox jumps over the lazy dog.';
+  const logger = {
+    error: () => t.fail(),
+    warn: () => t.fail(),
+  };
+  transcludeString(input, null, logger, (output) => {
+    t.same(output, expected);
+    t.end();
+  });
+});
diff --git a/test/units/transcludeStringSync.js b/test/units/transcludeStringSync.js
new file mode 100644
index 0000000..b888181
--- /dev/null
+++ b/test/units/transcludeStringSync.js
@@ -0,0 +1,39 @@
+import test from 'ava';
+
+import { transcludeStringSync } from '../../lib/hercule';
+
+let major;
+let minor;
+
+[major, minor] = process.versions.node.split('.');
+
+if (major < 1 && minor < 12) {
+  test.only(`synchronous support not available < 0.12`, (t) => {
+    t.pass();
+  });
+}
+
+test(`should transclude with only required arguments`, (t) => {
+  const input = 'The quick brown fox jumps over the lazy dog.';
+  const expected = 'The quick brown fox jumps over the lazy dog.';
+  const output = transcludeStringSync(input);
+  t.same(output, expected);
+});
+
+test(`should transclude with optional relativePath argument`, (t) => {
+  const input = 'The quick brown fox jumps over the lazy dog.';
+  const expected = 'The quick brown fox jumps over the lazy dog.';
+  const output = transcludeStringSync(input, { relativePath: 'test' });
+  t.same(output, expected);
+});
+
+test(`should transclude with optional log handler`, (t) => {
+  const input = 'The quick brown fox jumps over the lazy dog.';
+  const expected = 'The quick brown fox jumps over the lazy dog.';
+  const logger = {
+    error: () => t.fail(),
+    warn: () => t.fail(),
+  };
+  const output = transcludeStringSync(input, null, logger);
+  t.same(output, expected);
+});