Skip to content

Commit

Permalink
add support for Node.JS native ES modules
Browse files Browse the repository at this point in the history
  • Loading branch information
giltayar committed Jan 10, 2020
1 parent 69339a3 commit c316915
Show file tree
Hide file tree
Showing 26 changed files with 470 additions and 110 deletions.
7 changes: 6 additions & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ overrides:
ecmaVersion: 2017
env:
browser: no

- files:
- esm-utils.js
parserOptions:
ecmaVersion: 2018
sourceType: module
parser: babel-eslint
- files:
- test/**/*.{js,mjs}
env:
Expand Down
42 changes: 38 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Mocha is a feature-rich JavaScript test framework running on [Node.js][] and in
- [mocha.opts file support](#-opts-path)
- clickable suite titles to filter test execution
- [node debugger support](#-inspect-inspect-brk-inspect)
- [node native ES modules support](#nodejs-native-esm-support)
- [detects multiple calls to `done()`](#detects-multiple-calls-to-done)
- [use any assertion library you want](#assertions)
- [extensible reporting, bundled with 9+ reporters](#reporters)
Expand Down Expand Up @@ -1539,6 +1540,38 @@ Alias: `HTML`, `html`

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

## Node.JS native ESM support

Mocha supports writing your tests as ES modules (without needing to use the `esm` polyfill module), and not just using CommonJS. For example:

```js
// test.mjs
import {add} from './add.mjs';
import assert from 'assert';

it('should add to numbers from an es module', () => {
assert.equal(add(3, 5), 8);
});
```

To enable this you don't need to do anything special. Write your test file as an ES module. In Node.js
this means either ending the file with a `.mjs` extension, or, if you want to use the regular `.js` extension, by
adding `"type": "module"` to your `package.json`.
More information can be found in the [Node.js documentation](https://nodejs.org/api/esm.html).

> Mocha supports ES modules only from Node.js v12.11.0 and above. Also note that
> to enable this in vesions smaller than 13.2.0, you need to add `--experimental-modules` when running
> Mocha. From version 13.2.0 of Node.js, you can use ES modules without any flags.
### Limitations

- "Watch mode" (i.e. using `--watch` options) does not currently support ES Module test files,
although we intend to support it in the future
- [Custom reporters](#third-party-reporters) and [custom interfaces](#interfaces)
can currently only be CommonJS files, although we intend to support it in the future
- `mocharc.js` can only be a CommonJS file (can also be called `mocharc.cjs`),
although we intend to support ESM in the future

## Running Mocha in the Browser

Mocha runs in the browser. Every release of Mocha will have new builds of `./mocha.js` and `./mocha.css` for use in the browser.
Expand Down Expand Up @@ -1608,17 +1641,17 @@ mocha.setup({

### Browser-specific Option(s)

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

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

`reporter` _{string|constructor}_
`reporter` _{string|constructor}_
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.

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

`noHighlighting` _{boolean}_
`noHighlighting` _{boolean}_
If set to `true`, do not attempt to use syntax highlighting on output test code.

### Reporting
Expand Down Expand Up @@ -1700,7 +1733,8 @@ tests as shown below:
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:

- **JavaScript**: Create a `.mocharc.js` in your project's root directory, and export an object (`module.exports = {/* ... */}`) containing your configuration.
- **JavaScript**: Create a `.mocharc.js` (or `mocharc.cjs` when using [`"type"="module"`](#nodejs-native-esm-support) in your `package.json`)
in your project's root directory, and export an object (`module.exports = {/* ... */}`) containing your configuration.
- **YAML**: Create a `.mocharc.yaml` (or `.mocharc.yml`) in your project's root directory.
- **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.
- **package.json**: Create a `mocha` property in your project's `package.json`.
Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module.exports = config => {
.ignore('chokidar')
.ignore('fs')
.ignore('glob')
.ignore('./lib/esm-utils.js')
.ignore('path')
.ignore('supports-color')
.on('bundled', (err, content) => {
Expand Down
8 changes: 5 additions & 3 deletions lib/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
*/

const debug = require('debug')('mocha:cli:cli');
const symbols = require('log-symbols');
const yargs = require('yargs/yargs');
const path = require('path');
const ansi = require('ansi-colors');
const symbols = require('log-symbols');
const {loadOptions, YARGS_PARSER_CONFIG} = require('./options');
const commands = require('./commands');
const ansi = require('ansi-colors');
const {repository, homepage, version, gitter} = require('../../package.json');

/**
Expand Down Expand Up @@ -48,7 +48,9 @@ exports.main = (argv = process.argv.slice(2)) => {
.fail((msg, err, yargs) => {
debug(err);
yargs.showHelp();
console.error(`\n${symbols.error} ${ansi.red('ERROR:')} ${msg}`);
console.error(
`\n${symbols.error} ${ansi.red('ERROR:')} ${err ? err.message : msg}`
);
process.exit(1);
})
.help('help', 'Show usage information & exit')
Expand Down
3 changes: 2 additions & 1 deletion lib/cli/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const findUp = require('find-up');
* @private
*/
exports.CONFIG_FILES = [
'.mocharc.cjs',
'.mocharc.js',
'.mocharc.yaml',
'.mocharc.yml',
Expand Down Expand Up @@ -75,7 +76,7 @@ exports.loadConfig = filepath => {
try {
if (ext === '.yml' || ext === '.yaml') {
config = parsers.yaml(filepath);
} else if (ext === '.js') {
} else if (ext === '.js' || ext === '.cjs') {
config = parsers.js(filepath);
} else {
config = parsers.json(filepath);
Expand Down
2 changes: 1 addition & 1 deletion lib/cli/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ module.exports.loadPkgRc = loadPkgRc;
* Priority list:
*
* 1. Command-line args
* 2. RC file (`.mocharc.js`, `.mocharc.ya?ml`, `mocharc.json`)
* 2. RC file (`.mocharc.c?js`, `.mocharc.ya?ml`, `mocharc.json`)
* 3. `mocha` prop of `package.json`
* 4. `mocha.opts`
* 5. default configuration (`lib/mocharc.json`)
Expand Down
14 changes: 11 additions & 3 deletions lib/cli/run-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,29 @@ exports.handleRequires = (requires = []) => {
* @param {boolean} [opts.exit] - Whether or not to force-exit after tests are complete
* @param {Object} fileCollectParams - Parameters that control test
* file collection. See `lib/cli/collect-files.js`.
* @returns {Runner}
* @returns {Promise<Runner>}
* @private
*/
exports.singleRun = (mocha, {exit}, fileCollectParams) => {
const files = collectFiles(fileCollectParams);
debug('running tests with files', files);
mocha.files = files;
return mocha.run(exit ? exitMocha : exitMochaLater);

return mocha.runAsync().then(exitCode => {
if (exit) {
exitMocha(exitCode);
} else {
exitMochaLater(exitCode);
}
});
};

/**
* Actually run tests
* @param {Mocha} mocha - Mocha instance
* @param {Object} opts - Command line options
* @private
* @returns {Promise}
*/
exports.runMocha = (mocha, options) => {
const {
Expand Down Expand Up @@ -140,7 +148,7 @@ exports.runMocha = (mocha, options) => {
if (watch) {
watchRun(mocha, {watchFiles, watchIgnore}, fileCollectParams);
} else {
exports.singleRun(mocha, {exit}, fileCollectParams);
return exports.singleRun(mocha, {exit}, fileCollectParams);
}
};

Expand Down
12 changes: 9 additions & 3 deletions lib/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ exports.builder = yargs =>
},
extension: {
default: defaults.extension,
defaultDescription: 'js',
defaultDescription: 'js,cjs,mjs',
description: 'File extension(s) to load',
group: GROUPS.FILES,
requiresArg: true,
Expand Down Expand Up @@ -299,8 +299,14 @@ exports.builder = yargs =>
.number(types.number)
.alias(aliases);

exports.handler = argv => {
exports.handler = async function(argv) {
debug('post-yargs config', argv);
const mocha = new Mocha(argv);
runMocha(mocha, argv);

try {
await runMocha(mocha, argv);
} catch (err) {
console.error(err.stack || `Error: ${err.message || err}`);
process.exit(1);
}
};
35 changes: 35 additions & 0 deletions lib/esm-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// This file is allowed to use async/await because it is not exposed to browsers (see the `eslintrc`),
// and Node supports async/await in all its non-dead version.

const url = require('url');
const path = require('path');

exports.requireOrImport = async file => {
file = path.resolve(file);

if (path.extname(file) === '.mjs') {
return import(url.pathToFileURL(file));
}
// This way of figuring out whether a test file is CJS or ESM 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.
// Another option here would be to always use `import()`, as this also supports CJS, but I would be
// wary of using it for _all_ existing test files, till ESM is fully stable.
try {
return require(file);
} catch (err) {
if (err.code === 'ERR_REQUIRE_ESM') {
return import(url.pathToFileURL(file));
} else {
throw err;
}
}
};

exports.loadFilesAsync = async (files, preLoadFunc, postLoadFunc) => {
for (const file of files) {
preLoadFunc(file);
const result = await exports.requireOrImport(file);
postLoadFunc(file, result);
}
};

0 comments on commit c316915

Please sign in to comment.