Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 44c3792

Browse files
authored
fix: fall back to require(esm) when CJS parses .js with ESM syntax (#16078)
1 parent fe708d8 commit 44c3792

7 files changed

Lines changed: 100 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- `[jest-circus, jest-jasmine2]` Include `Error.cause` in JSON `failureMessages` output ([#15949](https://github.com/jestjs/jest/issues/15949))
1919
- `[jest-runtime]` Improve CJS-from-ESM interop: `__esModule`/Babel default unwrap, broader named-export coverage, and shared CJS singleton across importers ([#16050](https://github.com/jestjs/jest/pull/16050))
2020
- `[jest-runtime]` Load `.js` files with ESM syntax but no `"type":"module"` marker as native ESM ([#16050](https://github.com/jestjs/jest/pull/16050))
21+
- `[jest-runtime]` Extend the `.js`-with-ESM-syntax fallback to `require()` on Node v24.9+ - falls back to `require(esm)` when the CJS parser rejects ESM syntax ([#16078](https://github.com/jestjs/jest/pull/16078))
2122
- `[jest-runtime]` Fix deadlocks and double-evaluation in concurrent ESM and wasm imports ([#16050](https://github.com/jestjs/jest/pull/16050))
2223
- `[jest-runtime]` Fix error when `require()` is called after the Jest environment has been torn down ([#15951](https://github.com/jestjs/jest/pull/15951))
2324

e2e/__tests__/nativeEsmRequireModule.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ onNodeVersions('>=24.9.0', () => {
2020
expect(stderr).toContain('4 passed');
2121
expect(exitCode).toBe(0);
2222
});
23+
24+
test('require()/import() fall back to ESM for .js with ESM syntax and no "type":"module" marker', () => {
25+
const {exitCode, stderr} = runJest(
26+
DIR,
27+
['__tests__/syntax-fallback.test.js'],
28+
{nodeOptions: '--experimental-vm-modules --no-warnings'},
29+
);
30+
31+
expect(stderr).toContain('2 passed');
32+
expect(exitCode).toBe(0);
33+
});
2334
});
2435

2536
onNodeVersions('<24.9.0', () => {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
test('require() of .js with ESM syntax falls back to require(esm)', () => {
11+
const ns = require('../fake-esm-js.js');
12+
expect(ns.fakeEsmValue).toBe(123);
13+
});
14+
15+
test('await import() of .js with ESM syntax falls back to ESM', async () => {
16+
const ns = await import('../fake-esm-js.js');
17+
expect(ns.fakeEsmValue).toBe(123);
18+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
export const fakeEsmValue = 123;

packages/jest-runtime/src/__tests__/runtime_esm_cjs_interop.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@
1717
import * as path from 'path';
1818

1919
// SyntheticModule is only available when --experimental-vm-modules is active.
20-
const {SyntheticModule} = require('vm') as typeof import('vm');
20+
const {SourceTextModule, SyntheticModule} =
21+
require('vm') as typeof import('vm');
2122
const vmAvailable = typeof SyntheticModule === 'function';
2223
const itVm = vmAvailable ? it : it.skip;
24+
// `require(esm)` needs the v24.9+ sync ESM core.
25+
const syncCoreAvailable =
26+
// @ts-expect-error - hasAsyncGraph is in Node v24.9+, not yet typed
27+
typeof SourceTextModule?.prototype.hasAsyncGraph === 'function';
28+
const itSyncOnly = syncCoreAvailable ? it : it.skip;
2329

2430
const ROOT_DIR = path.join(__dirname, 'test_esm_interop_root');
2531
const FROM = path.join(ROOT_DIR, 'test.js');
@@ -125,4 +131,26 @@ describe('Runtime loadCjsAsEsm SyntaxError fallback', () => {
125131
expect(m.namespace.fakeEsmValue).toBe(123);
126132
},
127133
);
134+
135+
itSyncOnly(
136+
'require()s a .js file with ESM syntax that has no "type":"module" marker',
137+
async () => {
138+
const runtime = await createRuntime(__filename, {rootDir: ROOT_DIR});
139+
const ns = runtime.requireModule(FROM, './fake-esm-js.js');
140+
expect(ns.fakeEsmValue).toBe(123);
141+
},
142+
);
143+
144+
// Runtime SyntaxError from inside the CJS body (vs. a parse-time one)
145+
// must not trigger the ESM fallback — surfacing the original error
146+
// unchanged is the right behavior.
147+
itVm(
148+
'does not retry as ESM when the CJS body throws a runtime SyntaxError',
149+
async () => {
150+
const runtime = await createRuntime(__filename, {rootDir: ROOT_DIR});
151+
expect(() =>
152+
runtime.requireModule(FROM, './throws-syntaxerror.cjs'),
153+
).toThrow('user-thrown SyntaxError from CJS body');
154+
},
155+
);
128156
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
throw new SyntaxError('user-thrown SyntaxError from CJS body');

packages/jest-runtime/src/index.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ import {
6464
const esmIsAvailable = typeof SourceTextModule === 'function';
6565
const supportsDynamicImport = esmIsAvailable;
6666

67+
// Marker used by the CJS-as-ESM SyntaxError fallback paths to distinguish
68+
// parse-time errors (where retrying as ESM is correct) from runtime errors
69+
// a user might throw from inside a module body.
70+
const CJS_PARSE_ERROR = Symbol('jest-runtime CJS parse error');
71+
const isCjsParseError = (error: unknown): error is Error =>
72+
isError(error) &&
73+
(error as unknown as Record<symbol, unknown>)[CJS_PARSE_ERROR] === true;
74+
6775
const dataURIRegex =
6876
/^data:(?<mime>text\/javascript|application\/json|application\/wasm)(?:;(?<encoding>charset=utf-8|base64))?,(?<code>.*)$/;
6977

@@ -1663,20 +1671,16 @@ export default class Runtime {
16631671
try {
16641672
synthetic = this._buildCjsAsEsmSyntheticModule(from, modulePath, context);
16651673
} catch (error) {
1666-
// Use .name check instead of instanceof: the error comes from
1667-
// vm.compileFunction with a sandbox parsingContext, so its prototype
1668-
// may cross context boundaries.
1669-
if ((error as Error).name !== 'SyntaxError') {
1674+
if (!isCjsParseError(error)) {
16701675
throw error;
16711676
}
16721677
// The file may contain ESM syntax with no ESM marker (.mjs /
16731678
// "type":"module") - try loading as native ESM. If the ESM parser also
16741679
// rejects it, the original CJS error was the genuine one; surface it
16751680
// instead of the (less useful) ESM diagnostic.
1676-
const cjsSyntaxError = error as Error;
16771681
return this.loadEsmModule(modulePath).catch(esmError => {
16781682
throw isError(esmError) && esmError.name === 'SyntaxError'
1679-
? cjsSyntaxError
1683+
? error
16801684
: esmError;
16811685
});
16821686
}
@@ -1877,6 +1881,15 @@ export default class Runtime {
18771881
);
18781882
} catch (error) {
18791883
moduleRegistry.delete(modulePath);
1884+
// Mirror of `loadCjsAsEsm`'s SyntaxError fallback for `require()`.
1885+
if (supportsSyncEvaluate && isCjsParseError(error)) {
1886+
try {
1887+
return this._requireEsmModule<T>(modulePath);
1888+
} catch (esmError) {
1889+
if (isError(esmError) && esmError.name === 'SyntaxError') throw error;
1890+
throw esmError;
1891+
}
1892+
}
18801893
throw error;
18811894
}
18821895

@@ -2629,6 +2642,12 @@ export default class Runtime {
26292642
},
26302643
) as ModuleWrapper;
26312644
} catch (error: any) {
2645+
// Tag so callers can distinguish parse-time SyntaxErrors (where the
2646+
// ESM-syntax-in-CJS fallback applies) from runtime SyntaxErrors a user
2647+
// might throw from inside a CJS module body.
2648+
if (isError(error)) {
2649+
(error as unknown as Record<symbol, unknown>)[CJS_PARSE_ERROR] = true;
2650+
}
26322651
throw handlePotentialSyntaxError(error);
26332652
}
26342653
}

0 commit comments

Comments
 (0)