Skip to content
15 changes: 14 additions & 1 deletion lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,21 @@ translators.set('module', function moduleStrategy(url, translateContext, parentU
* @param {boolean} isMain - Whether the module is the entrypoint
*/
function loadCJSModule(module, source, url, filename, isMain) {
const compileResult = compileFunctionForCJSLoader(source, filename, false /* is_sea_main */, false);
// Validate source before compilation to prevent internal assertion errors.
// Without this check, null or undefined source causes ERR_INTERNAL_ASSERTION
// when passed to compileFunctionForCJSLoader.
// Refs: https://github.com/nodejs/node/issues/60401
if (source === null || source === undefined) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'non-empty string',
'load',
'source',
source,
);
}

const compileResult = compileFunctionForCJSLoader(source, filename, false /* is_sea_main */, false);

const { function: compiledWrapper, sourceMapURL, sourceURL } = compileResult;
// Cache the source map for the cjs module if present.
if (sourceMapURL) {
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/test-null-source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// test/fixtures/test-null-source.js
export {};
166 changes: 166 additions & 0 deletions test/parallel/test-esm-loader-null-source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
'use strict';
import { pathToFileURL } from 'url';
import path from 'path';

// Test that ESM loader handles null/undefined source gracefully
// and throws meaningful error instead of ERR_INTERNAL_ASSERTION.
// Refs: https://github.com/nodejs/node/issues/60401
const fixturePath = pathToFileURL(path.join(__dirname, '../fixtures/test-null-source.js')).href;
const common = require('../common');
const assert = require('assert');
const { spawnSync } = require('child_process');
const fixtures = require('../common/fixtures');

// Test case: Loader returning null source for CommonJS module
// This should throw ERR_INVALID_RETURN_PROPERTY_VALUE, not ERR_INTERNAL_ASSERTION
{
function load(url, context, next) {
if (url.includes("test-null-source")) {
return { format: "commonjs", source: null, shortCircuit: true };
}
return next(url);
}
const result = spawnSync(
process.execPath,
[
'--no-warnings',
'--input-type=module',
'--eval',
`
import { register } from 'node:module';

// Register a custom loader that returns null source
register('data:text/javascript,export ' + encodeURIComponent(${load}));

await assert.rejects(import('file:///test-null-source.js'), { code: 'ERR_INVALID_RETURN_PROPERTY_VALUE' });
`,
],
{ encoding: 'utf8' }
);

const output = result.stdout + result.stderr;

// Verify test passed
assert.ok(
output.includes('PASS: Got expected error'),
'Should pass with expected error. Output: ' + output
);

assert.strictEqual(
result.status,
0,
'Process should exit with code 0. Output: ' + output
);
}

// Test case: Loader returning undefined source
{
const result = spawnSync(
process.execPath,
[
'--no-warnings',
'--input-type=module',
'--eval',
`
import { register } from 'node:module';

const code = 'export function load(url, context, next) {' +
' if (url.includes("test-undefined-source")) {' +
' return { format: "commonjs", source: undefined, shortCircuit: true };' +
' }' +
' return next(url);' +
'}';

register('data:text/javascript,' + encodeURIComponent(code));

try {
await import('file:///test-undefined-source.js');
console.log('ERROR: Should have thrown');
process.exit(1);
} catch (err) {
if (err.code === 'ERR_INTERNAL_ASSERTION') {
console.log('FAIL: Got ERR_INTERNAL_ASSERTION');
process.exit(1);
}
if (err.code === 'ERR_INVALID_RETURN_PROPERTY_VALUE') {
console.log('PASS: Got expected error');
process.exit(0);
}
console.log('ERROR: Got unexpected error:', err.code);
process.exit(1);
}
`,
],
{ encoding: 'utf8' }
);

const output = result.stdout + result.stderr;

assert.ok(
output.includes('PASS: Got expected error'),
'Should pass with expected error for undefined. Output: ' + output
);

assert.strictEqual(
result.status,
0,
'Process should exit with code 0. Output: ' + output
);
}

// Test case: Loader returning empty string source
{
const result = spawnSync(
process.execPath,
[
'--no-warnings',
'--input-type=module',
'--eval',
`
import { register } from 'node:module';

const code = 'export function load(url, context, next) {' +
' if (url.includes("test-empty-source")) {' +
' return { format: "commonjs", source: "", shortCircuit: true };' +
' }' +
' return next(url);' +
'}';

register('data:text/javascript,' + encodeURIComponent(code));

try {
await import(fixturePath);
console.log('ERROR: Should have thrown');
process.exit(1);
} catch (err) {
if (err.code === 'ERR_INTERNAL_ASSERTION') {
console.log('FAIL: Got ERR_INTERNAL_ASSERTION');
process.exit(1);
}
if (err.code === 'ERR_INVALID_RETURN_PROPERTY_VALUE') {
console.log('PASS: Got expected error');
process.exit(0);
}
console.log('ERROR: Got unexpected error:', err.code);
process.exit(1);
}
`,
],
{ encoding: 'utf8' }
);

const output = result.stdout + result.stderr;

assert.ok(
output.includes('PASS: Got expected error'),
'Should pass with expected error for empty string. Output: ' + output
);

assert.strictEqual(
result.status,
0,
'Process should exit with code 0. Output: ' + output
);
}

console.log('All tests passed!');