diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 0ea268bcec7ece..3a8a34051eaa9d 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1929,6 +1929,15 @@ Module._extensions['.js'] = function(module, filename) { } else { format = 'typescript'; } + } else if (path.extname(filename) === '') { + // Extensionless files skip the .js suffix check above. When type is explicit, + // follow it so ESM syntax surfaces as SyntaxError for commonjs instead of + // silently delegating to ESM. + pkg = packageJsonReader.getNearestParentPackageJSON(filename); + const typeFromPjson = pkg?.data?.type; + if (typeFromPjson === 'commonjs' || typeFromPjson === 'module') { + format = typeFromPjson; + } } const { source, format: loadedFormat } = loadSource(module, filename, format); // Function require shouldn't be used in ES modules when require(esm) is disabled. diff --git a/test/es-module/test-extensionless-esm-type-commonjs.js b/test/es-module/test-extensionless-esm-type-commonjs.js new file mode 100644 index 00000000000000..0f171a82a430c7 --- /dev/null +++ b/test/es-module/test-extensionless-esm-type-commonjs.js @@ -0,0 +1,69 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const commonjsDir = tmpdir.resolve('extensionless-esm-commonjs'); +fs.mkdirSync(commonjsDir, { recursive: true }); + +// package.json with type: commonjs +fs.writeFileSync( + path.join(commonjsDir, 'package.json'), + '{\n "type": "commonjs"\n}\n', + 'utf8' +); + +// Extensionless executable with shebang + ESM syntax. +// NOTE: Execute via process.execPath to avoid PATH/env differences. +const commonjsScriptPath = path.join(commonjsDir, 'script'); // no extension +fs.writeFileSync( + commonjsScriptPath, + '#!/usr/bin/env node\n' + + "console.log('script STARTED')\n" + + "import { version } from 'node:process'\n" + + 'console.log(version)\n', + 'utf8' +); +fs.chmodSync(commonjsScriptPath, 0o755); + +spawnSyncAndAssert(process.execPath, ['./script'], { + cwd: commonjsDir, + encoding: 'utf8', +}, { + status: 1, + stderr: /.+/, + trim: true, +}); + +const moduleDir = tmpdir.resolve('extensionless-esm-module'); +fs.mkdirSync(moduleDir, { recursive: true }); + +// package.json with type: module +fs.writeFileSync( + path.join(moduleDir, 'package.json'), + '{\n "type": "module"\n}\n', + 'utf8' +); + +const moduleScriptPath = path.join(moduleDir, 'script'); // no extension +fs.writeFileSync( + moduleScriptPath, + '#!/usr/bin/env node\n' + + "console.log('script STARTED')\n" + + "import { version } from 'node:process'\n" + + 'console.log(version)\n', + 'utf8' +); +fs.chmodSync(moduleScriptPath, 0o755); + +spawnSyncAndAssert(process.execPath, ['./script'], { + cwd: moduleDir, + encoding: 'utf8', +}, { + stdout: /script STARTED[\s\S]*v\d+\./, + trim: true, +});