Skip to content

Fix auto-imports inserting import in CJS files when moduleDetection is force#3328

Draft
Copilot wants to merge 5 commits intomainfrom
copilot/fix-auto-imports-insert-import-statements
Draft

Fix auto-imports inserting import in CJS files when moduleDetection is force#3328
Copilot wants to merge 5 commits intomainfrom
copilot/fix-auto-imports-insert-import-statements

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

With moduleDetection: "force" (explicitly or implicitly via Node module kinds like node16/node18/node20/nodenext), auto-imports incorrectly emit import statements in CJS files instead of require() calls.

The root problem is that --moduleDetection force sets ExternalModuleIndicator to the source file node itself on all files unconditionally, making the CJS/ESM content heuristic in computeShouldUseRequire() unable to distinguish CJS files from ESM files. A secondary issue is that GetEmitModuleKind() < ModuleKindES2015 returns false for Node module kinds (ModuleKindNode20 = 102) even though the file may be CJS.

Fix

Created detectSyntax/detectSyntaxIndicators utility functions that properly detect ESM vs CJS syntax regardless of moduleDetection mode:

  • When moduleDetection is not "force": uses ExternalModuleIndicator and CommonJSModuleIndicator directly (they are reliable)
  • When moduleDetection is "force": checks if ExternalModuleIndicator points to a genuine statement node (not file.AsNode()), and falls back to scanning the source file's Imports() for actual import/export declarations, skipping synthesized imports

CommonJSModuleIndicator is always reliable regardless of moduleDetection mode, since it is set by the binder based on actual require()/module.exports syntax.

Also reordered the fallback steps in computeShouldUseRequire so that GetImpliedNodeFormatForEmit() is checked before the config file's module kind. This naturally handles Node module kinds (which determine the runtime format from file extension and package.json "type") without needing a special case.

Tests

Three new fourslash tests:

  • CJS file with existing module.exports syntax + module: "node20" + package.json "type": "commonjs"
  • Empty CJS file (same config) — the edge case where ExternalModuleIndicator alone causes the wrong result
  • --module preserve + --moduleDetection force + JS file with only require() syntax and no package.json "type"

… module kinds

When using module: "node16"/"node18"/"node20"/"nodenext", moduleDetection
defaults to "force", which sets ExternalModuleIndicator on ALL files regardless
of actual syntax. This made the file content indicators unreliable for
determining CJS vs ESM, and the simple module kind comparison
(GetEmitModuleKind() < ModuleKindES2015) also failed because Node module kinds
have numeric values >= 100.

The fix adds an early check for Node module kinds that uses
GetImpliedNodeFormatForEmit(), which correctly determines the runtime module
format based on file extension and package.json "type" field.

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/9405eb29-fef7-4989-a974-aba9130e0d67

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix auto-imports inserting import statement into CJS files Fix auto-imports inserting import in CJS files under node16/node20/nodenext Apr 2, 2026
Copilot AI requested a review from andrewbranch April 2, 2026 16:56
Copy link
Copy Markdown
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any time --moduleDetection is force (explicitly or implicitly) we shouldn't be using ExternalModuleIndicator. The real problem scenario is this:

  • --module preserve
  • --moduleDetection force
  • no package.json type
  • JS file that only uses require so far

The intent of looking at ExternalModuleIndicator and CommonJSModuleIndicator is to use existing syntax file as a strong indicator of what should come next. If --moduleDetection force makes that approach unusable, we need a different detection mechanism for existing syntax, because in the scenario I just described, I can't think of a more reliable signal outside of existing syntax.

You should make a utility function that uses ExternalModuleIndicator and CommonJSModuleIndicator if possible, and if not, achieves the same thing by inspecting the source file's .Imports(). Don't forget to ignore synthesized imports.

Copilot AI and others added 2 commits April 21, 2026 00:09
Instead of special-casing Node module kinds, create a general-purpose
detectSyntax utility that handles moduleDetection "force" properly.

When moduleDetection is NOT "force", ExternalModuleIndicator and
CommonJSModuleIndicator are reliable indicators of the file's syntax.

When moduleDetection IS "force", ExternalModuleIndicator may be set to
the source file node itself (not a genuine syntax indicator), so we:
1. Check if ExternalModuleIndicator points to a real statement node
2. Fall back to scanning Imports() for actual import/export declarations
3. Skip synthesized imports

This handles the broader problem: any time moduleDetection is "force"
(explicitly or via Node module kinds), the syntax-based detection works
correctly. Also added a test for --module preserve + --moduleDetection
force with a JS file containing only require() syntax.

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/16d8ed48-1daa-42b8-a8b2-c20032f4bbdb

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 21, 2026

Any time --moduleDetection is force (explicitly or implicitly) we shouldn't be using ExternalModuleIndicator. The real problem scenario is this:

  • --module preserve...

Implemented in 8e33931. Created detectSyntax/detectSyntaxIndicators utility that:

  • When moduleDetection is NOT force: uses ExternalModuleIndicator/CommonJSModuleIndicator directly
  • When moduleDetection IS force: checks if ExternalModuleIndicator is genuine (not file.AsNode()), and falls back to scanning Imports() for actual import/export declarations, skipping synthesized imports

Also applied the same utility to the "other files" fallback (step 5), and kept the GetImpliedNodeFormatForEmit fallback for Node module kinds in step 3 (for empty/ambiguous files with a tsconfig).

Added a third test for --module preserve + --moduleDetection force + JS file with only require() syntax.

Copilot AI changed the title Fix auto-imports inserting import in CJS files under node16/node20/nodenext Fix auto-imports inserting import in CJS files when moduleDetection is force Apr 21, 2026
Copilot AI requested a review from andrewbranch April 21, 2026 00:20
Comment thread internal/ls/autoimport/fix.go Outdated
Comment on lines +942 to +944
if core.ModuleKindNode16 <= moduleKind && moduleKind <= core.ModuleKindNodeNext {
return v.program.GetImpliedNodeFormatForEmit(v.importingFile) != core.ModuleKindESNext
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Instead of adding this new condition within here, I think you can just swap the order of 3 and 4

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0661560. Swapped the order so GetImpliedNodeFormatForEmit is checked first (step 3) and the config file module kind check is now step 4, removing the special Node module kind condition entirely.

…seRequire

Move GetImpliedNodeFormatForEmit check (step 3) before the config file
module kind check (step 4). This removes the need for the special Node
module kind condition since GetImpliedNodeFormatForEmit already handles
Node module kinds correctly via file extension and package.json "type".

Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/7983d9fc-3c93-4331-b786-935d5355882e

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-imports insert import statement into CJS files

2 participants