diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 2264390ea1faff..ded8e57f462929 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -130,8 +130,21 @@ 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) { + 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/fixtures/test-null-source.js b/test/fixtures/test-null-source.js new file mode 100644 index 00000000000000..867f629c0efaf2 --- /dev/null +++ b/test/fixtures/test-null-source.js @@ -0,0 +1,2 @@ +// test/fixtures/test-null-source.js +export {}; 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..a96d35eb796f41 --- /dev/null +++ b/test/parallel/test-esm-loader-null-source.js @@ -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!');