Skip to content

esm: detect ESM syntax in extensionless files under type:commonjs#61737

Open
scott-memco wants to merge 1 commit intonodejs:mainfrom
scott-memco:fix/esm-silent-failure-commonjs
Open

esm: detect ESM syntax in extensionless files under type:commonjs#61737
scott-memco wants to merge 1 commit intonodejs:mainfrom
scott-memco:fix/esm-silent-failure-commonjs

Conversation

@scott-memco
Copy link

Summary

Fixes a silent failure where extensionless files containing ES module syntax produce no output and exit with code 0 when the nearest package.json has "type": "commonjs".

This is a common scenario for CLI tools that use shebangs (#!/usr/bin/env node) without a file extension.

Reproduction

echo '{ "type": "commonjs" }' > package.json
printf '#!/usr/bin/env node\nconsole.log("started")\nimport { version } from "node:process"\nconsole.log(version)\n' > script
chmod +x script
./script ; echo "exit: $?"
# Output: exit: 0
# Expected: either the script runs, or an error is printed

Root Cause

In lib/internal/modules/esm/get_format.js, getFileProtocolModuleFormat() handles extensionless files (line 159-177). When packageType is 'commonjs', it returns 'commonjs' immediately without checking the file content for ESM syntax (line 164-165).

This causes the ESM loader to treat the file as a CJS module, routing it through createCJSModuleWrap in the translators, which wraps the ESM code as CJS — silently producing an empty/non-functional module.

Compare with:

  • The 'none' (no type field) case for extensionless files, which does call detectModuleFormat(source, url) at line 176
  • The .js extension case with no type field, which does call detectModuleFormat(source, url) at line 127

Fix

For extensionless files when packageType !== 'none' (i.e., explicitly 'commonjs'), check the source content via detectModuleFormat() before returning the package type. If the file contains ES module syntax, return 'module' so it's loaded correctly.

if (packageType !== 'none') {
  if (source) {
    const detected = detectModuleFormat(source, url);
    if (detected === 'module') {
      return detected;
    }
  }
  return packageType;
}

This is consistent with how ambiguous files are already handled elsewhere in the same function, and relies on the existing containsModuleSyntax V8 binding (used by detectModuleFormat) which is already enabled by default via --experimental-detect-module.

Test

test/parallel/test-esm-extensionless-commonjs-type.js — creates an extensionless ESM file in a type: commonjs project, runs it, and asserts it does not silently exit with code 0.

Fixes: #61104

Made with Cursor

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: nodejs#61104
Co-authored-by: Cursor <cursoragent@cursor.com>
@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added esm Issues and PRs related to the ECMAScript Modules implementation. needs-ci PRs that need a full CI run. labels Feb 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

esm Issues and PRs related to the ECMAScript Modules implementation. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

With "type": "commonjs", #!/usr/bin/env node file does not execute but returns success and no error messages when the file is ESM

3 participants