Skip to content

Commit 57be455

Browse files
author
Gil Tayar
authored
Add support for Node.JS native ES modules (#4038)
1 parent a995e33 commit 57be455

25 files changed

+372
-79
lines changed

.eslintrc.yml

+8-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,14 @@ overrides:
3131
ecmaVersion: 2017
3232
env:
3333
browser: false
34-
34+
- files:
35+
- esm-utils.js
36+
parserOptions:
37+
ecmaVersion: 2018
38+
sourceType: module
39+
parser: babel-eslint
40+
env:
41+
browser: false
3542
- files:
3643
- test/**/*.{js,mjs}
3744
env:

docs/index.md

+47-7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Mocha is a feature-rich JavaScript test framework running on [Node.js][] and in
3939
- [mocha.opts file support](#-opts-path)
4040
- clickable suite titles to filter test execution
4141
- [node debugger support](#-inspect-inspect-brk-inspect)
42+
- [node native ES modules support](#nodejs-native-esm-support)
4243
- [detects multiple calls to `done()`](#detects-multiple-calls-to-done)
4344
- [use any assertion library you want](#assertions)
4445
- [extensible reporting, bundled with 9+ reporters](#reporters)
@@ -70,6 +71,7 @@ Mocha is a feature-rich JavaScript test framework running on [Node.js][] and in
7071
- [Command-Line Usage](#command-line-usage)
7172
- [Interfaces](#interfaces)
7273
- [Reporters](#reporters)
74+
- [Node.JS native ESM support](#nodejs-native-esm-support)
7375
- [Running Mocha in the Browser](#running-mocha-in-the-browser)
7476
- [Desktop Notification Support](#desktop-notification-support)
7577
- [Configuring Mocha (Node.js)](#configuring-mocha-nodejs)
@@ -354,11 +356,11 @@ With its default "BDD"-style interface, Mocha provides the hooks `before()`, `af
354356
```js
355357
describe('hooks', function() {
356358
before(function() {
357-
// runs before all tests in this block
359+
// runs once before the first test in this block
358360
});
359361

360362
after(function() {
361-
// runs after all tests in this block
363+
// runs once after the last test in this block
362364
});
363365

364366
beforeEach(function() {
@@ -868,7 +870,8 @@ Configuration
868870
--package Path to package.json for config [string]
869871
870872
File Handling
871-
--extension File extension(s) to load [array] [default: js]
873+
--extension File extension(s) to load
874+
[array] [default: ["js","cjs","mjs"]]
872875
--file Specify file(s) to be loaded prior to root suite
873876
execution [array] [default: (none)]
874877
--ignore, --exclude Ignore file(s) or glob pattern(s)
@@ -1538,6 +1541,42 @@ Alias: `HTML`, `html`
15381541

15391542
**The HTML reporter is not intended for use on the command-line.**
15401543

1544+
## Node.JS native ESM support
1545+
1546+
> _New in v7.1.0_
1547+
1548+
Mocha supports writing your tests as ES modules, and not just using CommonJS. For example:
1549+
1550+
```js
1551+
// test.mjs
1552+
import {add} from './add.mjs';
1553+
import assert from 'assert';
1554+
1555+
it('should add to numbers from an es module', () => {
1556+
assert.equal(add(3, 5), 8);
1557+
});
1558+
```
1559+
1560+
To enable this you don't need to do anything special. Write your test file as an ES module. In Node.js
1561+
this means either ending the file with a `.mjs` extension, or, if you want to use the regular `.js` extension, by
1562+
adding `"type": "module"` to your `package.json`.
1563+
More information can be found in the [Node.js documentation](https://nodejs.org/api/esm.html).
1564+
1565+
> Mocha supports ES modules only from Node.js v12.11.0 and above. To enable this in versions smaller than 13.2.0, you need to add `--experimental-modules` when running
1566+
> Mocha. From version 13.2.0 of Node.js, you can use ES modules without any flags.
1567+
1568+
### Current Limitations
1569+
1570+
Node.JS native ESM support still has status: **Stability: 1 - Experimental**
1571+
1572+
- [Watch mode](#-watch-w) does not support ES Module test files
1573+
- [Custom reporters](#third-party-reporters) and [custom interfaces](#interfaces)
1574+
can only be CommonJS files
1575+
- [Required modules](#-require-module-r-module) can only be CommonJS files
1576+
- [Configuration file](#configuring-mocha-nodejs) can only be a CommonJS file (`mocharc.js` or `mocharc.cjs`)
1577+
- When using module-level mocks via libs like `proxyquire`, `rewiremock` or `rewire`, hold off on using ES modules for your test files
1578+
- Node.JS native ESM support does not work with [esm][npm-esm] module
1579+
15411580
## Running Mocha in the Browser
15421581

15431582
Mocha runs in the browser. Every release of Mocha will have new builds of `./mocha.js` and `./mocha.css` for use in the browser.
@@ -1609,17 +1648,17 @@ mocha.setup({
16091648

16101649
### Browser-specific Option(s)
16111650

1612-
Browser Mocha supports many, but not all [cli options](#command-line-usage).
1651+
Browser Mocha supports many, but not all [cli options](#command-line-usage).
16131652
To use a [cli option](#command-line-usage) that contains a "-", please convert the option to camel-case, (eg. `check-leaks` to `checkLeaks`).
16141653

16151654
#### Options that differ slightly from [cli options](#command-line-usage):
16161655

1617-
`reporter` _{string|constructor}_
1656+
`reporter` _{string|constructor}_
16181657
You can pass a reporter's name or a custom reporter's constructor. You can find **recommended** reporters for the browser [here](#reporting). It is possible to use [built-in reporters](#reporters) as well. Their employment in browsers is neither recommended nor supported, open the console to see the test results.
16191658

16201659
#### Options that _only_ function in browser context:
16211660

1622-
`noHighlighting` _{boolean}_
1661+
`noHighlighting` _{boolean}_
16231662
If set to `true`, do not attempt to use syntax highlighting on output test code.
16241663

16251664
### Reporting
@@ -1701,7 +1740,8 @@ tests as shown below:
17011740
17021741
In addition to supporting the deprecated [`mocha.opts`](#mochaopts) run-control format, Mocha now supports configuration files, typical of modern command-line tools, in several formats:
17031742

1704-
- **JavaScript**: Create a `.mocharc.js` in your project's root directory, and export an object (`module.exports = {/* ... */}`) containing your configuration.
1743+
- **JavaScript**: Create a `.mocharc.js` (or `mocharc.cjs` when using [`"type"="module"`](#nodejs-native-esm-support) in your `package.json`)
1744+
in your project's root directory, and export an object (`module.exports = {/* ... */}`) containing your configuration.
17051745
- **YAML**: Create a `.mocharc.yaml` (or `.mocharc.yml`) in your project's root directory.
17061746
- **JSON**: Create a `.mocharc.json` (or `.mocharc.jsonc`) in your project's root directory. Comments — while not valid JSON — are allowed in this file, and will be ignored by Mocha.
17071747
- **package.json**: Create a `mocha` property in your project's `package.json`.

karma.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module.exports = config => {
3535
.ignore('chokidar')
3636
.ignore('fs')
3737
.ignore('glob')
38+
.ignore('./lib/esm-utils.js')
3839
.ignore('path')
3940
.ignore('supports-color')
4041
.on('bundled', (err, content) => {

lib/cli/config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const findUp = require('find-up');
2121
* @private
2222
*/
2323
exports.CONFIG_FILES = [
24+
'.mocharc.cjs',
2425
'.mocharc.js',
2526
'.mocharc.yaml',
2627
'.mocharc.yml',
@@ -75,7 +76,7 @@ exports.loadConfig = filepath => {
7576
try {
7677
if (ext === '.yml' || ext === '.yaml') {
7778
config = parsers.yaml(filepath);
78-
} else if (ext === '.js') {
79+
} else if (ext === '.js' || ext === '.cjs') {
7980
config = parsers.js(filepath);
8081
} else {
8182
config = parsers.json(filepath);

lib/cli/options.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ module.exports.loadPkgRc = loadPkgRc;
265265
* Priority list:
266266
*
267267
* 1. Command-line args
268-
* 2. RC file (`.mocharc.js`, `.mocharc.ya?ml`, `mocharc.json`)
268+
* 2. RC file (`.mocharc.c?js`, `.mocharc.ya?ml`, `mocharc.json`)
269269
* 3. `mocha` prop of `package.json`
270270
* 4. `mocha.opts`
271271
* 5. default configuration (`lib/mocharc.json`)

lib/cli/run-helpers.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ const collectFiles = require('./collect-files');
1515

1616
const cwd = (exports.cwd = process.cwd());
1717

18-
exports.watchRun = watchRun;
19-
2018
/**
2119
* Exits Mocha when tests + code under test has finished execution (default)
2220
* @param {number} code - Exit code; typically # of failures
@@ -92,19 +90,21 @@ exports.handleRequires = (requires = []) => {
9290
};
9391

9492
/**
95-
* Collect test files and run mocha instance.
93+
* Collect and load test files, then run mocha instance.
9694
* @param {Mocha} mocha - Mocha instance
9795
* @param {Options} [opts] - Command line options
9896
* @param {boolean} [opts.exit] - Whether or not to force-exit after tests are complete
9997
* @param {Object} fileCollectParams - Parameters that control test
10098
* file collection. See `lib/cli/collect-files.js`.
101-
* @returns {Runner}
99+
* @returns {Promise<Runner>}
102100
* @private
103101
*/
104-
exports.singleRun = (mocha, {exit}, fileCollectParams) => {
102+
const singleRun = async (mocha, {exit}, fileCollectParams) => {
105103
const files = collectFiles(fileCollectParams);
106104
debug('running tests with files', files);
107105
mocha.files = files;
106+
107+
await mocha.loadFilesAsync();
108108
return mocha.run(exit ? exitMocha : exitMochaLater);
109109
};
110110

@@ -113,8 +113,9 @@ exports.singleRun = (mocha, {exit}, fileCollectParams) => {
113113
* @param {Mocha} mocha - Mocha instance
114114
* @param {Object} opts - Command line options
115115
* @private
116+
* @returns {Promise}
116117
*/
117-
exports.runMocha = (mocha, options) => {
118+
exports.runMocha = async (mocha, options) => {
118119
const {
119120
watch = false,
120121
extension = [],
@@ -140,7 +141,7 @@ exports.runMocha = (mocha, options) => {
140141
if (watch) {
141142
watchRun(mocha, {watchFiles, watchIgnore}, fileCollectParams);
142143
} else {
143-
exports.singleRun(mocha, {exit}, fileCollectParams);
144+
await singleRun(mocha, {exit}, fileCollectParams);
144145
}
145146
};
146147

lib/cli/run.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ exports.builder = yargs =>
8787
},
8888
extension: {
8989
default: defaults.extension,
90-
defaultDescription: 'js',
9190
description: 'File extension(s) to load',
9291
group: GROUPS.FILES,
9392
requiresArg: true,
@@ -299,8 +298,14 @@ exports.builder = yargs =>
299298
.number(types.number)
300299
.alias(aliases);
301300

302-
exports.handler = argv => {
301+
exports.handler = async function(argv) {
303302
debug('post-yargs config', argv);
304303
const mocha = new Mocha(argv);
305-
runMocha(mocha, argv);
304+
305+
try {
306+
await runMocha(mocha, argv);
307+
} catch (err) {
308+
console.error('\n' + (err.stack || `Error: ${err.message || err}`));
309+
process.exit(1);
310+
}
306311
};

lib/esm-utils.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const url = require('url');
2+
const path = require('path');
3+
4+
const requireOrImport = async file => {
5+
file = path.resolve(file);
6+
7+
if (path.extname(file) === '.mjs') {
8+
return import(url.pathToFileURL(file));
9+
}
10+
// This is currently the only known way of figuring out whether a file is CJS or ESM.
11+
// If Node.js or the community establish a better procedure for that, we can fix this code.
12+
// Another option here would be to always use `import()`, as this also supports CJS, but I would be
13+
// wary of using it for _all_ existing test files, till ESM is fully stable.
14+
try {
15+
return require(file);
16+
} catch (err) {
17+
if (err.code === 'ERR_REQUIRE_ESM') {
18+
return import(url.pathToFileURL(file));
19+
} else {
20+
throw err;
21+
}
22+
}
23+
};
24+
25+
exports.loadFilesAsync = async (files, preLoadFunc, postLoadFunc) => {
26+
for (const file of files) {
27+
preLoadFunc(file);
28+
const result = await requireOrImport(file);
29+
postLoadFunc(file, result);
30+
}
31+
};

lib/mocha.js

+55-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var utils = require('./utils');
1414
var mocharc = require('./mocharc.json');
1515
var errors = require('./errors');
1616
var Suite = require('./suite');
17+
var esmUtils = utils.supportsEsModules() ? require('./esm-utils') : undefined;
1718
var createStatsCollector = require('./stats-collector');
1819
var createInvalidReporterError = errors.createInvalidReporterError;
1920
var createInvalidInterfaceError = errors.createInvalidInterfaceError;
@@ -290,16 +291,18 @@ Mocha.prototype.ui = function(ui) {
290291
};
291292

292293
/**
293-
* Loads `files` prior to execution.
294+
* Loads `files` prior to execution. Does not support ES Modules.
294295
*
295296
* @description
296297
* The implementation relies on Node's `require` to execute
297298
* the test interface functions and will be subject to its cache.
299+
* Supports only CommonJS modules. To load ES modules, use Mocha#loadFilesAsync.
298300
*
299301
* @private
300302
* @see {@link Mocha#addFile}
301303
* @see {@link Mocha#run}
302304
* @see {@link Mocha#unloadFiles}
305+
* @see {@link Mocha#loadFilesAsync}
303306
* @param {Function} [fn] - Callback invoked upon completion.
304307
*/
305308
Mocha.prototype.loadFiles = function(fn) {
@@ -314,6 +317,49 @@ Mocha.prototype.loadFiles = function(fn) {
314317
fn && fn();
315318
};
316319

320+
/**
321+
* Loads `files` prior to execution. Supports Node ES Modules.
322+
*
323+
* @description
324+
* The implementation relies on Node's `require` and `import` to execute
325+
* the test interface functions and will be subject to its cache.
326+
* Supports both CJS and ESM modules.
327+
*
328+
* @public
329+
* @see {@link Mocha#addFile}
330+
* @see {@link Mocha#run}
331+
* @see {@link Mocha#unloadFiles}
332+
* @returns {Promise}
333+
* @example
334+
*
335+
* // loads ESM (and CJS) test files asynchronously, then runs root suite
336+
* mocha.loadFilesAsync()
337+
* .then(() => mocha.run(failures => process.exitCode = failures ? 1 : 0))
338+
* .catch(() => process.exitCode = 1);
339+
*/
340+
Mocha.prototype.loadFilesAsync = function() {
341+
var self = this;
342+
var suite = this.suite;
343+
this.loadAsync = true;
344+
345+
if (!esmUtils) {
346+
return new Promise(function(resolve) {
347+
self.loadFiles(resolve);
348+
});
349+
}
350+
351+
return esmUtils.loadFilesAsync(
352+
this.files,
353+
function(file) {
354+
suite.emit(EVENT_FILE_PRE_REQUIRE, global, file, self);
355+
},
356+
function(file, resultModule) {
357+
suite.emit(EVENT_FILE_REQUIRE, resultModule, file, self);
358+
suite.emit(EVENT_FILE_POST_REQUIRE, global, file, self);
359+
}
360+
);
361+
};
362+
317363
/**
318364
* Removes a previously loaded file from Node's `require` cache.
319365
*
@@ -330,8 +376,9 @@ Mocha.unloadFile = function(file) {
330376
* Unloads `files` from Node's `require` cache.
331377
*
332378
* @description
333-
* This allows files to be "freshly" reloaded, providing the ability
379+
* This allows required files to be "freshly" reloaded, providing the ability
334380
* to reuse a Mocha instance programmatically.
381+
* Note: does not clear ESM module files from the cache
335382
*
336383
* <strong>Intended for consumers &mdash; not used internally</strong>
337384
*
@@ -842,10 +889,14 @@ Object.defineProperty(Mocha.prototype, 'version', {
842889
* @see {@link Mocha#unloadFiles}
843890
* @see {@link Runner#run}
844891
* @param {DoneCB} [fn] - Callback invoked when test execution completed.
845-
* @return {Runner} runner instance
892+
* @returns {Runner} runner instance
893+
* @example
894+
*
895+
* // exit with non-zero status if there were test failures
896+
* mocha.run(failures => process.exitCode = failures ? 1 : 0);
846897
*/
847898
Mocha.prototype.run = function(fn) {
848-
if (this.files.length) {
899+
if (this.files.length && !this.loadAsync) {
849900
this.loadFiles();
850901
}
851902
var suite = this.suite;

lib/mocharc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"diff": true,
3-
"extension": ["js"],
3+
"extension": ["js", "cjs", "mjs"],
44
"opts": "./test/mocha.opts",
55
"package": "./package.json",
66
"reporter": "spec",

0 commit comments

Comments
 (0)