Description of the Change
This PR adds support for Node.js's native ESM (ES modules) support.
Specifically, up till now, Mocha could only load test files that were CommonJS,
and this PR enables it to load test files that are ES modules,
and thus enable the test files to import stuff from other ES modules
(not to mention other goodies coming up the line, like enabling top-level await, which is an ESM only JS feature).
Note that this support is for Node.js only, and has no connection to browser ESM support.
Also, prior to Node v12, there was no or quirky support for ESM, so this support is only for Node v12 and above.
Alternate Designs
This is not a trivial change. The main problem is that loading an ESM is an async operation, and yet
the call path to Mocha.prototype.loadFiles
is a sync one. Changing the call path would necessitate a
semver-major change, which I'm not sure I have the ability to push, so I decided on a design
that keeps the signatures of all the functions as they are, unless there is an ESM test file to load, by which
time they morph to async functions. So their return type in the signature looks
something like Runnner|Promise<Runner>
, where Runner
is for cases where there are no ESM test files,
and Promise<Runner>
is for the case there are
This is not a good solution, but it enables us to push this change without it being semver-major, and once
there is a decision to change to semver-major, the signature change and code change that does this is trivial.
If y'all decide that this PR can change the major version of Mocha, I can fix it so that the signatures
change and the weirdness goes away.
Another not insignificant change was the error path. In some cases, errors found by Mocha were synchronous, i.e.
they occur during the yargs.parse
phase, which throws the error. But, because the call path to loadFiles
could
be asynchronous, I changed the command handler (in lib/cli/run.js
) to be async (i.e to return a Promise
).
This means that errors (such as bad tap versions) are no more synchronous, and will always arrive at the
fail
yargs handler (in lib/cli.js
). To make them look like the old errors, I changed the code
there. No tests fail now, so it seems OK, but please check this in the review.
Why should this be in core?
I'm assuming this is obvious: it has to change Mocha.prototype.loadFiles
to be async. There is the esm
module that simulates ESM in userland, but native support has to be async. And native support is crucial if the developer's code is running using Node's native ESM support.
Benefits
Developers starting to write Node.js code in ES modules will benefit by being able to write their tests using
ESM too, and not have to dynamically import their ES modules using dynamic import (aka await import
), which is a hassle and a kludge.
Possible Drawbacks
This may impact code using Mocha's API, as we are changing the signature (even if not in the short run,
definitely in the long run). Also, error handling has subtly changed, and this may impact error handling reporting
in ways I cannot know.
Applicable issues
This is currently an enhancement (minor release), that should be turned in the future into a semver-major,
as described in the "Alternate Designs" section.
Limitations
- "Watch" mode does not currently work with ESM, as there is currently no way to remove an ESM module from Node.js' module cache. We would have to either wait for it to be implemented in Node (slim chance, IMHO), or refactor watch mode to use subprocesses every time a change is detected.
- Module-level mocks via libs like proxyquire or rewiremock or rewire cannot work. If you are using these libs or similar, hold off on using ESM for your test files.. This is not inherently a limitation in Mocha, but rather a limitation in Node's ESM support, which will probably be rectified sometime in Node v13.
- ESM is not supported for custom reporters, nor is it supported for custom interfaces.
feature semver-minor node.js async