From 194300759e94145faec47619fed598011eed4522 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:45:50 +0000 Subject: [PATCH 1/4] Initial plan From ca7a605ccaa9fb5879136547a923914e645d4277 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:53:38 +0000 Subject: [PATCH 2/4] Guard nil package export lookup tables Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- tsc/internal/module/resolver.go | 4 ++++ tsc/internal/module/resolver_internal_test.go | 12 ++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 tsc/internal/module/resolver_internal_test.go diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index 367884c90698a..4286b20720927 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -694,6 +694,10 @@ func (r *resolutionState) loadModuleFromExportsOrImports( scope *packagejson.InfoCacheEntry, isImports bool, ) *resolved { + if lookupTable == nil { + return continueSearching() + } + if !strings.HasSuffix(moduleName, "/") && !strings.Contains(moduleName, "*") { if target, ok := lookupTable.Get(moduleName); ok { return r.loadModuleFromTargetExportOrImport(extensions, moduleName, scope, isImports, target, "", false /*isPattern*/, moduleName) diff --git a/tsc/internal/module/resolver_internal_test.go b/tsc/internal/module/resolver_internal_test.go new file mode 100644 index 0000000000000..ff70ed664efb5 --- /dev/null +++ b/tsc/internal/module/resolver_internal_test.go @@ -0,0 +1,12 @@ +package module + +import "testing" + +func TestLoadModuleFromNilExportsOrImports(t *testing.T) { + t.Parallel() + + var state resolutionState + if result := state.loadModuleFromExportsOrImports(0, "pkg", nil, nil, false); !result.shouldContinueSearching() { + t.Fatal("expected nil exports or imports to continue searching") + } +} From 6a139dce1a7bb3de1d8ef93280db5b039c8f575f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:44:28 +0000 Subject: [PATCH 3/4] Changes before error encountered Agent-Logs-Url: https://github.com/microsoft/TypeScript/sessions/19c127c7-891c-4c63-9768-a1600188ea6c Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- tsc/internal/module/resolver_internal_test.go | 12 ----- tsc/internal/module/resolver_test.go | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) delete mode 100644 tsc/internal/module/resolver_internal_test.go diff --git a/tsc/internal/module/resolver_internal_test.go b/tsc/internal/module/resolver_internal_test.go deleted file mode 100644 index ff70ed664efb5..0000000000000 --- a/tsc/internal/module/resolver_internal_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package module - -import "testing" - -func TestLoadModuleFromNilExportsOrImports(t *testing.T) { - t.Parallel() - - var state resolutionState - if result := state.loadModuleFromExportsOrImports(0, "pkg", nil, nil, false); !result.shouldContinueSearching() { - t.Fatal("expected nil exports or imports to continue searching") - } -} diff --git a/tsc/internal/module/resolver_test.go b/tsc/internal/module/resolver_test.go index 9de8cf9c8c223..713bb0cd7906f 100644 --- a/tsc/internal/module/resolver_test.go +++ b/tsc/internal/module/resolver_test.go @@ -6,8 +6,10 @@ import ( "sync/atomic" "testing" + "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/module" + "github.com/microsoft/TypeScript/tsc/internal/packagejson" "github.com/microsoft/TypeScript/tsc/internal/vfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" ) @@ -49,6 +51,48 @@ func TestResolveModuleNameTrailingSlash(t *testing.T) { } } +func TestResolvePackageImportWithNilLookupTable(t *testing.T) { + t.Parallel() + + fs := vfstest.FromMap(map[string]string{ + "/repo/package.json": `{"imports": {}}`, + "/repo/src/file.ts": "", + }, true) + fields, err := packagejson.Parse([]byte(`{"imports": {}}`)) + if err != nil { + t.Fatal(err) + } + var imports *collections.OrderedMap[string, packagejson.ExportsOrImports] + fields.Imports.Value = imports + cache := packagejson.NewInfoCache("/repo", true) + cache.Set("/repo/package.json", &packagejson.InfoCacheEntry{ + PackageDirectory: "/repo", + DirectoryExists: true, + Contents: &packagejson.PackageJson{ + Fields: fields, + Parseable: true, + }, + }) + opts := &core.CompilerOptions{ + ModuleResolution: core.ModuleResolutionKindNodeNext, + Module: core.ModuleKindNodeNext, + Target: core.ScriptTargetESNext, + } + resolver := module.NewResolver(module.ResolverOptions{ + Host: &resolutionHostStub{fs: fs, cwd: "/repo"}, + CompilerOptions: opts, + PackageJsonCache: cache, + }) + + resolved, _, err := resolver.ResolveModuleName("#missing", "/repo/src/file.ts", core.ModuleKindESNext, nil) + if err != nil { + t.Fatal(err) + } + if resolved.IsResolved() { + t.Fatalf("expected #missing to be unresolved, got %q", resolved.ResolvedFileName) + } +} + // blockingFS wraps a vfs.FS and forces FileExists calls for `targetPath` to // block on `gate` until released. Each caller sends on `arrived` when it // reaches the gate. This is used to deterministically reproduce the From cb5755f1bbf9631e035676e7bb9899f69247425d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:04:14 +0000 Subject: [PATCH 4/4] Replace synthetic resolver test with compiler regression Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- tsc/internal/module/resolver_test.go | 44 ------------------- .../packageJsonExportsTrailingSlashNoCrash.ts | 23 ++++++++++ 2 files changed, 23 insertions(+), 44 deletions(-) create mode 100644 tsc/testdata/tests/cases/compiler/packageJsonExportsTrailingSlashNoCrash.ts diff --git a/tsc/internal/module/resolver_test.go b/tsc/internal/module/resolver_test.go index 713bb0cd7906f..9de8cf9c8c223 100644 --- a/tsc/internal/module/resolver_test.go +++ b/tsc/internal/module/resolver_test.go @@ -6,10 +6,8 @@ import ( "sync/atomic" "testing" - "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/module" - "github.com/microsoft/TypeScript/tsc/internal/packagejson" "github.com/microsoft/TypeScript/tsc/internal/vfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" ) @@ -51,48 +49,6 @@ func TestResolveModuleNameTrailingSlash(t *testing.T) { } } -func TestResolvePackageImportWithNilLookupTable(t *testing.T) { - t.Parallel() - - fs := vfstest.FromMap(map[string]string{ - "/repo/package.json": `{"imports": {}}`, - "/repo/src/file.ts": "", - }, true) - fields, err := packagejson.Parse([]byte(`{"imports": {}}`)) - if err != nil { - t.Fatal(err) - } - var imports *collections.OrderedMap[string, packagejson.ExportsOrImports] - fields.Imports.Value = imports - cache := packagejson.NewInfoCache("/repo", true) - cache.Set("/repo/package.json", &packagejson.InfoCacheEntry{ - PackageDirectory: "/repo", - DirectoryExists: true, - Contents: &packagejson.PackageJson{ - Fields: fields, - Parseable: true, - }, - }) - opts := &core.CompilerOptions{ - ModuleResolution: core.ModuleResolutionKindNodeNext, - Module: core.ModuleKindNodeNext, - Target: core.ScriptTargetESNext, - } - resolver := module.NewResolver(module.ResolverOptions{ - Host: &resolutionHostStub{fs: fs, cwd: "/repo"}, - CompilerOptions: opts, - PackageJsonCache: cache, - }) - - resolved, _, err := resolver.ResolveModuleName("#missing", "/repo/src/file.ts", core.ModuleKindESNext, nil) - if err != nil { - t.Fatal(err) - } - if resolved.IsResolved() { - t.Fatalf("expected #missing to be unresolved, got %q", resolved.ResolvedFileName) - } -} - // blockingFS wraps a vfs.FS and forces FileExists calls for `targetPath` to // block on `gate` until released. Each caller sends on `arrived` when it // reaches the gate. This is used to deterministically reproduce the diff --git a/tsc/testdata/tests/cases/compiler/packageJsonExportsTrailingSlashNoCrash.ts b/tsc/testdata/tests/cases/compiler/packageJsonExportsTrailingSlashNoCrash.ts new file mode 100644 index 0000000000000..42a618f755d34 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/packageJsonExportsTrailingSlashNoCrash.ts @@ -0,0 +1,23 @@ +// @module: nodenext +// @moduleResolution: nodenext +// @noEmit: true +// @noTypesAndSymbols: true + +// @filename: /node_modules/pkg/package.json +{ + "name": "pkg", + "exports": { + "./": "./" + } +} + +// @filename: /node_modules/pkg/subpath.d.ts +export declare const value: number; + +// @filename: /src/a.ts +import { value } from "pkg/subpath.js"; +value; + +// @filename: /src/b.ts +import { value } from "pkg/subpath.js"; +value;