@ttsc/paths 0.19.3 → 0.20.0

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 (2) hide show
  1. package/driver/paths.go +63 -25
  2. package/package.json +1 -1
package/driver/paths.go CHANGED
@@ -6,6 +6,7 @@ import (
6
6
  "strings"
7
7
 
8
8
  shimast "github.com/microsoft/typescript-go/shim/ast"
9
+ shimchecker "github.com/microsoft/typescript-go/shim/checker"
9
10
  shimcore "github.com/microsoft/typescript-go/shim/core"
10
11
  shimtspath "github.com/microsoft/typescript-go/shim/tspath"
11
12
 
@@ -32,12 +33,14 @@ func (plugin) ApplyProgram(prog *driver.Program, _ driver.PluginContext) error {
32
33
  // rewriter holds the resolved tsconfig paths configuration used to rewrite
33
34
  // module specifiers across an entire program.
34
35
  type rewriter struct {
35
- basePath string
36
- jsxPreserve bool
37
- outDir string
38
- patterns []pathPattern
39
- rootDir string
40
- sourceFiles map[string]string // normalized source path → same path (used as a set)
36
+ checker *shimchecker.Checker
37
+ basePath string
38
+ canonicalFileName func(string) string
39
+ jsxPreserve bool
40
+ outDir string
41
+ patterns []pathPattern
42
+ rootDir string
43
+ sourceFiles map[string]string // canonical source path → original normalized path
41
44
  }
42
45
 
43
46
  // pathPattern is a single tsconfig paths entry with its wildcard pattern and
@@ -56,10 +59,17 @@ var sourceLookupExtensions = []string{
56
59
  // Patterns are sorted by decreasing specificity (longer literal prefix first)
57
60
  // so the most-specific match wins on overlapping patterns.
58
61
  func newRewriter(prog *driver.Program) *rewriter {
59
- out := &rewriter{sourceFiles: map[string]string{}}
62
+ caseSensitive := useCaseSensitiveFileNames(prog)
63
+ out := &rewriter{
64
+ canonicalFileName: func(name string) string {
65
+ return shimtspath.GetCanonicalFileName(name, caseSensitive)
66
+ },
67
+ sourceFiles: map[string]string{},
68
+ }
60
69
  if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
61
70
  return out
62
71
  }
72
+ out.checker = prog.Checker
63
73
  options := prog.ParsedConfig.ParsedConfig.CompilerOptions
64
74
  cwd := prog.Host.GetCurrentDirectory()
65
75
  out.basePath = filepath.Clean(options.GetPathsBasePath(cwd))
@@ -75,7 +85,7 @@ func newRewriter(prog *driver.Program) *rewriter {
75
85
  out.rootDir = inferredRootDir(options.ConfigFilePath, fileNames, cwd, useCaseSensitiveFileNames(prog))
76
86
  }
77
87
  for _, name := range fileNames {
78
- out.sourceFiles[name] = name
88
+ out.sourceFiles[out.sourceKey(name)] = name
79
89
  }
80
90
  if options.Paths != nil {
81
91
  for key, targets := range options.Paths.Entries() {
@@ -104,7 +114,7 @@ func (r *rewriter) apply(file *shimast.SourceFile) {
104
114
  if r == nil || file == nil || len(r.patterns) == 0 {
105
115
  return
106
116
  }
107
- visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
117
+ visitModuleSpecifiers(file, r.checker, func(lit *shimast.Node) {
108
118
  if lit == nil || lit.Kind != shimast.KindStringLiteral {
109
119
  return
110
120
  }
@@ -118,16 +128,16 @@ func (r *rewriter) apply(file *shimast.SourceFile) {
118
128
  })
119
129
  }
120
130
 
121
- // visitModuleSpecifiers recursively walks the AST rooted at node, calling
122
- // visit for every string-literal module specifier it finds. Covered nodes
123
- // include import/export declarations, require() calls, dynamic import()
124
- // expressions, import-equals declarations, and import-type nodes.
131
+ // visitModuleSpecifiers recursively walks file, calling visit for every
132
+ // string-literal module reference it finds. Syntax identifies candidates, then
133
+ // SourceFile and Checker facts exclude declarations and calls whose literal is
134
+ // not actually a module reference in the containing file.
125
135
  //
126
136
  // The recursion runs through one closure created per walk, not one per node:
127
137
  // handing ForEachChild a fresh closure at every node would allocate once per
128
138
  // AST node across the whole program on every apply pass.
129
- func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
130
- if node == nil {
139
+ func visitModuleSpecifiers(file *shimast.SourceFile, checker *shimchecker.Checker, visit func(*shimast.Node)) {
140
+ if file == nil {
131
141
  return
132
142
  }
133
143
  var walk func(node *shimast.Node) bool
@@ -152,24 +162,24 @@ func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
152
162
  }
153
163
  case shimast.KindModuleDeclaration:
154
164
  decl := node.AsModuleDeclaration()
155
- if decl != nil {
165
+ if file.ExternalModuleIndicator != nil && decl != nil {
156
166
  visit(decl.Name())
157
167
  }
158
168
  case shimast.KindCallExpression:
159
169
  call := node.AsCallExpression()
160
- if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
170
+ if isModuleSpecifierCall(checker, call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
161
171
  visit(call.Arguments.Nodes[0])
162
172
  }
163
173
  }
164
174
  node.ForEachChild(walk)
165
175
  return false
166
176
  }
167
- walk(node)
177
+ walk(file.AsNode())
168
178
  }
169
179
 
170
180
  // isModuleSpecifierCall reports whether call is a dynamic import() or a
171
- // CommonJS require() expression.
172
- func isModuleSpecifierCall(call *shimast.CallExpression) bool {
181
+ // CommonJS require() expression that resolves to the module loader.
182
+ func isModuleSpecifierCall(checker *shimchecker.Checker, call *shimast.CallExpression) bool {
173
183
  if call == nil || call.Expression == nil {
174
184
  return false
175
185
  }
@@ -177,12 +187,28 @@ func isModuleSpecifierCall(call *shimast.CallExpression) bool {
177
187
  case shimast.KindImportKeyword:
178
188
  return true
179
189
  case shimast.KindIdentifier:
180
- return call.Expression.Text() == "require"
190
+ return call.Expression.Text() == "require" && isModuleLoader(checker, call.Expression)
181
191
  default:
182
192
  return false
183
193
  }
184
194
  }
185
195
 
196
+ // isModuleLoader reports whether expression names the CommonJS loader rather
197
+ // than a parameter, local, or imported binding named require. An unresolved
198
+ // bare require is the runtime CommonJS global; declared loaders are ambient.
199
+ func isModuleLoader(checker *shimchecker.Checker, expression *shimast.Node) bool {
200
+ if checker == nil || expression == nil {
201
+ return false
202
+ }
203
+ symbol := checker.GetSymbolAtLocation(expression)
204
+ if symbol == nil {
205
+ return true
206
+ }
207
+ return symbol.Flags&shimast.SymbolFlagsAlias == 0 &&
208
+ symbol.ValueDeclaration != nil &&
209
+ symbol.ValueDeclaration.Flags&shimast.NodeFlagsAmbient != 0
210
+ }
211
+
186
212
  // rewrite resolves specifier from fromSource using the tsconfig paths table and
187
213
  // returns the relative output path. Returns (specifier, false) when the specifier
188
214
  // is already relative, absolute, or does not match any paths pattern.
@@ -236,23 +262,35 @@ func (r *rewriter) resolveSource(specifier string) (string, bool) {
236
262
  // extension) corresponds to a known source file. It tries the exact path, stem
237
263
  // with each known TypeScript/JavaScript source extension, and index files.
238
264
  func (r *rewriter) lookupSource(candidate string) (string, bool) {
239
- if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
265
+ normalized := normalizePath(candidate)
266
+ if source, ok := r.sourceFiles[r.sourceKey(normalized)]; ok {
240
267
  return source, true
241
268
  }
242
- stem := stripKnownSourceExtension(normalizePath(candidate))
269
+ stem := stripKnownSourceExtension(normalized)
243
270
  for _, ext := range sourceLookupExtensions {
244
- if source, ok := r.sourceFiles[stem+ext]; ok {
271
+ if source, ok := r.sourceFiles[r.sourceKey(stem+ext)]; ok {
245
272
  return source, true
246
273
  }
247
274
  }
248
275
  for _, ext := range sourceLookupExtensions {
249
- if source, ok := r.sourceFiles[normalizePath(filepath.Join(stem, "index"+ext))]; ok {
276
+ if source, ok := r.sourceFiles[r.sourceKey(filepath.Join(stem, "index"+ext))]; ok {
250
277
  return source, true
251
278
  }
252
279
  }
253
280
  return "", false
254
281
  }
255
282
 
283
+ // sourceKey applies the compiler host's filesystem identity rule to one path.
284
+ // Synthetic test rewriters leave canonicalFileName nil and retain the previous
285
+ // exact-key behavior.
286
+ func (r *rewriter) sourceKey(value string) string {
287
+ normalized := normalizePath(value)
288
+ if r.canonicalFileName == nil {
289
+ return normalized
290
+ }
291
+ return r.canonicalFileName(normalized)
292
+ }
293
+
256
294
  // outputPathForSource maps a source file path to its emitted output path under
257
295
  // outDir, swapping the source extension for the appropriate JS extension. Returns
258
296
  // "" when outDir or rootDir is unset, or when source is outside rootDir.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/paths",
3
- "version": "0.19.3",
3
+ "version": "0.20.0",
4
4
  "description": "First-party ttsc plugin that rewrites emitted module specifiers from tsconfig paths.",
5
5
  "main": "src/index.cjs",
6
6
  "exports": {