diff --git a/.eslintrc b/.eslintrc index 6a6eed21..5b749b5f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,6 +29,16 @@ "strict": "error", }, "overrides": [ + { + "files": ["*.mjs", "test/import/package_type/*.js"], + "extends": "@ljharb/eslint-config/esm", + }, + { + "files": ["bin/import-or-require.js"], + "parserOptions": { + "ecmaVersion": 2020, + }, + }, { "files": ["test/async-await/*"], "parserOptions": { diff --git a/.nycrc b/.nycrc index c4187917..daab59ba 100644 --- a/.nycrc +++ b/.nycrc @@ -3,6 +3,7 @@ "check-coverage": false, "reporter": ["text-summary", "text", "html", "json"], "exclude": [ + "bin/import-or-require.js", "coverage", "example", "test" diff --git a/bin/import-or-require.js b/bin/import-or-require.js new file mode 100644 index 00000000..8b72edd3 --- /dev/null +++ b/bin/import-or-require.js @@ -0,0 +1,13 @@ +'use strict'; + +const { extname: extnamePath } = require('path'); +const getPackageType = require('get-package-type'); + +module.exports = function (file) { + const ext = extnamePath(file); + + if (ext === '.mjs' || (ext === '.js' && getPackageType.sync(file) === 'module')) { + return import(file); + } + require(file); +}; diff --git a/bin/tape b/bin/tape index 03f9b273..a912c087 100755 --- a/bin/tape +++ b/bin/tape @@ -8,6 +8,9 @@ var readFileSync = require('fs').readFileSync; var parseOpts = require('minimist'); var glob = require('glob'); var ignore = require('dotignore'); +var hasImport = require('has-dynamic-import'); + +var tape = require('../'); var opts = parseOpts(process.argv.slice(2), { alias: { r: 'require', i: 'ignore' }, @@ -47,7 +50,7 @@ var files = opts._.reduce(function (result, arg) { var files = glob.sync(arg); if (!Array.isArray(files)) { - throw new TypeError('unknown error: glob.sync did not return an array or throw. Please report this.'); + throw new TypeError('unknown error: glob.sync("' + arg + '") did not return an array or throw. Please report this.'); } return result.concat(files); @@ -57,6 +60,28 @@ var files = opts._.reduce(function (result, arg) { return resolvePath(cwd, file); }); -files.forEach(function (x) { require(x); }); +hasImport().then(function (hasSupport) { + // the nextTick callback gets called outside the promise chain, avoiding + // promises and unhandled rejections when only loading commonjs files + process.nextTick(importFiles, hasSupport); +}); + +function importFiles(hasSupport) { + if (!hasSupport) { + return files.forEach(function (x) { require(x); }); + } + + var importOrRequire = require('./import-or-require'); + + tape.wait(); + + var promise = files.reduce(function (promise, file) { + return promise ? promise.then(function () { + return importOrRequire(file); + }) : importOrRequire(file); + }, null); + + return promise ? promise.then(function () { tape.run(); }) : tape.run(); +} // vim: ft=javascript diff --git a/index.js b/index.js index 19cacb38..fb2a8621 100644 --- a/index.js +++ b/index.js @@ -14,11 +14,22 @@ var canExit = typeof process !== 'undefined' && process ; exports = module.exports = (function () { + var wait = false; var harness; var lazyLoad = function () { return getHarness().apply(this, arguments); }; + lazyLoad.wait = function () { + wait = true; + }; + + lazyLoad.run = function () { + var run = getHarness().run; + + if (run) run(); + }; + lazyLoad.only = function () { return getHarness().only.apply(this, arguments); }; @@ -48,26 +59,25 @@ exports = module.exports = (function () { function getHarness(opts) { if (!opts) opts = {}; opts.autoclose = !canEmitExit; - if (!harness) harness = createExitHarness(opts); + if (!harness) harness = createExitHarness(opts, wait); return harness; } })(); -function createExitHarness(conf) { +function createExitHarness(conf, wait) { if (!conf) conf = {}; var harness = createHarness({ autoclose: defined(conf.autoclose, false) }); + var running = false; + var ended = false; - var stream = harness.createStream({ objectMode: conf.objectMode }); - var es = stream.pipe(conf.stream || createDefaultStream()); - if (canEmitExit) { - es.on('error', function (err) { harness._exitCode = 1; }); + if (wait) { + harness.run = run; + } else { + run(); } - var ended = false; - stream.on('end', function () { ended = true; }); - if (conf.exit === false) return harness; if (!canEmitExit || !canExit) return harness; @@ -92,6 +102,17 @@ function createExitHarness(conf) { }); return harness; + + function run() { + if (running) return; + running = true; + var stream = harness.createStream({ objectMode: conf.objectMode }); + var es = stream.pipe(conf.stream || createDefaultStream()); + if (canEmitExit) { + es.on('error', function (err) { harness._exitCode = 1; }); + } + stream.on('end', function () { ended = true; }); + }; } exports.createHarness = createHarness; diff --git a/package.json b/package.json index bbdae7de..8292e6e9 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,10 @@ "defined": "^1.0.0", "dotignore": "^0.1.2", "for-each": "^0.3.3", + "get-package-type": "^0.1.0", "glob": "^7.1.7", "has": "^1.0.3", + "has-dynamic-import": "^2.0.0", "inherits": "^2.0.4", "is-regex": "^1.1.3", "minimist": "^1.2.5", @@ -41,6 +43,7 @@ "through": "^2.3.8" }, "devDependencies": { + "@ljharb/eslint-config": "^17.6.0", "array.prototype.flatmap": "^1.2.4", "aud": "^1.1.5", "concat-stream": "^1.6.2", diff --git a/test/import.js b/test/import.js new file mode 100644 index 00000000..b30952be --- /dev/null +++ b/test/import.js @@ -0,0 +1,93 @@ +'use strict'; + +var tap = require('tap'); +var spawn = require('child_process').spawn; +var concat = require('concat-stream'); +var hasDynamicImport = require('has-dynamic-import'); + +tap.test('importing mjs files', function (t) { + hasDynamicImport().then(function (hasSupport) { + if (hasSupport) { + var tc = function (rows) { + t.same(rows.toString('utf8'), [ + 'TAP version 13', + '# mjs-a', + 'ok 1 test ran', + '# mjs-b', + 'ok 2 test ran after mjs-a', + '# mjs-c', + 'ok 3 test ran after mjs-b', + '# mjs-d', + 'ok 4 test ran after mjs-c', + '# mjs-e', + 'ok 5 test ran after mjs-d', + '# mjs-f', + 'ok 6 test ran after mjs-e', + '# mjs-g', + 'ok 7 test ran after mjs-f', + '# mjs-h', + 'ok 8 test ran after mjs-g', + '', + '1..8', + '# tests 8', + '# pass 8', + '', + '# ok' + ].join('\n') + '\n\n'); + }; + + var ps = tape('import/*.mjs'); + ps.stdout.pipe(concat(tc)); + ps.stderr.pipe(process.stderr); + ps.on('exit', function (code) { + t.equal(code, 0); + t.end(); + }); + } else { + t.pass('does not support dynamic import'); + t.end(); + } + }); +}); + +tap.test('importing type: "module" files', function (t) { + hasDynamicImport().then(function (hasSupport) { + if (hasSupport) { + var tc = function (rows) { + t.same(rows.toString('utf8'), [ + 'TAP version 13', + '# package-type-a', + 'ok 1 test ran', + '# package-type-b', + 'ok 2 test ran after package-type-a', + '# package-type-c', + 'ok 3 test ran after package-type-b', + '', + '1..3', + '# tests 3', + '# pass 3', + '', + '# ok' + ].join('\n') + '\n\n'); + }; + + var ps = tape('import/package_type/*.js'); + ps.stdout.pipe(concat(tc)); + ps.stderr.pipe(process.stderr); + ps.on('exit', function (code) { + t.equal(code, 0); + t.end(); + }); + } else { + t.pass('does not support dynamic import'); + t.end(); + } + }); +}); + +function tape(args) { + var proc = require('child_process'); + var bin = __dirname + '/../bin/tape'; + + return proc.spawn('node', [bin].concat(args.split(' ')), { cwd: __dirname }); +} diff --git a/test/import/mjs-a.mjs b/test/import/mjs-a.mjs new file mode 100644 index 00000000..39244e0b --- /dev/null +++ b/test/import/mjs-a.mjs @@ -0,0 +1,8 @@ +import tape from '../../index.js'; + +tape.test('mjs-a', function (t) { + t.pass('test ran'); + t.end(); + global.mjs_a = true; +}); + diff --git a/test/import/mjs-b.mjs b/test/import/mjs-b.mjs new file mode 100644 index 00000000..69317cd6 --- /dev/null +++ b/test/import/mjs-b.mjs @@ -0,0 +1,7 @@ +import tape from '../../index.js'; + +tape.test('mjs-b', function (t) { + t.ok(global.mjs_a, 'test ran after mjs-a'); + t.end(); + global.mjs_b = true; +}); diff --git a/test/import/mjs-c.mjs b/test/import/mjs-c.mjs new file mode 100644 index 00000000..aeaffcd7 --- /dev/null +++ b/test/import/mjs-c.mjs @@ -0,0 +1,7 @@ +import tape from '../../index.js'; + +tape.test('mjs-c', function (t) { + t.ok(global.mjs_b, 'test ran after mjs-b'); + t.end(); + global.mjs_c = true; +}); diff --git a/test/import/mjs-d.mjs b/test/import/mjs-d.mjs new file mode 100644 index 00000000..d6b13422 --- /dev/null +++ b/test/import/mjs-d.mjs @@ -0,0 +1,7 @@ +import tape from '../../index.js'; + +tape.test('mjs-d', function (t) { + t.ok(global.mjs_c, 'test ran after mjs-c'); + t.end(); + global.mjs_d = true; +}); diff --git a/test/import/mjs-e.mjs b/test/import/mjs-e.mjs new file mode 100644 index 00000000..f089e3d2 --- /dev/null +++ b/test/import/mjs-e.mjs @@ -0,0 +1,7 @@ +import tape from '../../index.js'; + +tape.test('mjs-e', function (t) { + t.ok(global.mjs_d, 'test ran after mjs-d'); + t.end(); + global.mjs_e = true; +}); diff --git a/test/import/mjs-f.mjs b/test/import/mjs-f.mjs new file mode 100644 index 00000000..d1e68539 --- /dev/null +++ b/test/import/mjs-f.mjs @@ -0,0 +1,7 @@ +import tape from '../../index.js'; + +tape.test('mjs-f', function (t) { + t.ok(global.mjs_e, 'test ran after mjs-e'); + t.end(); + global.mjs_f = true; +}); diff --git a/test/import/mjs-g.mjs b/test/import/mjs-g.mjs new file mode 100644 index 00000000..fb7728bf --- /dev/null +++ b/test/import/mjs-g.mjs @@ -0,0 +1,7 @@ +import tape from '../../index.js'; + +tape.test('mjs-g', function (t) { + t.ok(global.mjs_f, 'test ran after mjs-f'); + t.end(); + global.mjs_g = true; +}); diff --git a/test/import/mjs-h.mjs b/test/import/mjs-h.mjs new file mode 100644 index 00000000..e4af5d81 --- /dev/null +++ b/test/import/mjs-h.mjs @@ -0,0 +1,6 @@ +import tape from '../../index.js'; + +tape.test('mjs-h', function (t) { + t.ok(global.mjs_g, 'test ran after mjs-g'); + t.end(); +}); diff --git a/test/import/package_type/package-a.js b/test/import/package_type/package-a.js new file mode 100644 index 00000000..af4b097a --- /dev/null +++ b/test/import/package_type/package-a.js @@ -0,0 +1,8 @@ +import tape from '../../../index.js'; + +tape.test('package-type-a', function (t) { + t.pass('test ran'); + t.end(); + global.package_type_a = true; +}); + diff --git a/test/import/package_type/package-b.js b/test/import/package_type/package-b.js new file mode 100644 index 00000000..74b8d052 --- /dev/null +++ b/test/import/package_type/package-b.js @@ -0,0 +1,7 @@ +import tape from '../../../index.js'; + +tape.test('package-type-b', function (t) { + t.ok(global.package_type_a, 'test ran after package-type-a'); + t.end(); + global.package_type_b = true; +}); diff --git a/test/import/package_type/package-c.js b/test/import/package_type/package-c.js new file mode 100644 index 00000000..8fa8fac3 --- /dev/null +++ b/test/import/package_type/package-c.js @@ -0,0 +1,6 @@ +import tape from '../../../index.js'; + +tape.test('package-type-c', function (t) { + t.ok(global.package_type_b, 'test ran after package-type-b'); + t.end(); +}); diff --git a/test/import/package_type/package.json b/test/import/package_type/package.json new file mode 100644 index 00000000..47200257 --- /dev/null +++ b/test/import/package_type/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +}