@ttsc/paths 0.16.6 → 0.16.8

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.
package/README.md CHANGED
@@ -42,7 +42,7 @@ Configure those fields under `compilerOptions`:
42
42
 
43
43
  An import such as `import { value } from "@lib/value"` becomes a relative JavaScript import such as `import { value } from "./modules/value.js"`. Declaration output follows the same source rewrite.
44
44
 
45
- `outDir` must be set for the rewrite to run. Without it, `@ttsc/paths` cannot map a source path to its emitted location and makes no changes. `rootDir` is optional: when omitted it is derived from the common source directory.
45
+ `outDir` must be set for the rewrite to run. Without it, `@ttsc/paths` cannot map a source path to its emitted location and makes no changes. `rootDir` is optional: when omitted it defaults to the tsconfig's directory, matching where TypeScript-Go anchors emitted output.
46
46
 
47
47
  ## Sponsors
48
48
 
package/driver/paths.go CHANGED
@@ -7,6 +7,7 @@ import (
7
7
 
8
8
  shimast "github.com/microsoft/typescript-go/shim/ast"
9
9
  shimcore "github.com/microsoft/typescript-go/shim/core"
10
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
10
11
 
11
12
  "github.com/samchon/ttsc/packages/ttsc/driver"
12
13
  )
@@ -60,16 +61,20 @@ func newRewriter(prog *driver.Program) *rewriter {
60
61
  return out
61
62
  }
62
63
  options := prog.ParsedConfig.ParsedConfig.CompilerOptions
63
- out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
64
+ cwd := prog.Host.GetCurrentDirectory()
65
+ out.basePath = filepath.Clean(options.GetPathsBasePath(cwd))
64
66
  out.jsxPreserve = options.Jsx == shimcore.JsxEmitPreserve
65
- out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
66
- out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
67
+ out.outDir = optionalPath(options.OutDir, cwd)
68
+ out.rootDir = optionalPath(options.RootDir, cwd)
67
69
  files := prog.SourceFiles()
70
+ fileNames := make([]string, 0, len(files))
71
+ for _, file := range files {
72
+ fileNames = append(fileNames, normalizePath(file.FileName()))
73
+ }
68
74
  if out.rootDir == "" {
69
- out.rootDir = commonSourceDir(files)
75
+ out.rootDir = inferredRootDir(options.ConfigFilePath, fileNames, cwd, useCaseSensitiveFileNames(prog))
70
76
  }
71
- for _, file := range files {
72
- name := normalizePath(file.FileName())
77
+ for _, name := range fileNames {
73
78
  out.sourceFiles[name] = name
74
79
  }
75
80
  if options.Paths != nil {
@@ -80,12 +85,20 @@ func newRewriter(prog *driver.Program) *rewriter {
80
85
  })
81
86
  }
82
87
  }
83
- sort.SliceStable(out.patterns, func(i, j int) bool {
84
- return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
85
- })
88
+ orderPatterns(out.patterns)
86
89
  return out
87
90
  }
88
91
 
92
+ // useCaseSensitiveFileNames reports the host filesystem's case sensitivity,
93
+ // defaulting to case-sensitive when the program carries no filesystem (bare
94
+ // rewriters built by unit tests).
95
+ func useCaseSensitiveFileNames(prog *driver.Program) bool {
96
+ if prog == nil || prog.FS == nil {
97
+ return true
98
+ }
99
+ return prog.FS.UseCaseSensitiveFileNames()
100
+ }
101
+
89
102
  // apply rewrites all module specifiers in file that match a tsconfig paths pattern.
90
103
  func (r *rewriter) apply(file *shimast.SourceFile) {
91
104
  if r == nil || file == nil || len(r.patterns) == 0 {
@@ -109,40 +122,49 @@ func (r *rewriter) apply(file *shimast.SourceFile) {
109
122
  // visit for every string-literal module specifier it finds. Covered nodes
110
123
  // include import/export declarations, require() calls, dynamic import()
111
124
  // expressions, import-equals declarations, and import-type nodes.
125
+ //
126
+ // The recursion runs through one closure created per walk, not one per node:
127
+ // handing ForEachChild a fresh closure at every node would allocate once per
128
+ // AST node across the whole program on every apply pass.
112
129
  func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
113
130
  if node == nil {
114
131
  return
115
132
  }
116
- switch node.Kind {
117
- case shimast.KindImportDeclaration:
118
- visit(node.AsImportDeclaration().ModuleSpecifier)
119
- case shimast.KindExportDeclaration:
120
- visit(node.AsExportDeclaration().ModuleSpecifier)
121
- case shimast.KindImportEqualsDeclaration:
122
- ref := node.AsImportEqualsDeclaration().ModuleReference
123
- if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
124
- visit(ref.AsExternalModuleReference().Expression)
125
- }
126
- case shimast.KindImportType:
127
- arg := node.AsImportTypeNode().Argument
128
- if arg != nil && arg.Kind == shimast.KindLiteralType {
129
- visit(arg.AsLiteralTypeNode().Literal)
133
+ var walk func(node *shimast.Node) bool
134
+ walk = func(node *shimast.Node) bool {
135
+ if node == nil {
136
+ return false
130
137
  }
131
- case shimast.KindModuleDeclaration:
132
- decl := node.AsModuleDeclaration()
133
- if decl != nil {
134
- visit(decl.Name())
135
- }
136
- case shimast.KindCallExpression:
137
- call := node.AsCallExpression()
138
- if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
139
- visit(call.Arguments.Nodes[0])
138
+ switch node.Kind {
139
+ case shimast.KindImportDeclaration:
140
+ visit(node.AsImportDeclaration().ModuleSpecifier)
141
+ case shimast.KindExportDeclaration:
142
+ visit(node.AsExportDeclaration().ModuleSpecifier)
143
+ case shimast.KindImportEqualsDeclaration:
144
+ ref := node.AsImportEqualsDeclaration().ModuleReference
145
+ if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
146
+ visit(ref.AsExternalModuleReference().Expression)
147
+ }
148
+ case shimast.KindImportType:
149
+ arg := node.AsImportTypeNode().Argument
150
+ if arg != nil && arg.Kind == shimast.KindLiteralType {
151
+ visit(arg.AsLiteralTypeNode().Literal)
152
+ }
153
+ case shimast.KindModuleDeclaration:
154
+ decl := node.AsModuleDeclaration()
155
+ if decl != nil {
156
+ visit(decl.Name())
157
+ }
158
+ case shimast.KindCallExpression:
159
+ call := node.AsCallExpression()
160
+ if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
161
+ visit(call.Arguments.Nodes[0])
162
+ }
140
163
  }
141
- }
142
- node.ForEachChild(func(child *shimast.Node) bool {
143
- visitModuleSpecifiers(child, visit)
164
+ node.ForEachChild(walk)
144
165
  return false
145
- })
166
+ }
167
+ walk(node)
146
168
  }
147
169
 
148
170
  // isModuleSpecifierCall reports whether call is a dynamic import() or a
@@ -185,9 +207,13 @@ func (r *rewriter) rewrite(fromSource string, specifier string) (string, bool) {
185
207
  return rel, true
186
208
  }
187
209
 
188
- // resolveSource finds the source file that a tsconfig paths specifier resolves to.
189
- // It iterates over sorted patterns and, for each match, tries all substitution
190
- // targets (with and without known extensions) including index files.
210
+ // resolveSource finds the source file that a tsconfig paths specifier
211
+ // resolves to. Patterns are pre-sorted into tsc's precedence order, and like
212
+ // tsc's tryLoadModuleUsingPaths the resolution commits to the first (best)
213
+ // matching pattern: only that pattern's substitution targets are tried, in
214
+ // order, with extension and index fallbacks. When none of them names a
215
+ // program source the specifier stays unrewritten — falling through to a
216
+ // weaker pattern would rewrite at a module the type checker never resolved.
191
217
  func (r *rewriter) resolveSource(specifier string) (string, bool) {
192
218
  for _, pattern := range r.patterns {
193
219
  star, ok := matchPattern(pattern.pattern, specifier)
@@ -201,6 +227,7 @@ func (r *rewriter) resolveSource(specifier string) (string, bool) {
201
227
  return source, true
202
228
  }
203
229
  }
230
+ return "", false
204
231
  }
205
232
  return "", false
206
233
  }
@@ -261,23 +288,53 @@ func emittedJavaScriptExtension(source string, jsxPreserve bool) string {
261
288
  // matchPattern matches specifier against a tsconfig paths pattern (which may
262
289
  // contain at most one "*" wildcard). Returns the captured wildcard segment and
263
290
  // true on a match, or ("", false) otherwise. Exact patterns are matched with
264
- // simple equality.
291
+ // simple equality. The length guard mirrors tsc's isPatternMatch: a specifier
292
+ // shorter than the pattern's literal halves combined can still satisfy both
293
+ // the prefix and suffix probes ("@lib/x" against "@lib/x*x"), and slicing the
294
+ // star capture out of it would panic on inverted bounds.
265
295
  func matchPattern(pattern string, specifier string) (string, bool) {
266
296
  if !strings.Contains(pattern, "*") {
267
297
  return "", pattern == specifier
268
298
  }
269
299
  parts := strings.SplitN(pattern, "*", 2)
270
- if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
300
+ if strings.Contains(parts[1], "*") {
301
+ // More than one wildcard is not a pattern at all in tsc
302
+ // (TryParsePattern discards it), so it must never match here either.
303
+ return "", false
304
+ }
305
+ if len(specifier) < len(parts[0])+len(parts[1]) ||
306
+ !strings.HasPrefix(specifier, parts[0]) ||
307
+ !strings.HasSuffix(specifier, parts[1]) {
271
308
  return "", false
272
309
  }
273
310
  return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
274
311
  }
275
312
 
276
- // patternRank returns the length of a tsconfig paths pattern after removing its
277
- // wildcard character. Patterns with a higher rank (longer literal content) are
278
- // preferred when multiple patterns match the same specifier.
279
- func patternRank(pattern string) int {
280
- return len(strings.ReplaceAll(pattern, "*", ""))
313
+ // orderPatterns sorts patterns in place into tsc's matchPatternOrExact
314
+ // precedence: exact patterns (no wildcard) first, then wildcard patterns by
315
+ // decreasing literal-prefix length. Ranking by total literal length instead
316
+ // would steer a specifier at a long-suffix pattern ("*-styles") even though
317
+ // tsc resolves it through the longer prefix ("@app/*"), making the rewriter
318
+ // disagree with the type checker's own module resolution. Ties keep the
319
+ // tsconfig's declaration order, matching tsc's first-longest-prefix-wins scan.
320
+ func orderPatterns(patterns []pathPattern) {
321
+ sort.SliceStable(patterns, func(i, j int) bool {
322
+ a, b := patterns[i].pattern, patterns[j].pattern
323
+ aExact, bExact := !strings.Contains(a, "*"), !strings.Contains(b, "*")
324
+ if aExact != bExact {
325
+ return aExact
326
+ }
327
+ return patternPrefixLength(a) > patternPrefixLength(b)
328
+ })
329
+ }
330
+
331
+ // patternPrefixLength returns the length of the literal text before the "*"
332
+ // wildcard, or the whole pattern length for exact patterns.
333
+ func patternPrefixLength(pattern string) int {
334
+ if i := strings.IndexByte(pattern, '*'); i >= 0 {
335
+ return i
336
+ }
337
+ return len(pattern)
281
338
  }
282
339
 
283
340
  // optionalPath resolves value as a path relative to cwd when it is non-empty
@@ -292,25 +349,55 @@ func optionalPath(value string, cwd string) string {
292
349
  return normalizePath(filepath.Join(cwd, value))
293
350
  }
294
351
 
295
- // commonSourceDir returns the longest common directory prefix of all file paths
296
- // in files. It is used as rootDir when the tsconfig does not specify one.
297
- // Returns "" when files is empty.
298
- func commonSourceDir(files []*shimast.SourceFile) string {
299
- if len(files) == 0 {
300
- return ""
352
+ // inferredRootDir mirrors TypeScript-Go's GetCommonSourceDirectory fallback
353
+ // chain for a project without an explicit rootDir: the tsconfig's directory
354
+ // when the program was loaded from one, else the deepest directory shared by
355
+ // every input file. The rewriter must anchor output paths exactly where tsgo
356
+ // anchors its own emit, or the rewritten specifiers drift from the real
357
+ // output layout.
358
+ func inferredRootDir(configFilePath string, fileNames []string, currentDirectory string, useCaseSensitiveFileNames bool) string {
359
+ if configFilePath != "" {
360
+ return normalizePath(filepath.Dir(configFilePath))
301
361
  }
302
- common := normalizePath(filepath.Dir(files[0].FileName()))
303
- for _, file := range files[1:] {
304
- dir := normalizePath(filepath.Dir(file.FileName()))
305
- for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
306
- next := filepath.Dir(common)
307
- if next == common {
308
- return common
309
- }
310
- common = normalizePath(next)
362
+ return commonSourceDir(fileNames, currentDirectory, useCaseSensitiveFileNames)
363
+ }
364
+
365
+ // commonSourceDir mirrors TypeScript-Go's
366
+ // computeCommonSourceDirectoryOfFilenames: the deepest directory shared by
367
+ // every file, intersected per normalized path component under the host's case
368
+ // sensitivity. Returns "" when the files share no root at all — on Windows, a
369
+ // `files` list spanning two volumes — so the caller skips output mapping
370
+ // instead of guessing. The previous byte-oriented walk hung there (#310):
371
+ // once the shared prefix shrank to a volume root, filepath.Dir handed back
372
+ // the backslash form ("C:\") while the termination guard compared it against
373
+ // the slash-normalized cursor ("C:/"), re-normalizing the same directory
374
+ // forever.
375
+ func commonSourceDir(fileNames []string, currentDirectory string, useCaseSensitiveFileNames bool) string {
376
+ var common []string
377
+ for _, fileName := range fileNames {
378
+ components := shimtspath.GetNormalizedPathComponents(fileName, currentDirectory)
379
+ // The base file name is not part of the common directory path.
380
+ components = components[:len(components)-1]
381
+ if common == nil {
382
+ common = components
383
+ continue
311
384
  }
385
+ shared := 0
386
+ limit := min(len(common), len(components))
387
+ for shared < limit &&
388
+ shimtspath.GetCanonicalFileName(common[shared], useCaseSensitiveFileNames) ==
389
+ shimtspath.GetCanonicalFileName(components[shared], useCaseSensitiveFileNames) {
390
+ shared++
391
+ }
392
+ if shared == 0 {
393
+ return ""
394
+ }
395
+ common = common[:shared]
396
+ }
397
+ if len(common) == 0 {
398
+ return ""
312
399
  }
313
- return common
400
+ return shimtspath.GetPathFromPathComponents(common)
314
401
  }
315
402
 
316
403
  // normalizePath cleans and converts a file path to forward-slash form.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/paths",
3
- "version": "0.16.6",
3
+ "version": "0.16.8",
4
4
  "description": "First-party ttsc plugin that rewrites emitted module specifiers from tsconfig paths.",
5
5
  "main": "src/index.cjs",
6
6
  "exports": {