mocha icon indicating copy to clipboard operation
mocha copied to clipboard

🚀 Feature: Support ESM (ECMAScript Modules) in watch mode

Open Swivelgames opened this issue 3 years ago • 16 comments

Prerequisites

  • [x] Checked that your issue hasn't already been filed by cross-referencing issues with the faq label
  • [x] Checked next-gen ES issues and syntax problems by using the same environment and/or transpiler configuration without Mocha to ensure it isn't just a feature that actually isn't supported in the environment in question or a bug in your code.
  • [x] 'Smoke tested' the code to be tested by running it outside the real test suite to get a better sense of whether the problem is in the code under test, your usage of Mocha, or Mocha itself
  • [x] Ensured that there is no discrepancy between the locally and globally installed versions of Mocha. You can find them with: node node_modules/.bin/mocha --version(Local) and mocha --version(Global). We recommend that you not install Mocha globally.

Description

When running --watch within a package that has "type": "module" set in its package.json, Mocha fails to properly import, instead attempting to require() the file.

$ npm run test-mocha:watch

> @ test-mocha:watch /home/jdalrymple/src/bugs/mochajs
> mocha --require @babel/register $(find ./src -type f -name '*.test.js') --watch

internal/modules/cjs/loader.js:1217
      throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
      ^

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /.../src/index.test.js
require() of ES modules is not supported.
require() of /.../src/index.test.js from /.../node_modules/mocha/lib/mocha.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename index.test.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /.../package.json.

    at Module._extensions..js (internal/modules/cjs/loader.js:1217:13)
    at Object.newLoader [as .js] (/home/jdalrymple/src/bugs/mochajs/node_modules/pirates/lib/index.js:104:7)
    at Module.load (internal/modules/cjs/loader.js:1050:32)
    at Function.Module._load (internal/modules/cjs/loader.js:938:14)
    at Module.require (internal/modules/cjs/loader.js:1090:19)
    at require (internal/modules/cjs/helpers.js:75:18)
    at /home/jdalrymple/src/bugs/mochajs/node_modules/mocha/lib/mocha.js:384:36
    at Array.forEach (<anonymous>)
    at Mocha.loadFiles (/home/jdalrymple/src/bugs/mochajs/node_modules/mocha/lib/mocha.js:381:14)
    at Mocha.run (/home/jdalrymple/src/bugs/mochajs/node_modules/mocha/lib/mocha.js:954:10)
    at Object.run (/home/jdalrymple/src/bugs/mochajs/node_modules/mocha/lib/cli/watch-run.js:223:20)
    at FSWatcher.<anonymous> (/home/jdalrymple/src/bugs/mochajs/node_modules/mocha/lib/cli/watch-run.js:169:14)
    at FSWatcher.emit (events.js:314:20)
    at /home/jdalrymple/src/bugs/mochajs/node_modules/mocha/node_modules/chokidar/index.js:364:35
    at processTicksAndRejections (internal/process/task_queues.js:75:11) {
  code: 'ERR_REQUIRE_ESM'
}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! @ test-mocha:watch: `mocha --require @babel/register $(find ./src -type f -name '*.test.js') --watch`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the @ test-mocha:watch script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/jdalrymple/.npm/_logs/2020-07-14T00_20_36_063Z-debug.log

Steps to Reproduce

For convenience, the following respository was created that adequately reproduces the issue:

  1. Ensure your node version matches the one listed below
  2. Clone the repo: https://github.com/Swivelgames/issues-mochajs-4374
  3. Install its dependencies: npm i
  4. Test the repo: npm run test-mocha
  5. Start a watch: npm run test-mocha:watch

Expected Behavior:

  • Mocha would begin watching the files

Actual Behavior:

  • Error is thrown when attempting to watch files

Versions

Package Versions
  • node: v14.5.0
  • mocha: 8.0.1
  • babel: v7.10.4
Environment
  • OS: Linux, Arch, x86_64
  • Shell: tmux + zsh

Additional Information

It is my understanding that the following scenarios are applicable:

import should be used under the following conditions:

  • If the target file's associated package.json file contains "type": "module" and its file extension is .js
  • If the target file's extension is .mjs, regardless of what is inside its associated package.json file

require() should be used under the following conditions:

  • If the target file's associated package.json file does NOT contain "type": "module" and its file extension is .js
  • If the target file's extension is .cjs, regardless of what is inside its associated package.json file

Conclusion

Presently, it appears as though --watch is assuming the target file is a CommonJS module when its extension is .js, and failing to yield to its package.json file's "type": "module".

Considerations

It's worth considering the fact that the package.json file associated with the file being imported may be different than the package.json in the directory that mocha is being called, even in the event that the target file's path does not include node_modules.

Swivelgames avatar Jul 14 '20 00:07 Swivelgames

Please see our docs: current limitations.

We do not (yet?) support ESModules in watch mode. Node's ESM implementation is using its separate cache (not require.cache), which currently can't be cleared before re-running the tests in watch mode.

juergba avatar Jul 14 '20 08:07 juergba

@Swivelgames Have you tried --parallel combined with --watch? It might work, since we dump the workers after each run, but I don't think we are testing it anywhere yet.

boneskull avatar Jul 30 '20 19:07 boneskull

@boneskull I'm not @Swivelgames but your suggestion of adding --parallel works for me. Thanks!

hovissimo avatar Dec 29 '20 19:12 hovissimo

To support ESM in watch mode, I think you will need to implement your own ESM loader hook. It can append query params to each imported module, effectively making them unique. By appending a different query param the second time, you can ensure a fresh copy of the module is imported. This may, however, be a memory leak, if node has no way to clear the previously-imported copies of those modules.

It is probably worth sharing this use-case with the node maintainers so they can add the necessary API surface to node.

https://github.com/nodejs/modules/issues

EDIT: I see this was already raised with the node team: https://github.com/nodejs/node/issues/49442

cspotcode avatar Mar 07 '21 18:03 cspotcode

Well I've "fixed" my issue by switching to jest... Very happy about the change so far.

djfm avatar Apr 28 '21 09:04 djfm

@djfm that's funny, as I've switched from Jest to Mocha using testdouble.js

I had a nightmare of a time getting Jest and ESM mocking to work: https://github.com/facebook/jest/issues/10025#issuecomment-921942214

jasonrberk avatar Sep 17 '21 21:09 jasonrberk

Based on the comment of @cspotcode and the experiment by @vsnthdev in https://github.com/nodejs/node/issues/49442, I've gone ahead and implemented this as a proof of concept for mocha --watch, and it does indeed seem to work:

https://github.com/lehni/mocha/commit/99d5a14d3a1b11f81dbb08e1895d43061e7b0541

To run mocha in this mode, you simply pass the --esm option along with --watch. Without --esm, nothing changes.

This may not be ready for primetime yet, but it works well for my use-case and it shows a viable path forward for ESM.

Perhaps it can be wrapped up for release?

lehni avatar Mar 20 '22 01:03 lehni

I've just made my branch work for NPM installs, so if you want to test the esm watch mode in your project, you can replace the mocha dependency:

yarn add -D mocha@https://github.com/lehni/mocha#99d5a14d3a1b11f81dbb08e1895d43061e7b0541
# Or:
npm install -D https://github.com/lehni/mocha#99d5a14d3a1b11f81dbb08e1895d43061e7b0541

And after that, simply run your test command with --watch --esm:

yarn test --watch --esm

Please let me know how this is working for you!

lehni avatar Mar 20 '22 09:03 lehni

I've added auto-detection of esm watch mode now, so the --esm argument is not required anymore:

https://github.com/lehni/mocha/commit/64c5fcc79ce4f2232b0450db2a7f5e87e0f538a7

lehni avatar Mar 21 '22 06:03 lehni

Just fyi as of May 2022 this is still an open issue and quite a significant impediment.

As an interim workaround I ended up with nodemon in front of Mocha, which works flawlessly and doesn't have a memleak issue mentioned in the PR 👆.

As a reference, here's the nodemon.test.json that works for me (I'm using TypeScript, hence out dir):

{
    "exec": "npx mocha --node-option=experimental-network-imports",
    "watch": [
        "./out/**/*"
    ],
    "delay": 100,
    "env": {
        "NODE_ENV": "test"
    }
}

inca avatar May 30 '22 08:05 inca

Possible workaround, using fswatch:

fswatch lib/ test/ --event Updated | xargs -I _ mocha test/file.js -b

Daghall avatar Jul 18 '22 11:07 Daghall

Having to use

    "test-watch":"nodemon --exec mocha spec",

in package.json scripts is a workaround, but it'd be better if it Just Worked

tomchiverton avatar Jul 28 '22 13:07 tomchiverton

Another simple workaround is to use entr to watch your source files:

{
  "scripts": {
    "test:watch": "ls lib/*.js | entr -r mocha"
  }
}

jackdbd avatar Dec 28 '22 10:12 jackdbd

I've tried @boneskull solution and combined the --parallel with --watch flag. It worked for me. Here is the part of my package.json scripts section "test:watch": "mocha --watch --parallel --slow 100"

kantbtrue avatar Dec 26 '23 02:12 kantbtrue

I found one pretty simple solution for typescript and esm. I use tsx in mocha config file. Post here. Now mocha works with simple script "test:watch": "mocha --watch"

gvozdenkov avatar Jan 17 '24 06:01 gvozdenkov