diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 87f271adcd3e50..c2ca465c905103 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -129,8 +129,20 @@ const { requestTypes: { kRequireInImportedCJS } } = require('internal/modules/es * @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 || source === '') { + 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) { diff --git a/test/parallel/test-esm-loader-null-source.js b/test/parallel/test-esm-loader-null-source.js new file mode 100644 index 00000000000000..3e57eae08a4a91 --- /dev/null +++ b/test/parallel/test-esm-loader-null-source.js @@ -0,0 +1,131 @@ +'use strict'; + +// 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 common = require('../common'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const fixtures = require('../common/fixtures'); + +// Reusable loader functions +function createNullLoader() { + return function load(url, context, next) { + if (url.includes('test-null-source')) { + return { format: 'commonjs', source: null, shortCircuit: true }; + } + return next(url); + }; +} + +function createUndefinedLoader() { + return function load(url, context, next) { + if (url.includes('test-undefined-source')) { + return { format: 'commonjs', source: undefined, shortCircuit: true }; + } + return next(url); + }; +} + +function createEmptyLoader() { + return function load(url, context, next) { + if (url.includes('test-empty-source')) { + return { format: 'commonjs', source: '', shortCircuit: true }; + } + return next(url); + }; +} + +// Helper to run test with custom loader +function runTestWithLoader(loaderFn, testUrl) { + const loaderCode = `export ${loaderFn.toString()}`; + + const result = spawnSync( + process.execPath, + [ + '--no-warnings', + '--input-type=module', + '--eval', + ` + import { register } from 'node:module'; + import assert from 'node:assert'; + + register('data:text/javascript,' + encodeURIComponent(${JSON.stringify(loaderCode)})); + + await assert.rejects( + import(${JSON.stringify(testUrl)}), + { code: 'ERR_INVALID_RETURN_PROPERTY_VALUE' } + ); + `, + ], + { encoding: 'utf8' } + ); + + return result; +} + +// Test case 1: Loader returning null source +{ + const result = runTestWithLoader( + createNullLoader(), + 'file:///test-null-source.js' + ); + + const output = result.stdout + result.stderr; + + assert.ok( + !output.includes('ERR_INTERNAL_ASSERTION'), + 'Should not throw ERR_INTERNAL_ASSERTION. Output: ' + output + ); + + assert.strictEqual( + result.status, + 0, + 'Process should exit with code 0. Output: ' + output + ); +} + +// Test case 2: Loader returning undefined source +{ + const result = runTestWithLoader( + createUndefinedLoader(), + 'file:///test-undefined-source.js' + ); + + const output = result.stdout + result.stderr; + + assert.ok( + !output.includes('ERR_INTERNAL_ASSERTION'), + 'Should not throw ERR_INTERNAL_ASSERTION. Output: ' + output + ); + + assert.strictEqual( + result.status, + 0, + 'Process should exit with code 0. Output: ' + output + ); +} + +// Test case 3: Loader returning empty string source +{ + const fixtureURL = fixtures.fileURL('es-modules/loose.js'); + + const result = runTestWithLoader( + createEmptyLoader(), + fixtureURL.href + ); + + const output = result.stdout + result.stderr; + + assert.ok( + !output.includes('ERR_INTERNAL_ASSERTION'), + 'Should not throw ERR_INTERNAL_ASSERTION. Output: ' + output + ); + + assert.strictEqual( + result.status, + 0, + 'Process should exit with code 0. Output: ' + output + ); +}