From 3d847b0aa2b4aadbabbe9e2a375a8a1f519a342a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 17 Mar 2026 09:16:37 -0700 Subject: [PATCH 1/5] Investigate and fix auto-import root wildcard handling --- ...mport_issue2984_rootWildcardVitest_test.go | 66 +++++++++++++++++++ internal/modulespecifiers/specifiers.go | 3 +- .../issue-2984-autoimport-root-wildcard.md | 57 ++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go create mode 100644 investigations/issue-2984-autoimport-root-wildcard.md diff --git a/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go b/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go new file mode 100644 index 00000000000..61930a83a1b --- /dev/null +++ b/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go @@ -0,0 +1,66 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestAutoImport_issue2984_rootWildcardVitest(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /tsconfig.json +{ + "compilerOptions": { + "module": "node20", + "moduleResolution": "nodenext", + "rootDir": "./", + "outDir": "build" + } +} +// @Filename: /package.json +{ + "imports": { + "#/*": { + "vitest": "./src/*", + "types": "./src/*", + "node": "./build/*", + "default": "./src/*" + } + } +} +// @Filename: /src/domain/entities/entity.ts +export const entity = 1; +// @Filename: /feature/very/deep/path/consumer.ts +entit/**/` + + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + + assertBest := func(prefs *lsutil.UserPreferences, prefName string) { + f.GoToMarker(t, "") + completions := f.GetCompletions(t, prefs) + if completions == nil { + t.Fatalf("%s: expected completions list", prefName) + } + var entitySpecifiers []string + for _, item := range completions.Items { + if item.Label != "entity" || item.Data == nil || item.Data.AutoImport == nil { + continue + } + entitySpecifiers = append(entitySpecifiers, item.Data.AutoImport.ModuleSpecifier) + } + if len(entitySpecifiers) == 0 { + t.Fatalf("%s: expected auto-import completion for entity", prefName) + } + t.Logf("%s entity specifiers: %v", prefName, entitySpecifiers) + if entitySpecifiers[0] != "#/domain/entities/entity.js" { + t.Fatalf("%s: expected top module specifier %q, got %q", prefName, "#/domain/entities/entity.js", entitySpecifiers[0]) + } + } + + assertBest(&lsutil.UserPreferences{ImportModuleSpecifierPreference: "shortest"}, "shortest") + assertBest(&lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}, "non-relative") +} diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 5c8eb9d2fe5..2c6d69cd739 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -1031,7 +1031,8 @@ func tryGetModuleNameFromPackageJsonImports( top := imports.AsObject() entries := top.Entries() for k, value := range entries { - if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") { + // Node-style imports keys cannot start with "#/" except for the root wildcard form "#/*". + if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") && !strings.HasPrefix(k, "#/*") { continue // invalid imports entry } mode := MatchingModeExact diff --git a/investigations/issue-2984-autoimport-root-wildcard.md b/investigations/issue-2984-autoimport-root-wildcard.md new file mode 100644 index 00000000000..9665d1f5ab1 --- /dev/null +++ b/investigations/issue-2984-autoimport-root-wildcard.md @@ -0,0 +1,57 @@ +# Investigation: microsoft/typescript-go#2984 + +## Summary +Auto-import did not prioritize (or even offer) `package.json#imports` root wildcard specifiers (`#/*`) for deep files, even with `importModuleSpecifierPreference` set to `shortest`/`non-relative`. + +## Reproduction +Added fourslash test: +- `internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go` + +Scenario: +- `package.json` contains: + - `"#/*": { "vitest": "./src/*", "types": "./src/*", "node": "./build/*", "default": "./src/*" }` +- Importing file at `/feature/very/deep/path/consumer.ts` +- Exported symbol at `/src/domain/entities/entity.ts` + +### Failing behavior before fix +For `ImportModuleSpecifierPreference: "shortest"`: +- **Actual top suggestion:** `../../../../src/domain/entities/entity` +- **Expected:** `#/domain/entities/entity.js` (non-relative package import alias) + +Same behavior reproduced for `ImportModuleSpecifierPreference: "non-relative"`. + +## Root cause +File: `internal/modulespecifiers/specifiers.go` +Function: `tryGetModuleNameFromPackageJsonImports` + +The imports-key validation incorrectly rejected all keys starting with `"#/"`: + +```go +if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") { + continue // invalid imports entry +} +``` + +This rejects valid root wildcard key `#/*`, so `tryGetModuleNameFromPackageJsonImports` returned `""`, forcing fallback to relative specifiers. + +## Minimal patch +Allow `#/*` while keeping other invalid `#/...` keys rejected: + +```go +if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") && !strings.HasPrefix(k, "#/*") { + continue // invalid imports entry +} +``` + +## Validation after patch +Targeted test: + +```bash +go test ./internal/fourslash/tests -run TestAutoImport_issue2984_rootWildcardVitest -count=1 -v +``` + +Observed: +- `shortest entity specifiers: [#/domain/entities/entity.js]` +- `non-relative entity specifiers: [#/domain/entities/entity.js]` + +This confirms root wildcard package import is now preferred over deep relative import. From cb06ee28efe0e1658d3698b8278e0d545a015795 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 17 Mar 2026 09:22:19 -0700 Subject: [PATCH 2/5] Refactor auto-import test to improve module specifier verification and streamline completion checks --- ...mport_issue2984_rootWildcardVitest_test.go | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go b/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go index 61930a83a1b..017bab55eb2 100644 --- a/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go +++ b/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go @@ -4,7 +4,9 @@ import ( "testing" "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -35,32 +37,30 @@ func TestAutoImport_issue2984_rootWildcardVitest(t *testing.T) { export const entity = 1; // @Filename: /feature/very/deep/path/consumer.ts entit/**/` - f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() + f.GoToMarker(t, "") - assertBest := func(prefs *lsutil.UserPreferences, prefName string) { - f.GoToMarker(t, "") - completions := f.GetCompletions(t, prefs) - if completions == nil { - t.Fatalf("%s: expected completions list", prefName) - } - var entitySpecifiers []string + verifyTopSpecifier := func(pref modulespecifiers.ImportModuleSpecifierPreference, expected string) { + completions := f.GetCompletions(t, &lsutil.UserPreferences{ImportModuleSpecifierPreference: pref}) + var moduleSpecifiers []string for _, item := range completions.Items { - if item.Label != "entity" || item.Data == nil || item.Data.AutoImport == nil { + if item.Label != "entity" || item.SortText == nil || *item.SortText != string(ls.SortTextAutoImportSuggestions) { + continue + } + if item.Data == nil || item.Data.AutoImport == nil { continue } - entitySpecifiers = append(entitySpecifiers, item.Data.AutoImport.ModuleSpecifier) + moduleSpecifiers = append(moduleSpecifiers, item.Data.AutoImport.ModuleSpecifier) } - if len(entitySpecifiers) == 0 { - t.Fatalf("%s: expected auto-import completion for entity", prefName) + if len(moduleSpecifiers) == 0 { + t.Fatalf("No auto-import completion specifier found for 'entity' with preference %q", pref) } - t.Logf("%s entity specifiers: %v", prefName, entitySpecifiers) - if entitySpecifiers[0] != "#/domain/entities/entity.js" { - t.Fatalf("%s: expected top module specifier %q, got %q", prefName, "#/domain/entities/entity.js", entitySpecifiers[0]) + if moduleSpecifiers[0] != expected { + t.Fatalf("Unexpected first auto-import module specifier for preference %q.\nExpected: %s\nActual: %s\nAll: %v", pref, expected, moduleSpecifiers[0], moduleSpecifiers) } } - assertBest(&lsutil.UserPreferences{ImportModuleSpecifierPreference: "shortest"}, "shortest") - assertBest(&lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}, "non-relative") + verifyTopSpecifier(modulespecifiers.ImportModuleSpecifierPreferenceShortest, "#/domain/entities/entity.js") + verifyTopSpecifier(modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, "#/domain/entities/entity.js") } From d85d8e2434525be077a4c666a4817b93f8e1e9dd Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 17 Mar 2026 09:24:34 -0700 Subject: [PATCH 3/5] Add test for root wildcard import restrictions in Node 16 and update validation logic --- ...mport_issue2984_rootWildcardVitest_test.go | 46 +++++++++++++++++++ internal/modulespecifiers/specifiers.go | 8 +++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go b/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go index 017bab55eb2..1bda2835052 100644 --- a/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go +++ b/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go @@ -1,6 +1,7 @@ package fourslash_test import ( + "strings" "testing" "github.com/microsoft/typescript-go/internal/fourslash" @@ -64,3 +65,48 @@ entit/**/` verifyTopSpecifier(modulespecifiers.ImportModuleSpecifierPreferenceShortest, "#/domain/entities/entity.js") verifyTopSpecifier(modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, "#/domain/entities/entity.js") } + +func TestAutoImport_issue2984_rootWildcardNotAllowedInNode16(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /tsconfig.json +{ + "compilerOptions": { + "module": "node16", + "moduleResolution": "node16" + } +} +// @Filename: /package.json +{ + "imports": { + "#/*": "./src/*" + } +} +// @Filename: /src/domain/entities/entity.ts +export const entity = 1; +// @Filename: /deep/feature/path/consumer.ts +entit/**/` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToMarker(t, "") + + completions := f.GetCompletions(t, &lsutil.UserPreferences{ + ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceShortest, + }) + var moduleSpecifiers []string + for _, item := range completions.Items { + if item.Label != "entity" || item.SortText == nil || *item.SortText != string(ls.SortTextAutoImportSuggestions) { + continue + } + if item.Data == nil || item.Data.AutoImport == nil { + continue + } + moduleSpecifiers = append(moduleSpecifiers, item.Data.AutoImport.ModuleSpecifier) + } + if len(moduleSpecifiers) == 0 { + t.Fatalf("No auto-import completion specifier found for 'entity'") + } + if strings.HasPrefix(moduleSpecifiers[0], "#/") { + t.Fatalf("Expected root wildcard imports key to be ignored outside nodenext/bundler, got %q", moduleSpecifiers[0]) + } +} diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 2c6d69cd739..899353d3d1b 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -1031,8 +1031,12 @@ func tryGetModuleNameFromPackageJsonImports( top := imports.AsObject() entries := top.Entries() for k, value := range entries { - // Node-style imports keys cannot start with "#/" except for the root wildcard form "#/*". - if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") && !strings.HasPrefix(k, "#/*") { + // Node-style imports keys cannot start with "#/" except for the root wildcard form + // "#/*", which we only accept in moduleResolution nodenext/bundler. + allowRootWildcardImportsKey := strings.HasPrefix(k, "#/*") && + (options.GetModuleResolutionKind() == core.ModuleResolutionKindNodeNext || + options.GetModuleResolutionKind() == core.ModuleResolutionKindBundler) + if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") && !allowRootWildcardImportsKey { continue // invalid imports entry } mode := MatchingModeExact From 4858e368c1a3310e5e81cd89c07fea6f15e3702b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 17 Mar 2026 09:41:30 -0700 Subject: [PATCH 4/5] Fix #/ imports key validation: allow all #/ keys in nodenext/bundler, not just #/* Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/modulespecifiers/specifiers.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 899353d3d1b..9ba82ef595f 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -1031,12 +1031,11 @@ func tryGetModuleNameFromPackageJsonImports( top := imports.AsObject() entries := top.Entries() for k, value := range entries { - // Node-style imports keys cannot start with "#/" except for the root wildcard form - // "#/*", which we only accept in moduleResolution nodenext/bundler. - allowRootWildcardImportsKey := strings.HasPrefix(k, "#/*") && - (options.GetModuleResolutionKind() == core.ModuleResolutionKindNodeNext || - options.GetModuleResolutionKind() == core.ModuleResolutionKindBundler) - if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") && !allowRootWildcardImportsKey { + // Keys starting with "#/" are only valid in moduleResolution nodenext/bundler. + rejectHashSlash := strings.HasPrefix(k, "#/") && + options.GetModuleResolutionKind() != core.ModuleResolutionKindNodeNext && + options.GetModuleResolutionKind() != core.ModuleResolutionKindBundler + if !strings.HasPrefix(k, "#") || k == "#" || rejectHashSlash { continue // invalid imports entry } mode := MatchingModeExact From 2ce94b2e1cf6503952bd95fef5cf01b1deec5b4e Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 17 Mar 2026 10:05:46 -0700 Subject: [PATCH 5/5] Clean up #/ imports key validation and rewrite tests - Restructure validation logic for clarity - Rewrite fourslash tests using BaselineAutoImports - Delete investigation notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...oImportPackageJsonImportsHashSlash_test.go | 61 ++++++++++ ...mport_issue2984_rootWildcardVitest_test.go | 112 ------------------ internal/modulespecifiers/specifiers.go | 9 +- .../issue-2984-autoimport-root-wildcard.md | 57 --------- ...kageJsonImportsHashSlashNode16.baseline.md | 10 ++ ...geJsonImportsHashSlashNodenext.baseline.md | 10 ++ 6 files changed, 85 insertions(+), 174 deletions(-) create mode 100644 internal/fourslash/tests/autoImportPackageJsonImportsHashSlash_test.go delete mode 100644 internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go delete mode 100644 investigations/issue-2984-autoimport-root-wildcard.md create mode 100644 testdata/baselines/reference/fourslash/autoImports/autoImportPackageJsonImportsHashSlashNode16.baseline.md create mode 100644 testdata/baselines/reference/fourslash/autoImports/autoImportPackageJsonImportsHashSlashNodenext.baseline.md diff --git a/internal/fourslash/tests/autoImportPackageJsonImportsHashSlash_test.go b/internal/fourslash/tests/autoImportPackageJsonImportsHashSlash_test.go new file mode 100644 index 00000000000..d6e79016711 --- /dev/null +++ b/internal/fourslash/tests/autoImportPackageJsonImportsHashSlash_test.go @@ -0,0 +1,61 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestAutoImportPackageJsonImportsHashSlashNodenext(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /tsconfig.json +{ + "compilerOptions": { + "module": "nodenext", + "rootDir": "./", + "outDir": "build" + } +} +// @Filename: /package.json +{ + "imports": { + "#/*": { + "types": "./src/*", + "default": "./src/*" + } + } +} +// @Filename: /src/domain/entities/entity.ts +export const entity = 1; +// @Filename: /src/features/deep/consumer.ts +entit/**/` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.BaselineAutoImportsCompletions(t, []string{""}) +} + +func TestAutoImportPackageJsonImportsHashSlashNode16(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /tsconfig.json +{ + "compilerOptions": { + "module": "node16" + } +} +// @Filename: /package.json +{ + "imports": { + "#/*": "./src/*" + } +} +// @Filename: /src/domain/entities/entity.ts +export const entity = 1; +// @Filename: /src/consumer.ts +entit/**/` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.BaselineAutoImportsCompletions(t, []string{""}) +} diff --git a/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go b/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go deleted file mode 100644 index 1bda2835052..00000000000 --- a/internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package fourslash_test - -import ( - "strings" - "testing" - - "github.com/microsoft/typescript-go/internal/fourslash" - "github.com/microsoft/typescript-go/internal/ls" - "github.com/microsoft/typescript-go/internal/ls/lsutil" - "github.com/microsoft/typescript-go/internal/modulespecifiers" - "github.com/microsoft/typescript-go/internal/testutil" -) - -func TestAutoImport_issue2984_rootWildcardVitest(t *testing.T) { - t.Parallel() - defer testutil.RecoverAndFail(t, "Panic on fourslash test") - const content = `// @Filename: /tsconfig.json -{ - "compilerOptions": { - "module": "node20", - "moduleResolution": "nodenext", - "rootDir": "./", - "outDir": "build" - } -} -// @Filename: /package.json -{ - "imports": { - "#/*": { - "vitest": "./src/*", - "types": "./src/*", - "node": "./build/*", - "default": "./src/*" - } - } -} -// @Filename: /src/domain/entities/entity.ts -export const entity = 1; -// @Filename: /feature/very/deep/path/consumer.ts -entit/**/` - f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) - defer done() - f.GoToMarker(t, "") - - verifyTopSpecifier := func(pref modulespecifiers.ImportModuleSpecifierPreference, expected string) { - completions := f.GetCompletions(t, &lsutil.UserPreferences{ImportModuleSpecifierPreference: pref}) - var moduleSpecifiers []string - for _, item := range completions.Items { - if item.Label != "entity" || item.SortText == nil || *item.SortText != string(ls.SortTextAutoImportSuggestions) { - continue - } - if item.Data == nil || item.Data.AutoImport == nil { - continue - } - moduleSpecifiers = append(moduleSpecifiers, item.Data.AutoImport.ModuleSpecifier) - } - if len(moduleSpecifiers) == 0 { - t.Fatalf("No auto-import completion specifier found for 'entity' with preference %q", pref) - } - if moduleSpecifiers[0] != expected { - t.Fatalf("Unexpected first auto-import module specifier for preference %q.\nExpected: %s\nActual: %s\nAll: %v", pref, expected, moduleSpecifiers[0], moduleSpecifiers) - } - } - - verifyTopSpecifier(modulespecifiers.ImportModuleSpecifierPreferenceShortest, "#/domain/entities/entity.js") - verifyTopSpecifier(modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, "#/domain/entities/entity.js") -} - -func TestAutoImport_issue2984_rootWildcardNotAllowedInNode16(t *testing.T) { - t.Parallel() - defer testutil.RecoverAndFail(t, "Panic on fourslash test") - const content = `// @Filename: /tsconfig.json -{ - "compilerOptions": { - "module": "node16", - "moduleResolution": "node16" - } -} -// @Filename: /package.json -{ - "imports": { - "#/*": "./src/*" - } -} -// @Filename: /src/domain/entities/entity.ts -export const entity = 1; -// @Filename: /deep/feature/path/consumer.ts -entit/**/` - f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) - defer done() - f.GoToMarker(t, "") - - completions := f.GetCompletions(t, &lsutil.UserPreferences{ - ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceShortest, - }) - var moduleSpecifiers []string - for _, item := range completions.Items { - if item.Label != "entity" || item.SortText == nil || *item.SortText != string(ls.SortTextAutoImportSuggestions) { - continue - } - if item.Data == nil || item.Data.AutoImport == nil { - continue - } - moduleSpecifiers = append(moduleSpecifiers, item.Data.AutoImport.ModuleSpecifier) - } - if len(moduleSpecifiers) == 0 { - t.Fatalf("No auto-import completion specifier found for 'entity'") - } - if strings.HasPrefix(moduleSpecifiers[0], "#/") { - t.Fatalf("Expected root wildcard imports key to be ignored outside nodenext/bundler, got %q", moduleSpecifiers[0]) - } -} diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 9ba82ef595f..9ced1e8ecd5 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -1031,13 +1031,12 @@ func tryGetModuleNameFromPackageJsonImports( top := imports.AsObject() entries := top.Entries() for k, value := range entries { - // Keys starting with "#/" are only valid in moduleResolution nodenext/bundler. - rejectHashSlash := strings.HasPrefix(k, "#/") && - options.GetModuleResolutionKind() != core.ModuleResolutionKindNodeNext && - options.GetModuleResolutionKind() != core.ModuleResolutionKindBundler - if !strings.HasPrefix(k, "#") || k == "#" || rejectHashSlash { + if k == "#" || k == "#/" || !strings.HasPrefix(k, "#") { continue // invalid imports entry } + if strings.HasPrefix(k, "#/") && options.GetModuleResolutionKind() != core.ModuleResolutionKindNodeNext && options.GetModuleResolutionKind() != core.ModuleResolutionKindBundler { + continue // "#/" imports keys are only valid in nodenext/bundler + } mode := MatchingModeExact if strings.HasSuffix(k, "/") { mode = MatchingModeDirectory diff --git a/investigations/issue-2984-autoimport-root-wildcard.md b/investigations/issue-2984-autoimport-root-wildcard.md deleted file mode 100644 index 9665d1f5ab1..00000000000 --- a/investigations/issue-2984-autoimport-root-wildcard.md +++ /dev/null @@ -1,57 +0,0 @@ -# Investigation: microsoft/typescript-go#2984 - -## Summary -Auto-import did not prioritize (or even offer) `package.json#imports` root wildcard specifiers (`#/*`) for deep files, even with `importModuleSpecifierPreference` set to `shortest`/`non-relative`. - -## Reproduction -Added fourslash test: -- `internal/fourslash/tests/autoImport_issue2984_rootWildcardVitest_test.go` - -Scenario: -- `package.json` contains: - - `"#/*": { "vitest": "./src/*", "types": "./src/*", "node": "./build/*", "default": "./src/*" }` -- Importing file at `/feature/very/deep/path/consumer.ts` -- Exported symbol at `/src/domain/entities/entity.ts` - -### Failing behavior before fix -For `ImportModuleSpecifierPreference: "shortest"`: -- **Actual top suggestion:** `../../../../src/domain/entities/entity` -- **Expected:** `#/domain/entities/entity.js` (non-relative package import alias) - -Same behavior reproduced for `ImportModuleSpecifierPreference: "non-relative"`. - -## Root cause -File: `internal/modulespecifiers/specifiers.go` -Function: `tryGetModuleNameFromPackageJsonImports` - -The imports-key validation incorrectly rejected all keys starting with `"#/"`: - -```go -if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") { - continue // invalid imports entry -} -``` - -This rejects valid root wildcard key `#/*`, so `tryGetModuleNameFromPackageJsonImports` returned `""`, forcing fallback to relative specifiers. - -## Minimal patch -Allow `#/*` while keeping other invalid `#/...` keys rejected: - -```go -if !strings.HasPrefix(k, "#") || k == "#" || strings.HasPrefix(k, "#/") && !strings.HasPrefix(k, "#/*") { - continue // invalid imports entry -} -``` - -## Validation after patch -Targeted test: - -```bash -go test ./internal/fourslash/tests -run TestAutoImport_issue2984_rootWildcardVitest -count=1 -v -``` - -Observed: -- `shortest entity specifiers: [#/domain/entities/entity.js]` -- `non-relative entity specifiers: [#/domain/entities/entity.js]` - -This confirms root wildcard package import is now preferred over deep relative import. diff --git a/testdata/baselines/reference/fourslash/autoImports/autoImportPackageJsonImportsHashSlashNode16.baseline.md b/testdata/baselines/reference/fourslash/autoImports/autoImportPackageJsonImportsHashSlashNode16.baseline.md new file mode 100644 index 00000000000..9aeec692c32 --- /dev/null +++ b/testdata/baselines/reference/fourslash/autoImports/autoImportPackageJsonImportsHashSlashNode16.baseline.md @@ -0,0 +1,10 @@ +// === Auto Imports === +```ts +// @FileName: /src/consumer.ts +entit/**/ +``````ts +import { entity } from "./domain/entities/entity"; + +entit +``` + diff --git a/testdata/baselines/reference/fourslash/autoImports/autoImportPackageJsonImportsHashSlashNodenext.baseline.md b/testdata/baselines/reference/fourslash/autoImports/autoImportPackageJsonImportsHashSlashNodenext.baseline.md new file mode 100644 index 00000000000..e7ff918d3d0 --- /dev/null +++ b/testdata/baselines/reference/fourslash/autoImports/autoImportPackageJsonImportsHashSlashNodenext.baseline.md @@ -0,0 +1,10 @@ +// === Auto Imports === +```ts +// @FileName: /src/features/deep/consumer.ts +entit/**/ +``````ts +import { entity } from "#/domain/entities/entity.js"; + +entit +``` +