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
11 changes: 10 additions & 1 deletion tsc/internal/compiler/fileloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ type fileLoader struct {
pathForLibFileCache collections.SyncMap[string, *LibFile]
pathForLibFileResolutions collections.SyncMap[tspath.Path, *libResolution]

// toPath is called repeatedly for the same file names (once per import edge);
// caching avoids re-normalizing and re-lowercasing the same path.
toPathCache collections.SyncMap[string, tspath.Path]

// contentMapperMu guards the content-mapper bookkeeping below, which is written concurrently as
// content-mapped files are parsed across worker goroutines.
contentMapperMu sync.Mutex
Expand Down Expand Up @@ -213,7 +217,12 @@ func processAllProgramFiles(
}

func (p *fileLoader) toPath(file string) tspath.Path {
return tspath.ToPath(file, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames())
if path, ok := p.toPathCache.Load(file); ok {
return path
}
path := tspath.ToPath(file, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames())
p.toPathCache.Store(file, path)
return path
}

func (p *fileLoader) addRootTask(fileName string, libFile *LibFile, includeReason *FileIncludeReason) {
Expand Down
50 changes: 40 additions & 10 deletions tsc/internal/module/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,14 @@ func newResolutionState(
case core.ModuleResolutionKindNode16:
state.features = NodeResolutionFeaturesNode16Default
state.esmMode = resolutionMode == core.ModuleKindESNext
state.conditions = GetConditions(compilerOptions, resolutionMode)
state.conditions = resolver.getConditions(compilerOptions, resolutionMode)
case core.ModuleResolutionKindNodeNext:
state.features = NodeResolutionFeaturesNodeNextDefault
state.esmMode = resolutionMode == core.ModuleKindESNext
state.conditions = GetConditions(compilerOptions, resolutionMode)
state.conditions = resolver.getConditions(compilerOptions, resolutionMode)
case core.ModuleResolutionKindBundler:
state.features = getNodeResolutionFeatures(compilerOptions)
state.conditions = GetConditions(compilerOptions, resolutionMode)
state.conditions = resolver.getConditions(compilerOptions, resolutionMode)
}
return state
}
Expand All @@ -161,6 +161,29 @@ type Resolver struct {
projectName string
extraExtensions []string
// reportDiagnostic: DiagnosticReporter

// Conditions depend only on (options, import/require), so they are computed
// once for the resolver's own options; project reference redirects (rare)
// fall back to GetConditions. These slices are shared across all resolutions
// of this resolver and must be treated as read-only.
esmConditions []string
cjsConditions []string
}

func (r *Resolver) initConditionCaches() {
// Clip so that an accidental append by a consumer cannot overwrite the shared backing array.
r.esmConditions = slices.Clip(GetConditions(r.compilerOptions, core.ModuleKindESNext))
r.cjsConditions = slices.Clip(GetConditions(r.compilerOptions, core.ModuleKindCommonJS))
}

func (r *Resolver) getConditions(options *core.CompilerOptions, resolutionMode core.ResolutionMode) []string {
if options != r.compilerOptions {
return GetConditions(options, resolutionMode)
}
if conditionsUseImport(options, resolutionMode) {
return r.esmConditions
}
return r.cjsConditions
}

type ResolverOptions struct {
Expand All @@ -174,14 +197,16 @@ func NewResolver(
projectName string,
extraExtensions []string,
) *Resolver {
return &Resolver{
r := &Resolver{
host: host,
caches: newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), options),
compilerOptions: options,
typingsLocation: typingsLocation,
projectName: projectName,
extraExtensions: extraExtensions,
}
r.initConditionCaches()
return r
}

func NewResolverWithOptions(
Expand All @@ -197,6 +222,7 @@ func NewResolverWithOptions(
typingsLocation: typingsLocation,
projectName: projectName,
}
r.initConditionCaches()
if opts.PackageJsonCache != nil {
r.packageJsonInfoCache = opts.PackageJsonCache
} else {
Expand Down Expand Up @@ -1935,13 +1961,17 @@ func (r *resolutionState) getTraceFunc() func(m *diagnostics.Message, args ...an
return nil
}

// conditionsUseImport reports whether GetConditions yields the "import" condition
// (as opposed to "require") for this options/mode pair. Single source of truth for
// the mode classification, shared with Resolver.getConditions.
func conditionsUseImport(options *core.CompilerOptions, resolutionMode core.ResolutionMode) bool {
return resolutionMode == core.ModuleKindESNext ||
(resolutionMode == core.ModuleKindNone && options.GetModuleResolutionKind() == core.ModuleResolutionKindBundler)
}

func GetConditions(options *core.CompilerOptions, resolutionMode core.ResolutionMode) []string {
moduleResolution := options.GetModuleResolutionKind()
if resolutionMode == core.ModuleKindNone && moduleResolution == core.ModuleResolutionKindBundler {
resolutionMode = core.ModuleKindESNext
}
conditions := make([]string, 0, 3+len(options.CustomConditions))
if resolutionMode == core.ModuleKindESNext {
if conditionsUseImport(options, resolutionMode) {
conditions = append(conditions, "import")
} else {
conditions = append(conditions, "require")
Expand All @@ -1950,7 +1980,7 @@ func GetConditions(options *core.CompilerOptions, resolutionMode core.Resolution
if options.NoDtsResolution != core.TSTrue {
conditions = append(conditions, "types")
}
if moduleResolution != core.ModuleResolutionKindBundler {
if options.GetModuleResolutionKind() != core.ModuleResolutionKindBundler {
conditions = append(conditions, "node")
}
conditions = core.Concatenate(conditions, options.CustomConditions)
Expand Down
35 changes: 35 additions & 0 deletions tsc/internal/module/resolver_conditions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package module

import (
"slices"
"testing"

"github.com/microsoft/TypeScript/tsc/internal/core"
)

// The cached esm/cjs conditions must stay equivalent to GetConditions for every
// resolution mode, since getConditions re-derives the import/require selection.
func TestGetConditionsCacheMatchesGetConditions(t *testing.T) {
t.Parallel()

optionsList := []*core.CompilerOptions{
{ModuleResolution: core.ModuleResolutionKindNode16},
{ModuleResolution: core.ModuleResolutionKindNodeNext},
{ModuleResolution: core.ModuleResolutionKindBundler},
{ModuleResolution: core.ModuleResolutionKindBundler, NoDtsResolution: core.TSTrue},
{ModuleResolution: core.ModuleResolutionKindNode16, CustomConditions: []string{"custom1", "custom2"}},
}
modes := []core.ResolutionMode{core.ModuleKindNone, core.ModuleKindCommonJS, core.ModuleKindESNext}

for _, options := range optionsList {
r := &Resolver{compilerOptions: options}
r.initConditionCaches()
for _, mode := range modes {
got := r.getConditions(options, mode)
want := GetConditions(options, mode)
if !slices.Equal(got, want) {
t.Errorf("options=%+v mode=%v: getConditions=%v, GetConditions=%v", options, mode, got, want)
}
}
}
}