-
Notifications
You must be signed in to change notification settings - Fork 177
Description
Runtime
node.js
Runtime version
24.13.1 (any version >= 22.12.0 with unflagged require(esm))
Module version
26.0.0
Last module version without issue
N/A — the bug has existed since addTsEsmHook was introduced, but only manifests on Node 22.12+ where require(esm) is unflagged.
Used with
standalone (reproducible with minimal setup)
Any other relevant information
Affects projects using lab --typescript whose package.json has "type": "module" and that depend on an ESM package which itself has CJS sub-dependencies. The bug triggers when Node resolves a CJS sub-dependency of an ESM module during loadESMFromCJS, calling Module._resolveFilename with parent set to undefined.
What are you trying to achieve or the steps to reproduce?
Run TypeScript tests with lab --typescript in an ESM project ("type": "module") that depends on an ESM package which has its own CJS dependencies.
Trigger conditions
All four must be true:
- The project has
"type": "module"in itspackage.json(soaddTsEsmHookis activated). - Lab is invoked with
--typescript(ortypescript: truein.labrc). - Test code (transitively) imports from an ESM package (a dependency with
"type": "module"). - That ESM dependency itself imports a CJS sub-dependency whose specifier ends in
.js.
Condition 4 is the non-obvious one: a leaf ESM package with zero dependencies does not trigger the bug. The crash only occurs when Node resolves a .js request during the ESM→CJS boundary transition inside loadESMFromCJS, at which point Module._resolveFilename is called with parent === undefined.
Minimal reproduction
Create a directory with the following structure:
lab-esm-repro/
├── package.json
├── tsconfig.json
├── src/
│ └── math.ts
├── test/
│ └── math.ts
└── node_modules/
├── esm-dep/
│ ├── package.json
│ └── index.js
└── cjs-helper/
├── package.json
└── lib/
└── utils.js
package.json
{
"name": "lab-esm-repro",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "lab test --typescript"
},
"devDependencies": {
"@hapi/lab": "^26.0.0",
"@hapi/code": "^9.0.3",
"typescript": "^5.6.0"
}
}tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"outDir": "dist",
"rootDir": ".",
"strict": true
}
}src/math.ts
import { add } from 'esm-dep';
export function addOne(n: number): number {
return add(n, 1);
}test/math.ts
import * as Lab from '@hapi/lab';
import { expect } from '@hapi/code';
import { addOne } from '../src/math.js';
export const lab = Lab.script();
const { describe, it } = lab;
describe('addOne', () => {
it('adds 1 to a number', () => {
expect(addOne(4)).to.equal(5);
});
});node_modules/esm-dep/package.json
{
"name": "esm-dep",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./index.js"
},
"dependencies": {
"cjs-helper": "1.0.0"
}
}node_modules/esm-dep/index.js — ESM module that imports a CJS sub-dependency
import { double } from 'cjs-helper/lib/utils.js';
export function add(a, b) {
return double(a) - a + b;
}node_modules/cjs-helper/package.json — plain CJS package
{
"name": "cjs-helper",
"version": "1.0.0"
}node_modules/cjs-helper/lib/utils.js
'use strict';
exports.double = function (n) {
return n * 2;
};Then run:
npm install
npx lab test --typescriptWhy the CJS sub-dependency matters
Lab's addTsEsmHook patches Module._resolveFilename globally. When lab loads a .ts test file via require.extensions['.ts'] → Module._compile, the compiled code require()s esm-dep. Because esm-dep is ESM, Node delegates to loadESMFromCJS → ESM translators. Inside the ESM module graph, when esm-dep/index.js imports cjs-helper/lib/utils.js, Node calls Module._resolveFilename to resolve the CJS module — but in this ESM-to-CJS transition context, parent is undefined. The patched hook does not guard against this and crashes.
A leaf ESM package with no imports does not trigger this path because Module._resolveFilename is never called within the ESM graph for its own dependencies.
What was the result you got?
TypeError: Cannot read properties of undefined (reading 'filename')
at Module._resolveFilename (/path/to/node_modules/@hapi/lab/lib/cli.js:92:48)
at defaultResolveImpl (node:internal/modules/cjs/loader:1059:19)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1064:22)
at Module._load (node:internal/modules/cjs/loader:1234:25)
at loadCJSModuleWithModuleLoad (node:internal/modules/esm/translators:339:3)
at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:242:7)
at ModuleJobSync.runSync (node:internal/modules/esm/module_job:534:37)
at ModuleLoader.importSyncForRequire (node:internal/modules/esm/loader:388:47)
at loadESMFromCJS (node:internal/modules/cjs/loader:1621:24)
at Module._compile (node:internal/modules/cjs/loader:1786:5)
at Object.require.extensions.<computed> [as .js] (.../lab/lib/modules/transform.js:41:28)
Lab crashes when Node resolves the CJS sub-dependency of the ESM package.
What result did you expect?
The test should run and pass:
addOne
✔ 1) adds 1 to a number (1 ms)
1 tests complete