From 5ef04be6976065b723010dfe5f2c4ac17ea2217d Mon Sep 17 00:00:00 2001 From: Jacob Ley Date: Wed, 27 May 2020 14:54:43 -0400 Subject: [PATCH 1/7] Support --require of ESM; closes #4281 Allow files/modules specified in `--require` to be ESM. CommonJS loading is still supported and the default. --- lib/cli/run-helpers.js | 13 ++++++++----- lib/esm-utils.js | 11 ++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js index 172fae654a..513e930e2a 100644 --- a/lib/cli/run-helpers.js +++ b/lib/cli/run-helpers.js @@ -15,6 +15,7 @@ const collectFiles = require('./collect-files'); const {type} = require('../utils'); const {format} = require('util'); const {createInvalidPluginError, createUnsupportedError} = require('../errors'); +const {requireOrImport} = require('../esm-utils'); /** * Exits Mocha when tests + code under test has finished execution (default) @@ -81,15 +82,16 @@ exports.list = str => * @returns {Promise} Any root hooks * @private */ -exports.handleRequires = async (requires = []) => - requires.reduce((acc, mod) => { +exports.handleRequires = async (requires = []) => { + const acc = []; + for (const mod of requires) { let modpath = mod; // this is relative to cwd if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) { modpath = path.resolve(mod); debug('resolved required file %s to %s', mod, modpath); } - const requiredModule = require(modpath); + const requiredModule = await requireOrImport(modpath); if (type(requiredModule) === 'object' && requiredModule.mochaHooks) { const mochaHooksType = type(requiredModule.mochaHooks); if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') { @@ -102,8 +104,9 @@ exports.handleRequires = async (requires = []) => } } debug('loaded required module "%s"', mod); - return acc; - }, []); + } + return acc; +}; /** * Loads root hooks as exported via `mochaHooks` from required files. diff --git a/lib/esm-utils.js b/lib/esm-utils.js index df2b5fed0e..e94580eea8 100644 --- a/lib/esm-utils.js +++ b/lib/esm-utils.js @@ -1,11 +1,8 @@ -const url = require('url'); const path = require('path'); -const requireOrImport = async file => { - file = path.resolve(file); - +exports.requireOrImport = async file => { if (path.extname(file) === '.mjs') { - return import(url.pathToFileURL(file)); + return import(file); } // This is currently the only known way of figuring out whether a file is CJS or ESM. // If Node.js or the community establish a better procedure for that, we can fix this code. @@ -15,7 +12,7 @@ const requireOrImport = async file => { return require(file); } catch (err) { if (err.code === 'ERR_REQUIRE_ESM') { - return import(url.pathToFileURL(file)); + return import(file); } else { throw err; } @@ -25,7 +22,7 @@ const requireOrImport = async file => { exports.loadFilesAsync = async (files, preLoadFunc, postLoadFunc) => { for (const file of files) { preLoadFunc(file); - const result = await requireOrImport(file); + const result = await exports.requireOrImport(path.resolve(file)); postLoadFunc(file, result); } }; From f28fd6728c7ffce6d12e1c9340d6f55063a93310 Mon Sep 17 00:00:00 2001 From: Jacob Ley Date: Thu, 28 May 2020 10:54:42 -0400 Subject: [PATCH 2/7] Conditionally generate url for import Windows compatible --- lib/esm-utils.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/esm-utils.js b/lib/esm-utils.js index e94580eea8..604f883d9a 100644 --- a/lib/esm-utils.js +++ b/lib/esm-utils.js @@ -1,8 +1,16 @@ const path = require('path'); +const url = require('url'); + +const formattedImport = async file => { + if (path.isAbsolute(file)) { + return import(url.pathToFileURL(file)); + } + return import(file); +}; exports.requireOrImport = async file => { if (path.extname(file) === '.mjs') { - return import(file); + return formattedImport(file); } // This is currently the only known way of figuring out whether a file is CJS or ESM. // If Node.js or the community establish a better procedure for that, we can fix this code. @@ -12,7 +20,7 @@ exports.requireOrImport = async file => { return require(file); } catch (err) { if (err.code === 'ERR_REQUIRE_ESM') { - return import(file); + return formattedImport(file); } else { throw err; } From e9834fdc898678ffb5b8757c9cc5cdd2a4e0a8de Mon Sep 17 00:00:00 2001 From: Jacob Ley Date: Thu, 28 May 2020 16:21:09 -0400 Subject: [PATCH 3/7] Add tests for --require ESM As both .mjs and type=module (combined with cjs for good measure). Updated linter to allow tests to use spread operator (ecmaVersion 2018) Allow --require'd module to be an object, or "module" --- .eslintrc.yml | 2 + lib/cli/run-helpers.js | 5 +- test/integration/fixtures/esm/require.mjs | 0 .../fixtures/options/require/esm/package.json | 1 + .../require/esm/root-hook-defs-esm.fixture.js | 8 ++++ .../require/root-hook-defs-esm.fixture.mjs | 8 ++++ test/integration/options/require.spec.js | 46 +++++++++++++++++++ 7 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 test/integration/fixtures/esm/require.mjs create mode 100644 test/integration/fixtures/options/require/esm/package.json create mode 100644 test/integration/fixtures/options/require/esm/root-hook-defs-esm.fixture.js create mode 100644 test/integration/fixtures/options/require/root-hook-defs-esm.fixture.mjs diff --git a/.eslintrc.yml b/.eslintrc.yml index 2a3fa281df..a10eb0ae21 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -56,6 +56,8 @@ overrides: browser: false - files: - test/**/*.{js,mjs} + parserOptions: + ecmaVersion: 2018 env: mocha: true globals: diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js index 513e930e2a..10cb06ab07 100644 --- a/lib/cli/run-helpers.js +++ b/lib/cli/run-helpers.js @@ -92,7 +92,10 @@ exports.handleRequires = async (requires = []) => { debug('resolved required file %s to %s', mod, modpath); } const requiredModule = await requireOrImport(modpath); - if (type(requiredModule) === 'object' && requiredModule.mochaHooks) { + if ( + ['object', 'module'].includes(type(requiredModule)) && + requiredModule.mochaHooks + ) { const mochaHooksType = type(requiredModule.mochaHooks); if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') { debug('found root hooks in required file %s', mod); diff --git a/test/integration/fixtures/esm/require.mjs b/test/integration/fixtures/esm/require.mjs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/fixtures/options/require/esm/package.json b/test/integration/fixtures/options/require/esm/package.json new file mode 100644 index 0000000000..5ffd9800b9 --- /dev/null +++ b/test/integration/fixtures/options/require/esm/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/test/integration/fixtures/options/require/esm/root-hook-defs-esm.fixture.js b/test/integration/fixtures/options/require/esm/root-hook-defs-esm.fixture.js new file mode 100644 index 0000000000..3e0a2f775c --- /dev/null +++ b/test/integration/fixtures/options/require/esm/root-hook-defs-esm.fixture.js @@ -0,0 +1,8 @@ +export const mochaHooks = () => ({ + beforeEach() { + console.log('esm beforeEach'); + }, + afterEach() { + console.log('esm afterEach'); + }, +}); diff --git a/test/integration/fixtures/options/require/root-hook-defs-esm.fixture.mjs b/test/integration/fixtures/options/require/root-hook-defs-esm.fixture.mjs new file mode 100644 index 0000000000..6597d65be0 --- /dev/null +++ b/test/integration/fixtures/options/require/root-hook-defs-esm.fixture.mjs @@ -0,0 +1,8 @@ +export const mochaHooks = { + beforeAll() { + console.log('mjs beforeAll'); + }, + afterAll() { + console.log('mjs afterAll'); + }, +}; diff --git a/test/integration/options/require.spec.js b/test/integration/options/require.spec.js index ca50af8607..e24b605f1d 100644 --- a/test/integration/options/require.spec.js +++ b/test/integration/options/require.spec.js @@ -1,6 +1,7 @@ 'use strict'; var invokeMochaAsync = require('../helpers').invokeMochaAsync; +var utils = require('../../../lib/utils'); describe('--require', function() { describe('when mocha run in serial mode', function() { @@ -45,6 +46,51 @@ describe('--require', function() { /beforeAll[\s\S]+?beforeAll array 1[\s\S]+?beforeAll array 2[\s\S]+?beforeEach[\s\S]+?beforeEach array 1[\s\S]+?beforeEach array 2[\s\S]+?afterEach[\s\S]+?afterEach array 1[\s\S]+?afterEach array 2[\s\S]+?afterAll[\s\S]+?afterAll array 1[\s\S]+?afterAll array 2/ ); }); + + describe('support ESM when style=module or .mjs extension', function() { + before(function() { + if (!utils.supportsEsModules()) this.skip(); + }); + + it('should run root hooks when provided via mochaHooks', function() { + return expect( + invokeMochaAsync( + [ + '--require=' + + require.resolve( + // as object + '../fixtures/options/require/root-hook-defs-esm.fixture.mjs' + ), + '--require=' + + require.resolve( + // as function + '../fixtures/options/require/esm/root-hook-defs-esm.fixture.js' + ), + '--require=' + + require.resolve( + // mixed with commonjs + '../fixtures/options/require/root-hook-defs-a.fixture.js' + ), + require.resolve( + '../fixtures/options/require/root-hook-test.fixture.js' + ) + ], + { + env: { + ...process.env, + NODE_OPTIONS: + +process.versions.node.split('.')[0] >= 13 + ? '' + : '--experimental-modules' + } + } + )[1], + 'when fulfilled', + 'to contain output', + /mjs beforeAll[\s\S]+?beforeAll[\s\S]+?esm beforeEach[\s\S]+?beforeEach[\s\S]+?esm afterEach[\s\S]+?afterEach[\s\S]+?mjs afterAll[\s\S]+?afterAll/ + ); + }); + }); }); describe('when mocha in parallel mode', function() { From ebc2a07151530d5e215a6b8ac656e19497b84f64 Mon Sep 17 00:00:00 2001 From: Jacob Ley Date: Thu, 28 May 2020 16:41:53 -0400 Subject: [PATCH 4/7] Revert change to eslintrc, use mocha to pass experimental flag --- .eslintrc.yml | 2 -- test/integration/options/require.spec.js | 15 +++++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index a10eb0ae21..2a3fa281df 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -56,8 +56,6 @@ overrides: browser: false - files: - test/**/*.{js,mjs} - parserOptions: - ecmaVersion: 2018 env: mocha: true globals: diff --git a/test/integration/options/require.spec.js b/test/integration/options/require.spec.js index e24b605f1d..11b869a5f0 100644 --- a/test/integration/options/require.spec.js +++ b/test/integration/options/require.spec.js @@ -74,16 +74,11 @@ describe('--require', function() { require.resolve( '../fixtures/options/require/root-hook-test.fixture.js' ) - ], - { - env: { - ...process.env, - NODE_OPTIONS: - +process.versions.node.split('.')[0] >= 13 - ? '' - : '--experimental-modules' - } - } + ].concat( + +process.versions.node.split('.')[0] >= 13 + ? [] + : '--experimental-modules' + ) )[1], 'when fulfilled', 'to contain output', From c411c30aec1664a900597e6256f2d7d89242a62a Mon Sep 17 00:00:00 2001 From: Jacob Ley Date: Thu, 28 May 2020 17:46:36 -0400 Subject: [PATCH 5/7] Replace type() -> typeof Add truthy check to handle null edge case type(ES Module) => "module", but we treat it the same as an object --- lib/cli/run-helpers.js | 3 ++- test/integration/fixtures/esm/require.mjs | 0 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 test/integration/fixtures/esm/require.mjs diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js index 10cb06ab07..a3519c0a96 100644 --- a/lib/cli/run-helpers.js +++ b/lib/cli/run-helpers.js @@ -93,7 +93,8 @@ exports.handleRequires = async (requires = []) => { } const requiredModule = await requireOrImport(modpath); if ( - ['object', 'module'].includes(type(requiredModule)) && + requiredModule && + typeof requiredModule === 'object' && requiredModule.mochaHooks ) { const mochaHooksType = type(requiredModule.mochaHooks); diff --git a/test/integration/fixtures/esm/require.mjs b/test/integration/fixtures/esm/require.mjs deleted file mode 100644 index e69de29bb2..0000000000 From a6029b9a6a7f664869db874eb4898f10453673fa Mon Sep 17 00:00:00 2001 From: Jacob Ley Date: Fri, 29 May 2020 10:40:59 -0400 Subject: [PATCH 6/7] Remove doc limitation for --require ESM --- docs/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index cd2ed9c328..726db44da1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1450,7 +1450,6 @@ Node.JS native ESM support still has status: **Stability: 1 - Experimental** - [Watch mode](#-watch-w) does not support ES Module test files - [Custom reporters](#third-party-reporters) and [custom interfaces](#interfaces) can only be CommonJS files -- [Required modules](#-require-module-r-module) can only be CommonJS files - [Configuration file](#configuring-mocha-nodejs) can only be a CommonJS file (`.mocharc.js` or `.mocharc.cjs`) - When using module-level mocks via libs like `proxyquire`, `rewiremock` or `rewire`, hold off on using ES modules for your test files - Node.JS native ESM support does not work with [esm][npm-esm] module From cc401ce1562aabb6a51065dd0c32944af444b846 Mon Sep 17 00:00:00 2001 From: Jacob Ley Date: Fri, 29 May 2020 16:26:30 -0400 Subject: [PATCH 7/7] Add note to --require docs about ESM support --- docs/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.md b/docs/index.md index 726db44da1..ad26db226b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1007,6 +1007,8 @@ Modules required in this manner are expected to do work synchronously; Mocha won Note you cannot use `--require` to set a global `beforeEach()` hook, for example — use `--file` instead, which allows you to specify an explicit order in which test files are loaded. +> As of v7.3.0, Mocha supports `--require` for [NodeJS native ESM](#nodejs-native-esm-support). There is no separate `--import` flag. + ### `--sort, -S` Sort test files (by absolute path) using [Array.prototype.sort][mdn-array-sort].