mirror of
https://github.com/hanzoai/jest.git
synced 2026-09-24 16:59:57 +00:00
fix: fall back to require(esm) when CJS parses .js with ESM syntax (#16078)
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
- `[jest-circus, jest-jasmine2]` Include `Error.cause` in JSON `failureMessages` output ([#15949](https://github.com/jestjs/jest/issues/15949))
|
||||
- `[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))
|
||||
- `[jest-runtime]` Load `.js` files with ESM syntax but no `"type":"module"` marker as native ESM ([#16050](https://github.com/jestjs/jest/pull/16050))
|
||||
- `[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))
|
||||
- `[jest-runtime]` Fix deadlocks and double-evaluation in concurrent ESM and wasm imports ([#16050](https://github.com/jestjs/jest/pull/16050))
|
||||
- `[jest-runtime]` Fix error when `require()` is called after the Jest environment has been torn down ([#15951](https://github.com/jestjs/jest/pull/15951))
|
||||
|
||||
|
||||
@@ -20,6 +20,17 @@ onNodeVersions('>=24.9.0', () => {
|
||||
expect(stderr).toContain('4 passed');
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('require()/import() fall back to ESM for .js with ESM syntax and no "type":"module" marker', () => {
|
||||
const {exitCode, stderr} = runJest(
|
||||
DIR,
|
||||
['__tests__/syntax-fallback.test.js'],
|
||||
{nodeOptions: '--experimental-vm-modules --no-warnings'},
|
||||
);
|
||||
|
||||
expect(stderr).toContain('2 passed');
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
onNodeVersions('<24.9.0', () => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
test('require() of .js with ESM syntax falls back to require(esm)', () => {
|
||||
const ns = require('../fake-esm-js.js');
|
||||
expect(ns.fakeEsmValue).toBe(123);
|
||||
});
|
||||
|
||||
test('await import() of .js with ESM syntax falls back to ESM', async () => {
|
||||
const ns = await import('../fake-esm-js.js');
|
||||
expect(ns.fakeEsmValue).toBe(123);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export const fakeEsmValue = 123;
|
||||
@@ -17,9 +17,15 @@
|
||||
import * as path from 'path';
|
||||
|
||||
// SyntheticModule is only available when --experimental-vm-modules is active.
|
||||
const {SyntheticModule} = require('vm') as typeof import('vm');
|
||||
const {SourceTextModule, SyntheticModule} =
|
||||
require('vm') as typeof import('vm');
|
||||
const vmAvailable = typeof SyntheticModule === 'function';
|
||||
const itVm = vmAvailable ? it : it.skip;
|
||||
// `require(esm)` needs the v24.9+ sync ESM core.
|
||||
const syncCoreAvailable =
|
||||
// @ts-expect-error - hasAsyncGraph is in Node v24.9+, not yet typed
|
||||
typeof SourceTextModule?.prototype.hasAsyncGraph === 'function';
|
||||
const itSyncOnly = syncCoreAvailable ? it : it.skip;
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, 'test_esm_interop_root');
|
||||
const FROM = path.join(ROOT_DIR, 'test.js');
|
||||
@@ -125,4 +131,26 @@ describe('Runtime loadCjsAsEsm SyntaxError fallback', () => {
|
||||
expect(m.namespace.fakeEsmValue).toBe(123);
|
||||
},
|
||||
);
|
||||
|
||||
itSyncOnly(
|
||||
'require()s a .js file with ESM syntax that has no "type":"module" marker',
|
||||
async () => {
|
||||
const runtime = await createRuntime(__filename, {rootDir: ROOT_DIR});
|
||||
const ns = runtime.requireModule(FROM, './fake-esm-js.js');
|
||||
expect(ns.fakeEsmValue).toBe(123);
|
||||
},
|
||||
);
|
||||
|
||||
// Runtime SyntaxError from inside the CJS body (vs. a parse-time one)
|
||||
// must not trigger the ESM fallback — surfacing the original error
|
||||
// unchanged is the right behavior.
|
||||
itVm(
|
||||
'does not retry as ESM when the CJS body throws a runtime SyntaxError',
|
||||
async () => {
|
||||
const runtime = await createRuntime(__filename, {rootDir: ROOT_DIR});
|
||||
expect(() =>
|
||||
runtime.requireModule(FROM, './throws-syntaxerror.cjs'),
|
||||
).toThrow('user-thrown SyntaxError from CJS body');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
throw new SyntaxError('user-thrown SyntaxError from CJS body');
|
||||
@@ -64,6 +64,14 @@ import {
|
||||
const esmIsAvailable = typeof SourceTextModule === 'function';
|
||||
const supportsDynamicImport = esmIsAvailable;
|
||||
|
||||
// Marker used by the CJS-as-ESM SyntaxError fallback paths to distinguish
|
||||
// parse-time errors (where retrying as ESM is correct) from runtime errors
|
||||
// a user might throw from inside a module body.
|
||||
const CJS_PARSE_ERROR = Symbol('jest-runtime CJS parse error');
|
||||
const isCjsParseError = (error: unknown): error is Error =>
|
||||
isError(error) &&
|
||||
(error as unknown as Record<symbol, unknown>)[CJS_PARSE_ERROR] === true;
|
||||
|
||||
const dataURIRegex =
|
||||
/^data:(?<mime>text\/javascript|application\/json|application\/wasm)(?:;(?<encoding>charset=utf-8|base64))?,(?<code>.*)$/;
|
||||
|
||||
@@ -1663,20 +1671,16 @@ export default class Runtime {
|
||||
try {
|
||||
synthetic = this._buildCjsAsEsmSyntheticModule(from, modulePath, context);
|
||||
} catch (error) {
|
||||
// Use .name check instead of instanceof: the error comes from
|
||||
// vm.compileFunction with a sandbox parsingContext, so its prototype
|
||||
// may cross context boundaries.
|
||||
if ((error as Error).name !== 'SyntaxError') {
|
||||
if (!isCjsParseError(error)) {
|
||||
throw error;
|
||||
}
|
||||
// The file may contain ESM syntax with no ESM marker (.mjs /
|
||||
// "type":"module") - try loading as native ESM. If the ESM parser also
|
||||
// rejects it, the original CJS error was the genuine one; surface it
|
||||
// instead of the (less useful) ESM diagnostic.
|
||||
const cjsSyntaxError = error as Error;
|
||||
return this.loadEsmModule(modulePath).catch(esmError => {
|
||||
throw isError(esmError) && esmError.name === 'SyntaxError'
|
||||
? cjsSyntaxError
|
||||
? error
|
||||
: esmError;
|
||||
});
|
||||
}
|
||||
@@ -1877,6 +1881,15 @@ export default class Runtime {
|
||||
);
|
||||
} catch (error) {
|
||||
moduleRegistry.delete(modulePath);
|
||||
// Mirror of `loadCjsAsEsm`'s SyntaxError fallback for `require()`.
|
||||
if (supportsSyncEvaluate && isCjsParseError(error)) {
|
||||
try {
|
||||
return this._requireEsmModule<T>(modulePath);
|
||||
} catch (esmError) {
|
||||
if (isError(esmError) && esmError.name === 'SyntaxError') throw error;
|
||||
throw esmError;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -2629,6 +2642,12 @@ export default class Runtime {
|
||||
},
|
||||
) as ModuleWrapper;
|
||||
} catch (error: any) {
|
||||
// Tag so callers can distinguish parse-time SyntaxErrors (where the
|
||||
// ESM-syntax-in-CJS fallback applies) from runtime SyntaxErrors a user
|
||||
// might throw from inside a CJS module body.
|
||||
if (isError(error)) {
|
||||
(error as unknown as Record<symbol, unknown>)[CJS_PARSE_ERROR] = true;
|
||||
}
|
||||
throw handlePotentialSyntaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user