From 27ba1cb331ee9e661ca6e501d1e2f5fc4cff14e4 Mon Sep 17 00:00:00 2001 From: inoway46 Date: Sun, 1 Feb 2026 00:10:53 +0900 Subject: [PATCH 1/6] test: add regression for extensionless ESM under type=commonjs --- .../test-extensionless-esm-type-commonjs.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 test/es-module/test-extensionless-esm-type-commonjs.js 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..979bebfde50bc0 --- /dev/null +++ b/test/es-module/test-extensionless-esm-type-commonjs.js @@ -0,0 +1,50 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const tmpdir = require('../common/tmpdir'); + +test('extensionless entry point with ESM syntax under type=commonjs should not silently exit 0', () => { + tmpdir.refresh(); + + const dir = tmpdir.resolve('extensionless-esm-commonjs'); + fs.mkdirSync(dir, { recursive: true }); + + // package.json with type: commonjs + fs.writeFileSync( + path.join(dir, '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 scriptPath = path.join(dir, 'script'); // no extension + fs.writeFileSync( + scriptPath, + '#!/usr/bin/env node\n' + + "console.log('script STARTED')\n" + + "import { version } from 'node:process'\n" + + 'console.log(version)\n', + 'utf8' + ); + fs.chmodSync(scriptPath, 0o755); + + const r = spawnSync(process.execPath, ['./script'], { + cwd: dir, + encoding: 'utf8' + }); + + assert.ifError(r.error); + + assert.notEqual( + r.status, + 0, + `unexpected exit code 0; stdout=${JSON.stringify(r.stdout)} stderr=${JSON.stringify(r.stderr)}` + ); + assert.ok(r.stderr && r.stderr.length > 0, 'expected stderr to contain an error message'); +}); From 682ec4f0f14fceb76907fd2730585fff88fbcb08 Mon Sep 17 00:00:00 2001 From: inoway46 Date: Sun, 1 Feb 2026 00:18:41 +0900 Subject: [PATCH 2/6] module: handle extensionless entry with explicit type=commonjs --- lib/internal/modules/cjs/loader.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 0ea268bcec7ece..ac7b514c89d435 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1929,6 +1929,14 @@ Module._extensions['.js'] = function(module, filename) { } else { format = 'typescript'; } + } else if (path.extname(filename) === '') { + // Extensionless files skip the .js suffix check above. When type: commonjs + // is explicit, force commonjs format so ESM syntax surfaces as SyntaxError + // instead of silently delegating to ESM. + pkg = packageJsonReader.getNearestParentPackageJSON(filename); + if (pkg?.data?.type === 'commonjs') { + format = 'commonjs'; + } } const { source, format: loadedFormat } = loadSource(module, filename, format); // Function require shouldn't be used in ES modules when require(esm) is disabled. From 06a7ad20944c55799ab51dc2e0924981b849a946 Mon Sep 17 00:00:00 2001 From: inoway46 Date: Sun, 1 Feb 2026 12:20:19 +0900 Subject: [PATCH 3/6] test: remove unnecessary node:test wrapper from single test --- .../test-extensionless-esm-type-commonjs.js | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/test/es-module/test-extensionless-esm-type-commonjs.js b/test/es-module/test-extensionless-esm-type-commonjs.js index 979bebfde50bc0..d72f1d690c04a1 100644 --- a/test/es-module/test-extensionless-esm-type-commonjs.js +++ b/test/es-module/test-extensionless-esm-type-commonjs.js @@ -1,6 +1,5 @@ 'use strict'; -const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); @@ -8,43 +7,41 @@ const { spawnSync } = require('node:child_process'); const tmpdir = require('../common/tmpdir'); -test('extensionless entry point with ESM syntax under type=commonjs should not silently exit 0', () => { - tmpdir.refresh(); - - const dir = tmpdir.resolve('extensionless-esm-commonjs'); - fs.mkdirSync(dir, { recursive: true }); - - // package.json with type: commonjs - fs.writeFileSync( - path.join(dir, '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 scriptPath = path.join(dir, 'script'); // no extension - fs.writeFileSync( - scriptPath, - '#!/usr/bin/env node\n' + - "console.log('script STARTED')\n" + - "import { version } from 'node:process'\n" + - 'console.log(version)\n', - 'utf8' - ); - fs.chmodSync(scriptPath, 0o755); - - const r = spawnSync(process.execPath, ['./script'], { - cwd: dir, - encoding: 'utf8' - }); - - assert.ifError(r.error); - - assert.notEqual( - r.status, - 0, - `unexpected exit code 0; stdout=${JSON.stringify(r.stdout)} stderr=${JSON.stringify(r.stderr)}` - ); - assert.ok(r.stderr && r.stderr.length > 0, 'expected stderr to contain an error message'); +tmpdir.refresh(); + +const dir = tmpdir.resolve('extensionless-esm-commonjs'); +fs.mkdirSync(dir, { recursive: true }); + +// package.json with type: commonjs +fs.writeFileSync( + path.join(dir, '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 scriptPath = path.join(dir, 'script'); // no extension +fs.writeFileSync( + scriptPath, + '#!/usr/bin/env node\n' + + "console.log('script STARTED')\n" + + "import { version } from 'node:process'\n" + + 'console.log(version)\n', + 'utf8' +); +fs.chmodSync(scriptPath, 0o755); + +const r = spawnSync(process.execPath, ['./script'], { + cwd: dir, + encoding: 'utf8' }); + +assert.ifError(r.error); + +assert.notEqual( + r.status, + 0, + `unexpected exit code 0; stdout=${JSON.stringify(r.stdout)} stderr=${JSON.stringify(r.stderr)}` +); +assert.ok(r.stderr && r.stderr.length > 0, 'expected stderr to contain an error message'); From 98a519bb80fe67265685df02b0c3fe26a2802b5d Mon Sep 17 00:00:00 2001 From: inoway46 Date: Sun, 1 Feb 2026 12:42:00 +0900 Subject: [PATCH 4/6] test: use spawnSyncAndAssert in extensionless ESM test --- .../test-extensionless-esm-type-commonjs.js | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/test/es-module/test-extensionless-esm-type-commonjs.js b/test/es-module/test-extensionless-esm-type-commonjs.js index d72f1d690c04a1..4222b0134ff7bf 100644 --- a/test/es-module/test-extensionless-esm-type-commonjs.js +++ b/test/es-module/test-extensionless-esm-type-commonjs.js @@ -1,10 +1,8 @@ 'use strict'; -const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); -const { spawnSync } = require('node:child_process'); - +const { spawnSyncAndAssert } = require('../common/child_process'); const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); @@ -32,16 +30,11 @@ fs.writeFileSync( ); fs.chmodSync(scriptPath, 0o755); -const r = spawnSync(process.execPath, ['./script'], { +spawnSyncAndAssert(process.execPath, ['./script'], { cwd: dir, - encoding: 'utf8' + encoding: 'utf8', +}, { + status: 1, + stderr: /.+/, + trim: true, }); - -assert.ifError(r.error); - -assert.notEqual( - r.status, - 0, - `unexpected exit code 0; stdout=${JSON.stringify(r.stdout)} stderr=${JSON.stringify(r.stderr)}` -); -assert.ok(r.stderr && r.stderr.length > 0, 'expected stderr to contain an error message'); From 79cdbd017d943fdafa779f795f22945829e31aca Mon Sep 17 00:00:00 2001 From: inoway46 Date: Sun, 1 Feb 2026 12:56:30 +0900 Subject: [PATCH 5/6] test: cover extensionless ESM with explicit module type --- lib/internal/modules/cjs/loader.js | 11 ++--- .../test-extensionless-esm-type-commonjs.js | 43 ++++++++++++++++--- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index ac7b514c89d435..3a8a34051eaa9d 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1930,12 +1930,13 @@ Module._extensions['.js'] = function(module, filename) { format = 'typescript'; } } else if (path.extname(filename) === '') { - // Extensionless files skip the .js suffix check above. When type: commonjs - // is explicit, force commonjs format so ESM syntax surfaces as SyntaxError - // instead of silently delegating to ESM. + // 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); - if (pkg?.data?.type === 'commonjs') { - format = 'commonjs'; + const typeFromPjson = pkg?.data?.type; + if (typeFromPjson === 'commonjs' || typeFromPjson === 'module') { + format = typeFromPjson; } } const { source, format: loadedFormat } = loadSource(module, filename, format); diff --git a/test/es-module/test-extensionless-esm-type-commonjs.js b/test/es-module/test-extensionless-esm-type-commonjs.js index 4222b0134ff7bf..0f171a82a430c7 100644 --- a/test/es-module/test-extensionless-esm-type-commonjs.js +++ b/test/es-module/test-extensionless-esm-type-commonjs.js @@ -7,34 +7,63 @@ const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); -const dir = tmpdir.resolve('extensionless-esm-commonjs'); -fs.mkdirSync(dir, { recursive: true }); +const commonjsDir = tmpdir.resolve('extensionless-esm-commonjs'); +fs.mkdirSync(commonjsDir, { recursive: true }); // package.json with type: commonjs fs.writeFileSync( - path.join(dir, 'package.json'), + 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 scriptPath = path.join(dir, 'script'); // no extension +const commonjsScriptPath = path.join(commonjsDir, 'script'); // no extension fs.writeFileSync( - scriptPath, + commonjsScriptPath, '#!/usr/bin/env node\n' + "console.log('script STARTED')\n" + "import { version } from 'node:process'\n" + 'console.log(version)\n', 'utf8' ); -fs.chmodSync(scriptPath, 0o755); +fs.chmodSync(commonjsScriptPath, 0o755); spawnSyncAndAssert(process.execPath, ['./script'], { - cwd: dir, + 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, +}); From 538e80eb6a45282ef272e0172fb0f3813a1ee912 Mon Sep 17 00:00:00 2001 From: inoway46 Date: Mon, 2 Feb 2026 02:05:33 +0900 Subject: [PATCH 6/6] test: add fixtures for extensionless esm type commonjs --- .../test-extensionless-esm-type-commonjs.js | 54 ++----------------- .../extensionless-esm-commonjs/package.json | 3 ++ .../extensionless-esm-commonjs/script | 4 ++ .../extensionless-esm-module/package.json | 3 ++ .../extensionless-esm-module/script | 4 ++ 5 files changed, 19 insertions(+), 49 deletions(-) create mode 100644 test/fixtures/es-modules/extensionless-esm-commonjs/package.json create mode 100755 test/fixtures/es-modules/extensionless-esm-commonjs/script create mode 100644 test/fixtures/es-modules/extensionless-esm-module/package.json create mode 100755 test/fixtures/es-modules/extensionless-esm-module/script diff --git a/test/es-module/test-extensionless-esm-type-commonjs.js b/test/es-module/test-extensionless-esm-type-commonjs.js index 0f171a82a430c7..c5aef8fde8b47c 100644 --- a/test/es-module/test-extensionless-esm-type-commonjs.js +++ b/test/es-module/test-extensionless-esm-type-commonjs.js @@ -1,36 +1,11 @@ 'use strict'; -const fs = require('node:fs'); -const path = require('node:path'); const { spawnSyncAndAssert } = require('../common/child_process'); -const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); -tmpdir.refresh(); +const commonjsDir = fixtures.path('es-modules', 'extensionless-esm-commonjs'); -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'], { +spawnSyncAndAssert(process.execPath, ['--no-experimental-detect-module', './script'], { cwd: commonjsDir, encoding: 'utf8', }, { @@ -39,28 +14,9 @@ spawnSyncAndAssert(process.execPath, ['./script'], { 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); +const moduleDir = fixtures.path('es-modules', 'extensionless-esm-module'); -spawnSyncAndAssert(process.execPath, ['./script'], { +spawnSyncAndAssert(process.execPath, ['--no-experimental-detect-module', './script'], { cwd: moduleDir, encoding: 'utf8', }, { diff --git a/test/fixtures/es-modules/extensionless-esm-commonjs/package.json b/test/fixtures/es-modules/extensionless-esm-commonjs/package.json new file mode 100644 index 00000000000000..5bbefffbabee39 --- /dev/null +++ b/test/fixtures/es-modules/extensionless-esm-commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/fixtures/es-modules/extensionless-esm-commonjs/script b/test/fixtures/es-modules/extensionless-esm-commonjs/script new file mode 100755 index 00000000000000..75798ebfc99202 --- /dev/null +++ b/test/fixtures/es-modules/extensionless-esm-commonjs/script @@ -0,0 +1,4 @@ +#!/usr/bin/env node +console.log('script STARTED') +import { version } from 'node:process' +console.log(version) diff --git a/test/fixtures/es-modules/extensionless-esm-module/package.json b/test/fixtures/es-modules/extensionless-esm-module/package.json new file mode 100644 index 00000000000000..3dbc1ca591c055 --- /dev/null +++ b/test/fixtures/es-modules/extensionless-esm-module/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/fixtures/es-modules/extensionless-esm-module/script b/test/fixtures/es-modules/extensionless-esm-module/script new file mode 100755 index 00000000000000..75798ebfc99202 --- /dev/null +++ b/test/fixtures/es-modules/extensionless-esm-module/script @@ -0,0 +1,4 @@ +#!/usr/bin/env node +console.log('script STARTED') +import { version } from 'node:process' +console.log(version)