Skip to content

Commit f4b05c0

Browse files
st-imdevcursoragent
andcommitted
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: #61104 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b220fbe commit f4b05c0

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

lib/internal/modules/esm/get_format.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
162162
return getFormatOfExtensionlessFile(url);
163163
}
164164
if (packageType !== 'none') {
165+
// When source is available, check if an extensionless file in a "type": "commonjs"
166+
// package actually contains ES module syntax. Without this, ESM files without an
167+
// extension (common for CLI scripts with shebangs) silently fail when loaded as CJS.
168+
// See https://github.com/nodejs/node/issues/61104
169+
if (source) {
170+
const detected = detectModuleFormat(source, url);
171+
if (detected === 'module') {
172+
return detected;
173+
}
174+
}
165175
return packageType; // 'commonjs' or future package types
166176
}
167177

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
// Test that extensionless files containing ESM syntax are not silently
4+
// swallowed when the nearest package.json has "type": "commonjs".
5+
// Regression test for https://github.com/nodejs/node/issues/61104
6+
7+
const common = require('../common');
8+
const assert = require('assert');
9+
const { execFileSync } = require('child_process');
10+
const fs = require('fs');
11+
const path = require('path');
12+
const tmpdir = require('../common/tmpdir');
13+
14+
tmpdir.refresh();
15+
16+
const dir = path.join(tmpdir.path, 'esm-extensionless');
17+
fs.mkdirSync(dir, { recursive: true });
18+
19+
// Create package.json with "type": "commonjs"
20+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
21+
type: 'commonjs',
22+
}));
23+
24+
// Create an extensionless script with ESM syntax (simulating a CLI tool with a shebang)
25+
const script = path.join(dir, 'script');
26+
fs.writeFileSync(script, `#!/usr/bin/env node
27+
process.exitCode = 42;
28+
export {};
29+
`);
30+
fs.chmodSync(script, 0o755);
31+
32+
// The script should either run as ESM (exit code 42) or throw an error.
33+
// It must NOT silently exit with code 0.
34+
try {
35+
const result = execFileSync(process.execPath, [script], {
36+
encoding: 'utf8',
37+
stdio: ['pipe', 'pipe', 'pipe'],
38+
});
39+
// If we reach here, the script ran without error.
40+
// The exit code should be 42 (set by process.exitCode in the ESM script).
41+
assert.fail('Expected the script to either exit with code 42 or throw an error, but it exited with code 0');
42+
} catch (err) {
43+
// execFileSync throws if exit code is non-zero, which is expected.
44+
// Either exit code 42 (ESM ran correctly) or an error was thrown (also acceptable).
45+
if (err.status !== null) {
46+
// The script ran but exited non-zero — ESM was properly detected and executed.
47+
assert.strictEqual(err.status, 42,
48+
`Expected exit code 42 from ESM script, got ${err.status}. stderr: ${err.stderr}`);
49+
}
50+
// If there's a stderr message about ESM/CommonJS mismatch, that's also acceptable
51+
// as long as it's not a silent success.
52+
}

0 commit comments

Comments
 (0)