From f4b05c0f161a51a041fd2ee104cc552cc16dbcfe Mon Sep 17 00:00:00 2001 From: st-imdev <124273900+st-imdev@users.noreply.github.com> Date: Sun, 8 Feb 2026 14:22:49 +0000 Subject: [PATCH] esm: detect ESM syntax in extensionless files under type:commonjs When an extensionless file (common for CLI scripts with shebangs) contains ES module syntax but the nearest package.json has "type": "commonjs", Node.js silently exits with code 0 and produces no output or error. This happens because getFileProtocolModuleFormat() returns 'commonjs' for extensionless files based solely on the package type, without checking the file content for ESM syntax. For extensionless files, when source is available, run detectModuleFormat() before returning the package type. If the file contains ES module syntax, return 'module' so it is loaded as ESM rather than silently failing as CJS. This is consistent with how the 'none' (no type field) case already works for extensionless files, where detectModuleFormat() is called at line 176. Fixes: https://github.com/nodejs/node/issues/61104 Co-authored-by: Cursor --- lib/internal/modules/esm/get_format.js | 10 ++++ .../test-esm-extensionless-commonjs-type.js | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 test/parallel/test-esm-extensionless-commonjs-type.js diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 48ccb97a6244ea..a6ba25b170b653 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -162,6 +162,16 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE return getFormatOfExtensionlessFile(url); } if (packageType !== 'none') { + // When source is available, check if an extensionless file in a "type": "commonjs" + // package actually contains ES module syntax. Without this, ESM files without an + // extension (common for CLI scripts with shebangs) silently fail when loaded as CJS. + // See https://github.com/nodejs/node/issues/61104 + if (source) { + const detected = detectModuleFormat(source, url); + if (detected === 'module') { + return detected; + } + } return packageType; // 'commonjs' or future package types } diff --git a/test/parallel/test-esm-extensionless-commonjs-type.js b/test/parallel/test-esm-extensionless-commonjs-type.js new file mode 100644 index 00000000000000..41d50130e131a7 --- /dev/null +++ b/test/parallel/test-esm-extensionless-commonjs-type.js @@ -0,0 +1,52 @@ +'use strict'; + +// Test that extensionless files containing ESM syntax are not silently +// swallowed when the nearest package.json has "type": "commonjs". +// Regression test for https://github.com/nodejs/node/issues/61104 + +const common = require('../common'); +const assert = require('assert'); +const { execFileSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const dir = path.join(tmpdir.path, 'esm-extensionless'); +fs.mkdirSync(dir, { recursive: true }); + +// Create package.json with "type": "commonjs" +fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ + type: 'commonjs', +})); + +// Create an extensionless script with ESM syntax (simulating a CLI tool with a shebang) +const script = path.join(dir, 'script'); +fs.writeFileSync(script, `#!/usr/bin/env node +process.exitCode = 42; +export {}; +`); +fs.chmodSync(script, 0o755); + +// The script should either run as ESM (exit code 42) or throw an error. +// It must NOT silently exit with code 0. +try { + const result = execFileSync(process.execPath, [script], { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + // If we reach here, the script ran without error. + // The exit code should be 42 (set by process.exitCode in the ESM script). + assert.fail('Expected the script to either exit with code 42 or throw an error, but it exited with code 0'); +} catch (err) { + // execFileSync throws if exit code is non-zero, which is expected. + // Either exit code 42 (ESM ran correctly) or an error was thrown (also acceptable). + if (err.status !== null) { + // The script ran but exited non-zero — ESM was properly detected and executed. + assert.strictEqual(err.status, 42, + `Expected exit code 42 from ESM script, got ${err.status}. stderr: ${err.stderr}`); + } + // If there's a stderr message about ESM/CommonJS mismatch, that's also acceptable + // as long as it's not a silent success. +}