Skip to content

Commit e66f236

Browse files
gwklineclaude
andcommitted
Cache exports and paths lookups when generating module specifiers
Generating a specifier for a file in a package with an "exports" map scanned every entry of the map, and lookups through a "paths" or "typesVersions" table resolved every pattern against the table's base directory and recomputed the candidate specifiers for each one. The nodebuilder caches only the final specifier per importing file, so declaration emit repeated these scans for every file that referenced the same target, which dominated emit on builds with large exports maps or paths tables. - ModuleSpecifierGenerationHost gains ModuleSpecifierGenerationCache, which may return nil. Program owns a GenerationCache and its emit host forwards to it. The cache memoizes the subpath an "exports" map publishes a target under, keyed by the parsed package.json, package directory and name, target file, resolution mode and compiler options, and the resolved patterns of each paths table, keyed by the table and its base directory. - Candidate specifiers, and the target's emitted file names for "exports" and "imports" lookups, are computed once per lookup instead of once per entry; an entry's specifier is built only when it matches. - GetVersionPaths returns VersionPaths by value and GetPaths built the table lazily on each copy, so module resolution rebuilt it on every resolution and it had no stable identity to cache by. It is now built once when the version paths are selected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent aa81492 commit e66f236

11 files changed

Lines changed: 622 additions & 143 deletions

File tree

‎tsc/internal/compiler/emitHost.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/microsoft/TypeScript/tsc/internal/checker"
88
"github.com/microsoft/TypeScript/tsc/internal/core"
99
"github.com/microsoft/TypeScript/tsc/internal/module"
10+
"github.com/microsoft/TypeScript/tsc/internal/modulespecifiers"
1011
"github.com/microsoft/TypeScript/tsc/internal/outputpaths"
1112
"github.com/microsoft/TypeScript/tsc/internal/packagejson"
1213
"github.com/microsoft/TypeScript/tsc/internal/printer"
@@ -76,6 +77,10 @@ func (host *emitHost) GetPackageJsonInfo(pkgJsonPath tspath.RootedFilePath) *pac
7677
return host.program.GetPackageJsonInfo(pkgJsonPath)
7778
}
7879

80+
func (host *emitHost) ModuleSpecifierGenerationCache() *modulespecifiers.GenerationCache {
81+
return host.program.ModuleSpecifierGenerationCache()
82+
}
83+
7984
func (host *emitHost) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) tspath.RootedFilePath {
8085
return host.program.GetSourceOfProjectReferenceIfOutputIncluded(file)
8186
}

‎tsc/internal/compiler/program.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ type Program struct {
118118

119119
declarationDiagnosticCache collections.SyncMap[*ast.SourceFile, []*ast.Diagnostic]
120120

121+
moduleSpecifierGenerationCache modulespecifiers.GenerationCache
122+
121123
programDiagnostics []*ast.Diagnostic
122124
hasEmitBlockingDiagnostics collections.Set[tspath.PathKey]
123125
contentMapperOptionDiagnostics []*ast.Diagnostic
@@ -173,6 +175,11 @@ func (p *Program) GetNearestAncestorDirectoryWithPackageJson(dirname tspath.Root
173175
return ""
174176
}
175177

178+
// ModuleSpecifierGenerationCache implements modulespecifiers.ModuleSpecifierGenerationHost.
179+
func (p *Program) ModuleSpecifierGenerationCache() *modulespecifiers.GenerationCache {
180+
return &p.moduleSpecifierGenerationCache
181+
}
182+
176183
// GetPackageJsonInfo implements checker.Program.
177184
func (p *Program) GetPackageJsonInfo(pkgJsonPath tspath.RootedFilePath) *packagejson.InfoCacheEntry {
178185
directory := pkgJsonPath.Directory()

‎tsc/internal/ls/autoimport/aliasresolver.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/microsoft/TypeScript/tsc/internal/collections"
88
"github.com/microsoft/TypeScript/tsc/internal/core"
99
"github.com/microsoft/TypeScript/tsc/internal/module"
10+
"github.com/microsoft/TypeScript/tsc/internal/modulespecifiers"
1011
"github.com/microsoft/TypeScript/tsc/internal/packagejson"
1112
"github.com/microsoft/TypeScript/tsc/internal/symlinks"
1213
"github.com/microsoft/TypeScript/tsc/internal/tsoptions"
@@ -186,6 +187,12 @@ func (r *aliasResolver) GetJSXRuntimeImportSpecifier(path tspath.PathKey) (modul
186187
return "", nil
187188
}
188189

190+
// ModuleSpecifierGenerationCache implements checker.Program. Module specifier generation needs host
191+
// methods the alias resolver does not implement, so it keeps no cache.
192+
func (r *aliasResolver) ModuleSpecifierGenerationCache() *modulespecifiers.GenerationCache {
193+
return nil
194+
}
195+
189196
// GetNearestAncestorDirectoryWithPackageJson implements checker.Program.
190197
func (r *aliasResolver) GetNearestAncestorDirectoryWithPackageJson(dirname tspath.RootedDirectoryPath) tspath.RootedDirectoryPath {
191198
panic("unimplemented")
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package modulespecifiers
2+
3+
import (
4+
"strings"
5+
6+
"github.com/microsoft/TypeScript/tsc/internal/collections"
7+
"github.com/microsoft/TypeScript/tsc/internal/core"
8+
"github.com/microsoft/TypeScript/tsc/internal/module"
9+
"github.com/microsoft/TypeScript/tsc/internal/packagejson"
10+
"github.com/microsoft/TypeScript/tsc/internal/tspath"
11+
)
12+
13+
// GenerationCache holds results that a host reuses across module specifier generation. Finding the
14+
// subpath that a package.json "exports" map assigns to a file, and resolving the patterns of a "paths"
15+
// or "typesVersions" table, each scan a whole table, and the same table is consulted once per file that
16+
// needs a specifier, so hosts that generate many specifiers keep one of these. Entries are keyed by the
17+
// table they were derived from and the compiler options, so neither a re-read package.json nor a
18+
// different set of options is served stale results. The zero value is ready to use and safe for
19+
// concurrent use.
20+
type GenerationCache struct {
21+
exports collections.SyncMap[packageExportsSpecifierKey, tspath.ModuleSpecifier]
22+
pathPatterns collections.SyncMap[pathPatternsKey, []pathPattern]
23+
}
24+
25+
type packageExportsSpecifierKey struct {
26+
options *core.CompilerOptions // conditions, custom conditions, and output extensions
27+
packageJson *packagejson.PackageJson
28+
packageDirectory tspath.RootedDirectoryPath
29+
packageName string
30+
targetFileName tspath.RootedFilePath
31+
mode core.ResolutionMode
32+
}
33+
34+
// getModuleNameFromExports returns the specifier under which packageJson's "exports" map publishes
35+
// targetFileName, or "" when it is not exported, consulting the host's cache when it has one.
36+
func getModuleNameFromExports(
37+
options *core.CompilerOptions,
38+
host ModuleSpecifierGenerationHost,
39+
targetFileName tspath.RootedFilePath,
40+
packageDirectory tspath.RootedDirectoryPath,
41+
packageName string,
42+
packageJson *packagejson.PackageJson,
43+
mode core.ResolutionMode,
44+
) tspath.ModuleSpecifier {
45+
cache := host.ModuleSpecifierGenerationCache()
46+
if cache == nil {
47+
return tryGetModuleNameFromExports(options, host, targetFileName, packageDirectory, packageName, packageJson.Fields.Exports, module.GetConditions(options, mode))
48+
}
49+
key := packageExportsSpecifierKey{
50+
options: options,
51+
packageJson: packageJson,
52+
packageDirectory: packageDirectory,
53+
packageName: packageName,
54+
targetFileName: targetFileName,
55+
mode: mode,
56+
}
57+
if specifier, ok := cache.exports.Load(key); ok {
58+
return specifier
59+
}
60+
specifier := tryGetModuleNameFromExports(options, host, targetFileName, packageDirectory, packageName, packageJson.Fields.Exports, module.GetConditions(options, mode))
61+
cache.exports.Store(key, specifier)
62+
return specifier
63+
}
64+
65+
type pathPatternsKey struct {
66+
paths *collections.OrderedMap[string, []string]
67+
baseDirectory tspath.RootedDirectoryPath
68+
}
69+
70+
// pathPattern is one pattern of a "paths" or "typesVersions" table, resolved against the table's base
71+
// directory and split at its wildcard.
72+
type pathPattern struct {
73+
key string // the table key the pattern belongs to
74+
pattern string
75+
prefix string // the pattern before its "*", or the whole pattern when it has none
76+
suffix string // the pattern after its "*"
77+
hasWildcard bool
78+
hasExtension bool
79+
}
80+
81+
// getPathPatterns returns the patterns of paths in table order, resolved against baseDirectory. They
82+
// depend only on the table, so they are resolved once per host when the host keeps a cache. The cache
83+
// is keyed by the table's identity, so paths must be a table that lives as long as its source: the
84+
// compiler options' "paths", or the "typesVersions" table of a parsed package.json.
85+
func getPathPatterns(host ModuleSpecifierGenerationHost, paths *collections.OrderedMap[string, []string], baseDirectory tspath.RootedDirectoryPath) []pathPattern {
86+
cache := host.ModuleSpecifierGenerationCache()
87+
if cache == nil {
88+
return resolvePathPatterns(paths, baseDirectory, host.CaseSensitivity())
89+
}
90+
key := pathPatternsKey{paths: paths, baseDirectory: baseDirectory}
91+
if patterns, ok := cache.pathPatterns.Load(key); ok {
92+
return patterns
93+
}
94+
patterns, _ := cache.pathPatterns.LoadOrStore(key, resolvePathPatterns(paths, baseDirectory, host.CaseSensitivity()))
95+
return patterns
96+
}
97+
98+
func resolvePathPatterns(paths *collections.OrderedMap[string, []string], baseDirectory tspath.RootedDirectoryPath, caseSensitivity tspath.CaseSensitivity) []pathPattern {
99+
patterns := make([]pathPattern, 0, paths.Size())
100+
for key, values := range paths.Entries() {
101+
for _, patternText := range values {
102+
normalized := tspath.NormalizePath(patternText)
103+
pattern := resolvePathPatternIfInSameVolume(normalized, baseDirectory, caseSensitivity)
104+
if len(pattern) == 0 {
105+
pattern = normalized
106+
}
107+
prefix, suffix, hasWildcard := strings.Cut(pattern, "*")
108+
patterns = append(patterns, pathPattern{
109+
key: key,
110+
pattern: pattern,
111+
prefix: prefix,
112+
suffix: suffix,
113+
hasWildcard: hasWildcard,
114+
hasExtension: len(tspath.TryGetExtensionFromPath(pattern)) > 0,
115+
})
116+
}
117+
}
118+
return patterns
119+
}

0 commit comments

Comments
 (0)