From 273dbbbf4c21debded604c2094591bdbcbed7705 Mon Sep 17 00:00:00 2001 From: JacobLey <37151850+JacobLey@users.noreply.github.com> Date: Mon, 1 Jun 2020 18:53:24 -0400 Subject: [PATCH] Support --require of ESM; closes #4281 (#4304) * Support --require of ESM; closes #4281 Allow files/modules specified in `--require` to be ESM. CommonJS loading is still supported and the default. * Conditionally generate url for import Windows compatible * 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" * Revert change to eslintrc, use mocha to pass experimental flag * Replace type() -> typeof Add truthy check to handle null edge case type(ES Module) => "module", but we treat it the same as an object * Remove doc limitation for --require ESM * Add note to --require docs about ESM support --- docs/index.md | 3 +- lib/cli/run-helpers.js | 19 ++++++--- lib/esm-utils.js | 17 +++++--- .../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 | 41 +++++++++++++++++++ 7 files changed, 84 insertions(+), 13 deletions(-) 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/docs/index.md b/docs/index.md index cd2ed9c328..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]. @@ -1450,7 +1452,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 diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js index e09338f2c6..017d914f4d 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,16 +82,21 @@ 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); - if (type(requiredModule) === 'object' && requiredModule.mochaHooks) { + const requiredModule = await requireOrImport(modpath); + if ( + requiredModule && + typeof requiredModule === 'object' && + requiredModule.mochaHooks + ) { const mochaHooksType = type(requiredModule.mochaHooks); if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') { debug('found root hooks in required file %s', mod); @@ -102,8 +108,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..604f883d9a 100644 --- a/lib/esm-utils.js +++ b/lib/esm-utils.js @@ -1,11 +1,16 @@ -const url = require('url'); const path = require('path'); +const url = require('url'); -const requireOrImport = async file => { - file = path.resolve(file); +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(url.pathToFileURL(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. @@ -15,7 +20,7 @@ const requireOrImport = async file => { return require(file); } catch (err) { if (err.code === 'ERR_REQUIRE_ESM') { - return import(url.pathToFileURL(file)); + return formattedImport(file); } else { throw err; } @@ -25,7 +30,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); } }; 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..11b869a5f0 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,46 @@ 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' + ) + ].concat( + +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() {