Skip to content

Commit 5ab0ce4

Browse files
committed
refactor source-phase resolution and reuse cached typings
1 parent a5ddba4 commit 5ab0ce4

8 files changed

Lines changed: 215 additions & 42 deletions

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,9 +1477,7 @@ func (c *Checker) mergeModuleAugmentation(moduleName *ast.Node) {
14771477
// the pattern ('*.foo'), so that 'getMergedSymbol()' on a.foo gives you
14781478
// all the exports both from the pattern and from the augmentation, but
14791479
// 'getMergedSymbol()' on *.foo only gives you exports from *.foo.
1480-
if core.Some(c.patternAmbientModules, func(module *ast.PatternAmbientModule) bool {
1481-
return mainModule == c.getMergedSymbol(module.Symbol)
1482-
}) {
1480+
if c.isPatternAmbientModuleSymbol(mainModule) {
14831481
merged := c.mergeSymbol(moduleAugmentation.Symbol, mainModule, true /*unidirectional*/)
14841482
// moduleName will be a StringLiteral since this is not `declare global`.
14851483
ast.GetSymbolTable(&c.patternAmbientModuleAugmentations)[moduleName.Text()] = merged

‎tsc/internal/compiler/fileloader.go‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -906,15 +906,19 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) {
906906
continue
907907
}
908908
if phase == module.ImportPhaseSource && module.IsResolvedModuleForJavaScriptSource(resolvedModule) {
909-
typeResolution, typeTrace, err := p.resolver.ResolveModuleNameWithPhase(moduleName, fileName, mode, module.ImportPhaseEvaluation, redirect)
910-
if err != nil {
911-
p.moduleResolutionErrorOnce.Do(func() { p.moduleResolutionError = err })
912-
}
913-
if typeResolution == nil {
914-
typeResolution = &module.ResolvedModule{}
909+
key := module.ModeAwareCacheKey{Name: moduleName, Mode: mode, Phase: module.ImportPhaseEvaluation}
910+
typeResolution := resolutionsInFile[key]
911+
if typeResolution == nil || len(resolutionsTrace) != 0 {
912+
typeResolution, trace, err = p.resolver.ResolveModuleNameWithPhase(moduleName, fileName, mode, module.ImportPhaseEvaluation, redirect)
913+
if err != nil {
914+
p.moduleResolutionErrorOnce.Do(func() { p.moduleResolutionError = err })
915+
}
916+
if typeResolution == nil {
917+
typeResolution = &module.ResolvedModule{}
918+
}
919+
resolutionsInFile[key] = typeResolution
920+
resolutionsTrace = append(resolutionsTrace, trace...)
915921
}
916-
resolutionsInFile[module.ModeAwareCacheKey{Name: moduleName, Mode: mode, Phase: module.ImportPhaseEvaluation}] = typeResolution
917-
resolutionsTrace = append(resolutionsTrace, typeTrace...)
918922
if typeResolution.IsResolved() {
919923
resolvedModule = typeResolution
920924
}

‎tsc/internal/compiler/program_test.go‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88
"slices"
99
"strings"
10+
"sync"
1011
"testing"
1112

1213
"github.com/microsoft/TypeScript/tsc/internal/ast"
@@ -577,6 +578,97 @@ import type { Value } from "f" with { "resolution-mode": "require" };`,
577578
assert.DeepEqual(t, program.UnresolvedPackageNames().Keys(), map[string]struct{}{"missing": {}, "evaluation-missing": {}})
578579
}
579580

581+
type moduleResolutionRecorder struct {
582+
module.Resolver
583+
mu sync.Mutex
584+
requests []module.ModeAwareCacheKey
585+
unresolvedTypes bool
586+
}
587+
588+
func (r *moduleResolutionRecorder) ResolveModuleNameWithPhase(name, file string, mode core.ResolutionMode, phase module.ImportPhase, redirect module.ResolvedProjectReference) (*module.ResolvedModule, []module.DiagAndArgs, error) {
589+
r.mu.Lock()
590+
r.requests = append(r.requests, module.ModeAwareCacheKey{Name: name, Mode: mode, Phase: phase})
591+
r.mu.Unlock()
592+
if r.unresolvedTypes && phase == module.ImportPhaseEvaluation {
593+
return nil, nil, nil
594+
}
595+
return r.Resolver.ResolveModuleNameWithPhase(name, file, mode, phase, redirect)
596+
}
597+
598+
func TestProgramModuleResolutionReuse(t *testing.T) {
599+
t.Parallel()
600+
for _, test := range []struct {
601+
name string
602+
prefix string
603+
trace core.Tristate
604+
resolverTrace core.Tristate
605+
unresolvedTypes bool
606+
evaluationCalls int
607+
requireCalls int
608+
}{
609+
{name: "source", evaluationCalls: 1},
610+
{name: "mixed", prefix: `import "./a.js";`, evaluationCalls: 1},
611+
{name: "modes", prefix: `import type { Value } from "./a.js" with { "resolution-mode": "require" };`, evaluationCalls: 1, requireCalls: 1},
612+
{name: "unresolved", unresolvedTypes: true, evaluationCalls: 1},
613+
{name: "trace", trace: core.TSTrue, evaluationCalls: 2},
614+
{name: "resolver trace", resolverTrace: core.TSTrue, evaluationCalls: 2},
615+
} {
616+
t.Run(test.name, func(t *testing.T) {
617+
t.Parallel()
618+
fs := vfstest.FromMap(map[string]string{
619+
"/src/index.ts": test.prefix + `
620+
import source a from "./a.js";
621+
import.source("./a.js");`,
622+
"/src/a.js": "export const value = 1;",
623+
"/src/a.d.ts": "export declare const value: number; export interface Value {}",
624+
}, true)
625+
var resolver *moduleResolutionRecorder
626+
program := NewProgram(ProgramOptions{
627+
Config: &tsoptions.ParsedCommandLine{
628+
ParsedConfig: &tsoptions.ParsedOptions{
629+
FileNames: []string{"/src/index.ts"},
630+
CompilerOptions: &core.CompilerOptions{
631+
Module: core.ModuleKindESNext,
632+
ModuleResolution: core.ModuleResolutionKindBundler,
633+
NoLib: core.TSTrue,
634+
AllowJs: core.TSTrue,
635+
TraceResolution: test.trace,
636+
},
637+
},
638+
},
639+
Host: NewCompilerHost("/src", fs, "", nil, nil, nil),
640+
CreateModuleResolver: func(options module.ResolverOptions) module.Resolver {
641+
if test.resolverTrace.IsTrue() {
642+
options.CompilerOptions = options.CompilerOptions.Clone()
643+
options.CompilerOptions.TraceResolution = test.resolverTrace
644+
}
645+
resolver = &moduleResolutionRecorder{Resolver: module.NewResolver(options), unresolvedTypes: test.unresolvedTypes}
646+
return resolver
647+
},
648+
})
649+
counts := make(map[module.ModeAwareCacheKey]int)
650+
for _, request := range resolver.requests {
651+
counts[request]++
652+
}
653+
assert.Equal(t, counts[module.ModeAwareCacheKey{Name: "./a.js", Mode: core.ResolutionModeESM, Phase: module.ImportPhaseSource}], 2)
654+
assert.Equal(t, counts[module.ModeAwareCacheKey{Name: "./a.js", Mode: core.ResolutionModeESM}], test.evaluationCalls)
655+
assert.Equal(t, counts[module.ModeAwareCacheKey{Name: "./a.js", Mode: core.ResolutionModeCommonJS}], test.requireCalls)
656+
file := program.GetSourceFile("/src/index.ts")
657+
for _, entry := range file.Imports() {
658+
if module.GetImportPhaseForUsage(entry) == module.ImportPhaseSource {
659+
assert.Equal(t, program.GetResolvedModuleFromModuleSpecifier(file, entry).ResolvedFileName, "/src/a.js")
660+
}
661+
}
662+
assert.Equal(t, program.GetResolvedModule(file, "./a.js", core.ResolutionModeESM).IsResolved(), !test.unresolvedTypes)
663+
if test.unresolvedTypes {
664+
assert.Assert(t, program.GetSourceFile("/src/a.js") != nil)
665+
} else {
666+
assert.Assert(t, program.GetSourceFile("/src/a.d.ts") != nil)
667+
}
668+
})
669+
}
670+
}
671+
580672
func BenchmarkNewProgram(b *testing.B) {
581673
if !bundled.Embedded {
582674
// Without embedding, we'd need to read all of the lib files out from disk into the MapFS.

‎tsc/internal/fourslash/tests/completions_sourcePhase_test.go‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,53 @@ import("%[3]s/*evaluation*/");`, test.config, test.directory, test.prefix)
185185
}
186186
}
187187

188+
func TestSourcePhaseImportCustomConditionCompletions(t *testing.T) {
189+
t.Parallel()
190+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
191+
const content = `// @module: esnext
192+
// @moduleResolution: bundler
193+
// @customConditions: types
194+
// @target: esnext
195+
196+
// @filename: /node_modules/pkg/package.json
197+
{"exports":{"./*":{"types@>=0":"./a/*.d.ts","types":"./b/*.wasm","default":"./c/*.wasm"}}}
198+
199+
// @filename: /node_modules/pkg/a/a.d.ts
200+
export {};
201+
202+
// @filename: /node_modules/pkg/b/b.wasm
203+
wasm
204+
205+
// @filename: /node_modules/pkg/c/c.wasm
206+
wasm
207+
208+
// @filename: /src/index.ts
209+
import source a from "pkg//*source*/";
210+
import.source("pkg//*dynamic*/");
211+
import("pkg//*evaluation*/");`
212+
f, done := fourslash.NewFourslash(t, nil, content)
213+
defer done()
214+
for _, marker := range []string{"source", "dynamic", "evaluation"} {
215+
includes := []fourslash.CompletionsExpectedItem{"b"}
216+
excludes := []string{"a", "c"}
217+
if marker == "evaluation" {
218+
includes = []fourslash.CompletionsExpectedItem{"a"}
219+
excludes = []string{"b", "c"}
220+
}
221+
f.VerifyCompletions(t, marker, &fourslash.CompletionsExpectedList{
222+
IsIncomplete: false,
223+
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
224+
CommitCharacters: &[]string{},
225+
EditRange: Ignored,
226+
},
227+
Items: &fourslash.CompletionsExpectedItems{
228+
Includes: includes,
229+
Excludes: excludes,
230+
},
231+
})
232+
}
233+
}
234+
188235
func TestSourcePhaseImportPackageCompletionsIgnoreTypesVersions(t *testing.T) {
189236
t.Parallel()
190237
defer testutil.RecoverAndFail(t, "Panic on fourslash test")

‎tsc/internal/ls/string_completions.go‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ func (l *LanguageService) getCompletionEntriesForNonRelativeModules(
787787
if !ok {
788788
return nil
789789
}
790-
pattern := getPatternFromFirstMatchingCondition(&keyValue, conditions)
790+
pattern := getPatternFromFirstMatchingCondition(&keyValue, conditions, extensionOptions.importPhase)
791791
if pattern == "" {
792792
return nil
793793
}
@@ -894,18 +894,17 @@ func getFragmentDirectory(fragment string) string {
894894
return tspath.GetDirectoryPath(fragment)
895895
}
896896

897-
func getPatternFromFirstMatchingCondition(target *packagejson.ExportsOrImports, conditions []string) string {
897+
func getPatternFromFirstMatchingCondition(target *packagejson.ExportsOrImports, conditions []string, importPhase module.ImportPhase) string {
898898
if target.Type == packagejson.JSONValueTypeString {
899899
return target.AsString()
900900
}
901901
if target.Type == packagejson.JSONValueTypeObject {
902902
obj := target.AsObject()
903903
for condition := range obj.Keys() {
904-
if condition == "default" || slices.Contains(conditions, condition) ||
905-
(slices.Contains(conditions, "types") && module.IsApplicableVersionedTypesKey(condition)) {
904+
if module.ConditionMatches(condition, conditions, importPhase) {
906905
pattern, ok := obj.Get(condition)
907906
if ok {
908-
return getPatternFromFirstMatchingCondition(&pattern, conditions)
907+
return getPatternFromFirstMatchingCondition(&pattern, conditions, importPhase)
909908
}
910909
}
911910
}

‎tsc/internal/module/resolver.go‎

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ func (r *resolutionState) loadModuleFromTargetExportOrImportWorker(extensions ex
856856
r.tracer.write(diagnostics.Entering_conditional_exports)
857857
}
858858
for condition := range target.AsObject().Keys() {
859-
if r.conditionMatches(condition) {
859+
if ConditionMatches(condition, r.conditions, r.importPhase) {
860860
if r.tracer != nil {
861861
r.tracer.write(diagnostics.Matched_0_condition_1, core.IfElse(isImports, "imports", "exports"), condition)
862862
}
@@ -1012,7 +1012,7 @@ func (r *resolutionState) getOutputDirectoriesForBaseDirectory(commonSourceDirGu
10121012

10131013
func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOnly bool) *resolved {
10141014
mode := core.ResolutionModeCommonJS
1015-
if r.esmMode || r.conditionMatches("import") {
1015+
if r.esmMode || slices.Contains(r.conditions, "import") {
10161016
mode = core.ResolutionModeESM
10171017
}
10181018
// Do (up to) two passes through node_modules:
@@ -1973,14 +1973,11 @@ func (r *resolutionState) getPackageJSONPathField(fieldName string, field *packa
19731973
return path, true
19741974
}
19751975

1976-
func (r *resolutionState) conditionMatches(condition string) bool {
1977-
if condition == "default" || slices.Contains(r.conditions, condition) {
1976+
func ConditionMatches(condition string, conditions []string, importPhase ImportPhase) bool {
1977+
if condition == "default" || slices.Contains(conditions, condition) {
19781978
return true
19791979
}
1980-
if !slices.Contains(r.conditions, "types") {
1981-
return false // only apply versioned types conditions if the types condition is applied
1982-
}
1983-
return IsApplicableVersionedTypesKey(condition)
1980+
return importPhase == ImportPhaseEvaluation && slices.Contains(conditions, "types") && IsApplicableVersionedTypesKey(condition)
19841981
}
19851982

19861983
func (r *resolutionState) getTraceFunc() func(m *diagnostics.Message, args ...any) {
@@ -1991,6 +1988,10 @@ func (r *resolutionState) getTraceFunc() func(m *diagnostics.Message, args ...an
19911988
}
19921989

19931990
func GetConditions(options *core.CompilerOptions, resolutionMode core.ResolutionMode) []string {
1991+
return GetConditionsForImportPhase(options, resolutionMode, ImportPhaseEvaluation)
1992+
}
1993+
1994+
func GetConditionsForImportPhase(options *core.CompilerOptions, resolutionMode core.ResolutionMode, importPhase ImportPhase) []string {
19941995
moduleResolution := options.GetModuleResolutionKind()
19951996
if resolutionMode == core.ModuleKindNone && moduleResolution == core.ModuleResolutionKindBundler {
19961997
resolutionMode = core.ModuleKindESNext
@@ -2002,7 +2003,7 @@ func GetConditions(options *core.CompilerOptions, resolutionMode core.Resolution
20022003
conditions = append(conditions, "require")
20032004
}
20042005

2005-
if options.NoDtsResolution != core.TSTrue {
2006+
if importPhase == ImportPhaseEvaluation && options.NoDtsResolution != core.TSTrue {
20062007
conditions = append(conditions, "types")
20072008
}
20082009
if moduleResolution != core.ModuleResolutionKindBundler {
@@ -2012,21 +2013,6 @@ func GetConditions(options *core.CompilerOptions, resolutionMode core.Resolution
20122013
return conditions
20132014
}
20142015

2015-
func GetConditionsForImportPhase(options *core.CompilerOptions, resolutionMode core.ResolutionMode, importPhase ImportPhase) []string {
2016-
conditions := GetConditions(options, resolutionMode)
2017-
if importPhase == ImportPhaseSource {
2018-
filtered := conditions[:0]
2019-
for _, condition := range conditions {
2020-
if condition == "types" {
2021-
continue
2022-
}
2023-
filtered = append(filtered, condition)
2024-
}
2025-
return filtered
2026-
}
2027-
return conditions
2028-
}
2029-
20302016
func getNodeResolutionFeatures(options *core.CompilerOptions) NodeResolutionFeatures {
20312017
features := NodeResolutionFeaturesNone
20322018

‎tsc/internal/module/resolver_test.go‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ func TestSourcePhasePackageResolutionUsesRuntimeTarget(t *testing.T) {
189189
ModuleResolution: core.ModuleResolutionKindBundler,
190190
Module: core.ModuleKindESNext,
191191
Target: core.ScriptTargetESNext,
192-
CustomConditions: []string{"types"},
193192
}
194193
resolver := NewResolver(ResolverOptions{Host: host, CompilerOptions: opts})
195194

@@ -294,6 +293,54 @@ func TestSourcePhasePackageTargetSelection(t *testing.T) {
294293
}
295294
}
296295

296+
func TestSourcePhaseCustomConditions(t *testing.T) {
297+
t.Parallel()
298+
for _, mode := range []core.ModuleResolutionKind{core.ModuleResolutionKindNodeNext, core.ModuleResolutionKindBundler} {
299+
for _, test := range []struct {
300+
name string
301+
conditions []string
302+
fileName string
303+
}{
304+
{"default", nil, "d.wasm"},
305+
{"types", []string{"types"}, "b.wasm"},
306+
{"versioned types", []string{"types@>=0"}, "a.wasm"},
307+
{"development", []string{"development"}, "c.wasm"},
308+
} {
309+
t.Run(mode.String()+"/"+test.name, func(t *testing.T) {
310+
t.Parallel()
311+
fs := vfstest.FromMap(map[string]string{
312+
"/repo/node_modules/pkg/package.json": `{"exports":{".":{"types@>=0":"./a.wasm","types":"./b.wasm","development":"./c.wasm","default":"./d.wasm"},"./declaration":{"types":"./index.d.ts","default":"./d.wasm"}}}`,
313+
"/repo/node_modules/pkg/a.wasm": "wasm",
314+
"/repo/node_modules/pkg/a.d.wasm.ts": "export const value: number;",
315+
"/repo/node_modules/pkg/b.wasm": "wasm",
316+
"/repo/node_modules/pkg/c.wasm": "wasm",
317+
"/repo/node_modules/pkg/d.wasm": "wasm",
318+
"/repo/node_modules/pkg/index.d.ts": "export const value: number;",
319+
"/repo/src/index.mts": "",
320+
}, true)
321+
resolver := NewResolver(ResolverOptions{
322+
Host: &resolutionHostStub{fs: fs, cwd: "/repo"},
323+
CompilerOptions: &core.CompilerOptions{
324+
ModuleResolution: mode,
325+
CustomConditions: test.conditions,
326+
},
327+
})
328+
resolved, _, _ := resolver.ResolveModuleNameWithPhase("pkg", "/repo/src/index.mts", core.ResolutionModeESM, ImportPhaseSource, nil)
329+
assertResolvedFileName(t, resolved, "/repo/node_modules/pkg/"+test.fileName)
330+
resolved, _, _ = resolver.ResolveModuleName("pkg", "/repo/src/index.mts", core.ResolutionModeESM, nil)
331+
assertResolvedFileName(t, resolved, "/repo/node_modules/pkg/a.d.wasm.ts")
332+
333+
resolved, _, _ = resolver.ResolveModuleNameWithPhase("pkg/declaration", "/repo/src/index.mts", core.ResolutionModeESM, ImportPhaseSource, nil)
334+
if test.name == "types" {
335+
assert.Assert(t, !resolved.IsResolved())
336+
} else {
337+
assertResolvedFileName(t, resolved, "/repo/node_modules/pkg/d.wasm")
338+
}
339+
})
340+
}
341+
}
342+
}
343+
297344
func TestSourcePhasePackageImportsExternalTarget(t *testing.T) {
298345
t.Parallel()
299346

‎tsc/internal/modulespecifiers/specifiers.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1311,7 +1311,7 @@ func tryGetModuleNameFromExportsOrImports(
13111311
// conditional mapping
13121312
obj := exports.AsObject()
13131313
for key, value := range obj.Entries() {
1314-
if key == "default" || slices.Contains(conditions, key) || slices.Contains(conditions, "types") && module.IsApplicableVersionedTypesKey(key) {
1314+
if module.ConditionMatches(key, conditions, module.ImportPhaseEvaluation) {
13151315
result := tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, value, conditions, mode, isImports, preferTsExtension)
13161316
if len(result) > 0 {
13171317
return result

0 commit comments

Comments
 (0)