Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/esm/hook/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,26 @@ const prepareJsonAttributes = (
};
};

const commonJsFromEsmBridgeStackFrame = 'loadAndTranslateForRequireInImportedCJS';

const isCommonJsRequireContext = (
{ conditions }: Parameters<LoadHook>[1],
) => (
conditions?.includes('require') === true
&& !conditions.includes('import')
);

const isCommonJsFromEsmBridgeRequireContext = (
{ conditions }: Parameters<LoadHook>[1],
) => (
// Yarn PnP on Node 24+ can send `import` conditions for JSON required
// by a CommonJS module reached through the ESM->CJS bridge. Node still
// parses the hook result as JSON on that path, so transforming it to ESM
// breaks the CommonJS consumer.
conditions?.includes('import') === true
&& new Error('Detect CommonJS JSON require bridge').stack?.includes(commonJsFromEsmBridgeStackFrame) === true
);

export const createLoad = (
hookData: Data,
): LoadHook => {
Expand Down Expand Up @@ -377,7 +390,11 @@ export const createLoad = (
const code = decodeSource(loaded.source);
// CJS JSON require still parses hook source as JSON after module hooks.
// https://github.com/nodejs/node/blob/v24.15.0/lib/internal/modules/cjs/loader.js#L1969-L1978
const shouldTransformJson = loadedFormat === 'json' && !isCommonJsRequireContext(context);
const shouldTransformJson = (
loadedFormat === 'json'
&& !isCommonJsRequireContext(context)
&& !isCommonJsFromEsmBridgeRequireContext(context)
);

if (loadedFormat === 'commonjs-typescript') {
const transformed = transformSync(
Expand Down Expand Up @@ -528,7 +545,11 @@ export const createLoadSync = (
const code = decodeSource(loaded.source);
// CJS JSON require still parses hook source as JSON after module hooks.
// https://github.com/nodejs/node/blob/v24.15.0/lib/internal/modules/cjs/loader.js#L1969-L1978
const shouldTransformJson = loadedFormat === 'json' && !isCommonJsRequireContext(context);
const shouldTransformJson = (
loadedFormat === 'json'
&& !isCommonJsRequireContext(context)
&& !isCommonJsFromEsmBridgeRequireContext(context)
);

if (loadedFormat === 'commonjs-typescript') {
const transformed = transformSync(
Expand Down
39 changes: 39 additions & 0 deletions tests/specs/version-sensitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
} from 'manten';
import { execaNode } from 'execa';
import { createFixture } from 'fs-fixture';
import { createData } from '../../src/esm/hook/initialize.js';
import { createLoadSync } from '../../src/esm/hook/load.js';
import { tsxEsmApiPath, tsxEsmPath, type NodeApis } from '../utils/tsx';
import { createPackageJson } from '../fixtures';
import { processInteract } from '../utils/process-interact.js';
Expand Down Expand Up @@ -314,6 +316,43 @@ export const versionSensitiveTests = (node: NodeApis) => describe('Version-sensi
});
});

test('sync ESM hook preserves ESM-to-CJS bridge JSON require', async () => {
await using fixture = await createFixture({
'mocharc.json': JSON.stringify({
diff: true,
extension: ['js', 'cjs', 'mjs'],
}),
});
const jsonUrl = pathToFileURL(fixture.getPath('mocharc.json')).toString();
const loadSync = createLoadSync(createData({ tsconfig: false }));

// Simulates Node's internal ESM->CJS bridge frame for JSON requires.
const loadAndTranslateForRequireInImportedCJS = () => loadSync(
jsonUrl,
{
conditions: ['node', 'import'],
format: 'json',
importAttributes: { type: 'json' },
},
() => ({
format: 'json',
responseURL: jsonUrl,
source: JSON.stringify({
diff: true,
extension: ['js', 'cjs', 'mjs'],
}),
}),
);

const loaded = loadAndTranslateForRequireInImportedCJS();

expect(loaded.format).toBe('json');
expect(JSON.parse(loaded.source as string)).toEqual({
diff: true,
extension: ['js', 'cjs', 'mjs'],
});
});

await test('watch reruns when imported TypeScript file changes', async () => {
await using fixture = await createFixture({
'package.json': createPackageJson({ type: 'commonjs' }),
Expand Down