@ttsc/paths 0.12.3 → 0.12.4

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 +269 -217
  2. package/package.json +1 -1
package/driver/paths.go CHANGED
@@ -1,288 +1,340 @@
1
1
  package paths
2
2
 
3
3
  import (
4
- "path/filepath"
5
- "sort"
6
- "strings"
4
+ "path/filepath"
5
+ "sort"
6
+ "strings"
7
7
 
8
- shimast "github.com/microsoft/typescript-go/shim/ast"
9
- shimcore "github.com/microsoft/typescript-go/shim/core"
8
+ shimast "github.com/microsoft/typescript-go/shim/ast"
9
+ shimcore "github.com/microsoft/typescript-go/shim/core"
10
10
 
11
- "github.com/samchon/ttsc/packages/ttsc/driver"
11
+ "github.com/samchon/ttsc/packages/ttsc/driver"
12
12
  )
13
13
 
14
14
  func init() {
15
- driver.RegisterPlugin(plugin{})
15
+ driver.RegisterPlugin(plugin{})
16
16
  }
17
17
 
18
+ // plugin implements driver.ProgramPlugin for @ttsc/paths.
18
19
  type plugin struct{}
19
20
 
21
+ // ApplyProgram rewrites tsconfig paths aliases to relative import specifiers
22
+ // across every source file in the program.
20
23
  func (plugin) ApplyProgram(prog *driver.Program, _ driver.PluginContext) error {
21
- rewriter := newRewriter(prog)
22
- for _, file := range prog.SourceFiles() {
23
- rewriter.apply(file)
24
- }
25
- return nil
24
+ rewriter := newRewriter(prog)
25
+ for _, file := range prog.SourceFiles() {
26
+ rewriter.apply(file)
27
+ }
28
+ return nil
26
29
  }
27
30
 
31
+ // rewriter holds the resolved tsconfig paths configuration used to rewrite
32
+ // module specifiers across an entire program.
28
33
  type rewriter struct {
29
- basePath string
30
- outDir string
31
- patterns []pathPattern
32
- rootDir string
33
- sourceFiles map[string]string
34
+ basePath string
35
+ outDir string
36
+ patterns []pathPattern
37
+ rootDir string
38
+ sourceFiles map[string]string // normalized source path → same path (used as a set)
34
39
  }
35
40
 
41
+ // pathPattern is a single tsconfig paths entry with its wildcard pattern and
42
+ // ordered list of substitution targets.
36
43
  type pathPattern struct {
37
- pattern string
38
- targets []string
44
+ pattern string
45
+ targets []string
39
46
  }
40
47
 
48
+ // newRewriter builds a rewriter from the program's compiler options.
49
+ // Patterns are sorted by decreasing specificity (longer literal prefix first)
50
+ // so the most-specific match wins on overlapping patterns.
41
51
  func newRewriter(prog *driver.Program) *rewriter {
42
- out := &rewriter{sourceFiles: map[string]string{}}
43
- if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
44
- return out
45
- }
46
- options := prog.ParsedConfig.ParsedConfig.CompilerOptions
47
- out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
48
- out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
49
- out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
50
- files := prog.SourceFiles()
51
- if out.rootDir == "" {
52
- out.rootDir = commonSourceDir(files)
53
- }
54
- for _, file := range files {
55
- name := normalizePath(file.FileName())
56
- out.sourceFiles[name] = name
57
- out.sourceFiles[stripKnownSourceExtension(name)] = name
58
- }
59
- if options.Paths != nil {
60
- for key, targets := range options.Paths.Entries() {
61
- out.patterns = append(out.patterns, pathPattern{
62
- pattern: key,
63
- targets: append([]string(nil), targets...),
64
- })
65
- }
66
- }
67
- sort.SliceStable(out.patterns, func(i, j int) bool {
68
- return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
69
- })
70
- return out
52
+ out := &rewriter{sourceFiles: map[string]string{}}
53
+ if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
54
+ return out
55
+ }
56
+ options := prog.ParsedConfig.ParsedConfig.CompilerOptions
57
+ out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
58
+ out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
59
+ out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
60
+ files := prog.SourceFiles()
61
+ if out.rootDir == "" {
62
+ out.rootDir = commonSourceDir(files)
63
+ }
64
+ for _, file := range files {
65
+ name := normalizePath(file.FileName())
66
+ out.sourceFiles[name] = name
67
+ out.sourceFiles[stripKnownSourceExtension(name)] = name
68
+ }
69
+ if options.Paths != nil {
70
+ for key, targets := range options.Paths.Entries() {
71
+ out.patterns = append(out.patterns, pathPattern{
72
+ pattern: key,
73
+ targets: append([]string(nil), targets...),
74
+ })
75
+ }
76
+ }
77
+ sort.SliceStable(out.patterns, func(i, j int) bool {
78
+ return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
79
+ })
80
+ return out
71
81
  }
72
82
 
83
+ // apply rewrites all module specifiers in file that match a tsconfig paths pattern.
73
84
  func (r *rewriter) apply(file *shimast.SourceFile) {
74
- if r == nil || file == nil || len(r.patterns) == 0 {
75
- return
76
- }
77
- visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
78
- if lit == nil || lit.Kind != shimast.KindStringLiteral {
79
- return
80
- }
81
- spec := lit.Text()
82
- rewritten, ok := r.rewrite(file.FileName(), spec)
83
- if ok && rewritten != spec {
84
- lit.AsStringLiteral().Text = rewritten
85
- lit.Flags |= shimast.NodeFlagsSynthesized
86
- lit.Loc = shimcore.UndefinedTextRange()
87
- }
88
- })
85
+ if r == nil || file == nil || len(r.patterns) == 0 {
86
+ return
87
+ }
88
+ visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
89
+ if lit == nil || lit.Kind != shimast.KindStringLiteral {
90
+ return
91
+ }
92
+ spec := lit.Text()
93
+ rewritten, ok := r.rewrite(file.FileName(), spec)
94
+ if ok && rewritten != spec {
95
+ lit.AsStringLiteral().Text = rewritten
96
+ lit.Flags |= shimast.NodeFlagsSynthesized
97
+ lit.Loc = shimcore.UndefinedTextRange()
98
+ }
99
+ })
89
100
  }
90
101
 
102
+ // visitModuleSpecifiers recursively walks the AST rooted at node, calling
103
+ // visit for every string-literal module specifier it finds. Covered nodes
104
+ // include import/export declarations, require() calls, dynamic import()
105
+ // expressions, import-equals declarations, and import-type nodes.
91
106
  func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
92
- if node == nil {
93
- return
94
- }
95
- switch node.Kind {
96
- case shimast.KindImportDeclaration:
97
- visit(node.AsImportDeclaration().ModuleSpecifier)
98
- case shimast.KindExportDeclaration:
99
- visit(node.AsExportDeclaration().ModuleSpecifier)
100
- case shimast.KindImportEqualsDeclaration:
101
- ref := node.AsImportEqualsDeclaration().ModuleReference
102
- if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
103
- visit(ref.AsExternalModuleReference().Expression)
104
- }
105
- case shimast.KindImportType:
106
- arg := node.AsImportTypeNode().Argument
107
- if arg != nil && arg.Kind == shimast.KindLiteralType {
108
- visit(arg.AsLiteralTypeNode().Literal)
109
- }
110
- case shimast.KindModuleDeclaration:
111
- decl := node.AsModuleDeclaration()
112
- if decl != nil {
113
- visit(decl.Name())
114
- }
115
- case shimast.KindCallExpression:
116
- call := node.AsCallExpression()
117
- if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
118
- visit(call.Arguments.Nodes[0])
119
- }
120
- }
121
- node.ForEachChild(func(child *shimast.Node) bool {
122
- visitModuleSpecifiers(child, visit)
123
- return false
124
- })
107
+ if node == nil {
108
+ return
109
+ }
110
+ switch node.Kind {
111
+ case shimast.KindImportDeclaration:
112
+ visit(node.AsImportDeclaration().ModuleSpecifier)
113
+ case shimast.KindExportDeclaration:
114
+ visit(node.AsExportDeclaration().ModuleSpecifier)
115
+ case shimast.KindImportEqualsDeclaration:
116
+ ref := node.AsImportEqualsDeclaration().ModuleReference
117
+ if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
118
+ visit(ref.AsExternalModuleReference().Expression)
119
+ }
120
+ case shimast.KindImportType:
121
+ arg := node.AsImportTypeNode().Argument
122
+ if arg != nil && arg.Kind == shimast.KindLiteralType {
123
+ visit(arg.AsLiteralTypeNode().Literal)
124
+ }
125
+ case shimast.KindModuleDeclaration:
126
+ decl := node.AsModuleDeclaration()
127
+ if decl != nil {
128
+ visit(decl.Name())
129
+ }
130
+ case shimast.KindCallExpression:
131
+ call := node.AsCallExpression()
132
+ if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
133
+ visit(call.Arguments.Nodes[0])
134
+ }
135
+ }
136
+ node.ForEachChild(func(child *shimast.Node) bool {
137
+ visitModuleSpecifiers(child, visit)
138
+ return false
139
+ })
125
140
  }
126
141
 
142
+ // isModuleSpecifierCall reports whether call is a dynamic import() or a
143
+ // CommonJS require() expression.
127
144
  func isModuleSpecifierCall(call *shimast.CallExpression) bool {
128
- if call == nil || call.Expression == nil {
129
- return false
130
- }
131
- switch call.Expression.Kind {
132
- case shimast.KindImportKeyword:
133
- return true
134
- case shimast.KindIdentifier:
135
- return call.Expression.Text() == "require"
136
- default:
137
- return false
138
- }
145
+ if call == nil || call.Expression == nil {
146
+ return false
147
+ }
148
+ switch call.Expression.Kind {
149
+ case shimast.KindImportKeyword:
150
+ return true
151
+ case shimast.KindIdentifier:
152
+ return call.Expression.Text() == "require"
153
+ default:
154
+ return false
155
+ }
139
156
  }
140
157
 
158
+ // rewrite resolves specifier from fromSource using the tsconfig paths table and
159
+ // returns the relative output path. Returns (specifier, false) when the specifier
160
+ // is already relative, absolute, or does not match any paths pattern.
141
161
  func (r *rewriter) rewrite(fromSource string, specifier string) (string, bool) {
142
- if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
143
- return specifier, false
144
- }
145
- targetSource, ok := r.resolveSource(specifier)
146
- if !ok {
147
- return specifier, false
148
- }
149
- fromOut := r.outputPathForSource(fromSource)
150
- targetOut := r.outputPathForSource(targetSource)
151
- if fromOut == "" || targetOut == "" {
152
- return specifier, false
153
- }
154
- rel, _ := filepath.Rel(filepath.Dir(fromOut), targetOut)
155
- rel = filepath.ToSlash(rel)
156
- if !strings.HasPrefix(rel, ".") {
157
- rel = "./" + rel
158
- }
159
- return rel, true
162
+ if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
163
+ return specifier, false
164
+ }
165
+ targetSource, ok := r.resolveSource(specifier)
166
+ if !ok {
167
+ return specifier, false
168
+ }
169
+ fromOut := r.outputPathForSource(fromSource)
170
+ targetOut := r.outputPathForSource(targetSource)
171
+ if fromOut == "" || targetOut == "" {
172
+ return specifier, false
173
+ }
174
+ rel, _ := filepath.Rel(filepath.Dir(fromOut), targetOut)
175
+ rel = filepath.ToSlash(rel)
176
+ if !strings.HasPrefix(rel, ".") {
177
+ rel = "./" + rel
178
+ }
179
+ return rel, true
160
180
  }
161
181
 
182
+ // resolveSource finds the source file that a tsconfig paths specifier resolves to.
183
+ // It iterates over sorted patterns and, for each match, tries all substitution
184
+ // targets (with and without known extensions) including index files.
162
185
  func (r *rewriter) resolveSource(specifier string) (string, bool) {
163
- for _, pattern := range r.patterns {
164
- star, ok := matchPattern(pattern.pattern, specifier)
165
- if !ok {
166
- continue
167
- }
168
- for _, target := range pattern.targets {
169
- candidate := strings.Replace(target, "*", star, 1)
170
- resolved := normalizePath(filepath.Join(r.basePath, candidate))
171
- if source, ok := r.lookupSource(resolved); ok {
172
- return source, true
173
- }
174
- }
175
- }
176
- return "", false
186
+ for _, pattern := range r.patterns {
187
+ star, ok := matchPattern(pattern.pattern, specifier)
188
+ if !ok {
189
+ continue
190
+ }
191
+ for _, target := range pattern.targets {
192
+ candidate := strings.Replace(target, "*", star, 1)
193
+ resolved := normalizePath(filepath.Join(r.basePath, candidate))
194
+ if source, ok := r.lookupSource(resolved); ok {
195
+ return source, true
196
+ }
197
+ }
198
+ }
199
+ return "", false
177
200
  }
178
201
 
202
+ // lookupSource checks whether candidate (a normalized path, possibly without
203
+ // extension) corresponds to a known source file. It tries the exact path, the
204
+ // extension-stripped stem, stem with each TS extension, and index files.
179
205
  func (r *rewriter) lookupSource(candidate string) (string, bool) {
180
- if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
181
- return source, true
182
- }
183
- stem := stripKnownSourceExtension(normalizePath(candidate))
184
- if source, ok := r.sourceFiles[stem]; ok {
185
- return source, true
186
- }
187
- for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
188
- if source, ok := r.sourceFiles[stem+ext]; ok {
189
- return source, true
190
- }
191
- }
192
- for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
193
- if source, ok := r.sourceFiles[normalizePath(filepath.Join(stem, "index"+ext))]; ok {
194
- return source, true
195
- }
196
- }
197
- return "", false
206
+ if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
207
+ return source, true
208
+ }
209
+ stem := stripKnownSourceExtension(normalizePath(candidate))
210
+ if source, ok := r.sourceFiles[stem]; ok {
211
+ return source, true
212
+ }
213
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
214
+ if source, ok := r.sourceFiles[stem+ext]; ok {
215
+ return source, true
216
+ }
217
+ }
218
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
219
+ if source, ok := r.sourceFiles[normalizePath(filepath.Join(stem, "index"+ext))]; ok {
220
+ return source, true
221
+ }
222
+ }
223
+ return "", false
198
224
  }
199
225
 
226
+ // outputPathForSource maps a source file path to its emitted output path under
227
+ // outDir, swapping the source extension for the appropriate JS extension. Returns
228
+ // "" when outDir or rootDir is unset, or when source is outside rootDir.
200
229
  func (r *rewriter) outputPathForSource(source string) string {
201
- if r.outDir == "" || r.rootDir == "" {
202
- return ""
203
- }
204
- rel, err := filepath.Rel(r.rootDir, source)
205
- if err != nil || isOutsideRelativePath(rel) {
206
- return ""
207
- }
208
- return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, emittedJavaScriptExtension(rel))))
230
+ if r.outDir == "" || r.rootDir == "" {
231
+ return ""
232
+ }
233
+ rel, err := filepath.Rel(r.rootDir, source)
234
+ if err != nil || isOutsideRelativePath(rel) {
235
+ return ""
236
+ }
237
+ return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, emittedJavaScriptExtension(rel))))
209
238
  }
210
239
 
240
+ // emittedJavaScriptExtension returns the JavaScript file extension that TypeScript
241
+ // emits for a given source path: ".mjs" for ".mts", ".cjs" for ".cts", ".js"
242
+ // for all other extensions.
211
243
  func emittedJavaScriptExtension(source string) string {
212
- switch strings.ToLower(filepath.Ext(source)) {
213
- case ".mts":
214
- return ".mjs"
215
- case ".cts":
216
- return ".cjs"
217
- default:
218
- return ".js"
219
- }
244
+ switch strings.ToLower(filepath.Ext(source)) {
245
+ case ".mts":
246
+ return ".mjs"
247
+ case ".cts":
248
+ return ".cjs"
249
+ default:
250
+ return ".js"
251
+ }
220
252
  }
221
253
 
254
+ // matchPattern matches specifier against a tsconfig paths pattern (which may
255
+ // contain at most one "*" wildcard). Returns the captured wildcard segment and
256
+ // true on a match, or ("", false) otherwise. Exact patterns are matched with
257
+ // simple equality.
222
258
  func matchPattern(pattern string, specifier string) (string, bool) {
223
- if !strings.Contains(pattern, "*") {
224
- return "", pattern == specifier
225
- }
226
- parts := strings.SplitN(pattern, "*", 2)
227
- if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
228
- return "", false
229
- }
230
- return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
259
+ if !strings.Contains(pattern, "*") {
260
+ return "", pattern == specifier
261
+ }
262
+ parts := strings.SplitN(pattern, "*", 2)
263
+ if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
264
+ return "", false
265
+ }
266
+ return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
231
267
  }
232
268
 
269
+ // patternRank returns the length of a tsconfig paths pattern after removing its
270
+ // wildcard character. Patterns with a higher rank (longer literal content) are
271
+ // preferred when multiple patterns match the same specifier.
233
272
  func patternRank(pattern string) int {
234
- return len(strings.ReplaceAll(pattern, "*", ""))
273
+ return len(strings.ReplaceAll(pattern, "*", ""))
235
274
  }
236
275
 
276
+ // optionalPath resolves value as a path relative to cwd when it is non-empty
277
+ // and not already absolute. Returns "" when value is empty.
237
278
  func optionalPath(value string, cwd string) string {
238
- if value == "" {
239
- return ""
240
- }
241
- if filepath.IsAbs(value) {
242
- return normalizePath(value)
243
- }
244
- return normalizePath(filepath.Join(cwd, value))
279
+ if value == "" {
280
+ return ""
281
+ }
282
+ if filepath.IsAbs(value) {
283
+ return normalizePath(value)
284
+ }
285
+ return normalizePath(filepath.Join(cwd, value))
245
286
  }
246
287
 
288
+ // commonSourceDir returns the longest common directory prefix of all file paths
289
+ // in files. It is used as rootDir when the tsconfig does not specify one.
290
+ // Returns "" when files is empty.
247
291
  func commonSourceDir(files []*shimast.SourceFile) string {
248
- if len(files) == 0 {
249
- return ""
250
- }
251
- common := normalizePath(filepath.Dir(files[0].FileName()))
252
- for _, file := range files[1:] {
253
- dir := normalizePath(filepath.Dir(file.FileName()))
254
- for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
255
- next := filepath.Dir(common)
256
- if next == common {
257
- return common
258
- }
259
- common = normalizePath(next)
260
- }
261
- }
262
- return common
292
+ if len(files) == 0 {
293
+ return ""
294
+ }
295
+ common := normalizePath(filepath.Dir(files[0].FileName()))
296
+ for _, file := range files[1:] {
297
+ dir := normalizePath(filepath.Dir(file.FileName()))
298
+ for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
299
+ next := filepath.Dir(common)
300
+ if next == common {
301
+ return common
302
+ }
303
+ common = normalizePath(next)
304
+ }
305
+ }
306
+ return common
263
307
  }
264
308
 
309
+ // normalizePath cleans and converts a file path to forward-slash form.
265
310
  func normalizePath(value string) string {
266
- if value == "" {
267
- return ""
268
- }
269
- return filepath.ToSlash(filepath.Clean(value))
311
+ if value == "" {
312
+ return ""
313
+ }
314
+ return filepath.ToSlash(filepath.Clean(value))
270
315
  }
271
316
 
317
+ // stripKnownSourceExtension removes a recognized TypeScript or JavaScript file
318
+ // extension from value. Declaration extensions (.d.ts, .d.mts, .d.cts) are
319
+ // tried first. Falls back to stripping any extension via filepath.Ext.
272
320
  func stripKnownSourceExtension(value string) string {
273
- lower := strings.ToLower(value)
274
- for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
275
- if strings.HasSuffix(lower, ext) {
276
- return value[:len(value)-len(ext)]
277
- }
278
- }
279
- return strings.TrimSuffix(value, filepath.Ext(value))
321
+ lower := strings.ToLower(value)
322
+ for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
323
+ if strings.HasSuffix(lower, ext) {
324
+ return value[:len(value)-len(ext)]
325
+ }
326
+ }
327
+ return strings.TrimSuffix(value, filepath.Ext(value))
280
328
  }
281
329
 
330
+ // replaceSourceExtension strips the known source extension from value and
331
+ // appends ext, producing the output file name.
282
332
  func replaceSourceExtension(value string, ext string) string {
283
- return stripKnownSourceExtension(filepath.ToSlash(value)) + ext
333
+ return stripKnownSourceExtension(filepath.ToSlash(value)) + ext
284
334
  }
285
335
 
336
+ // isOutsideRelativePath reports whether a relative path escapes its base
337
+ // directory (i.e. starts with "..").
286
338
  func isOutsideRelativePath(rel string) bool {
287
- return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
339
+ return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
288
340
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/paths",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "description": "First-party ttsc plugin that rewrites emitted module specifiers from tsconfig paths.",
5
5
  "main": "src/index.cjs",
6
6
  "exports": {