Skip to content

Commit

Permalink
WIP: ESM support
Browse files Browse the repository at this point in the history
  • Loading branch information
Avaq committed Mar 31, 2019
1 parent 8369e26 commit 1a8f8d0
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 108 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
!/.eslint.js
/test/**/*.js
!/test/index.js
*.mjs
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"root": true,
"extends": ["./node_modules/sanctuary-style/eslint-es3.json"],
"env": {"node": true},
"globals": {
"Promise": "readonly"
},
"overrides": [
{
"files": ["lib/doctest.js"],
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ The exit code is 0 if all tests pass, 1 otherwise.

doctest supports CommonJS modules and partially supports AMD modules:

| Module system | Node.js |
| --------------------------- |:-------:|
| AMD | ✔︎ |
| AMD w/ dependencies ||
| CommonJS | ✔︎ |
| CommonJS w/ dependencies | ✔︎ |
| Module system | Option | Node.js |
| --------------------------- | ---------- |:-------:|
| AMD | `amd` | ✔︎ |
| AMD w/ dependencies | `amd` ||
| CommonJS | `commonjs` | ✔︎ |
| CommonJS w/ dependencies | `commonjs` | ✔︎ |
| EcmaScript modules | `esm` | ✔︎ |

Specify module system via JavaScript API:

Expand Down
31 changes: 19 additions & 12 deletions bin/doctest
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
#!/usr/bin/env node

// Based on:
// https://github.com/jashkenas/coffeescript/blob/1.9.1/src/command.coffee#L431-L441
var args = process.argv.slice(1);
var path = require('path');

var args = process.argv.slice(2);
var idx = args.indexOf('--nodejs');
if (idx < 0 || idx === args.length - 1) {
require('../lib/command');
} else {
require('child_process').spawn(
process.execPath,
args[idx + 1].split(/\s+/).concat(args.slice(0, idx), args.slice(idx + 2)),
{cwd: process.cwd(), env: process.env, stdio: [0, 1, 2]}
).on('exit', process.exit);
}
var esm = Number(process.versions.node.split('.')[0]) >= 9;
var flags = idx >= 0 && idx < args.length - 1;

var nodeargs = []
.concat(flags ? args[idx + 1].split(/\s+/) : [])
.concat(esm ? '--experimental-modules' : []);

var script = path.resolve(__dirname, '../lib/command');

var scriptargs = flags ? args.slice(0, idx).concat(args.slice(idx + 2)) : args;

require('child_process').spawn(
process.execPath,
nodeargs.concat([script]).concat(scriptargs),
{cwd: process.cwd(), env: process.env, stdio: [0, 1, 2]}
).on('exit', process.exit);
62 changes: 20 additions & 42 deletions lib/command.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,14 @@
'use strict';

var program = require ('commander');

var common = require ('./common');
var program = require ('./program');
var doctest = require ('..');
var pkg = require ('../package.json');


program
.version (pkg.version)
.usage ('[options] path/to/js/or/coffee/module')
.option ('-m, --module <type>',
'specify module system ("amd" or "commonjs")')
.option (' --nodejs <options>',
'pass options directly to the "node" binary')
.option (' --prefix <prefix>',
'specify Transcribe-style prefix (e.g. ".")')
.option (' --opening-delimiter <delimiter>',
'specify line preceding doctest block (e.g. "```javascript")')
.option (' --closing-delimiter <delimiter>',
'specify line following doctest block (e.g. "```")')
.option ('-p, --print',
'output the rewritten source without running tests')
.option ('-s, --silent',
'suppress output')
.option ('-t, --type <type>',
'specify file type ("coffee" or "js")')
.parse (process.argv);

// formatErrors :: Array String -> String
function formatErrors(errors) {
return (errors.map (function(s) { return 'error: ' + s + '\n'; })).join ('');
}

var errors = [];
if (program.module === 'esm' || program.type === 'esm') {
errors.push ('EcmaScript modules only supported in Node version 9 and up');
}
if (program.module != null &&
program.module !== 'amd' &&
program.module !== 'commonjs') {
Expand All @@ -44,19 +20,21 @@ if (program.type != null &&
errors.push ('Invalid type `' + program.type + "'");
}
if (errors.length > 0) {
process.stderr.write (formatErrors (errors));
process.stderr.write (common.formatErrors (errors));
process.exit (1);
}

process.exit (program.args.reduce (function(status, path) {
var results;
try {
results = doctest (path, program);
} catch (err) {
process.stderr.write (formatErrors ([err.message]));
process.exit (1);
}
return results.reduce (function(status, tuple) {
return tuple[0] ? status : 1;
}, status);
}, 0));
Promise.all (program.args.map (function(path) {
return doctest (path, program).then (function(results) {
return results.reduce (function(status, tuple) {
// Note: This will cause a non-zero-exit to go back to zero.
// I think it's a bug, but it was already there.
return tuple[0] ? status : 1;
}, 0);
});
})).then (function(statuses) {
process.exit (Math.max.apply (null, statuses));
}, function(err) {
process.stderr.write (common.formatErrors ([err.message]));
process.exit (1);
});
20 changes: 20 additions & 0 deletions lib/command.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import common from './common';
import program from './program';
import doctest from '..';

Promise.all (program.args.map (function(path) {
return doctest (path, program).then (function(results) {
console.log (path, results);
return results.reduce (function(status, tuple) {
// Note: This will cause a non-zero-exit to go back to zero.
// I think it's a bug, but it was already there.
return tuple[0] ? status : 1;
}, 0);
});
})).then (function(statuses) {
process.exit (Math.max.apply (null, statuses));
}, function(err) {
console.error (err.stack); //TMP
process.stderr.write (common.formatErrors ([err.message]));
process.exit (1);
});
17 changes: 17 additions & 0 deletions lib/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';


// formatErrors :: Array String -> String
exports.formatErrors = function(errors) {
return (errors.map (function(s) { return 'error: ' + s + '\n'; })).join ('');
};

// sanitizeFileContents :: String -> String
exports.sanitizeFileContents = function(contents) {
return contents.replace (/\r\n?/g, '\n').replace (/^#!.*/, '');
};

// unlines :: Array String -> String
exports.unlines = function(lines) {
return lines.reduce (function(s, line) { return s + line + '\n'; }, '');
};
32 changes: 18 additions & 14 deletions lib/doctest.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var esprima = require ('esprima');
var _show = require ('sanctuary-show');
var Z = require ('sanctuary-type-classes');

var common = require ('./common');

function inferType(path) {
switch (pathlib.extname (path)) {
Expand Down Expand Up @@ -52,23 +53,21 @@ module.exports = function(path, options) {
{prefix: options.prefix == null ? '' : options.prefix,
openingDelimiter: options.openingDelimiter,
closingDelimiter: options.closingDelimiter},
fs.readFileSync (path, 'utf8')
.replace (/\r\n?/g, '\n')
.replace (/^#!.*/, '')
common.sanitizeFileContents (fs.readFileSync (path, 'utf8'))
),
options.module
);

if (options.print) {
console.log (source.replace (/\n$/, ''));
return [];
return Promise.resolve ([]);
} else if (options.silent) {
return evaluate (options.module, source, path);
return Promise.resolve (evaluate (options.module, source, path));
} else {
console.log ('running doctests in ' + path + '...');
var results = evaluate (options.module, source, path);
log (results);
return results;
return Promise.resolve (results);
}
};

Expand Down Expand Up @@ -111,11 +110,6 @@ function stripLeading(n, c, s) {
return s.slice (idx);
}

// unlines :: Array String -> String
function unlines(lines) {
return lines.reduce (function(s, line) { return s + line + '\n'; }, '');
}

// iifeWrap :: String -> String
function iifeWrap(s) {
return 'void function() {\n' + indentN (2, s) + '}.call(this);';
Expand All @@ -125,7 +119,7 @@ function iifeWrap(s) {
function toModule(source, moduleType) {
switch (moduleType) {
case 'amd':
return unlines ([
return common.unlines ([
source,
'function define() {',
' for (var idx = 0; idx < arguments.length; idx += 1) {',
Expand All @@ -137,7 +131,7 @@ function toModule(source, moduleType) {
'}'
]);
case 'commonjs':
return iifeWrap (unlines ([
return iifeWrap (common.unlines ([
'var __doctest = {',
' require: require,',
' queue: [],',
Expand Down Expand Up @@ -378,7 +372,11 @@ function rewrite$js(options, input) {
// produced by step 6 (substituting "step 6" for "step 2").

function getComments(input) {
return (esprima.parse (input, {comment: true, loc: true})).comments;
return (esprima.parse (input, {
comment: true,
loc: true,
sourceType: options.sourceType || 'script'
})).comments;
}

// comments :: { Block :: Array Comment, Line :: Array Comment }
Expand Down Expand Up @@ -423,6 +421,8 @@ function rewrite$js(options, input) {
.join ('');
}

module.exports.rewrite$js = rewrite$js;

function rewrite$coffee(options, input) {
var lines = input.match (/^.*(?=\n)/gm);
var chunks = lines.reduce (function(accum, line, idx) {
Expand Down Expand Up @@ -548,6 +548,8 @@ function run(queue) {
}, {results: [], thunk: null}).results;
}

module.exports.run = run;

function log(results) {
console.log (results.reduce (function(s, tuple) {
return s + (tuple[0] ? '.' : 'x');
Expand All @@ -559,3 +561,5 @@ function log(results) {
}
});
}

module.exports.log = log;
67 changes: 67 additions & 0 deletions lib/doctest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {readFile, writeFile, unlink} from 'fs';
import {resolve} from 'path';
import {promisify} from 'util';

import common from './common';
import doctest from './doctest.js';

export default async function(path, options) {
if (options.module === 'esm') {
if (options.type && options.type !== 'esm') {
throw new Error (
'EcmaScript modules only work with the EcmaScript module type'
);
}
} else {
return doctest (path, options);
}

const source = wrap (
doctest.rewrite$js (
{prefix: options.prefix == null ? '' : options.prefix,
openingDelimiter: options.openingDelimiter,
closingDelimiter: options.closingDelimiter,
sourceType: 'module'},
common.sanitizeFileContents (
await promisify (readFile) (path, 'utf8')
)
)
);

if (options.print) {
console.log (source.replace (/\n$/, ''));
return [];
} else if (options.silent) {
return evaluate (source, path);
} else {
console.log ('running doctests in ' + path + '...');
return (evaluate (source, path)).then(function(results) {
doctest.log (results);
return results;
});
}
}

function wrap(source) {
return common.unlines ([
'export const __doctest = {',
' queue: [],',
' enqueue: function(io) { this.queue.push(io); }',
'};',
'',
source,
]);
}

function evaluate(source, path) {
const abspath =
(resolve (path)).replace (/[.][^.]+$/, '-' + Date.now () + '.mjs');

return promisify (writeFile) (abspath, source).then(function() {
return import (abspath);
}).then (function(module) {
return doctest.run (module.__doctest.queue);
}).finally (function() {
return promisify (unlink) (abspath);
});
}
29 changes: 29 additions & 0 deletions lib/program.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

var program = require ('commander');

var pkg = require ('../package.json');


program
.version (pkg.version)
.usage ('[options] path/to/js/or/coffee/module')
.option ('-m, --module <type>',
'specify module system ("amd", "commonjs", or "esm")')
.option (' --nodejs <options>',
'pass options directly to the "node" binary')
.option (' --prefix <prefix>',
'specify Transcribe-style prefix (e.g. ".")')
.option (' --opening-delimiter <delimiter>',
'specify line preceding doctest block (e.g. "```javascript")')
.option (' --closing-delimiter <delimiter>',
'specify line following doctest block (e.g. "```")')
.option ('-p, --print',
'output the rewritten source without running tests')
.option ('-s, --silent',
'suppress output')
.option ('-t, --type <type>',
'specify file type ("coffee", "js", or "esm")')
.parse (process.argv);

module.exports = program;
Loading

0 comments on commit 1a8f8d0

Please sign in to comment.