Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions internal/fourslash/tests/autoImportCJSWithNodeModuleKind_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

// TestAutoImportCJSWithNodeModuleKind verifies that auto-imports use require()
// syntax in CJS files when using node16/node20/nodenext module kinds with a
// package.json that has "type": "commonjs".
//
// This is a regression test for https://github.com/nicolo-ribaudo/tc39-proposal-structs/issues/14
// where module: "node20" with moduleDetection defaulting to "force" caused
// auto-imports to incorrectly insert `import` statements in CJS files.
func TestAutoImportCJSWithNodeModuleKind(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @Filename: /tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"module": "node20",
"checkJs": true,
"noEmit": true
}
}
// @Filename: /package.json
{ "type": "commonjs" }
// @Filename: /lib.js
module.exports = { LIB_VERSION: 1 };
// @Filename: /main.js
module.exports.foo = 0;
LIB_VERSION/**/`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()

f.GoToMarker(t, "")
f.VerifyImportFixAtPosition(t, []string{
`const { LIB_VERSION } = require("./lib");

module.exports.foo = 0;
LIB_VERSION`,
}, nil /*preferences*/)
}

// TestAutoImportCJSWithNodeModuleKindEmptyFile verifies that auto-imports use
// require() syntax even in empty CJS files when using node module kinds.
func TestAutoImportCJSWithNodeModuleKindEmptyFile(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @Filename: /tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"module": "node20",
"checkJs": true,
"noEmit": true
}
}
// @Filename: /package.json
{ "type": "commonjs" }
// @Filename: /lib.js
module.exports = { LIB_VERSION: 1 };
// @Filename: /main.js
LIB_VERSION/**/`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()

f.GoToMarker(t, "")
f.VerifyImportFixAtPosition(t, []string{
`const { LIB_VERSION } = require("./lib");

LIB_VERSION`,
}, nil /*preferences*/)
}
22 changes: 16 additions & 6 deletions internal/ls/autoimport/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -865,21 +865,31 @@ func (v *View) computeShouldUseRequire() bool {
return false
}

// 2. If the current source file is unambiguously CJS or ESM, go with that
// 2. For Node module kinds (node16/node18/node20/nodenext), the runtime module
// format is determined by file extension and package.json "type" field, not
// by file content. moduleDetection defaults to "force" for these module kinds,
// which sets ExternalModuleIndicator on all files regardless of actual syntax,
// making the file content indicators unreliable. Use GetImpliedNodeFormatForEmit
// which correctly reflects the runtime module format.
moduleKind := v.program.Options().GetEmitModuleKind()
if core.ModuleKindNode16 <= moduleKind && moduleKind <= core.ModuleKindNodeNext {
return v.program.GetImpliedNodeFormatForEmit(v.importingFile) != core.ModuleKindESNext
}

// 3. If the current source file is unambiguously CJS or ESM, go with that
switch {
case v.importingFile.CommonJSModuleIndicator != nil && v.importingFile.ExternalModuleIndicator == nil:
return true
case v.importingFile.ExternalModuleIndicator != nil && v.importingFile.CommonJSModuleIndicator == nil:
return false
}

// 3. If there's a tsconfig/jsconfig, use its module setting
// 4. If there's a tsconfig/jsconfig, use its module setting
if v.program.Options().ConfigFilePath != "" {
return v.program.Options().GetEmitModuleKind() < core.ModuleKindES2015
return moduleKind < core.ModuleKindES2015
}

// 4. In --module nodenext, assume we're not emitting JS -> JS, so use
// whatever syntax Node expects based on the detected module kind
// 5. Use the implied node format to determine CJS vs ESM
// TODO: consider removing `impliedNodeFormatForEmit`
switch v.program.GetImpliedNodeFormatForEmit(v.importingFile) {
case core.ModuleKindCommonJS:
Expand All @@ -888,7 +898,7 @@ func (v *View) computeShouldUseRequire() bool {
return false
}

// 5. Match the first other JS file in the program that's unambiguously CJS or ESM
// 6. Match the first other JS file in the program that's unambiguously CJS or ESM
for _, otherFile := range v.program.GetSourceFiles() {
switch {
case otherFile == v.importingFile, !ast.IsSourceFileJS(otherFile), v.program.IsSourceFileFromExternalLibrary(otherFile):
Expand Down