ttsc 0.28.4 → 0.28.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/cmd/ttsc/api_compile.go +6 -4
  2. package/cmd/ttsc/api_transform.go +6 -4
  3. package/cmd/ttsc/build.go +7 -6
  4. package/cmd/ttscgraph/serve.go +70 -77
  5. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +26 -0
  6. package/cmd/ttscgraph/serve_session_tracks_all_module_specifier_forms_test.go +1 -2
  7. package/driver/graph.go +67 -35
  8. package/driver/input_observation.go +305 -115
  9. package/driver/input_observation_test.go +203 -0
  10. package/driver/program.go +28 -4
  11. package/driver/resolution_candidates.go +247 -741
  12. package/driver/resolution_candidates_test.go +341 -0
  13. package/lib/compiler/internal/project/readProjectConfig.js +9 -7
  14. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  15. package/lib/compiler/internal/runBuild.js +1 -0
  16. package/lib/compiler/internal/runBuild.js.map +1 -1
  17. package/lib/compiler/internal/sharedHostHelpers.d.ts +4 -0
  18. package/lib/compiler/internal/sharedHostHelpers.js +11 -1
  19. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  20. package/lib/compiler/internal/startResidentTransform.js +1 -0
  21. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  22. package/lib/compiler/internal/transformProjectInMemory.js +165 -8
  23. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  24. package/lib/internal/projectInputPathIdentity.js +8 -7
  25. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  26. package/lib/structures/ITtscCompilerTransformation.d.ts +57 -19
  27. package/package.json +8 -8
  28. package/shim/compiler/incremental.go +52 -0
  29. package/shim/compiler/resolution.go +444 -0
  30. package/shim/core/shim.go +15 -1
  31. package/src/compiler/internal/project/readProjectConfig.ts +9 -7
  32. package/src/compiler/internal/runBuild.ts +2 -0
  33. package/src/compiler/internal/sharedHostHelpers.ts +14 -0
  34. package/src/compiler/internal/startResidentTransform.ts +2 -0
  35. package/src/compiler/internal/transformProjectInMemory.ts +193 -8
  36. package/src/internal/projectInputPathIdentity.ts +8 -7
  37. package/src/structures/ITtscCompilerTransformation.ts +65 -19
  38. package/utility/host.go +8 -7
@@ -1,813 +1,319 @@
1
1
  package driver
2
2
 
3
3
  import (
4
- "bytes"
5
- "encoding/json"
6
- "os"
7
- "path/filepath"
4
+ "sort"
8
5
  "strings"
9
6
 
10
- "github.com/microsoft/typescript-go/shim/ast"
11
- shimcore "github.com/microsoft/typescript-go/shim/core"
12
- "github.com/microsoft/typescript-go/shim/tsoptions"
7
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
8
+ shimtsoptions "github.com/microsoft/typescript-go/shim/tsoptions"
9
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
13
10
  )
14
11
 
15
- // ModuleResolutionContext carries the exact TypeScript-Go configuration for a
16
- // module-specifier lookup. Candidate discovery must share its mode and package
17
- // resolution switches, otherwise a path the compiler cannot select becomes an
18
- // accidental freshness input.
19
- type ModuleResolutionContext struct {
20
- Options *shimcore.CompilerOptions
21
- Mode shimcore.ResolutionMode
22
- }
23
-
24
- // SupersedingModuleCandidates returns, per source-file envelope key, the
25
- // resolution candidates strictly ahead of each module target the loaded
26
- // program selected. When the selected target is reported by physical path, it
27
- // also retains that candidate's lexical spelling. A missing member or a link
28
- // retarget can therefore become a freshness input without making the whole
29
- // resolution search (including lower-priority paths) an invalidation input.
30
- //
31
- // Candidate enumeration is host-owned. It mirrors the resident graph session's
32
- // speculation for relative modules, paths, rootDirs, package imports/exports,
33
- // and node_modules ancestry, while the compiler itself remains the authority
34
- // that says which target won. The returned values use TransformOutputKey so
35
- // they travel in the same envelope vocabulary as graph edges.
36
- func SupersedingModuleCandidates(prog *Program, cwd string) map[string][]string {
37
- if prog == nil || prog.TSProgram == nil || prog.ParsedConfig == nil {
38
- return nil
39
- }
40
- configs := []*tsoptions.ParsedCommandLine{prog.ParsedConfig}
41
- output := map[string][]string{}
42
- caseSensitive := true
43
- if prog.FS != nil {
44
- caseSensitive = prog.FS.UseCaseSensitiveFileNames()
45
- }
46
- for _, source := range prog.TSProgram.SourceFiles() {
47
- if source == nil || strings.HasPrefix(source.FileName(), bundledScheme) {
48
- continue
49
- }
50
- key := TransformOutputKey(cwd, source.FileName())
51
- directory := filepath.Dir(source.FileName())
52
- for _, specifier := range SourceModuleSpecifiers(source) {
53
- context := moduleResolutionContextFor(configs, prog.TSProgram.GetModeForUsageLocation(source, specifier))
54
- resolved := prog.TSProgram.GetResolvedModuleFromModuleSpecifier(source, specifier)
55
- if resolved == nil || !resolved.IsResolved() {
56
- continue
57
- }
58
- before := ModuleResolutionPredecessors(
59
- configs,
60
- directory,
61
- cwd,
62
- specifier.Text(),
63
- resolved.ResolvedFileName,
64
- caseSensitive,
65
- context,
66
- )
67
- for _, candidate := range before {
68
- output[key] = append(output[key], TransformOutputKey(cwd, candidate))
12
+ // ProgramResolutionInput is one exact lexical filesystem input consulted by
13
+ // TypeScript-Go while resolving a resident Program.
14
+ type ProgramResolutionInput struct {
15
+ // DirectoryEntries is true when the compiler enumerated this directory, so
16
+ // membership changes as well as kind and identity must invalidate it.
17
+ DirectoryEntries bool
18
+ // IdentityOnly is true only for a selected lexical alias whose contents are
19
+ // already owned by the resolved graph target. A link retarget must still
20
+ // invalidate it, while an edit to the selected bytes is not counted twice.
21
+ IdentityOnly bool
22
+ Path string
23
+ }
24
+
25
+ // ProgramResolutionObservation is the deterministic result of replaying every
26
+ // resident module and type-reference resolution through TypeScript-Go itself.
27
+ type ProgramResolutionObservation struct {
28
+ // Candidates maps each source to every exact resolver input other than its
29
+ // identically spelled selected graph target.
30
+ Candidates map[string][]string
31
+ // Failures marks source files whose replay no longer matched the resident
32
+ // resolution. A universal failure is expanded to every graph source later.
33
+ Failures map[string]string
34
+ // Inputs is the flat form used by resident non-envelope hosts.
35
+ Inputs []ProgramResolutionInput
36
+ // Universal contains automatic type discovery and resolution inputs whose
37
+ // state can affect every source file.
38
+ Universal []string
39
+ universalFailure bool
40
+ }
41
+
42
+ // ObserveProgramResolutions replays the pinned TypeScript-Go resolver over a
43
+ // fresh observation transaction per source resolution set. This preserves the
44
+ // compiler's exact ordering and complete semantics without maintaining a
45
+ // second resolver in ttsc. Replay observations are merged into the original
46
+ // compiler transaction, so a state change between construction and replay
47
+ // becomes a proof failure.
48
+ func ObserveProgramResolutions(prog *Program, cwd string) ProgramResolutionObservation {
49
+ output := ProgramResolutionObservation{
50
+ Candidates: map[string][]string{},
51
+ Failures: map[string]string{},
52
+ }
53
+ if prog == nil || prog.TSProgram == nil || prog.FS == nil || prog.inputObserver == nil {
54
+ return output
55
+ }
56
+ caseSensitive := prog.FS.UseCaseSensitiveFileNames()
57
+ tasks := shimcompiler.ProgramResolutionTasks(prog.TSProgram)
58
+ sort.Slice(tasks, func(left, right int) bool {
59
+ a, b := tasks[left], tasks[right]
60
+ if a.Universal != b.Universal {
61
+ return !a.Universal
62
+ }
63
+ if a.SourceFile != b.SourceFile {
64
+ return a.SourceFile < b.SourceFile
65
+ }
66
+ if a.ContainingFile != b.ContainingFile {
67
+ return a.ContainingFile < b.ContainingFile
68
+ }
69
+ if a.Kind != b.Kind {
70
+ return a.Kind < b.Kind
71
+ }
72
+ if a.Name != b.Name {
73
+ return a.Name < b.Name
74
+ }
75
+ return a.Mode < b.Mode
76
+ })
77
+
78
+ if !prog.TSProgram.Options().NoResolve.IsTrue() {
79
+ observeProgramPathReferences(prog, cwd, caseSensitive, &output)
80
+ }
81
+
82
+ // Automatic type discovery is a separate compiler operation that enumerates
83
+ // each effective type root before the individual directive resolutions.
84
+ // Replaying it records directory membership itself, so adding a new @types
85
+ // package invalidates a resident program without an importer edit.
86
+ automatic := newInputObservationFS(prog.FS)
87
+ shimcompiler.ReplayAutomaticTypeDirectiveDiscovery(prog.TSProgram, automatic)
88
+ output.Universal = appendResolutionPaths(output.Universal, cwd, automatic, nil, caseSensitive, &output.Inputs)
89
+ prog.inputObserver.mergeFrom(automatic)
90
+
91
+ for first := 0; first < len(tasks); {
92
+ last := first + 1
93
+ for last < len(tasks) && sameResolutionTaskOwner(tasks[first], tasks[last]) {
94
+ last++
95
+ }
96
+ group := tasks[first:last]
97
+ replay := newInputObservationFS(prog.FS)
98
+ matches := shimcompiler.ReplayProgramResolutions(group, replay)
99
+ selected := make([]string, 0, len(group))
100
+ for _, task := range group {
101
+ // A resolved target is already content-owned only when it became a
102
+ // Program source. noResolve, allowJs, depth, and diagnostic gates can
103
+ // leave a successful resolution outside the graph; retain those targets
104
+ // as resolver inputs instead of silently dropping them.
105
+ if task.TargetFile != "" {
106
+ selected = append(selected, task.TargetFile)
69
107
  }
70
108
  }
71
- }
72
- for source, candidates := range output {
73
- output[source] = compactStringsInOrder(candidates)
74
- }
75
- return output
76
- }
77
-
78
- // ModuleResolutionPredecessors returns the paths in the host-owned candidate
79
- // search that precede resolvedFileName. When the compiler reports the winner by
80
- // physical realpath, the result also retains that selected candidate's lexical
81
- // spelling: retargeting its symlink/junction can change resolution without
82
- // changing the old or new target bytes. An unresolved specifier keeps the full
83
- // candidate list through ModuleResolutionCandidates; a resolved specifier must
84
- // not track a lower-priority path because creating it cannot alter resolution.
85
- func ModuleResolutionPredecessors(
86
- configs []*tsoptions.ParsedCommandLine,
87
- directory, cwd, specifier, resolvedFileName string,
88
- caseSensitive bool,
89
- context ModuleResolutionContext,
90
- ) []string {
91
- candidates := ModuleResolutionCandidates(configs, directory, cwd, specifier, context)
92
- for index, candidate := range candidates {
93
- if sameCandidatePath(candidate, resolvedFileName, caseSensitive) {
94
- return compactStringsInOrder(candidates[:index])
95
- }
96
- }
97
- // The winner is not spelled the way this search spells it. The compiler
98
- // resolves a `node_modules` package through the real path, so its selected
99
- // target can arrive symlink-resolved — and on Windows expanded out of an 8.3
100
- // short name such as `RUNNER~1` — while every candidate carries the importing
101
- // file's spelling. Ask the filesystem which candidate is that file instead of
102
- // discarding the whole search.
103
- //
104
- // This second pass runs only when the first found nothing, so a program whose
105
- // spellings already agree pays nothing for it.
106
- for index, candidate := range candidates {
107
- if sameExistingPath(candidate, resolvedFileName) {
108
- end := index
109
- if !sameCandidatePath(candidate, resolvedFileName, caseSensitive) {
110
- end++
109
+ owner := group[0]
110
+ if owner.Universal {
111
+ output.Universal = appendResolutionPaths(output.Universal, cwd, replay, selected, caseSensitive, &output.Inputs)
112
+ if !matches {
113
+ output.universalFailure = true
114
+ }
115
+ } else if owner.SourceFile != "" {
116
+ source := TransformOutputKey(cwd, owner.SourceFile)
117
+ output.Candidates[source] = appendResolutionPaths(output.Candidates[source], cwd, replay, selected, caseSensitive, &output.Inputs)
118
+ if !matches {
119
+ output.Failures[source] = string(inputProofResolutionChanged)
111
120
  }
112
- return compactStringsInOrder(candidates[:end])
113
- }
114
- }
115
- // Never widen freshness to candidates whose precedence relative to the
116
- // compiler's selected target we could not prove. A winner that is genuinely
117
- // outside this search remains covered by its realized graph edge.
118
- return nil
119
- }
120
-
121
- func moduleResolutionContextFor(configs []*tsoptions.ParsedCommandLine, mode shimcore.ResolutionMode) ModuleResolutionContext {
122
- context := ModuleResolutionContext{Mode: mode}
123
- for _, parsed := range configs {
124
- if parsed != nil && parsed.ParsedConfig != nil && parsed.ParsedConfig.CompilerOptions != nil {
125
- context.Options = parsed.ParsedConfig.CompilerOptions
126
- break
127
121
  }
122
+ prog.inputObserver.mergeFrom(replay)
123
+ first = last
128
124
  }
129
- return context
130
- }
131
-
132
- func (context ModuleResolutionContext) isESM() bool {
133
- return context.Mode == shimcore.ResolutionModeESM
134
- }
135
-
136
- func (context ModuleResolutionContext) moduleResolutionKind() shimcore.ModuleResolutionKind {
137
- if context.Options == nil {
138
- return shimcore.ModuleResolutionKindBundler
139
- }
140
- return context.Options.GetModuleResolutionKind()
141
- }
142
-
143
- func (context ModuleResolutionContext) usesPackageExports() bool {
144
- switch context.moduleResolutionKind() {
145
- case shimcore.ModuleResolutionKindNode16, shimcore.ModuleResolutionKindNodeNext:
146
- return true
147
- case shimcore.ModuleResolutionKindBundler:
148
- return context.Options == nil || context.Options.GetResolvePackageJsonExports()
149
- default:
150
- return false
151
- }
152
- }
153
-
154
- func (context ModuleResolutionContext) usesPackageImports() bool {
155
- switch context.moduleResolutionKind() {
156
- case shimcore.ModuleResolutionKindNode16, shimcore.ModuleResolutionKindNodeNext:
157
- return true
158
- case shimcore.ModuleResolutionKindBundler:
159
- return context.Options == nil || context.Options.GetResolvePackageJsonImports()
160
- default:
161
- return false
162
- }
163
- }
164
-
165
- func (context ModuleResolutionContext) packageConditions() map[string]struct{} {
166
- if context.Options == nil {
167
- return map[string]struct{}{"require": {}, "types": {}}
168
- }
169
- mode := context.Mode
170
- if mode == shimcore.ResolutionModeNone && context.moduleResolutionKind() == shimcore.ModuleResolutionKindBundler {
171
- mode = shimcore.ResolutionModeESM
172
- }
173
- conditions := map[string]struct{}{}
174
- if mode == shimcore.ResolutionModeESM {
175
- conditions["import"] = struct{}{}
176
- } else {
177
- conditions["require"] = struct{}{}
178
- }
179
- if context.Options.NoDtsResolution != shimcore.TSTrue {
180
- conditions["types"] = struct{}{}
181
- }
182
- if context.moduleResolutionKind() != shimcore.ModuleResolutionKindBundler {
183
- conditions["node"] = struct{}{}
184
- }
185
- for _, condition := range context.Options.CustomConditions {
186
- conditions[condition] = struct{}{}
187
- }
188
- return conditions
189
- }
190
-
191
- func sameCandidatePath(left, right string, caseSensitive bool) bool {
192
- left = filepath.Clean(left)
193
- right = filepath.Clean(right)
194
- if caseSensitive {
195
- return left == right
196
- }
197
- return strings.EqualFold(left, right)
198
- }
199
-
200
- // sameExistingPath reports whether two spellings name the same existing
201
- // filesystem object. A path that does not exist answers false, so a
202
- // speculative candidate costs one failing `os.Stat` and never reaches the
203
- // second call.
204
- func sameExistingPath(left, right string) bool {
205
- leftInfo, err := os.Stat(left)
206
- if err != nil {
207
- return false
208
- }
209
- rightInfo, err := os.Stat(right)
210
- if err != nil {
211
- return false
212
- }
213
- return os.SameFile(leftInfo, rightInfo)
214
- }
215
-
216
- // reachedWalkStop reports whether an upward `node_modules` walk has arrived at
217
- // the directory it must not pass. The walk starts from the importing file's
218
- // directory as the compiler spells it and stops at the project root as the
219
- // caller spells it, and the two need not agree character for character on a
220
- // case-insensitive or short-name-bearing filesystem. A missed stop walks every
221
- // remaining ancestor up to the volume root and watches probes the compiler
222
- // would never consult.
223
- //
224
- // Two spellings of one directory still share a final component up to case
225
- // unless that component is itself shortened, so the mismatching levels — every
226
- // level below the stop — are rejected without touching the filesystem. A stop
227
- // missed by the remaining heuristic costs breadth, never a wrong answer: the
228
- // extra ancestors are appended after the correct ones, so a predecessor list
229
- // cut at the winner is unaffected.
230
- func reachedWalkStop(current, stop string) bool {
231
- stop = filepath.Clean(stop)
232
- if current == stop {
233
- return true
234
- }
235
- if !strings.EqualFold(filepath.Base(current), filepath.Base(stop)) {
236
- return false
237
- }
238
- return sameExistingPath(current, stop)
239
- }
240
-
241
- // FileCandidates lists the ordered file and directory probes for base. The
242
- // suffix family follows the module specifier's explicit extension, matching the
243
- // resolver's extension branch instead of treating every source kind as a
244
- // possible predecessor.
245
- func FileCandidates(base string) []string {
246
- base, suffixes := fileCandidateBaseAndSuffixes(base)
247
- candidates := []string{}
248
- for _, suffix := range suffixes {
249
- candidates = append(candidates, base+suffix)
250
- }
251
- candidates = append(candidates, filepath.Join(base, "package.json"))
252
- for _, suffix := range suffixes {
253
- candidates = append(candidates, filepath.Join(base, "index"+suffix))
254
- }
255
- return candidates
256
- }
257
-
258
- func moduleFileCandidates(base string, context ModuleResolutionContext, includeDirectory bool) []string {
259
- explicitExtension := filepath.Ext(base)
260
- base, suffixes := fileCandidateBaseAndSuffixes(base)
261
- if context.Options != nil && context.Options.NoDtsResolution == shimcore.TSTrue {
262
- suffixes = withoutDeclarationSuffixes(suffixes)
263
- }
264
- if context.isESM() && explicitExtension == "" {
265
- return nil
266
- }
267
- candidates := []string{}
268
- for _, suffix := range suffixes {
269
- candidates = append(candidates, moduleSuffixCandidates(base+suffix, context)...)
270
- }
271
- if !includeDirectory || context.isESM() {
272
- return candidates
273
- }
274
- candidates = append(candidates, filepath.Join(base, "package.json"))
275
- for _, suffix := range suffixes {
276
- candidates = append(candidates, moduleSuffixCandidates(filepath.Join(base, "index"+suffix), context)...)
277
- }
278
- return candidates
279
- }
280
-
281
- func withoutDeclarationSuffixes(suffixes []string) []string {
282
- output := make([]string, 0, len(suffixes))
283
- for _, suffix := range suffixes {
284
- if !strings.HasPrefix(suffix, ".d.") {
285
- output = append(output, suffix)
125
+ for source, candidates := range output.Candidates {
126
+ compacted := compactStringsInOrder(candidates)
127
+ if len(compacted) == 0 {
128
+ delete(output.Candidates, source)
129
+ } else {
130
+ sort.Strings(compacted)
131
+ output.Candidates[source] = compacted
286
132
  }
287
133
  }
134
+ output.Universal = compactStringsInOrder(output.Universal)
135
+ sort.Strings(output.Universal)
136
+ output.Inputs = compactResolutionInputs(output.Inputs)
288
137
  return output
289
138
  }
290
139
 
291
- func moduleSuffixCandidates(path string, context ModuleResolutionContext) []string {
292
- if context.Options == nil || len(context.Options.ModuleSuffixes) == 0 {
293
- return []string{path}
294
- }
295
- extension := filepath.Ext(path)
296
- base := strings.TrimSuffix(path, extension)
297
- candidates := make([]string, 0, len(context.Options.ModuleSuffixes))
298
- for _, suffix := range context.Options.ModuleSuffixes {
299
- candidates = append(candidates, base+suffix+extension)
300
- }
301
- return candidates
302
- }
303
-
304
- func fileCandidateBaseAndSuffixes(base string) (string, []string) {
305
- lower := strings.ToLower(base)
306
- switch {
307
- case strings.HasSuffix(lower, ".d.mts"):
308
- return base[:len(base)-len(".d.mts")], []string{".mts", ".d.mts", ".mjs"}
309
- case strings.HasSuffix(lower, ".mjs"), strings.HasSuffix(lower, ".mts"):
310
- return base[:len(base)-len(filepath.Ext(base))], []string{".mts", ".d.mts", ".mjs"}
311
- case strings.HasSuffix(lower, ".d.cts"):
312
- return base[:len(base)-len(".d.cts")], []string{".cts", ".d.cts", ".cjs"}
313
- case strings.HasSuffix(lower, ".cjs"), strings.HasSuffix(lower, ".cts"):
314
- return base[:len(base)-len(filepath.Ext(base))], []string{".cts", ".d.cts", ".cjs"}
315
- case strings.HasSuffix(lower, ".tsx"), strings.HasSuffix(lower, ".jsx"):
316
- return base[:len(base)-len(filepath.Ext(base))], []string{".tsx", ".ts", ".d.ts", ".jsx", ".js"}
317
- case strings.HasSuffix(lower, ".d.ts"):
318
- return base[:len(base)-len(".d.ts")], []string{".ts", ".tsx", ".d.ts", ".js", ".jsx"}
319
- case strings.HasSuffix(lower, ".ts"), strings.HasSuffix(lower, ".js"):
320
- return base[:len(base)-len(filepath.Ext(base))], []string{".ts", ".tsx", ".d.ts", ".js", ".jsx"}
321
- default:
322
- return base, []string{".ts", ".tsx", ".d.ts", ".js", ".jsx"}
323
- }
140
+ func sameResolutionTaskOwner(left, right shimcompiler.ProgramResolutionTask) bool {
141
+ return left.Universal == right.Universal && left.SourceFile == right.SourceFile && (left.Universal || left.ContainingFile == right.ContainingFile)
324
142
  }
325
143
 
326
- // TypeReferenceCandidates lists the probes used for a triple-slash type
327
- // directive or compilerOptions.types entry.
328
- func TypeReferenceCandidates(configs []*tsoptions.ParsedCommandLine, directory, cwd, name string) []string {
329
- candidates := []string{}
330
- for _, parsed := range configs {
331
- if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
144
+ // observeProgramPathReferences replays the file predicates for triple-slash
145
+ // path references. These use the compiler's supported-extension list directly
146
+ // rather than the module resolver cache covered by ProgramResolutionTasks.
147
+ func observeProgramPathReferences(prog *Program, cwd string, caseSensitive bool, output *ProgramResolutionObservation) {
148
+ supported := shimtsoptions.GetSupportedExtensions(prog.TSProgram.Options(), nil)
149
+ supported = shimtsoptions.GetSupportedExtensionsWithJsonIfResolveJsonModule(prog.TSProgram.Options(), supported)
150
+ for _, source := range prog.TSProgram.SourceFiles() {
151
+ if source == nil || source.FileName() == "" || strings.HasPrefix(source.FileName(), bundledScheme) {
332
152
  continue
333
153
  }
334
- for _, root := range parsed.ParsedConfig.CompilerOptions.TypeRoots {
335
- candidates = append(candidates, FileCandidates(filepath.Join(root, filepath.FromSlash(name)))...)
336
- }
337
- }
338
- for current := filepath.Clean(directory); ; current = filepath.Dir(current) {
339
- candidates = append(candidates, FileCandidates(filepath.Join(current, "node_modules", "@types", filepath.FromSlash(name)))...)
340
- if reachedWalkStop(current, cwd) || filepath.Dir(current) == current {
341
- return candidates
342
- }
343
- }
344
- }
345
-
346
- // ModuleResolutionCandidates lists the bounded speculative probes for one
347
- // module specifier in compiler precedence order. The list is used unchanged for
348
- // unresolved specifiers and is cut at the compiler-selected target for resolved
349
- // specifiers by ModuleResolutionPredecessors.
350
- func ModuleResolutionCandidates(configs []*tsoptions.ParsedCommandLine, directory, cwd, specifier string, context ModuleResolutionContext) []string {
351
- if specifier == "" {
352
- return nil
353
- }
354
- if strings.HasPrefix(specifier, ".") {
355
- candidates := moduleFileCandidates(filepath.Clean(filepath.Join(directory, filepath.FromSlash(specifier))), context, true)
356
- return append(candidates, rootDirsCandidates(configs, directory, specifier, context)...)
357
- }
358
- candidates := compilerOptionCandidates(configs, specifier, context)
359
- if strings.HasPrefix(specifier, "#") {
360
- if !context.usesPackageImports() {
361
- return candidates
362
- }
363
- for current := filepath.Clean(directory); ; current = filepath.Dir(current) {
364
- fromManifest, _ := packageManifestCandidates(current, specifier, context)
365
- candidates = append(candidates, fromManifest...)
366
- if reachedWalkStop(current, cwd) || filepath.Dir(current) == current {
367
- return candidates
368
- }
369
- }
370
- }
371
- for current := filepath.Clean(directory); ; current = filepath.Dir(current) {
372
- base := filepath.Join(current, "node_modules", filepath.FromSlash(specifier))
373
- root := packageRoot(base, specifier)
374
- fromManifest, hasExports := packageManifestCandidates(root, packageSubpath(specifier), context)
375
- // Package exports block bare file and folder lookups. Recording them would
376
- // make a file that TypeScript will never select invalidate a resident
377
- // snapshot, so only the export-map paths participate in this branch.
378
- if !hasExports {
379
- candidates = append(candidates, moduleFileCandidates(base, context, true)...)
380
- }
381
- candidates = append(candidates, filepath.Join(root, "package.json"))
382
- candidates = append(candidates, fromManifest...)
383
- if reachedWalkStop(current, cwd) || filepath.Dir(current) == current {
384
- break
385
- }
386
- }
387
- return candidates
388
- }
389
-
390
- // SourceModuleSpecifiers returns all static and dynamic module-specifier
391
- // literals a source file carries, including imports, exports, import-equals,
392
- // import types, require calls, and dynamic imports.
393
- func SourceModuleSpecifiers(source *ast.SourceFile) []*ast.Node {
394
- if source == nil {
395
- return nil
396
- }
397
- specifiers := []*ast.Node{}
398
- var walk func(*ast.Node) bool
399
- walk = func(node *ast.Node) bool {
400
- if node == nil {
401
- return false
402
- }
403
- var specifier *ast.Node
404
- switch node.Kind {
405
- case ast.KindImportDeclaration, ast.KindJSImportDeclaration:
406
- specifier = node.AsImportDeclaration().ModuleSpecifier
407
- case ast.KindExportDeclaration:
408
- specifier = node.AsExportDeclaration().ModuleSpecifier
409
- case ast.KindImportEqualsDeclaration:
410
- reference := node.AsImportEqualsDeclaration().ModuleReference
411
- if reference != nil && reference.Kind == ast.KindExternalModuleReference {
412
- specifier = reference.AsExternalModuleReference().Expression
413
- }
414
- case ast.KindImportType:
415
- argument := node.AsImportTypeNode().Argument
416
- if argument != nil && argument.Kind == ast.KindLiteralType {
417
- specifier = argument.AsLiteralTypeNode().Literal
418
- }
419
- case ast.KindCallExpression:
420
- call := node.AsCallExpression()
421
- if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
422
- specifier = call.Arguments.Nodes[0]
154
+ sourceKey := TransformOutputKey(cwd, source.FileName())
155
+ for _, reference := range source.ReferencedFiles {
156
+ replay := newInputObservationFS(prog.FS)
157
+ selected := []string(nil)
158
+ for _, candidate := range pathReferenceCandidates(source.FileName(), reference.FileName, prog.TSProgram.Options().AllowNonTsExtensions.IsTrue(), supported, caseSensitive) {
159
+ if replay.FileExists(candidate) {
160
+ if resident := prog.TSProgram.GetSourceFileForResolvedModule(candidate); resident != nil {
161
+ selected = []string{resident.FileName()}
162
+ }
163
+ break
164
+ }
165
+ // TypeScript-Go's project-reference host can make an unbuilt output
166
+ // declaration exist by checking its mapped source. Replay both
167
+ // predicates so a later output appearance or source disappearance
168
+ // invalidates the same resolution without turning either spelling into
169
+ // a realized graph edge.
170
+ outputPath := shimtspath.ToPath(candidate, prog.TSProgram.GetCurrentDirectory(), caseSensitive)
171
+ redirect := prog.TSProgram.GetProjectReferenceFromOutputDts(outputPath)
172
+ if redirect == nil || !replay.FileExists(redirect.Source) {
173
+ continue
174
+ }
175
+ if resident := prog.TSProgram.GetSourceFile(redirect.Source); resident != nil {
176
+ selected = []string{resident.FileName()}
177
+ }
178
+ break
423
179
  }
180
+ output.Candidates[sourceKey] = appendResolutionPaths(output.Candidates[sourceKey], cwd, replay, selected, caseSensitive, &output.Inputs)
181
+ prog.inputObserver.mergeFrom(replay)
424
182
  }
425
- if specifier != nil && (specifier.Kind == ast.KindStringLiteral || specifier.Kind == ast.KindNoSubstitutionTemplateLiteral) {
426
- specifiers = append(specifiers, specifier)
427
- }
428
- node.ForEachChild(walk)
429
- return false
430
183
  }
431
- walk(source.AsNode())
432
- return specifiers
433
184
  }
434
185
 
435
- func isModuleSpecifierCall(call *ast.CallExpression) bool {
436
- if call == nil || call.Expression == nil {
437
- return false
438
- }
439
- if call.Expression.Kind == ast.KindImportKeyword {
440
- return true
186
+ func pathReferenceCandidates(containingFile, reference string, allowNonTsExtensions bool, supported [][]string, caseSensitive bool) []string {
187
+ base := reference
188
+ if !shimtspath.IsRootedDiskPath(base) {
189
+ base = shimtspath.CombinePaths(shimtspath.GetDirectoryPath(containingFile), base)
441
190
  }
442
- return call.Expression.Kind == ast.KindIdentifier && call.Expression.Text() == "require"
443
- }
444
-
445
- type packageTarget struct {
446
- path string
447
- wildcard string
448
- packageEntry bool
449
- usesCommonJS bool
450
- }
451
-
452
- // packageValue preserves a package.json object's declaration order. That order
453
- // is resolution semantics for conditional exports/imports, so decoding into a
454
- // Go map would make the speculative prefix nondeterministic.
455
- type packageValue struct {
456
- array []packageValue
457
- object []packageProperty
458
- text *string
459
- }
460
-
461
- type packageProperty struct {
462
- key string
463
- value packageValue
464
- }
465
-
466
- func packageManifestCandidates(root, wildcard string, context ModuleResolutionContext) ([]string, bool) {
467
- content, err := os.ReadFile(filepath.Join(root, "package.json"))
468
- if err != nil {
469
- return nil, false
470
- }
471
- var manifest struct {
472
- Main string `json:"main"`
473
- Module string `json:"module"`
474
- Types string `json:"types"`
475
- Typings string `json:"typings"`
476
- Exports json.RawMessage `json:"exports"`
477
- Imports json.RawMessage `json:"imports"`
478
- Type string `json:"type"`
479
- TypesVersions json.RawMessage `json:"typesVersions"`
480
- }
481
- if json.Unmarshal(content, &manifest) != nil {
482
- return nil, false
483
- }
484
- defaultWildcard := strings.TrimPrefix(strings.TrimPrefix(wildcard, "./"), "#")
485
- targets := []packageTarget{}
486
- if strings.HasPrefix(wildcard, "#") {
487
- if context.usesPackageImports() {
488
- if value, ok := decodePackageValue(manifest.Imports); ok {
489
- collectPackageMappingTargets(value, wildcard, defaultWildcard, context.packageConditions(), &targets)
490
- }
191
+ base = shimtspath.NormalizePath(base)
192
+ if shimtspath.HasExtension(base) {
193
+ canonicalBase := shimtspath.GetCanonicalFileName(base, caseSensitive)
194
+ if allowNonTsExtensions || supportedFileExtension(canonicalBase, supported) {
195
+ return []string{base}
491
196
  }
492
- return packageTargetCandidates(root, targets, context), false
197
+ return nil
493
198
  }
494
- exportRequest := "."
495
- if wildcard != "" && !strings.HasPrefix(wildcard, "#") {
496
- exportRequest = "./" + strings.TrimPrefix(wildcard, "./")
199
+ if allowNonTsExtensions {
200
+ return []string{base}
497
201
  }
498
- exports, hasExports := decodePackageValue(manifest.Exports)
499
- if hasExports && context.usesPackageExports() {
500
- collectPackageMappingTargets(exports, exportRequest, defaultWildcard, context.packageConditions(), &targets)
501
- } else {
502
- hasExports = false
503
- if value, ok := decodePackageValue(manifest.TypesVersions); ok {
504
- collectPackageTargets(value, defaultWildcard, nil, &targets)
505
- }
506
- // `typings`, `types`, and `main` are directory-entrypoint fields. A
507
- // subpath resolution never falls back to them, and `module` is not a
508
- // TypeScript-Go resolution field at all.
509
- if wildcard == "" {
510
- usesCommonJS := manifest.Type != "module"
511
- if context.Options == nil || context.Options.NoDtsResolution != shimcore.TSTrue {
512
- targets = append(targets,
513
- packageTarget{path: manifest.Typings, wildcard: defaultWildcard, packageEntry: true, usesCommonJS: usesCommonJS},
514
- packageTarget{path: manifest.Types, wildcard: defaultWildcard, packageEntry: true, usesCommonJS: usesCommonJS},
515
- )
516
- }
517
- targets = append(targets, packageTarget{path: manifest.Main, wildcard: defaultWildcard, packageEntry: true, usesCommonJS: usesCommonJS})
518
- }
519
- }
520
- return packageTargetCandidates(root, targets, context), hasExports
521
- }
522
-
523
- func packageTargetCandidates(root string, targets []packageTarget, context ModuleResolutionContext) []string {
524
202
  candidates := []string{}
525
- for _, target := range targets {
526
- if target.path == "" || filepath.IsAbs(target.path) || strings.Contains(target.path, "://") {
527
- continue
528
- }
529
- targetPath := strings.Replace(target.path, "*", target.wildcard, 1)
530
- targetContext := context
531
- if target.packageEntry && target.usesCommonJS {
532
- targetContext.Mode = shimcore.ResolutionModeCommonJS
203
+ if len(supported) != 0 {
204
+ for _, extension := range supported[0] {
205
+ candidates = append(candidates, base+extension)
533
206
  }
534
- candidates = append(candidates, moduleFileCandidates(filepath.Join(root, filepath.FromSlash(targetPath)), targetContext, target.packageEntry)...)
535
207
  }
536
208
  return candidates
537
209
  }
538
210
 
539
- func collectPackageMappingTargets(value packageValue, request, wildcard string, conditions map[string]struct{}, targets *[]packageTarget) {
540
- if value.object != nil {
541
- mapping := false
542
- for _, property := range value.object {
543
- if strings.HasPrefix(property.key, ".") || strings.HasPrefix(property.key, "#") {
544
- mapping = true
545
- break
546
- }
547
- }
548
- if mapping {
549
- property, matched, ok := matchingPackageProperty(value.object, request)
550
- if ok {
551
- collectPackageTargets(property.value, matched, conditions, targets)
552
- }
553
- return
211
+ func supportedFileExtension(file string, supported [][]string) bool {
212
+ for _, extensions := range supported {
213
+ if shimtspath.FileExtensionIsOneOf(file, extensions) {
214
+ return true
554
215
  }
555
216
  }
556
- collectPackageTargets(value, wildcard, conditions, targets)
217
+ return false
557
218
  }
558
219
 
559
- func collectPackageTargets(value packageValue, wildcard string, conditions map[string]struct{}, targets *[]packageTarget) {
560
- if value.text != nil {
561
- *targets = append(*targets, packageTarget{path: *value.text, wildcard: wildcard})
220
+ // ApplyUniversalResolutionFailure marks every source because automatic type
221
+ // discovery contributes to one global Program rather than to one importer.
222
+ func (observation ProgramResolutionObservation) ApplyUniversalResolutionFailure(sources map[string][]string) {
223
+ if !observation.universalFailure {
562
224
  return
563
225
  }
564
- for _, child := range value.array {
565
- collectPackageTargets(child, wildcard, conditions, targets)
566
- }
567
- for _, property := range value.object {
568
- if conditions == nil {
569
- collectPackageTargets(property.value, wildcard, nil, targets)
570
- continue
571
- }
572
- if property.key == "default" {
573
- collectPackageTargets(property.value, wildcard, conditions, targets)
574
- continue
575
- }
576
- if _, active := conditions[property.key]; active {
577
- collectPackageTargets(property.value, wildcard, conditions, targets)
578
- }
226
+ for source := range sources {
227
+ observation.Failures[source] = string(inputProofResolutionChanged)
579
228
  }
580
229
  }
581
230
 
582
- func decodePackageValue(raw json.RawMessage) (packageValue, bool) {
583
- trimmed := bytes.TrimSpace(raw)
584
- if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
585
- return packageValue{}, false
586
- }
587
- switch trimmed[0] {
588
- case '"':
589
- var text string
590
- if json.Unmarshal(trimmed, &text) != nil {
591
- return packageValue{}, false
592
- }
593
- return packageValue{text: &text}, true
594
- case '[':
595
- var rawChildren []json.RawMessage
596
- if json.Unmarshal(trimmed, &rawChildren) != nil {
597
- return packageValue{}, false
598
- }
599
- value := packageValue{array: make([]packageValue, 0, len(rawChildren))}
600
- for _, child := range rawChildren {
601
- parsed, ok := decodePackageValue(child)
602
- if ok {
603
- value.array = append(value.array, parsed)
604
- }
605
- }
606
- return value, true
607
- case '{':
608
- decoder := json.NewDecoder(bytes.NewReader(trimmed))
609
- if _, err := decoder.Token(); err != nil {
610
- return packageValue{}, false
231
+ func appendResolutionPaths(
232
+ target []string,
233
+ cwd string,
234
+ observer *inputObservationFS,
235
+ selected []string,
236
+ caseSensitive bool,
237
+ flat *[]ProgramResolutionInput,
238
+ ) []string {
239
+ for _, candidate := range observer.observedPaths() {
240
+ selectedPath := slicesContainResolutionPath(selected, candidate, caseSensitive)
241
+ identityOnly := selectedPath && observedResolutionPathHasDistinctIdentity(observer, candidate, caseSensitive)
242
+ if selectedPath && !identityOnly {
243
+ continue
611
244
  }
612
- value := packageValue{}
613
- for decoder.More() {
614
- token, err := decoder.Token()
615
- key, keyOK := token.(string)
616
- if err != nil || !keyOK {
617
- return packageValue{}, false
618
- }
619
- var child json.RawMessage
620
- if decoder.Decode(&child) != nil {
621
- return packageValue{}, false
622
- }
623
- parsed, ok := decodePackageValue(child)
624
- if ok {
625
- value.object = append(value.object, packageProperty{key: key, value: parsed})
245
+ if !identityOnly {
246
+ for _, target := range selected {
247
+ if observedResolutionAlias(observer, candidate, target, caseSensitive) {
248
+ identityOnly = true
249
+ break
250
+ }
626
251
  }
627
252
  }
628
- if _, err := decoder.Token(); err != nil {
629
- return packageValue{}, false
630
- }
631
- return value, true
632
- default:
633
- return packageValue{}, false
253
+ *flat = append(*flat, ProgramResolutionInput{
254
+ DirectoryEntries: observer.observedAccessibleEntries(candidate),
255
+ IdentityOnly: identityOnly,
256
+ Path: candidate,
257
+ })
258
+ target = append(target, TransformOutputKey(cwd, candidate))
634
259
  }
260
+ return target
635
261
  }
636
262
 
637
- func packageSubpath(specifier string) string {
638
- parts := strings.Split(filepath.ToSlash(specifier), "/")
639
- count := 1
640
- if strings.HasPrefix(specifier, "@") && len(parts) > 1 {
641
- count = 2
642
- }
643
- if len(parts) <= count {
644
- return ""
263
+ func observedResolutionPathHasDistinctIdentity(observer *inputObservationFS, candidate string, caseSensitive bool) bool {
264
+ if observer == nil || candidate == "" {
265
+ return false
645
266
  }
646
- return strings.Join(parts[count:], "/")
267
+ observation, failure := observer.predicateProof(candidate)
268
+ return failure == "" && observation.Realpath != nil && observation.Realpath.OK && !sameResolutionPath(observation.Realpath.Path, candidate, caseSensitive)
647
269
  }
648
270
 
649
- func compilerOptionCandidates(configs []*tsoptions.ParsedCommandLine, specifier string, context ModuleResolutionContext) []string {
650
- candidates := []string{}
651
- for _, parsed := range configs {
652
- if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
653
- continue
654
- }
655
- options := parsed.ParsedConfig.CompilerOptions
656
- if options.Paths == nil {
657
- continue
658
- }
659
- base := options.GetPathsBasePath(parsed.GetCurrentDirectory())
660
- pattern := ""
661
- matched := ""
662
- targets := []string(nil)
663
- for candidatePattern, candidateTargets := range options.Paths.Entries() {
664
- if candidatePattern == specifier {
665
- pattern = candidatePattern
666
- matched = ""
667
- targets = candidateTargets
668
- break
669
- }
670
- candidateMatched, ok := matchPathPattern(candidatePattern, specifier)
671
- if !ok || (pattern != "" && comparePatternKeys(candidatePattern, pattern) >= 0) {
672
- continue
673
- }
674
- pattern = candidatePattern
675
- matched = candidateMatched
676
- targets = candidateTargets
677
- }
678
- if pattern == "" {
679
- continue
680
- }
681
- for _, target := range targets {
682
- target = strings.Replace(target, "*", matched, 1)
683
- candidates = append(candidates, moduleFileCandidates(filepath.Join(base, filepath.FromSlash(target)), context, true)...)
271
+ func slicesContainResolutionPath(paths []string, candidate string, caseSensitive bool) bool {
272
+ for _, path := range paths {
273
+ if sameResolutionPath(candidate, path, caseSensitive) {
274
+ return true
684
275
  }
685
276
  }
686
- return candidates
277
+ return false
687
278
  }
688
279
 
689
- func rootDirsCandidates(configs []*tsoptions.ParsedCommandLine, directory, specifier string, context ModuleResolutionContext) []string {
690
- candidates := []string{}
691
- for _, parsed := range configs {
692
- if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
693
- continue
694
- }
695
- roots := parsed.ParsedConfig.CompilerOptions.RootDirs
696
- candidate := filepath.Clean(filepath.Join(directory, filepath.FromSlash(specifier)))
697
- matchedRoot := ""
698
- suffix := ""
699
- for _, root := range roots {
700
- relative, err := filepath.Rel(root, candidate)
701
- if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
702
- continue
703
- }
704
- if len(root) > len(matchedRoot) {
705
- matchedRoot = root
706
- suffix = relative
707
- }
708
- }
709
- if matchedRoot == "" {
710
- continue
711
- }
712
- for _, targetRoot := range roots {
713
- if filepath.Clean(targetRoot) == filepath.Clean(matchedRoot) {
714
- continue
715
- }
716
- candidates = append(candidates, moduleFileCandidates(filepath.Join(targetRoot, suffix), context, true)...)
717
- }
280
+ func sameResolutionPath(left, right string, caseSensitive bool) bool {
281
+ if left == "" || right == "" {
282
+ return false
718
283
  }
719
- return candidates
284
+ return shimtspath.GetCanonicalFileName(shimtspath.NormalizePath(left), caseSensitive) == shimtspath.GetCanonicalFileName(shimtspath.NormalizePath(right), caseSensitive)
720
285
  }
721
286
 
722
- func matchPathPattern(pattern, specifier string) (string, bool) {
723
- star := strings.IndexByte(pattern, '*')
724
- if star < 0 {
725
- return "", pattern == specifier
726
- }
727
- prefix := pattern[:star]
728
- suffix := pattern[star+1:]
729
- if len(specifier) < len(prefix)+len(suffix) || !strings.HasPrefix(specifier, prefix) || !strings.HasSuffix(specifier, suffix) {
730
- return "", false
287
+ func observedResolutionAlias(observer *inputObservationFS, candidate, target string, caseSensitive bool) bool {
288
+ if observer == nil || candidate == "" || target == "" {
289
+ return false
731
290
  }
732
- return specifier[len(prefix) : len(specifier)-len(suffix)], true
291
+ observation, failure := observer.predicateProof(candidate)
292
+ return failure == "" && observation.Realpath != nil && observation.Realpath.OK && sameResolutionPath(observation.Realpath.Path, target, caseSensitive)
733
293
  }
734
294
 
735
- func matchingPackageProperty(properties []packageProperty, request string) (packageProperty, string, bool) {
736
- for _, property := range properties {
737
- if property.key == request {
738
- return property, "", true
739
- }
740
- }
741
- var selected *packageProperty
742
- selectedText := ""
743
- for index := range properties {
744
- matched, ok := matchPackagePathPattern(properties[index].key, request)
745
- if !ok || (selected != nil && comparePatternKeys(properties[index].key, selected.key) >= 0) {
295
+ func compactResolutionInputs(inputs []ProgramResolutionInput) []ProgramResolutionInput {
296
+ byPath := make(map[string]ProgramResolutionInput, len(inputs))
297
+ for _, input := range inputs {
298
+ if strings.TrimSpace(input.Path) == "" {
746
299
  continue
747
300
  }
748
- selected = &properties[index]
749
- selectedText = matched
750
- }
751
- if selected == nil {
752
- return packageProperty{}, "", false
753
- }
754
- return *selected, selectedText, true
755
- }
756
-
757
- func matchPackagePathPattern(pattern, request string) (string, bool) {
758
- if matched, ok := matchPathPattern(pattern, request); ok {
759
- return matched, true
760
- }
761
- if strings.HasSuffix(pattern, "/") && strings.HasPrefix(request, pattern) {
762
- return request[len(pattern):], true
763
- }
764
- return "", false
765
- }
766
-
767
- // comparePatternKeys is the TypeScript-Go resolver's pattern ordering: the
768
- // longest prefix wins, then a pattern with the longest trailer. It lets the
769
- // host retain candidates from exactly the one mapping the compiler selected.
770
- func comparePatternKeys(left, right string) int {
771
- leftStar := strings.IndexByte(left, '*')
772
- rightStar := strings.IndexByte(right, '*')
773
- leftBaseLength := len(left)
774
- if leftStar >= 0 {
775
- leftBaseLength = leftStar + 1
776
- }
777
- rightBaseLength := len(right)
778
- if rightStar >= 0 {
779
- rightBaseLength = rightStar + 1
780
- }
781
- switch {
782
- case leftBaseLength > rightBaseLength:
783
- return -1
784
- case leftBaseLength < rightBaseLength:
785
- return 1
786
- case leftStar < 0:
787
- return 1
788
- case rightStar < 0:
789
- return -1
790
- case len(left) > len(right):
791
- return -1
792
- case len(left) < len(right):
793
- return 1
794
- default:
795
- return 0
301
+ if previous, found := byPath[input.Path]; found {
302
+ input.DirectoryEntries = previous.DirectoryEntries || input.DirectoryEntries
303
+ input.IdentityOnly = previous.IdentityOnly && input.IdentityOnly
304
+ }
305
+ byPath[input.Path] = input
796
306
  }
797
- }
798
-
799
- func packageRoot(base, specifier string) string {
800
- parts := strings.Split(filepath.ToSlash(specifier), "/")
801
- count := 1
802
- if strings.HasPrefix(specifier, "@") && len(parts) > 1 {
803
- count = 2
307
+ paths := make([]string, 0, len(byPath))
308
+ for path := range byPath {
309
+ paths = append(paths, path)
804
310
  }
805
- suffixCount := len(parts) - count
806
- root := base
807
- for range suffixCount {
808
- root = filepath.Dir(root)
311
+ sort.Strings(paths)
312
+ output := make([]ProgramResolutionInput, 0, len(paths))
313
+ for _, path := range paths {
314
+ output = append(output, byPath[path])
809
315
  }
810
- return root
316
+ return output
811
317
  }
812
318
 
813
319
  func compactStringsInOrder(input []string) []string {