Jump to content

Connect Leeroopedia MCP: Equip your AI agents to search best practices, build plans, verify code, diagnose failures, and look up hyperparameter defaults.

Heuristic:Nightwatchjs Nightwatch ESM Module Loading Tips

From Leeroopedia
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);
}

Related Pages

Page Connections

Double-click a node to navigate. Hold to expand connections.
Principle
Implementation
Heuristic
Environment