Skip to content

Commit 3a58df7

Browse files
Copilotandrewbranch
andcommitted
Fix module resolver bug exposed by malformed package.json import patterns (#2680)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> Co-authored-by: Andrew Branch <andrew@wheream.io>
1 parent 061c7d5 commit 3a58df7

4 files changed

Lines changed: 90 additions & 2 deletions

File tree

internal/checker/checker.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14822,7 +14822,18 @@ func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference stri
1482214822
if ast.FindAncestor(location, ast.IsEmittableImport) != nil {
1482314823
tsExtension := tspath.TryExtractTSExtension(moduleReference)
1482414824
if tsExtension == "" {
14825-
panic("should be able to extract TS extension from string that passes IsDeclarationFileName")
14825+
// Fallback: do a best-effort extraction using strings.Contains.
14826+
// This handles cases where a wildcard pattern matches a TS extension that's
14827+
// not at the end of the module specifier, e.g., "#/foo.ts.omg" through "#/*.omg": "./src/*"
14828+
for _, ext := range tspath.SupportedTSExtensionsFlat {
14829+
if strings.Contains(moduleReference, ext) {
14830+
tsExtension = ext
14831+
break
14832+
}
14833+
}
14834+
}
14835+
if tsExtension == "" {
14836+
panic("should be able to extract TS extension from string when resolvedUsingTsExtension is true")
1482614837
}
1482714838
c.error(
1482814839
errorNode,

internal/module/resolver.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1601,10 +1601,17 @@ func (r *resolutionState) loadFileNameFromPackageJSONField(extensions extensions
16011601
if extensions&extensionsTypeScript != 0 && tspath.HasImplementationTSFileExtension(candidate) || extensions&extensionsDeclaration != 0 && tspath.IsDeclarationFileName(candidate) {
16021602
if path, ok := r.tryFile(candidate, onlyRecordFailures); ok {
16031603
extension := tspath.TryExtractTSExtension(path)
1604+
// resolvedUsingTsExtension should be true when the pattern ends with * and the
1605+
// candidate file ends in a TS extension. This means the * matched a TS extension
1606+
// from the module specifier. For example:
1607+
// - import "pkg/foo.ts" with pattern "./*" -> true
1608+
// - import "pkg/foo.ts.omg" with pattern "./*.omg" -> true (star matched .ts)
1609+
// - import "pkg/foo" with pattern "./*.ts" -> false (extension in pattern, not specifier)
1610+
resolvedUsingTsExtension := strings.HasSuffix(packageJSONValue, "*") && extension != ""
16041611
return &resolved{
16051612
path: path,
16061613
extension: extension,
1607-
resolvedUsingTsExtension: packageJSONValue != "" && !strings.HasSuffix(packageJSONValue, extension),
1614+
resolvedUsingTsExtension: resolvedUsingTsExtension,
16081615
}
16091616
}
16101617
return continueSearching()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// @noEmit: true
2+
// @noTypesAndSymbols: true
3+
// @module: nodenext
4+
// @moduleResolution: nodenext
5+
// @filename: src/a.ts
6+
import * as b from "#/b.";
7+
8+
b.foo();
9+
10+
// @filename: src/b.ts
11+
export function foo() {}
12+
13+
// @filename: package.json
14+
{
15+
"imports": {
16+
"#/*": {
17+
"types": "./src/*ts",
18+
"default": "./dist/*js"
19+
}
20+
}
21+
}
22+
23+
// @filename: tsconfig.json
24+
{
25+
"compilerOptions": {
26+
"module": "nodenext",
27+
"moduleResolution": "nodenext",
28+
"rootDir": "src",
29+
"outDir": "dist"
30+
},
31+
"include": ["src"]
32+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// @noEmit: true
2+
// @noTypesAndSymbols: true
3+
// @module: nodenext
4+
// @moduleResolution: nodenext
5+
// Test verifies that when a module specifier contains ".ts" that gets matched by a
6+
// wildcard pattern, resolvedUsingTsExtension is correctly set to true.
7+
// Example: import "#/foo.ts.omg" with pattern "#/*.omg": "./src/*"
8+
// The * matches "foo.ts", and when expanded becomes "./src/foo.ts"
9+
// Since the wildcard matched ".ts" from the specifier, an error should be reported.
10+
11+
// @filename: src/foo.ts
12+
export function hello() {
13+
return "world";
14+
}
15+
16+
// @filename: src/index.ts
17+
import { hello } from "#/foo.ts.omg";
18+
19+
hello();
20+
21+
// @filename: package.json
22+
{
23+
"type": "module",
24+
"imports": {
25+
"#/*.omg": "./src/*"
26+
}
27+
}
28+
29+
// @filename: tsconfig.json
30+
{
31+
"compilerOptions": {
32+
"module": "nodenext",
33+
"moduleResolution": "nodenext",
34+
"rootDir": "src",
35+
"outDir": "dist"
36+
},
37+
"include": ["src"]
38+
}

0 commit comments

Comments
 (0)