Heuristic:Nightwatchjs Nightwatch ESM Module Loading Tips
| Knowledge Sources | |
|---|---|
| Domains | Infrastructure, Configuration |
| Last Updated | 2026-02-12 01:00 GMT |
Overview
Nightwatch automatically handles ESM/CommonJS module loading differences, but users must follow specific patterns when using ES6 `import`/`export` syntax in test files and config.
Description
Nightwatch's module loader (`lib/utils/requireModule.js`) supports both CommonJS `require()` and ES6 `import`/`export` syntax. When a `require()` call fails with `ERR_REQUIRE_ESM` or an ESM syntax error, Nightwatch automatically falls back to dynamic `import()`. However, users need to be aware of the interaction between Node.js module systems, `ts-node`, `.mjs` file extensions, and `package.json` type field settings. Incorrect configuration leads to confusing import errors that are not obviously related to the module system.
Usage
Apply this heuristic when writing test files using ES6 `import`/`export` syntax, when using `.mjs` file extensions, when integrating TypeScript test files, or when encountering `Cannot use import statement outside a module` or `ERR_REQUIRE_ESM` errors.
The Insight (Rule of Thumb)
- Action: Choose one of two approaches for ES module support:
- Option A: Add `"type": "module"` to `package.json` and use `.js` extension.
- Option B: Keep CommonJS default and use `.mjs` extension for ES module files.
- Value: Nightwatch handles both paths automatically via its module loader.
- Trade-off: Mixing CommonJS and ESM in the same project creates complexity. The `.mjs` approach avoids changing the project-wide module system.
- TypeScript: Install `ts-node` and create `tsconfig.nightwatch.json` or `nightwatch/tsconfig.json`. Nightwatch auto-configures `ts-node` with `transpileOnly: true` for fast compilation.
- Caution (`.mjs` + ts-node): On Node.js >= 20, `.mjs` files with `ts-node` active may throw `ERR_REQUIRE_ESM` or ESM syntax errors depending on whether `v8-compile-cache-lib` is in use. Nightwatch handles both cases.
Reasoning
Node.js has a complex dual module system where `.js` files default to CommonJS unless `"type": "module"` is set in package.json. The behavior of `require()` on ESM files changed across Node.js versions:
- Node.js < 20: `require()` on `.mjs` throws `ERR_REQUIRE_ESM`
- Node.js >= 20: `require()` on `.mjs` passes through without error normally, but with `ts-node` active may throw either `ERR_REQUIRE_ESM` or an ESM syntax error
Nightwatch's module loader detects all these cases and falls back to dynamic `import()` with `pathToFileURL()` for correct URL-based resolution.
Code evidence from `lib/utils/requireModule.js:18-43`:
module.exports = function (fullpath) {
let exported;
try {
exported = require(fullpath);
} catch (err) {
const isEsmSyntaxError = err.message === 'Cannot use import statement outside a module' ||
err.message.includes('Unexpected token \'export\'');
const isMjsFile = fullpath.endsWith('.mjs');
// calling require() on a .mjs file on Node.js < 20 throws ERR_REQUIRE_ESM.
// calling require() on a .mjs file on Node.js >= 20 passes through without
// any error, but if ts-node is activated, it throws ERR_REQUIRE_ESM in normal
// cases and isEsmSyntaxError if v8-compile-cache-lib is used.
if (err.code === 'ERR_REQUIRE_ESM' || (isMjsFile && isEsmSyntaxError)) {
const { pathToFileURL } = require('node:url');
return import(pathToFileURL(fullpath).href).then(mergeDefaultAndNamedExports);
}
if (isEsmSyntaxError) {
err.detailedErr = err.message;
err.help = ['Using ES6 import/export syntax? - make sure to specify ' +
'"type=module" in your package.json or use .mjs extension.'];
err.link = 'https://nodejs.org/api/esm.html';
}
throw err;
}
}
ESM default export handling from `lib/utils/requireModule.js:1-10`:
const mergeDefaultAndNamedExports = (module) => {
const _default = module.default || {};
return Object.keys(module).reduce((prev, val) => {
if (val !== 'default') {
prev[val] = module[val];
}
return prev;
}, _default);
}