Skip to content

Commit 38a534c

Browse files
committed
CLI: Add native support for ESM .mjs files on Node 12+
* Rename test/es2017 and update ESLint config to allow for import/export syntax to be used. We don't rely on ESLint to know what is supported in Node.js, our test matrix in CI covers that already. We should probably re-think the way these tests are organized, which I've filed #1511 for. * Update eslint config for src/cli to es2020. The dynamic `import()` statement was only spe'ced in es2020. This is first implemented (without experimental flag) in Node 12. We need to support Node 10. However it looks like Node 10 already tolerated this (maybe it sees it as a normal function, or maybe it was already recognised by the V8 parser it shipped with), so the try-catch suffices there. Again, the CI test matrix verifies for us that stuff works fine on older versions. Ref #1465.
1 parent 259f9af commit 38a534c

File tree

12 files changed

+89
-10
lines changed

12 files changed

+89
-10
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
],
5353
"parserOptions": {
5454
"sourceType": "script",
55-
"ecmaVersion": 2018
55+
"ecmaVersion": 2020
5656
},
5757
"env": {
5858
"node": true

Gruntfile.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ module.exports = function( grunt ) {
178178
"test/module-only",
179179
"test/module-skip",
180180
"test/module-todo",
181-
"test/es2017/async-functions",
182-
"test/es2017/throws"
181+
"test/es2018/async-functions",
182+
"test/es2018/throws"
183183
]
184184
},
185185
"watch-repeatable": {

src/cli/run.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const changedPendingPurge = [];
1212

1313
let QUnit;
1414

15-
function run( args, options ) {
15+
async function run( args, options ) {
1616

1717
// Default to non-zero exit code to avoid false positives
1818
process.exitCode = 1;
@@ -48,8 +48,42 @@ function run( args, options ) {
4848
const filePath = path.resolve( process.cwd(), files[ i ] );
4949
delete require.cache[ filePath ];
5050

51+
// Node.js 12.0.0 has node_module_version=72
52+
// https://nodejs.org/en/download/releases/
53+
const nodeVint = process.config.variables.node_module_version;
54+
5155
try {
52-
require( filePath );
56+
57+
// QUnit supports passing ESM files to the 'qunit' command when used on
58+
// Node.js 12 or later. The dynamic import() keyword supports both CommonJS files
59+
// (.js, .cjs) and ESM files (.mjs), so we could simply use that unconditionally on
60+
// newer Node versions, regardless of the given file path.
61+
//
62+
// But:
63+
// - Node.js 12 emits a confusing "ExperimentalWarning" when using import(),
64+
// even if just to load a non-ESM file. So we should try to avoid it on non-ESM.
65+
// - This Node.js feature is still considered experimental so to avoid unexpected
66+
// breakage we should continue using require(). Consider flipping once stable and/or
67+
// as part of QUnit 3.0.
68+
// - Plugins and CLI bootstrap scripts may be hooking into require.extensions to modify
69+
// or transform code as it gets loaded. For compatibility with that, we should
70+
// support that until at least QUnit 3.0.
71+
// - File extensions are not sufficient to differentiate between CJS and ESM.
72+
// Use of ".mjs" is optional, as a package may configure Node to default to ESM
73+
// and optionally use ".cjs" for CJS files.
74+
//
75+
// https://nodejs.org/docs/v12.7.0/api/modules.html#modules_addenda_the_mjs_extension
76+
// https://nodejs.org/docs/v12.7.0/api/esm.html#esm_code_import_code_expressions
77+
// https://github.com/qunitjs/qunit/issues/1465
78+
try {
79+
require( filePath );
80+
} catch ( e ) {
81+
if ( e.code === "ERR_REQUIRE_ESM" && ( !nodeVint || nodeVint >= 72 ) ) {
82+
await import( filePath );
83+
} else {
84+
throw e;
85+
}
86+
}
5387
} catch ( e ) {
5488

5589
// eslint-disable-next-line no-loop-func

src/cli/utils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ function getFilesFromArgs( args ) {
2727

2828
// Default to files in the test directory
2929
if ( !patterns.length ) {
30+
31+
// TODO: In QUnit 3.0, change the default to {js,mjs}
3032
patterns.push( "test/**/*.js" );
3133
}
3234

@@ -45,6 +47,8 @@ function getFilesFromArgs( args ) {
4547
files.add( pattern );
4648
} else {
4749
if ( stat && stat.isDirectory() ) {
50+
51+
// TODO: In QUnit 3.0, change the default to {js,mjs}
4852
pattern = `${pattern}/**/*.js`;
4953
}
5054
const results = glob( pattern, {

test/cli/fixtures/expected/tap-outputs.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ not ok 2 Example > bad
203203
# fail 1
204204
`,
205205

206+
// Node 12 enabled ESM by default, without experimental flag,
207+
// but left the warning. The warning was removed in Node 14.
208+
"qunit ../../es2018/esm.mjs":
209+
`(\n\\(node:\\d+\\) ExperimentalWarning: The ESM module loader is experimental.\n)?TAP version 13
210+
ok 1 ESM test suite > sum\\(\\)
211+
1..1
212+
# pass 1
213+
# skip 0
214+
# todo 0
215+
# fail 0`,
216+
206217
"qunit timeout":
207218
`TAP version 13
208219
not ok 1 timeout > first

test/cli/main.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,21 @@ QUnit.module( "CLI Main", function() {
135135
}
136136
} );
137137

138+
if ( semver.gte( process.versions.node, "12.0.0" ) ) {
139+
QUnit.test( "run ESM test suite with import statement", async function( assert ) {
140+
const command = "qunit ../../es2018/esm.mjs";
141+
const execution = await execute( command );
142+
143+
assert.equal( execution.code, 0 );
144+
assert.equal( execution.stderr, "" );
145+
const re = new RegExp( expectedOutput[ command ] );
146+
assert.equal( re.test( execution.stdout ), true );
147+
if ( !re.test( execution.stdout ) ) {
148+
assert.equal( execution.stdout, expectedOutput[ command ] );
149+
}
150+
} );
151+
}
152+
138153
// https://nodejs.org/dist/v12.12.0/docs/api/cli.html#cli_enable_source_maps
139154
if ( semver.gte( process.versions.node, "14.0.0" ) ) {
140155

test/es2017/.eslintrc.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

test/es2018/.eslintrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"parserOptions": {
3+
"ecmaVersion": 2018,
4+
"sourceType": "module"
5+
}
6+
}
File renamed without changes.

test/es2018/esm.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import sum from "./sum.mjs";
2+
3+
QUnit.module( "ESM test suite", () => {
4+
5+
QUnit.test( "sum()", assert => {
6+
assert.equal( 5, sum( 2, 3 ) );
7+
} );
8+
9+
} );

test/es2018/sum.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function sum( a, b ) {
2+
return a + b;
3+
}
4+
5+
export default sum;
File renamed without changes.

0 commit comments

Comments
 (0)