ttsc 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 (126) hide show
  1. package/cmd/platform/main.go +9 -0
  2. package/cmd/ttsc/api_compile.go +109 -101
  3. package/cmd/ttsc/api_compile_branches_test.go +55 -57
  4. package/cmd/ttsc/api_transform.go +54 -49
  5. package/cmd/ttsc/api_transform_branches_test.go +30 -32
  6. package/cmd/ttsc/build.go +111 -108
  7. package/cmd/ttsc/build_branches_test.go +95 -97
  8. package/cmd/ttsc/helpers_test.go +39 -28
  9. package/cmd/ttsc/main.go +111 -104
  10. package/cmd/ttsc/main_alias_branches_test.go +21 -23
  11. package/cmd/ttscserver/main.go +96 -90
  12. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +22 -22
  13. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +22 -22
  14. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +23 -23
  15. package/cmd/utility-host/main.go +30 -23
  16. package/driver/lsp.go +53 -7
  17. package/driver/plugins.go +88 -72
  18. package/driver/program.go +362 -339
  19. package/driver/rewrite.go +442 -380
  20. package/internal/lspserver/lsp_envelope.go +64 -64
  21. package/internal/lspserver/lsp_frame.go +60 -60
  22. package/internal/lspserver/lsp_plugin.go +57 -57
  23. package/internal/lspserver/lsp_proxy.go +265 -251
  24. package/internal/lspserver/lsp_server.go +155 -144
  25. package/lib/TtscCompiler.d.ts +7 -0
  26. package/lib/TtscCompiler.js +7 -0
  27. package/lib/TtscCompiler.js.map +1 -1
  28. package/lib/compiler/internal/buildNativeCompiler.d.ts +11 -1
  29. package/lib/compiler/internal/buildNativeCompiler.js +23 -3
  30. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  31. package/lib/compiler/internal/compileProjectInMemory.d.ts +9 -0
  32. package/lib/compiler/internal/compileProjectInMemory.js +36 -21
  33. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  34. package/lib/compiler/internal/paths.d.ts +27 -0
  35. package/lib/compiler/internal/paths.js +70 -0
  36. package/lib/compiler/internal/paths.js.map +1 -0
  37. package/lib/compiler/internal/project/readProjectConfig.d.ts +9 -1
  38. package/lib/compiler/internal/project/readProjectConfig.js +80 -1
  39. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  40. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +13 -1
  41. package/lib/compiler/internal/project/resolveProjectConfig.js +24 -1
  42. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  43. package/lib/compiler/internal/resolveBinary.d.ts +9 -1
  44. package/lib/compiler/internal/resolveBinary.js +17 -1
  45. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  46. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +16 -1
  47. package/lib/compiler/internal/resolveEmittedJavaScript.js +42 -10
  48. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  49. package/lib/compiler/internal/resolveTsgo.d.ts +21 -1
  50. package/lib/compiler/internal/resolveTsgo.js +23 -1
  51. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  52. package/lib/compiler/internal/runBuild.d.ts +19 -0
  53. package/lib/compiler/internal/runBuild.js +112 -46
  54. package/lib/compiler/internal/runBuild.js.map +1 -1
  55. package/lib/compiler/internal/runSingleFileEmit.d.ts +10 -1
  56. package/lib/compiler/internal/runSingleFileEmit.js +14 -1
  57. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  58. package/lib/compiler/internal/sharedHostHelpers.d.ts +5 -0
  59. package/lib/compiler/internal/sharedHostHelpers.js +6 -2
  60. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  61. package/lib/compiler/internal/spawnNative.d.ts +21 -0
  62. package/lib/compiler/internal/spawnNative.js +59 -0
  63. package/lib/compiler/internal/spawnNative.js.map +1 -0
  64. package/lib/compiler/internal/transformProjectInMemory.d.ts +15 -1
  65. package/lib/compiler/internal/transformProjectInMemory.js +57 -61
  66. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  67. package/lib/launcher/internal/getCompilerVersionText.js +7 -0
  68. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  69. package/lib/launcher/internal/prepareExecution.js +32 -0
  70. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  71. package/lib/launcher/internal/resolveCacheDir.d.ts +9 -0
  72. package/lib/launcher/internal/resolveCacheDir.js +22 -0
  73. package/lib/launcher/internal/resolveCacheDir.js.map +1 -0
  74. package/lib/launcher/internal/resolveTtscserverBinary.js +2 -0
  75. package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -1
  76. package/lib/launcher/internal/runTtsc.d.ts +10 -0
  77. package/lib/launcher/internal/runTtsc.js +22 -9
  78. package/lib/launcher/internal/runTtsc.js.map +1 -1
  79. package/lib/launcher/internal/runTtscserver.js +10 -35
  80. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  81. package/lib/launcher/internal/runTtsx.d.ts +8 -0
  82. package/lib/launcher/internal/runTtsx.js +33 -7
  83. package/lib/launcher/internal/runTtsx.js.map +1 -1
  84. package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -0
  85. package/lib/plugin/internal/buildSourcePlugin.js +40 -17
  86. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  87. package/lib/plugin/internal/loadProjectPlugins.d.ts +26 -0
  88. package/lib/plugin/internal/loadProjectPlugins.js +32 -18
  89. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  90. package/lib/structures/ITtscCompilerTransformation.d.ts +15 -2
  91. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +0 -2
  92. package/package.json +9 -9
  93. package/shim/ast/shim.go +24 -0
  94. package/shim/checker/shim.go +35 -0
  95. package/shim/core/shim.go +17 -0
  96. package/shim/diagnosticwriter/shim.go +4 -0
  97. package/shim/lsp/shim.go +3 -3
  98. package/shim/printer/shim.go +20 -0
  99. package/src/TtscCompiler.ts +7 -0
  100. package/src/compiler/internal/buildNativeCompiler.ts +23 -3
  101. package/src/compiler/internal/compileProjectInMemory.ts +34 -25
  102. package/src/compiler/internal/paths.ts +68 -0
  103. package/src/compiler/internal/project/readProjectConfig.ts +86 -1
  104. package/src/compiler/internal/project/resolveProjectConfig.ts +24 -1
  105. package/src/compiler/internal/resolveBinary.ts +17 -1
  106. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
  107. package/src/compiler/internal/resolveTsgo.ts +29 -1
  108. package/src/compiler/internal/runBuild.ts +110 -55
  109. package/src/compiler/internal/runSingleFileEmit.ts +14 -1
  110. package/src/compiler/internal/sharedHostHelpers.ts +7 -7
  111. package/src/compiler/internal/spawnNative.ts +64 -0
  112. package/src/compiler/internal/transformProjectInMemory.ts +47 -67
  113. package/src/launcher/internal/getCompilerVersionText.ts +7 -0
  114. package/src/launcher/internal/prepareExecution.ts +32 -0
  115. package/src/launcher/internal/resolveCacheDir.ts +19 -0
  116. package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
  117. package/src/launcher/internal/runTtsc.ts +22 -10
  118. package/src/launcher/internal/runTtscserver.ts +9 -1
  119. package/src/launcher/internal/runTtsx.ts +33 -8
  120. package/src/plugin/internal/buildSourcePlugin.ts +40 -14
  121. package/src/plugin/internal/loadProjectPlugins.ts +42 -18
  122. package/src/structures/ITtscCompilerTransformation.ts +15 -2
  123. package/src/structures/ITtscPlugin.ts +0 -1
  124. package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
  125. package/tools/gen_shims/main.go +43 -8
  126. package/utility/host.go +294 -245
package/driver/rewrite.go CHANGED
@@ -14,34 +14,34 @@
14
14
  package driver
15
15
 
16
16
  import (
17
- "context"
18
- "errors"
19
- "fmt"
20
- "os"
21
- "path/filepath"
22
- "regexp"
23
- "strings"
24
- "sync"
25
- "unicode"
26
-
27
- "github.com/microsoft/typescript-go/shim/ast"
28
- shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
17
+ "context"
18
+ "errors"
19
+ "fmt"
20
+ "os"
21
+ "path/filepath"
22
+ "regexp"
23
+ "strings"
24
+ "sync"
25
+ "unicode"
26
+
27
+ "github.com/microsoft/typescript-go/shim/ast"
28
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
29
29
  )
30
30
 
31
31
  // Rewrite describes one emit-time patch. Produced by CollectCallSites after
32
32
  // the engine has generated a replacement JS fragment for the call.
33
33
  type Rewrite struct {
34
- File *ast.SourceFile
35
- RootName string
36
- Namespaces []string
37
- Method string
38
- Replacement string
39
- ConsumeParens bool
34
+ File *ast.SourceFile
35
+ RootName string
36
+ Namespaces []string
37
+ Method string
38
+ Replacement string
39
+ ConsumeParens bool
40
40
  }
41
41
 
42
42
  // RewriteSet groups rewrites by file, preserving source order.
43
43
  type RewriteSet struct {
44
- byPath map[string][]Rewrite
44
+ byPath map[string][]Rewrite
45
45
  }
46
46
 
47
47
  // NewRewriteSet returns an empty set.
@@ -49,20 +49,20 @@ func NewRewriteSet() *RewriteSet { return &RewriteSet{byPath: map[string][]Rewri
49
49
 
50
50
  // Add registers a rewrite under the absolute path of its source file.
51
51
  func (rs *RewriteSet) Add(r Rewrite) {
52
- if r.File == nil {
53
- return
54
- }
55
- path := filepath.ToSlash(r.File.FileName())
56
- rs.byPath[path] = append(rs.byPath[path], r)
52
+ if r.File == nil {
53
+ return
54
+ }
55
+ path := filepath.ToSlash(r.File.FileName())
56
+ rs.byPath[path] = append(rs.byPath[path], r)
57
57
  }
58
58
 
59
59
  // Len returns the total number of rewrites across every file.
60
60
  func (rs *RewriteSet) Len() int {
61
- n := 0
62
- for _, rws := range rs.byPath {
63
- n += len(rws)
64
- }
65
- return n
61
+ n := 0
62
+ for _, rws := range rs.byPath {
63
+ n += len(rws)
64
+ }
65
+ return n
66
66
  }
67
67
 
68
68
  // RewriteSentinel is the marker inserted at the top of a patched file so
@@ -74,127 +74,135 @@ const RewriteSentinel = "/* @ttsc-rewritten */"
74
74
  // `writeFile` is nil, the patched JS is written to disk via the standard
75
75
  // tsgo WriteFile.
76
76
  func (p *Program) EmitAll(rs *RewriteSet, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
77
- return p.emit(rs, nil, writeFile)
77
+ return p.emit(rs, nil, writeFile)
78
78
  }
79
79
 
80
80
  // EmitAllRaw runs TypeScript-Go emit without ttsc output-text rewrites.
81
81
  func (p *Program) EmitAllRaw(writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
82
- if p == nil || p.TSProgram == nil {
83
- return nil, nil, errors.New("driver: nil program")
84
- }
85
- if err := p.ApplyLinkedPlugins(); err != nil {
86
- return nil, nil, err
87
- }
88
- wf := writeFile
89
- if wf == nil {
90
- wf = func(fileName, text string, data *shimcompiler.WriteFileData) error {
91
- return DefaultWriteFile(fileName, text)
92
- }
93
- }
94
- result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
95
- WriteFile: wf,
96
- })
97
- return result, convertDiagnostics(result.Diagnostics), nil
82
+ if p == nil || p.TSProgram == nil {
83
+ return nil, nil, errors.New("driver: nil program")
84
+ }
85
+ if err := p.ApplyLinkedPlugins(); err != nil {
86
+ return nil, nil, err
87
+ }
88
+ wf := writeFile
89
+ if wf == nil {
90
+ wf = func(fileName, text string, data *shimcompiler.WriteFileData) error {
91
+ return DefaultWriteFile(fileName, text)
92
+ }
93
+ }
94
+ result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
95
+ WriteFile: wf,
96
+ })
97
+ return result, convertDiagnostics(result.Diagnostics), nil
98
98
  }
99
99
 
100
100
  // EmitFile runs tsgo's emitter for one source file, applying the same rewrite
101
101
  // pipeline as EmitAll.
102
102
  func (p *Program) EmitFile(rs *RewriteSet, target *ast.SourceFile, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
103
- return p.emit(rs, target, writeFile)
103
+ return p.emit(rs, target, writeFile)
104
104
  }
105
105
 
106
106
  func (p *Program) emit(rs *RewriteSet, target *ast.SourceFile, writeFile shimcompiler.WriteFile) (*shimcompiler.EmitResult, []Diagnostic, error) {
107
- if p == nil || p.TSProgram == nil {
108
- return nil, nil, errors.New("driver: nil program")
109
- }
110
- if err := p.ApplyLinkedPlugins(); err != nil {
111
- return nil, nil, err
112
- }
113
- if rs == nil {
114
- rs = NewRewriteSet()
115
- }
116
- cursors := map[string]int{}
117
- wf := func(fileName, text string, data *shimcompiler.WriteFileData) error {
118
- // A patched file is idempotent: once the sentinel exists, the emitted text
119
- // is passed through unchanged. This matters for watch/rebuild loops and
120
- // tests that re-run emit over the same output directory.
121
- if strings.Contains(text, RewriteSentinel) {
122
- if writeFile != nil {
123
- return writeFile(fileName, text, data)
124
- }
125
- return DefaultWriteFile(fileName, text)
126
- }
127
- // Rewrites are matched after tsgo has printed JavaScript. The source-file
128
- // association is recovered from the output path because WriteFile receives
129
- // only the final file name and text.
130
- patched, err := applyRewrites(fileName, text, rs, cursors)
131
- if err != nil {
132
- return err
133
- }
134
- if patched != text {
135
- patched = insertSentinel(patched)
136
- }
137
- if writeFile != nil {
138
- return writeFile(fileName, patched, data)
139
- }
140
- return DefaultWriteFile(fileName, patched)
141
- }
142
-
143
- result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
144
- TargetSourceFile: target,
145
- WriteFile: wf,
146
- })
147
- return result, convertDiagnostics(result.Diagnostics), nil
107
+ if p == nil || p.TSProgram == nil {
108
+ return nil, nil, errors.New("driver: nil program")
109
+ }
110
+ if err := p.ApplyLinkedPlugins(); err != nil {
111
+ return nil, nil, err
112
+ }
113
+ if rs == nil {
114
+ rs = NewRewriteSet()
115
+ }
116
+ cursors := map[string]int{}
117
+ wf := func(fileName, text string, data *shimcompiler.WriteFileData) error {
118
+ // A patched file is idempotent: once the sentinel exists, the emitted text
119
+ // is passed through unchanged. This matters for watch/rebuild loops and
120
+ // tests that re-run emit over the same output directory.
121
+ if strings.Contains(text, RewriteSentinel) {
122
+ if writeFile != nil {
123
+ return writeFile(fileName, text, data)
124
+ }
125
+ return DefaultWriteFile(fileName, text)
126
+ }
127
+ // Rewrites are matched after tsgo has printed JavaScript. The source-file
128
+ // association is recovered from the output path because WriteFile receives
129
+ // only the final file name and text.
130
+ patched, err := applyRewrites(fileName, text, rs, cursors)
131
+ if err != nil {
132
+ return err
133
+ }
134
+ if patched != text {
135
+ patched = insertSentinel(patched)
136
+ }
137
+ if writeFile != nil {
138
+ return writeFile(fileName, patched, data)
139
+ }
140
+ return DefaultWriteFile(fileName, patched)
141
+ }
142
+
143
+ result := p.TSProgram.Emit(context.Background(), shimcompiler.EmitOptions{
144
+ TargetSourceFile: target,
145
+ WriteFile: wf,
146
+ })
147
+ return result, convertDiagnostics(result.Diagnostics), nil
148
148
  }
149
149
 
150
150
  // DefaultWriteFile is the default disk writer used when EmitAll's caller does not
151
151
  // supply a custom WriteFile callback.
152
152
  func DefaultWriteFile(fileName, text string) error {
153
- if dir := filepath.Dir(fileName); dir != "" {
154
- if err := os.MkdirAll(dir, 0o755); err != nil {
155
- return err
156
- }
157
- }
158
- return os.WriteFile(fileName, []byte(text), 0o644)
153
+ if dir := filepath.Dir(fileName); dir != "" {
154
+ if err := os.MkdirAll(dir, 0o755); err != nil {
155
+ return err
156
+ }
157
+ }
158
+ return os.WriteFile(fileName, []byte(text), 0o644)
159
159
  }
160
160
 
161
+ // insertSentinel prepends RewriteSentinel to the output text. When the file
162
+ // starts with a "use strict" directive (either quote style), the sentinel is
163
+ // inserted after it so the directive remains the first statement — ES modules
164
+ // and bundlers expect it at position zero.
161
165
  func insertSentinel(text string) string {
162
- for _, prefix := range []string{"\"use strict\";\n", "'use strict';\n"} {
163
- if strings.HasPrefix(text, prefix) {
164
- return prefix + RewriteSentinel + "\n" + text[len(prefix):]
165
- }
166
- }
167
- return RewriteSentinel + "\n" + text
166
+ for _, prefix := range []string{"\"use strict\";\n", "'use strict';\n"} {
167
+ if strings.HasPrefix(text, prefix) {
168
+ return prefix + RewriteSentinel + "\n" + text[len(prefix):]
169
+ }
170
+ }
171
+ return RewriteSentinel + "\n" + text
168
172
  }
169
173
 
174
+ // applyRewrites applies all registered rewrites for the source file that
175
+ // corresponds to outputName. cursors tracks how many rewrites have already
176
+ // been applied per source path across multiple WriteFile calls so incremental
177
+ // watch rebuilds resume at the right offset rather than re-scanning from zero.
170
178
  func applyRewrites(outputName, text string, rs *RewriteSet, cursors map[string]int) (string, error) {
171
- srcPath, ok := findSourceForOutput(outputName, rs)
172
- if !ok || len(rs.byPath[srcPath]) == 0 {
173
- return text, nil
174
- }
175
- rewrites := rs.byPath[srcPath]
176
- pos := cursors[srcPath]
177
- out := text
178
- searchFrom := 0
179
- for pos < len(rewrites) {
180
- r := rewrites[pos]
181
- replaced, nextSearchFrom, ok, err := spliceCall(out, r, searchFrom)
182
- if err != nil {
183
- return "", err
184
- }
185
- if !ok {
186
- preview := out
187
- if len(preview) > 400 {
188
- preview = preview[:400] + "…"
189
- }
190
- return "", fmt.Errorf("driver: could not locate %s.%s(…) call in %s (tried roots %v; preview: %q)", joinRootAndNamespaces(r), r.Method, outputName, candidateRoots(r.RootName), preview)
191
- }
192
- out = replaced
193
- searchFrom = nextSearchFrom
194
- pos++
195
- }
196
- cursors[srcPath] = pos
197
- return out, nil
179
+ srcPath, ok := findSourceForOutput(outputName, rs)
180
+ if !ok || len(rs.byPath[srcPath]) == 0 {
181
+ return text, nil
182
+ }
183
+ rewrites := rs.byPath[srcPath]
184
+ pos := cursors[srcPath]
185
+ out := text
186
+ searchFrom := 0
187
+ for pos < len(rewrites) {
188
+ r := rewrites[pos]
189
+ replaced, nextSearchFrom, ok, err := spliceCall(out, r, searchFrom)
190
+ if err != nil {
191
+ return "", err
192
+ }
193
+ if !ok {
194
+ preview := out
195
+ if len(preview) > 400 {
196
+ preview = preview[:400] + "…"
197
+ }
198
+ return "", fmt.Errorf("driver: could not locate %s.%s(…) call in %s (tried roots %v; preview: %q)", joinRootAndNamespaces(r), r.Method, outputName, candidateRoots(r.RootName), preview)
199
+ }
200
+ out = replaced
201
+ searchFrom = nextSearchFrom
202
+ pos++
203
+ }
204
+ cursors[srcPath] = pos
205
+ return out, nil
198
206
  }
199
207
 
200
208
  // findSourceForOutput recovers which registered source file produced a given
@@ -213,93 +221,99 @@ func applyRewrites(outputName, text string, rs *RewriteSet, cursors map[string]i
213
221
  // Ambiguous matches (two or more registered sources with the same tail) return
214
222
  // no match so the caller treats the output as having no rewrites.
215
223
  func findSourceForOutput(outputName string, rs *RewriteSet) (string, bool) {
216
- if len(rs.byPath) == 0 {
217
- return "", false
218
- }
219
- outStem := strings.TrimSuffix(filepath.ToSlash(outputName), filepath.Ext(outputName))
220
- commonDir := commonSourceDirectoryFor(rs)
221
- var matched string
222
- hits := 0
223
- for srcPath := range rs.byPath {
224
- tail := sourceTail(srcPath, commonDir)
225
- if tail == "" {
226
- continue
227
- }
228
- if outStem == tail || strings.HasSuffix(outStem, "/"+tail) {
229
- matched = srcPath
230
- hits++
231
- }
232
- }
233
- if hits != 1 {
234
- return "", false
235
- }
236
- return matched, true
224
+ if len(rs.byPath) == 0 {
225
+ return "", false
226
+ }
227
+ outStem := strings.TrimSuffix(filepath.ToSlash(outputName), filepath.Ext(outputName))
228
+ commonDir := commonSourceDirectoryFor(rs)
229
+ var matched string
230
+ hits := 0
231
+ for srcPath := range rs.byPath {
232
+ tail := sourceTail(srcPath, commonDir)
233
+ if tail == "" {
234
+ continue
235
+ }
236
+ if outStem == tail || strings.HasSuffix(outStem, "/"+tail) {
237
+ matched = srcPath
238
+ hits++
239
+ }
240
+ }
241
+ if hits != 1 {
242
+ return "", false
243
+ }
244
+ return matched, true
237
245
  }
238
246
 
239
247
  // commonSourceDirectoryFor returns the deepest directory (with trailing "/")
240
248
  // shared by every source path in rs.byPath. When rs has a single source this is
241
249
  // just that source's directory.
242
250
  func commonSourceDirectoryFor(rs *RewriteSet) string {
243
- var dirs [][]string
244
- for srcPath := range rs.byPath {
245
- dirs = append(dirs, strings.Split(filepath.ToSlash(filepath.Dir(srcPath)), "/"))
246
- }
247
- if len(dirs) == 0 {
248
- return ""
249
- }
250
- common := dirs[0]
251
- for _, other := range dirs[1:] {
252
- n := len(common)
253
- if len(other) < n {
254
- n = len(other)
255
- }
256
- shared := 0
257
- for i := 0; i < n; i++ {
258
- if common[i] != other[i] {
259
- break
260
- }
261
- shared++
262
- }
263
- common = common[:shared]
264
- if len(common) == 0 {
265
- break
266
- }
267
- }
268
- if len(common) == 0 {
269
- return ""
270
- }
271
- return strings.Join(common, "/") + "/"
251
+ var dirs [][]string
252
+ for srcPath := range rs.byPath {
253
+ dirs = append(dirs, strings.Split(filepath.ToSlash(filepath.Dir(srcPath)), "/"))
254
+ }
255
+ if len(dirs) == 0 {
256
+ return ""
257
+ }
258
+ common := dirs[0]
259
+ for _, other := range dirs[1:] {
260
+ n := len(common)
261
+ if len(other) < n {
262
+ n = len(other)
263
+ }
264
+ shared := 0
265
+ for i := 0; i < n; i++ {
266
+ if common[i] != other[i] {
267
+ break
268
+ }
269
+ shared++
270
+ }
271
+ common = common[:shared]
272
+ if len(common) == 0 {
273
+ break
274
+ }
275
+ }
276
+ if len(common) == 0 {
277
+ return ""
278
+ }
279
+ return strings.Join(common, "/") + "/"
272
280
  }
273
281
 
274
282
  // sourceTail returns the source stem (extension dropped) without the common
275
283
  // directory prefix. The leading "/" is also stripped so callers can match it as
276
284
  // a suffix segment.
277
285
  func sourceTail(srcPath, commonDir string) string {
278
- stem := strings.TrimSuffix(filepath.ToSlash(srcPath), filepath.Ext(srcPath))
279
- if commonDir != "" && strings.HasPrefix(stem, commonDir) {
280
- return stem[len(commonDir):]
281
- }
282
- return strings.TrimPrefix(stem, "/")
286
+ stem := strings.TrimSuffix(filepath.ToSlash(srcPath), filepath.Ext(srcPath))
287
+ if commonDir != "" && strings.HasPrefix(stem, commonDir) {
288
+ return stem[len(commonDir):]
289
+ }
290
+ return strings.TrimPrefix(stem, "/")
283
291
  }
284
292
 
293
+ // spliceCall locates the next call expression for r in text starting at
294
+ // searchFrom and splices in r.Replacement. When ConsumeParens is true the
295
+ // replacement covers the entire call including arguments; otherwise only the
296
+ // head (root.namespaces.method) is replaced and the argument list is kept.
297
+ // Returns the patched text, the byte position to resume from on the next
298
+ // call, a found flag, and any error from the paren-matching step.
285
299
  func spliceCall(text string, r Rewrite, searchFrom int) (string, int, bool, error) {
286
- aliases := candidateRoots(r.RootName)
287
- pattern := callRegexFor(aliases, r.Namespaces, r.Method)
288
- idx, needleLen := findCallMatch(text, pattern, searchFrom)
289
- if idx < 0 {
290
- return text, searchFrom, false, nil
291
- }
292
- parenPos := idx + needleLen
293
- closePos, ok := matchParen(text, parenPos)
294
- if !ok {
295
- return text, searchFrom, false, errors.New("driver: unbalanced parens while locating plugin call")
296
- }
297
- if r.ConsumeParens {
298
- replaced := text[:idx] + r.Replacement + text[closePos+1:]
299
- return replaced, idx + len(r.Replacement), true, nil
300
- }
301
- replaced := text[:idx] + r.Replacement + text[idx+needleLen:]
302
- return replaced, idx + len(r.Replacement), true, nil
300
+ aliases := candidateRoots(r.RootName)
301
+ pattern := callRegexFor(aliases, r.Namespaces, r.Method)
302
+ idx, needleLen := findCallMatch(text, pattern, searchFrom)
303
+ if idx < 0 {
304
+ return text, searchFrom, false, nil
305
+ }
306
+ parenPos := idx + needleLen
307
+ closePos, ok := matchParen(text, parenPos)
308
+ if !ok {
309
+ return text, searchFrom, false, errors.New("driver: unbalanced parens while locating plugin call")
310
+ }
311
+ if r.ConsumeParens {
312
+ replaced := text[:idx] + r.Replacement + text[closePos+1:]
313
+ return replaced, idx + len(r.Replacement), true, nil
314
+ }
315
+ replaced := text[:idx] + r.Replacement + text[idx+needleLen:]
316
+ return replaced, idx + len(r.Replacement), true, nil
303
317
  }
304
318
 
305
319
  // callRegexFor compiles the loose-match needle pattern used by spliceCall.
@@ -317,30 +331,30 @@ func spliceCall(text string, r Rewrite, searchFrom int) (string, int, bool, erro
317
331
  // same rewrite descriptor is re-checked once per emitted file in incremental
318
332
  // watch builds.
319
333
  func callRegexFor(aliases, namespaces []string, method string) *regexp.Regexp {
320
- key := strings.Join(aliases, "|") + "\x00" + strings.Join(namespaces, ".") + "\x00" + method
321
- if cached, ok := callRegexCache.Load(key); ok {
322
- return cached.(*regexp.Regexp)
323
- }
324
- rootAlternation := make([]string, 0, len(aliases))
325
- for _, alias := range aliases {
326
- rootAlternation = append(rootAlternation, regexp.QuoteMeta(alias))
327
- }
328
- var b strings.Builder
329
- b.WriteString(`(?:`)
330
- b.WriteString(strings.Join(rootAlternation, `|`))
331
- b.WriteString(`)`)
332
- for _, ns := range namespaces {
333
- b.WriteString(`\s*\.\s*`)
334
- b.WriteString(regexp.QuoteMeta(ns))
335
- }
336
- b.WriteString(`\s*\.\s*`)
337
- b.WriteString(regexp.QuoteMeta(method))
338
- b.WriteString(`\s*(\()`)
339
- // MustCompile is safe because every contributing string was QuoteMeta'd
340
- // and the surrounding template is a fixed regex grammar.
341
- re := regexp.MustCompile(b.String())
342
- callRegexCache.Store(key, re)
343
- return re
334
+ key := strings.Join(aliases, "|") + "\x00" + strings.Join(namespaces, ".") + "\x00" + method
335
+ if cached, ok := callRegexCache.Load(key); ok {
336
+ return cached.(*regexp.Regexp)
337
+ }
338
+ rootAlternation := make([]string, 0, len(aliases))
339
+ for _, alias := range aliases {
340
+ rootAlternation = append(rootAlternation, regexp.QuoteMeta(alias))
341
+ }
342
+ var b strings.Builder
343
+ b.WriteString(`(?:`)
344
+ b.WriteString(strings.Join(rootAlternation, `|`))
345
+ b.WriteString(`)`)
346
+ for _, ns := range namespaces {
347
+ b.WriteString(`\s*\.\s*`)
348
+ b.WriteString(regexp.QuoteMeta(ns))
349
+ }
350
+ b.WriteString(`\s*\.\s*`)
351
+ b.WriteString(regexp.QuoteMeta(method))
352
+ b.WriteString(`\s*(\()`)
353
+ // MustCompile is safe because every contributing string was QuoteMeta'd
354
+ // and the surrounding template is a fixed regex grammar.
355
+ re := regexp.MustCompile(b.String())
356
+ callRegexCache.Store(key, re)
357
+ return re
344
358
  }
345
359
 
346
360
  var callRegexCache sync.Map
@@ -351,197 +365,245 @@ var callRegexCache sync.Map
351
365
  // like `mytypia.foo(` don't shadow `typia.foo(`. Returns the start byte of the
352
366
  // match and the length up to (but not including) the captured `(`.
353
367
  func findCallMatch(text string, pattern *regexp.Regexp, searchFrom int) (int, int) {
354
- start := searchFrom
355
- if start < 0 {
356
- start = 0
357
- }
358
- for start <= len(text) {
359
- loc := pattern.FindStringSubmatchIndex(text[start:])
360
- if loc == nil {
361
- return -1, 0
362
- }
363
- matchStart := start + loc[0]
364
- parenStart := start + loc[2]
365
- if matchStart > 0 && isIdentifierPart(rune(text[matchStart-1])) {
366
- start = matchStart + 1
367
- continue
368
- }
369
- return matchStart, parenStart - matchStart
370
- }
371
- return -1, 0
368
+ start := searchFrom
369
+ if start < 0 {
370
+ start = 0
371
+ }
372
+ for start <= len(text) {
373
+ loc := pattern.FindStringSubmatchIndex(text[start:])
374
+ if loc == nil {
375
+ return -1, 0
376
+ }
377
+ matchStart := start + loc[0]
378
+ parenStart := start + loc[2]
379
+ if matchStart > 0 && isIdentifierPart(rune(text[matchStart-1])) {
380
+ start = matchStart + 1
381
+ continue
382
+ }
383
+ return matchStart, parenStart - matchStart
384
+ }
385
+ return -1, 0
372
386
  }
373
387
 
388
+ // candidateRoots expands a bare module name into the set of identifier stems
389
+ // tsgo's CommonJS emitter may produce for it. For example "typia" becomes
390
+ // ["typia", "typia_1.default", "typia_2.default", "typia.default",
391
+ // "typia_1", "typia_2"]. The regex alternation built from these candidates
392
+ // matches the emitted call regardless of which deduplication suffix tsgo chose.
374
393
  func candidateRoots(root string) []string {
375
- return []string{
376
- root,
377
- root + "_1.default",
378
- root + "_2.default",
379
- root + ".default",
380
- root + "_1",
381
- root + "_2",
382
- }
394
+ return []string{
395
+ root,
396
+ root + "_1.default",
397
+ root + "_2.default",
398
+ root + ".default",
399
+ root + "_1",
400
+ root + "_2",
401
+ }
383
402
  }
384
403
 
404
+ // joinRootAndNamespaces returns the human-readable "root.ns1.ns2" form of
405
+ // the rewrite's call head, used in error messages only.
385
406
  func joinRootAndNamespaces(r Rewrite) string {
386
- if len(r.Namespaces) == 0 {
387
- return r.RootName
388
- }
389
- return r.RootName + "." + strings.Join(r.Namespaces, ".")
407
+ if len(r.Namespaces) == 0 {
408
+ return r.RootName
409
+ }
410
+ return r.RootName + "." + strings.Join(r.Namespaces, ".")
390
411
  }
391
412
 
413
+ // needleTail returns the literal suffix of a call expression head, e.g.
414
+ // ".ns.method(". This was the original literal-search needle before the regex
415
+ // rewriter was introduced; it is kept for potential future use by callers
416
+ // that do a quick pre-filter before invoking the regex path.
392
417
  func needleTail(r Rewrite) string {
393
- if len(r.Namespaces) == 0 {
394
- return "." + r.Method + "("
395
- }
396
- return "." + strings.Join(r.Namespaces, ".") + "." + r.Method + "("
418
+ if len(r.Namespaces) == 0 {
419
+ return "." + r.Method + "("
420
+ }
421
+ return "." + strings.Join(r.Namespaces, ".") + "." + r.Method + "("
397
422
  }
398
423
 
424
+ // isIdentifierPart reports whether r can appear inside a JavaScript identifier.
425
+ // Used to ensure a regex match does not begin mid-identifier (e.g. "mytypia"
426
+ // must not match as "typia").
399
427
  func isIdentifierPart(r rune) bool {
400
- return r == '_' || r == '$' || unicode.IsLetter(r) || unicode.IsDigit(r)
428
+ return r == '_' || r == '$' || unicode.IsLetter(r) || unicode.IsDigit(r)
401
429
  }
402
430
 
431
+ // matchParen finds the closing ")" that matches the "(" at text[pos],
432
+ // skipping over nested parentheses, strings, template literals, comments, and
433
+ // regex literals. Returns the byte index of the closing ")" and true on
434
+ // success; (0, false) when pos does not point at "(" or the text ends before
435
+ // the paren is closed. The lastSignificant variable tracks the most recently
436
+ // seen non-whitespace byte so canStartRegexLiteral can distinguish division
437
+ // from a regex literal opener.
403
438
  func matchParen(text string, pos int) (int, bool) {
404
- if pos >= len(text) || text[pos] != '(' {
405
- return 0, false
406
- }
407
- depth := 1
408
- lastSignificant := byte('(')
409
- for i := pos + 1; i < len(text); i++ {
410
- ch := text[i]
411
- switch ch {
412
- case ' ', '\t', '\n', '\r', '\f':
413
- continue
414
- case '(':
415
- depth++
416
- lastSignificant = ch
417
- case ')':
418
- depth--
419
- if depth == 0 {
420
- return i, true
421
- }
422
- lastSignificant = ch
423
- case '"', '\'':
424
- end, ok := skipQuoted(text, i, ch)
425
- if !ok {
426
- return 0, false
427
- }
428
- i = end
429
- lastSignificant = 'x'
430
- case '`':
431
- end, ok := skipTemplate(text, i)
432
- if !ok {
433
- return 0, false
434
- }
435
- i = end
436
- lastSignificant = 'x'
437
- case '/':
438
- if i+1 < len(text) {
439
- switch text[i+1] {
440
- case '/':
441
- i = skipLineComment(text, i+2)
442
- continue
443
- case '*':
444
- end, ok := skipBlockComment(text, i+2)
445
- if !ok {
446
- return 0, false
447
- }
448
- i = end
449
- continue
450
- }
451
- }
452
- if canStartRegexLiteral(lastSignificant) {
453
- end, ok := skipRegexLiteral(text, i)
454
- if !ok {
455
- return 0, false
456
- }
457
- i = end
458
- lastSignificant = 'x'
459
- continue
460
- }
461
- lastSignificant = ch
462
- default:
463
- lastSignificant = ch
464
- }
465
- }
466
- return 0, false
439
+ if pos >= len(text) || text[pos] != '(' {
440
+ return 0, false
441
+ }
442
+ depth := 1
443
+ lastSignificant := byte('(')
444
+ for i := pos + 1; i < len(text); i++ {
445
+ ch := text[i]
446
+ switch ch {
447
+ case ' ', '\t', '\n', '\r', '\f':
448
+ continue
449
+ case '(':
450
+ depth++
451
+ lastSignificant = ch
452
+ case ')':
453
+ depth--
454
+ if depth == 0 {
455
+ return i, true
456
+ }
457
+ lastSignificant = ch
458
+ case '"', '\'':
459
+ end, ok := skipQuoted(text, i, ch)
460
+ if !ok {
461
+ return 0, false
462
+ }
463
+ i = end
464
+ lastSignificant = 'x'
465
+ case '`':
466
+ end, ok := skipTemplate(text, i)
467
+ if !ok {
468
+ return 0, false
469
+ }
470
+ i = end
471
+ lastSignificant = 'x'
472
+ case '/':
473
+ if i+1 < len(text) {
474
+ switch text[i+1] {
475
+ case '/':
476
+ i = skipLineComment(text, i+2)
477
+ continue
478
+ case '*':
479
+ end, ok := skipBlockComment(text, i+2)
480
+ if !ok {
481
+ return 0, false
482
+ }
483
+ i = end
484
+ continue
485
+ }
486
+ }
487
+ if canStartRegexLiteral(lastSignificant) {
488
+ end, ok := skipRegexLiteral(text, i)
489
+ if !ok {
490
+ return 0, false
491
+ }
492
+ i = end
493
+ lastSignificant = 'x'
494
+ continue
495
+ }
496
+ lastSignificant = ch
497
+ default:
498
+ lastSignificant = ch
499
+ }
500
+ }
501
+ return 0, false
467
502
  }
468
503
 
504
+ // skipQuoted advances past a single- or double-quoted string literal starting
505
+ // at pos. Returns the index of the closing quote and true, or (0, false) on
506
+ // unterminated literals. Newlines inside a non-template string are illegal in
507
+ // JS and also terminate the scan as a failure.
469
508
  func skipQuoted(text string, pos int, quote byte) (int, bool) {
470
- for i := pos + 1; i < len(text); i++ {
471
- switch text[i] {
472
- case '\\':
473
- i++
474
- case quote:
475
- return i, true
476
- case '\n', '\r':
477
- return 0, false
478
- }
479
- }
480
- return 0, false
509
+ for i := pos + 1; i < len(text); i++ {
510
+ switch text[i] {
511
+ case '\\':
512
+ i++
513
+ case quote:
514
+ return i, true
515
+ case '\n', '\r':
516
+ return 0, false
517
+ }
518
+ }
519
+ return 0, false
481
520
  }
482
521
 
522
+ // skipTemplate advances past a backtick template literal starting at pos.
523
+ // Nested template expressions (${...}) are not recursed into — the rewriter
524
+ // only needs to balance the outer backtick so it does not misinterpret a
525
+ // backtick inside the template as the end of a surrounding construct.
483
526
  func skipTemplate(text string, pos int) (int, bool) {
484
- for i := pos + 1; i < len(text); i++ {
485
- switch text[i] {
486
- case '\\':
487
- i++
488
- case '`':
489
- return i, true
490
- }
491
- }
492
- return 0, false
527
+ for i := pos + 1; i < len(text); i++ {
528
+ switch text[i] {
529
+ case '\\':
530
+ i++
531
+ case '`':
532
+ return i, true
533
+ }
534
+ }
535
+ return 0, false
493
536
  }
494
537
 
538
+ // skipLineComment advances past a "//" line comment starting at pos (pos
539
+ // should be the character after the second "/"). Returns the index of the
540
+ // line terminator, or the last valid index when no newline is found.
495
541
  func skipLineComment(text string, pos int) int {
496
- for i := pos; i < len(text); i++ {
497
- if text[i] == '\n' || text[i] == '\r' {
498
- return i
499
- }
500
- }
501
- return len(text) - 1
542
+ for i := pos; i < len(text); i++ {
543
+ if text[i] == '\n' || text[i] == '\r' {
544
+ return i
545
+ }
546
+ }
547
+ return len(text) - 1
502
548
  }
503
549
 
550
+ // skipBlockComment advances past a "/* … */" block comment starting at pos
551
+ // (pos should be the character after the opening "/*"). Returns the index of
552
+ // the closing "/" and true, or (0, false) when the comment is unterminated.
504
553
  func skipBlockComment(text string, pos int) (int, bool) {
505
- for i := pos; i+1 < len(text); i++ {
506
- if text[i] == '*' && text[i+1] == '/' {
507
- return i + 1, true
508
- }
509
- }
510
- return 0, false
554
+ for i := pos; i+1 < len(text); i++ {
555
+ if text[i] == '*' && text[i+1] == '/' {
556
+ return i + 1, true
557
+ }
558
+ }
559
+ return 0, false
511
560
  }
512
561
 
562
+ // canStartRegexLiteral reports whether the byte previous (the last
563
+ // non-whitespace character seen before a "/") allows a regex literal to
564
+ // start. This is the minimal set of characters that unambiguously precede a
565
+ // regex in emitted CommonJS output; false positives are safe (they cause an
566
+ // extra skipRegexLiteral attempt that quickly fails), false negatives would
567
+ // misparse a "/" as the start of a comment or regex.
513
568
  func canStartRegexLiteral(previous byte) bool {
514
- return strings.ContainsRune("([{=,:;!&|?+-*~^<>%", rune(previous))
569
+ return strings.ContainsRune("([{=,:;!&|?+-*~^<>%", rune(previous))
515
570
  }
516
571
 
572
+ // skipRegexLiteral advances past a "/" regex literal starting at pos.
573
+ // Character classes ("[…]") are tracked so a "/" inside them is not treated
574
+ // as the closing delimiter. Returns the index of the last flag character (or
575
+ // the closing "/" when no flags follow) and true, or (0, false) for unterminated
576
+ // or newline-terminated literals.
517
577
  func skipRegexLiteral(text string, pos int) (int, bool) {
518
- inClass := false
519
- for i := pos + 1; i < len(text); i++ {
520
- switch text[i] {
521
- case '\\':
522
- i++
523
- case '[':
524
- inClass = true
525
- case ']':
526
- inClass = false
527
- case '/':
528
- if inClass {
529
- continue
530
- }
531
- for i+1 < len(text) && isRegexFlag(text[i+1]) {
532
- i++
533
- }
534
- return i, true
535
- case '\n', '\r':
536
- return 0, false
537
- }
538
- }
539
- return 0, false
578
+ inClass := false
579
+ for i := pos + 1; i < len(text); i++ {
580
+ switch text[i] {
581
+ case '\\':
582
+ i++
583
+ case '[':
584
+ inClass = true
585
+ case ']':
586
+ inClass = false
587
+ case '/':
588
+ if inClass {
589
+ continue
590
+ }
591
+ for i+1 < len(text) && isRegexFlag(text[i+1]) {
592
+ i++
593
+ }
594
+ return i, true
595
+ case '\n', '\r':
596
+ return 0, false
597
+ }
598
+ }
599
+ return 0, false
540
600
  }
541
601
 
602
+ // isRegexFlag reports whether ch is a valid regex flag character (letter,
603
+ // digit, "_" or "$"). ES2025 allows any IdentifierPart after the closing "/".
542
604
  func isRegexFlag(ch byte) bool {
543
- return ch == '_' ||
544
- ch == '$' ||
545
- unicode.IsLetter(rune(ch)) ||
546
- unicode.IsDigit(rune(ch))
605
+ return ch == '_' ||
606
+ ch == '$' ||
607
+ unicode.IsLetter(rune(ch)) ||
608
+ unicode.IsDigit(rune(ch))
547
609
  }