@nestia/core 12.0.0-dev.20260601.1 → 12.0.0-dev.20260612.1

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 (116) hide show
  1. package/LICENSE +21 -21
  2. package/MIGRATION.md +169 -169
  3. package/README.md +93 -93
  4. package/lib/adaptors/McpAdaptor.d.ts +75 -0
  5. package/lib/adaptors/McpAdaptor.js +257 -0
  6. package/lib/adaptors/McpAdaptor.js.map +1 -0
  7. package/lib/adaptors/WebSocketAdaptor.js +4 -4
  8. package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
  9. package/lib/decorators/McpRoute.d.ts +69 -0
  10. package/lib/decorators/McpRoute.js +58 -0
  11. package/lib/decorators/McpRoute.js.map +1 -0
  12. package/lib/decorators/TypedParam.js +4 -4
  13. package/lib/decorators/TypedParam.js.map +1 -1
  14. package/lib/decorators/TypedRoute.js +1 -1
  15. package/lib/decorators/TypedRoute.js.map +1 -1
  16. package/lib/decorators/internal/IMcpRouteReflect.d.ts +2 -0
  17. package/lib/decorators/internal/IMcpRouteReflect.js +3 -0
  18. package/lib/decorators/internal/IMcpRouteReflect.js.map +1 -0
  19. package/lib/decorators/internal/get_path_and_querify.js +4 -4
  20. package/lib/decorators/internal/get_path_and_querify.js.map +1 -1
  21. package/lib/decorators/internal/get_path_and_stringify.js +4 -4
  22. package/lib/decorators/internal/get_path_and_stringify.js.map +1 -1
  23. package/lib/decorators/internal/load_controller.js +34 -65
  24. package/lib/decorators/internal/load_controller.js.map +1 -1
  25. package/lib/decorators/internal/validate_request_body.js +4 -4
  26. package/lib/decorators/internal/validate_request_body.js.map +1 -1
  27. package/lib/decorators/internal/validate_request_form_data.js +4 -4
  28. package/lib/decorators/internal/validate_request_form_data.js.map +1 -1
  29. package/lib/decorators/internal/validate_request_headers.js +4 -4
  30. package/lib/decorators/internal/validate_request_headers.js.map +1 -1
  31. package/lib/decorators/internal/validate_request_query.js +4 -4
  32. package/lib/decorators/internal/validate_request_query.js.map +1 -1
  33. package/lib/module.d.ts +2 -0
  34. package/lib/module.js +2 -0
  35. package/lib/module.js.map +1 -1
  36. package/native/cmd/ttsc-nestia/main.go +11 -11
  37. package/native/go.mod +32 -32
  38. package/native/go.sum +54 -54
  39. package/native/plugin/plan.go +102 -102
  40. package/native/transform/ast.go +32 -32
  41. package/native/transform/build.go +380 -444
  42. package/native/transform/cleanup.go +408 -408
  43. package/native/transform/contributor.go +97 -68
  44. package/native/transform/core_querify.go +231 -227
  45. package/native/transform/core_transform.go +1996 -1713
  46. package/native/transform/core_websocket.go +115 -115
  47. package/native/transform/exports.go +13 -13
  48. package/native/transform/mcp_transform.go +414 -0
  49. package/native/transform/node_transform.go +357 -0
  50. package/native/transform/path_rewrite.go +285 -285
  51. package/native/transform/printer.go +244 -244
  52. package/native/transform/rewrite.go +668 -662
  53. package/native/transform/run.go +73 -73
  54. package/native/transform/transform.go +336 -403
  55. package/native/transform/typia_fast.go +352 -326
  56. package/native/transform/typia_replacement.go +24 -24
  57. package/native/transform.cjs +43 -43
  58. package/package.json +15 -8
  59. package/src/adaptors/McpAdaptor.ts +276 -0
  60. package/src/adaptors/WebSocketAdaptor.ts +429 -429
  61. package/src/decorators/DynamicModule.ts +44 -44
  62. package/src/decorators/EncryptedBody.ts +97 -97
  63. package/src/decorators/EncryptedController.ts +40 -40
  64. package/src/decorators/EncryptedModule.ts +98 -98
  65. package/src/decorators/EncryptedRoute.ts +213 -213
  66. package/src/decorators/HumanRoute.ts +21 -21
  67. package/src/decorators/McpRoute.ts +154 -0
  68. package/src/decorators/NoTransformConfigurationError.ts +40 -40
  69. package/src/decorators/PlainBody.ts +76 -76
  70. package/src/decorators/SwaggerCustomizer.ts +97 -97
  71. package/src/decorators/SwaggerExample.ts +180 -180
  72. package/src/decorators/TypedBody.ts +57 -57
  73. package/src/decorators/TypedException.ts +147 -147
  74. package/src/decorators/TypedFormData.ts +187 -187
  75. package/src/decorators/TypedHeaders.ts +66 -66
  76. package/src/decorators/TypedParam.ts +77 -77
  77. package/src/decorators/TypedQuery.ts +234 -234
  78. package/src/decorators/TypedRoute.ts +198 -196
  79. package/src/decorators/WebSocketRoute.ts +242 -242
  80. package/src/decorators/doNotThrowTransformError.ts +5 -5
  81. package/src/decorators/internal/EncryptedConstant.ts +2 -2
  82. package/src/decorators/internal/IMcpRouteReflect.ts +40 -0
  83. package/src/decorators/internal/IWebSocketRouteReflect.ts +23 -23
  84. package/src/decorators/internal/get_path_and_querify.ts +94 -94
  85. package/src/decorators/internal/get_path_and_stringify.ts +110 -110
  86. package/src/decorators/internal/get_text_body.ts +16 -16
  87. package/src/decorators/internal/headers_to_object.ts +11 -11
  88. package/src/decorators/internal/is_request_body_undefined.ts +12 -12
  89. package/src/decorators/internal/load_controller.ts +91 -76
  90. package/src/decorators/internal/route_error.ts +43 -43
  91. package/src/decorators/internal/validate_request_body.ts +64 -64
  92. package/src/decorators/internal/validate_request_form_data.ts +67 -67
  93. package/src/decorators/internal/validate_request_headers.ts +76 -76
  94. package/src/decorators/internal/validate_request_query.ts +83 -83
  95. package/src/index.ts +5 -5
  96. package/src/module.ts +25 -23
  97. package/src/options/IRequestBodyValidator.ts +20 -20
  98. package/src/options/IRequestFormDataProps.ts +27 -27
  99. package/src/options/IRequestHeadersValidator.ts +22 -22
  100. package/src/options/IRequestQueryValidator.ts +20 -20
  101. package/src/options/IResponseBodyQuerifier.ts +25 -25
  102. package/src/options/IResponseBodyStringifier.ts +30 -30
  103. package/src/transform.ts +101 -101
  104. package/src/typings/Creator.ts +3 -3
  105. package/src/typings/get-function-location.d.ts +7 -7
  106. package/src/utils/ArrayUtil.ts +7 -7
  107. package/src/utils/ExceptionManager.ts +115 -115
  108. package/src/utils/Singleton.ts +16 -16
  109. package/src/utils/SourceFinder.ts +54 -54
  110. package/src/utils/VersioningStrategy.ts +27 -27
  111. package/native/transform/cleanup_test.go +0 -76
  112. package/native/transform/commonjs_import_alias_test.go +0 -49
  113. package/native/transform/core_dispatch_test.go +0 -127
  114. package/native/transform/path_rewrite_test.go +0 -243
  115. package/native/transform/rewrite_test.go +0 -118
  116. package/native/transform/rewrite_unique_base_test.go +0 -48
@@ -1,662 +1,668 @@
1
- package transform
2
-
3
- import (
4
- "fmt"
5
- "path/filepath"
6
- "sort"
7
- "strings"
8
- "unicode"
9
- )
10
-
11
- type nativeRewrite struct {
12
- FilePath string
13
- RootName string
14
- Namespaces []string
15
- Method string
16
- Replacement string
17
- ConsumeParens bool
18
- AppendArguments []string
19
- TargetExpressionCandidates []string
20
- SourceStart int
21
- ExpectedArgumentCount *int
22
- ExpectedArgumentsText string
23
- }
24
-
25
- type nativeRewriteSet struct {
26
- byPath map[string][]nativeRewrite
27
- aliasesByPath map[string]map[string]bool
28
- sortedAliasesByPath map[string][]string
29
- }
30
-
31
- func newNativeRewriteSet() *nativeRewriteSet {
32
- return &nativeRewriteSet{
33
- byPath: map[string][]nativeRewrite{},
34
- aliasesByPath: map[string]map[string]bool{},
35
- sortedAliasesByPath: map[string][]string{},
36
- }
37
- }
38
-
39
- func (rs *nativeRewriteSet) Add(r nativeRewrite) {
40
- if r.FilePath == "" {
41
- return
42
- }
43
- path := filepath.ToSlash(r.FilePath)
44
- rs.byPath[path] = append(rs.byPath[path], r)
45
- rs.addRuntimeAliases(path, r.Replacement)
46
- for _, argument := range r.AppendArguments {
47
- rs.addRuntimeAliases(path, argument)
48
- }
49
- }
50
-
51
- func (rs *nativeRewriteSet) addRuntimeAliases(path string, text string) {
52
- for _, alias := range collectCleanupRuntimeAliases(text) {
53
- if rs.aliasesByPath[path] == nil {
54
- rs.aliasesByPath[path] = map[string]bool{}
55
- }
56
- rs.aliasesByPath[path][alias] = true
57
- delete(rs.sortedAliasesByPath, path)
58
- }
59
- }
60
-
61
- func (rs *nativeRewriteSet) Len() int {
62
- if rs == nil {
63
- return 0
64
- }
65
- n := 0
66
- for _, rewrites := range rs.byPath {
67
- n += len(rewrites)
68
- }
69
- return n
70
- }
71
-
72
- func (rs *nativeRewriteSet) Apply(outputName string, text string, cursors map[string]int) (string, error) {
73
- if rs == nil || len(rs.byPath) == 0 {
74
- return text, nil
75
- }
76
- if strings.Contains(text, "/* @ttsc-rewritten */") {
77
- return text, nil
78
- }
79
- srcPath, ok := rs.findSourceForOutput(outputName)
80
- if !ok || len(rs.byPath[srcPath]) == 0 {
81
- return text, nil
82
- }
83
- rewrites := rs.byPath[srcPath]
84
- sort.SliceStable(rewrites, func(i, j int) bool {
85
- left, leftOK := nativeRewriteFirstIndex(text, rewrites[i])
86
- right, rightOK := nativeRewriteFirstIndex(text, rewrites[j])
87
- if leftOK && rightOK && left != right {
88
- return left < right
89
- }
90
- if leftOK != rightOK {
91
- return leftOK
92
- }
93
- return rewrites[i].SourceStart < rewrites[j].SourceStart
94
- })
95
- pos := cursors[srcPath]
96
- out := text
97
- for pos < len(rewrites) {
98
- rewrite := rewrites[pos]
99
- replaced, ok, err := spliceNativeCall(out, rewrite)
100
- if err != nil {
101
- return "", err
102
- }
103
- if !ok {
104
- preview := out
105
- if len(preview) > 400 {
106
- preview = preview[:400] + "..."
107
- }
108
- return "", fmt.Errorf(
109
- "native rewrite: could not locate %s.%s(...) call in %s (tried roots %v; preview: %q)",
110
- joinNativeRootAndNamespaces(rewrite),
111
- rewrite.Method,
112
- outputName,
113
- candidateNativeRoots(rewrite.RootName),
114
- preview,
115
- )
116
- }
117
- out = replaced
118
- pos++
119
- }
120
- cursors[srcPath] = pos
121
- if out != text {
122
- out = insertNativeRewriteSentinel(out)
123
- }
124
- return out, nil
125
- }
126
-
127
- func (rs *nativeRewriteSet) RuntimeAliasesForOutput(outputName string) []string {
128
- if rs == nil || len(rs.aliasesByPath) == 0 || isJavaScriptOutput(outputName) == false {
129
- return nil
130
- }
131
- srcPath, ok := rs.findSourceForOutput(outputName)
132
- if !ok {
133
- return nil
134
- }
135
- if aliases, ok := rs.sortedAliasesByPath[srcPath]; ok {
136
- return aliases
137
- }
138
- seen := rs.aliasesByPath[srcPath]
139
- if len(seen) == 0 {
140
- rs.sortedAliasesByPath[srcPath] = []string{}
141
- return rs.sortedAliasesByPath[srcPath]
142
- }
143
- aliases := sortCleanupRuntimeAliases(seen)
144
- rs.sortedAliasesByPath[srcPath] = aliases
145
- return aliases
146
- }
147
-
148
- func nativeRewriteFirstIndex(text string, rewrite nativeRewrite) (int, bool) {
149
- best := -1
150
- for _, target := range candidateNativeTargets(rewrite) {
151
- hit, ok := indexNativeFlexibleCall(text, target, rewrite, 0)
152
- if ok == false {
153
- continue
154
- }
155
- if best < 0 || hit.start < best {
156
- best = hit.start
157
- }
158
- }
159
- return best, best >= 0
160
- }
161
-
162
- func (rs *nativeRewriteSet) findSourceForOutput(outputName string) (string, bool) {
163
- outSlash := strings.TrimSuffix(filepath.ToSlash(outputName), filepath.Ext(outputName))
164
- for path := range rs.byPath {
165
- srcStem := strings.TrimSuffix(filepath.ToSlash(path), filepath.Ext(path))
166
- if OutputMatchesSourceStem(outSlash, srcStem) {
167
- return path, true
168
- }
169
- }
170
- return rs.findSourceByUniqueBase(outSlash)
171
- }
172
-
173
- func (rs *nativeRewriteSet) findSourceByUniqueBase(outputStem string) (string, bool) {
174
- // Only fall back to basename matching when the output looks like an actual
175
- // build product (sits inside lib/dist/bin/build). If the output lives in
176
- // the source tree (e.g. ttsc's virtual filesystem mirroring src/), basename
177
- // matching crosses unrelated files `src/index.ts` would silently absorb
178
- // the rewrite intended for every `src/.../index.ts` sibling.
179
- if outputHasBuildMarker(outputStem) == false {
180
- return "", false
181
- }
182
- base := pathBase(outputStem)
183
- if base == "" {
184
- return "", false
185
- }
186
- matched := ""
187
- count := 0
188
- for path := range rs.byPath {
189
- srcStem := strings.TrimSuffix(filepath.ToSlash(path), filepath.Ext(path))
190
- if pathBase(srcStem) != base {
191
- continue
192
- }
193
- matched = path
194
- count++
195
- if count > 1 {
196
- return "", false
197
- }
198
- }
199
- return matched, count == 1
200
- }
201
-
202
- func outputHasBuildMarker(stem string) bool {
203
- for _, marker := range []string{"lib", "dist", "bin", "build"} {
204
- if _, ok := suffixAfterPathMarker(stem, []string{marker}, false); ok {
205
- return true
206
- }
207
- }
208
- return false
209
- }
210
-
211
- func OutputMatchesSourceStem(outputStem string, sourceStem string) bool {
212
- if outputStem == sourceStem {
213
- return true
214
- }
215
- for _, sourceRel := range sourceOutputCandidates(sourceStem) {
216
- for _, outputRel := range outputSourceCandidates(outputStem) {
217
- if sourceRel == outputRel {
218
- return true
219
- }
220
- }
221
- // Suffix-only matching is a fallback for paths that don't share a
222
- // recognized build marker (e.g. ttsc's virtual filesystem mirroring
223
- // the source tree). Require the relative path to span at least two
224
- // segments so a top-level source like `src/index.ts` does not match
225
- // every output file that happens to end in `/index.js`.
226
- if strings.Contains(sourceRel, "/") && strings.HasSuffix(outputStem, "/"+sourceRel) {
227
- return true
228
- }
229
- }
230
- return false
231
- }
232
-
233
- func pathBase(stem string) string {
234
- stem = strings.TrimSuffix(filepath.ToSlash(stem), "/")
235
- if stem == "" {
236
- return ""
237
- }
238
- if idx := strings.LastIndexByte(stem, '/'); idx >= 0 {
239
- return stem[idx+1:]
240
- }
241
- return stem
242
- }
243
-
244
- func isJavaScriptOutput(fileName string) bool {
245
- switch strings.ToLower(filepath.Ext(fileName)) {
246
- case ".js", ".mjs", ".cjs":
247
- return true
248
- default:
249
- return false
250
- }
251
- }
252
-
253
- func sourceOutputCandidates(stem string) []string {
254
- candidates := []string{}
255
- if rel, ok := suffixAfterPathMarker(stem, []string{"src", "api"}, false); ok {
256
- candidates = append(candidates, rel)
257
- }
258
- if rel, ok := suffixAfterPathMarker(stem, []string{"src"}, false); ok {
259
- candidates = append(candidates, rel)
260
- }
261
- if rel, ok := suffixAfterPathMarker(stem, []string{"test"}, true); ok {
262
- candidates = append(candidates, rel)
263
- }
264
- return candidates
265
- }
266
-
267
- func outputSourceCandidates(stem string) []string {
268
- candidates := []string{}
269
- // Mirror sourceOutputCandidates so paths whose source and output trees
270
- // share the same `src/` (or `test/`) root e.g. ttsc's virtual filesystem
271
- // emits next to the source can intersect on the same relative tail.
272
- // We intentionally do NOT include the {"src","api"} marker here: that
273
- // marker exists on the source side to let a source under `src/api/`
274
- // pretend its rel is the leaf only (so it matches an output that lacks
275
- // the `api/` segment); mirroring it on the output side would let two
276
- // unrelated files (e.g. `src/index.ts` and `src/api/index.ts`) collide
277
- // on the rel "index".
278
- for _, marker := range [][]string{{"lib"}, {"bin"}, {"dist"}, {"build"}, {"src"}} {
279
- if rel, ok := suffixAfterPathMarker(stem, marker, false); ok {
280
- candidates = append(candidates, rel)
281
- }
282
- }
283
- if rel, ok := suffixAfterPathMarker(stem, []string{"test"}, true); ok {
284
- candidates = append(candidates, rel)
285
- }
286
- return append(candidates, stem)
287
- }
288
-
289
- func suffixAfterPathMarker(stem string, marker []string, includeMarker bool) (string, bool) {
290
- segments := strings.Split(filepath.ToSlash(stem), "/")
291
- for i := len(segments) - len(marker); i >= 0; i-- {
292
- matched := true
293
- for j := range marker {
294
- if segments[i+j] != marker[j] {
295
- matched = false
296
- break
297
- }
298
- }
299
- if !matched {
300
- continue
301
- }
302
- start := i + len(marker)
303
- if includeMarker {
304
- start = i
305
- }
306
- if start >= len(segments) {
307
- return "", false
308
- }
309
- return strings.Join(segments[start:], "/"), true
310
- }
311
- return "", false
312
- }
313
-
314
- func spliceNativeCall(text string, r nativeRewrite) (string, bool, error) {
315
- for _, target := range candidateNativeTargets(r) {
316
- searchFrom := 0
317
- for {
318
- hit, ok := indexNativeFlexibleCall(text, target, r, searchFrom)
319
- if !ok {
320
- break
321
- }
322
- closePos, ok := matchNativeParen(text, hit.paren)
323
- if !ok {
324
- searchFrom = advanceNativeSearch(searchFrom, hit)
325
- continue
326
- }
327
- if r.ExpectedArgumentCount != nil &&
328
- countNativeArguments(text[hit.paren+1:closePos]) != *r.ExpectedArgumentCount {
329
- searchFrom = advanceNativeSearch(searchFrom, hit)
330
- continue
331
- }
332
- current := strings.TrimSpace(text[hit.paren+1 : closePos])
333
- if r.ExpectedArgumentsText != "" && compactNativeArgumentText(current) != compactNativeArgumentText(r.ExpectedArgumentsText) {
334
- searchFrom = advanceNativeSearch(searchFrom, hit)
335
- continue
336
- }
337
- if len(r.AppendArguments) != 0 {
338
- next := strings.Join(r.AppendArguments, ", ")
339
- if current != "" {
340
- next = current + ", " + next
341
- }
342
- replaced := text[:hit.paren+1] + next + text[closePos:]
343
- return replaced, true, nil
344
- }
345
- if r.ConsumeParens {
346
- replaced := text[:hit.start] + r.Replacement + text[closePos+1:]
347
- return replaced, true, nil
348
- }
349
- replaced := text[:hit.start] + r.Replacement + text[hit.paren:]
350
- return replaced, true, nil
351
- }
352
- }
353
- return text, false, nil
354
- }
355
-
356
- func advanceNativeSearch(current int, hit nativeCallHit) int {
357
- next := hit.paren + 1
358
- if next <= current {
359
- return current + 1
360
- }
361
- return next
362
- }
363
-
364
- func compactNativeArgumentText(text string) string {
365
- var builder strings.Builder
366
- inString := byte(0)
367
- escaped := false
368
- for i := 0; i < len(text); i++ {
369
- ch := text[i]
370
- if inString != 0 {
371
- builder.WriteByte(ch)
372
- if escaped {
373
- escaped = false
374
- } else if ch == '\\' {
375
- escaped = true
376
- } else if ch == inString {
377
- inString = 0
378
- }
379
- continue
380
- }
381
- switch ch {
382
- case '"', '\'', '`':
383
- inString = ch
384
- builder.WriteByte(ch)
385
- case ' ', '\t', '\r', '\n':
386
- continue
387
- default:
388
- builder.WriteByte(ch)
389
- }
390
- }
391
- return builder.String()
392
- }
393
-
394
- type nativeCallHit struct {
395
- start int
396
- paren int
397
- }
398
-
399
- func indexNativeFlexibleCall(text string, target string, r nativeRewrite, searchFrom int) (nativeCallHit, bool) {
400
- parts := strings.Split(target, ".")
401
- if len(parts) == 0 || parts[0] == "" {
402
- return nativeCallHit{}, false
403
- }
404
- root := parts[0]
405
- parts = parts[1:]
406
- start := searchFrom
407
- if start < 0 {
408
- start = 0
409
- }
410
- for {
411
- hit := strings.Index(text[start:], root)
412
- if hit < 0 {
413
- return nativeCallHit{}, false
414
- }
415
- pos := start + hit
416
- if pos > 0 && isNativeIdentifierPart(rune(text[pos-1])) {
417
- start = pos + 1
418
- continue
419
- }
420
- cursor := pos + len(root)
421
- ok := true
422
- for _, part := range parts {
423
- cursor = skipNativeWhitespace(text, cursor)
424
- if cursor >= len(text) || text[cursor] != '.' {
425
- ok = false
426
- break
427
- }
428
- cursor++
429
- cursor = skipNativeWhitespace(text, cursor)
430
- if !strings.HasPrefix(text[cursor:], part) {
431
- ok = false
432
- break
433
- }
434
- cursor += len(part)
435
- }
436
- cursor = skipNativeWhitespace(text, cursor)
437
- if ok && cursor < len(text) && text[cursor] == '(' {
438
- return nativeCallHit{start: pos, paren: cursor}, true
439
- }
440
- if ok {
441
- if wrappedStart, paren, wrapped := matchNativeCommaWrappedCall(text, pos, cursor); wrapped {
442
- return nativeCallHit{start: wrappedStart, paren: paren}, true
443
- }
444
- }
445
- start = pos + 1
446
- }
447
- }
448
-
449
- func skipNativeWhitespace(text string, pos int) int {
450
- for pos < len(text) {
451
- switch text[pos] {
452
- case ' ', '\t', '\r', '\n':
453
- pos++
454
- default:
455
- return pos
456
- }
457
- }
458
- return pos
459
- }
460
-
461
- func countNativeArguments(text string) int {
462
- text = strings.TrimSpace(text)
463
- if text == "" {
464
- return 0
465
- }
466
- count := 1
467
- depth := 0
468
- // templateDepths records the bracket depth at which each currently-open
469
- // `${...}` substitution started. When depth drops back to that level on a
470
- // '}' token, we pop and re-enter template-string mode.
471
- templateDepths := []int{}
472
- i := 0
473
- for i < len(text) {
474
- ch := text[i]
475
- switch ch {
476
- case '(', '[', '{':
477
- depth++
478
- case ')', ']':
479
- if depth > 0 {
480
- depth--
481
- }
482
- case '}':
483
- if depth > 0 {
484
- depth--
485
- }
486
- if n := len(templateDepths); n > 0 && templateDepths[n-1] == depth {
487
- templateDepths = templateDepths[:n-1]
488
- i++
489
- skipTemplateLiteral(text, &i, &templateDepths, &depth)
490
- continue
491
- }
492
- case '"', '\'':
493
- i++
494
- for i < len(text) && text[i] != ch {
495
- if text[i] == '\\' && i+1 < len(text) {
496
- i++
497
- }
498
- i++
499
- }
500
- case '`':
501
- i++
502
- skipTemplateLiteral(text, &i, &templateDepths, &depth)
503
- continue
504
- case ',':
505
- if depth == 0 {
506
- count++
507
- }
508
- }
509
- i++
510
- }
511
- return count
512
- }
513
-
514
- // skipTemplateLiteral advances i through a template-literal body. It exits
515
- // either at the matching closing backtick (consuming it) or at the opening
516
- // of a `${...}` substitution, in which case the caller resumes regular
517
- // expression parsing and templateDepths records that we owe a return to
518
- // template-string mode on the matching '}'.
519
- func skipTemplateLiteral(text string, i *int, templateDepths *[]int, depth *int) {
520
- for *i < len(text) {
521
- c := text[*i]
522
- if c == '\\' && *i+1 < len(text) {
523
- *i += 2
524
- continue
525
- }
526
- if c == '`' {
527
- *i++
528
- return
529
- }
530
- if c == '$' && *i+1 < len(text) && text[*i+1] == '{' {
531
- *templateDepths = append(*templateDepths, *depth)
532
- *depth++
533
- *i += 2
534
- return
535
- }
536
- *i++
537
- }
538
- }
539
-
540
- func candidateNativeRoots(root string) []string {
541
- return []string{
542
- root,
543
- root + "_1.default",
544
- root + "_2.default",
545
- root + ".default",
546
- root + "_1",
547
- root + "_2",
548
- }
549
- }
550
-
551
- func candidateNativeTargets(r nativeRewrite) []string {
552
- seen := map[string]bool{}
553
- output := []string{}
554
- add := func(value string) {
555
- value = strings.TrimSpace(value)
556
- if value == "" || seen[value] {
557
- return
558
- }
559
- seen[value] = true
560
- output = append(output, value)
561
- }
562
- for _, candidate := range r.TargetExpressionCandidates {
563
- add(candidate)
564
- }
565
- if r.Method == "" && len(r.Namespaces) == 0 {
566
- for _, root := range candidateNativeRoots(r.RootName) {
567
- add(root)
568
- }
569
- return output
570
- }
571
- for _, root := range candidateNativeRoots(r.RootName) {
572
- parts := []string{root}
573
- parts = append(parts, r.Namespaces...)
574
- if r.Method != "" {
575
- parts = append(parts, r.Method)
576
- }
577
- add(strings.Join(parts, "."))
578
- }
579
- return output
580
- }
581
-
582
- func matchNativeCommaWrappedCall(text string, exprStart int, exprEnd int) (int, int, bool) {
583
- left := exprStart - 1
584
- for left >= 0 && (text[left] == ' ' || text[left] == '\t' || text[left] == '\r' || text[left] == '\n') {
585
- left--
586
- }
587
- if left < 0 || text[left] != ',' {
588
- return 0, 0, false
589
- }
590
- left--
591
- for left >= 0 && (text[left] == ' ' || text[left] == '\t' || text[left] == '\r' || text[left] == '\n') {
592
- left--
593
- }
594
- if left < 0 || text[left] != '0' {
595
- return 0, 0, false
596
- }
597
- left--
598
- for left >= 0 && (text[left] == ' ' || text[left] == '\t' || text[left] == '\r' || text[left] == '\n') {
599
- left--
600
- }
601
- if left < 0 || text[left] != '(' {
602
- return 0, 0, false
603
- }
604
- right := skipNativeWhitespace(text, exprEnd)
605
- if right >= len(text) || text[right] != ')' {
606
- return 0, 0, false
607
- }
608
- paren := skipNativeWhitespace(text, right+1)
609
- if paren >= len(text) || text[paren] != '(' {
610
- return 0, 0, false
611
- }
612
- return left, paren, true
613
- }
614
-
615
- func joinNativeRootAndNamespaces(r nativeRewrite) string {
616
- if len(r.Namespaces) == 0 {
617
- return r.RootName
618
- }
619
- return r.RootName + "." + strings.Join(r.Namespaces, ".")
620
- }
621
-
622
- func matchNativeParen(text string, pos int) (int, bool) {
623
- if pos >= len(text) || text[pos] != '(' {
624
- return 0, false
625
- }
626
- depth := 1
627
- for i := pos + 1; i < len(text); i++ {
628
- switch text[i] {
629
- case '(':
630
- depth++
631
- case ')':
632
- depth--
633
- if depth == 0 {
634
- return i, true
635
- }
636
- case '"', '\'', '`':
637
- q := text[i]
638
- j := i + 1
639
- for j < len(text) && text[j] != q {
640
- if text[j] == '\\' {
641
- j++
642
- }
643
- j++
644
- }
645
- i = j
646
- }
647
- }
648
- return 0, false
649
- }
650
-
651
- func isNativeIdentifierPart(r rune) bool {
652
- return r == '_' || r == '$' || unicode.IsLetter(r) || unicode.IsDigit(r)
653
- }
654
-
655
- func insertNativeRewriteSentinel(text string) string {
656
- for _, prefix := range []string{"\"use strict\";\n", "'use strict';\n"} {
657
- if strings.HasPrefix(text, prefix) {
658
- return prefix + "/* @ttsc-rewritten */\n" + text[len(prefix):]
659
- }
660
- }
661
- return "/* @ttsc-rewritten */\n" + text
662
- }
1
+ package transform
2
+
3
+ import (
4
+ "fmt"
5
+ "path/filepath"
6
+ "sort"
7
+ "strings"
8
+ "unicode"
9
+ )
10
+
11
+ type nativeRewrite struct {
12
+ FilePath string
13
+ RootName string
14
+ Namespaces []string
15
+ Method string
16
+ Replacement string
17
+ ConsumeParens bool
18
+ AppendArguments []string
19
+ ReplaceArguments bool
20
+ TargetExpressionCandidates []string
21
+ SourceStart int
22
+ ExpectedArgumentCount *int
23
+ ExpectedArgumentsText string
24
+ }
25
+
26
+ type nativeRewriteSet struct {
27
+ byPath map[string][]nativeRewrite
28
+ aliasesByPath map[string]map[string]bool
29
+ sortedAliasesByPath map[string][]string
30
+ }
31
+
32
+ func newNativeRewriteSet() *nativeRewriteSet {
33
+ return &nativeRewriteSet{
34
+ byPath: map[string][]nativeRewrite{},
35
+ aliasesByPath: map[string]map[string]bool{},
36
+ sortedAliasesByPath: map[string][]string{},
37
+ }
38
+ }
39
+
40
+ func (rs *nativeRewriteSet) Add(r nativeRewrite) {
41
+ if r.FilePath == "" {
42
+ return
43
+ }
44
+ path := filepath.ToSlash(r.FilePath)
45
+ rs.byPath[path] = append(rs.byPath[path], r)
46
+ rs.addRuntimeAliases(path, r.Replacement)
47
+ for _, argument := range r.AppendArguments {
48
+ rs.addRuntimeAliases(path, argument)
49
+ }
50
+ }
51
+
52
+ func (rs *nativeRewriteSet) addRuntimeAliases(path string, text string) {
53
+ for _, alias := range collectCleanupRuntimeAliases(text) {
54
+ if rs.aliasesByPath[path] == nil {
55
+ rs.aliasesByPath[path] = map[string]bool{}
56
+ }
57
+ rs.aliasesByPath[path][alias] = true
58
+ delete(rs.sortedAliasesByPath, path)
59
+ }
60
+ }
61
+
62
+ func (rs *nativeRewriteSet) Len() int {
63
+ if rs == nil {
64
+ return 0
65
+ }
66
+ n := 0
67
+ for _, rewrites := range rs.byPath {
68
+ n += len(rewrites)
69
+ }
70
+ return n
71
+ }
72
+
73
+ func (rs *nativeRewriteSet) Apply(outputName string, text string, cursors map[string]int) (string, error) {
74
+ if rs == nil || len(rs.byPath) == 0 {
75
+ return text, nil
76
+ }
77
+ if strings.Contains(text, "/* @ttsc-rewritten */") {
78
+ return text, nil
79
+ }
80
+ srcPath, ok := rs.findSourceForOutput(outputName)
81
+ if !ok || len(rs.byPath[srcPath]) == 0 {
82
+ return text, nil
83
+ }
84
+ rewrites := rs.byPath[srcPath]
85
+ sort.SliceStable(rewrites, func(i, j int) bool {
86
+ left, leftOK := nativeRewriteFirstIndex(text, rewrites[i])
87
+ right, rightOK := nativeRewriteFirstIndex(text, rewrites[j])
88
+ if leftOK && rightOK && left != right {
89
+ return left < right
90
+ }
91
+ if leftOK != rightOK {
92
+ return leftOK
93
+ }
94
+ return rewrites[i].SourceStart < rewrites[j].SourceStart
95
+ })
96
+ pos := cursors[srcPath]
97
+ out := text
98
+ for pos < len(rewrites) {
99
+ rewrite := rewrites[pos]
100
+ replaced, ok, err := spliceNativeCall(out, rewrite)
101
+ if err != nil {
102
+ return "", err
103
+ }
104
+ if !ok {
105
+ preview := out
106
+ if len(preview) > 400 {
107
+ preview = preview[:400] + "..."
108
+ }
109
+ return "", fmt.Errorf(
110
+ "native rewrite: could not locate %s.%s(...) call in %s (tried roots %v; preview: %q)",
111
+ joinNativeRootAndNamespaces(rewrite),
112
+ rewrite.Method,
113
+ outputName,
114
+ candidateNativeRoots(rewrite.RootName),
115
+ preview,
116
+ )
117
+ }
118
+ out = replaced
119
+ pos++
120
+ }
121
+ cursors[srcPath] = pos
122
+ if out != text {
123
+ out = insertNativeRewriteSentinel(out)
124
+ }
125
+ return out, nil
126
+ }
127
+
128
+ func (rs *nativeRewriteSet) RuntimeAliasesForOutput(outputName string) []string {
129
+ if rs == nil || len(rs.aliasesByPath) == 0 || isJavaScriptOutput(outputName) == false {
130
+ return nil
131
+ }
132
+ srcPath, ok := rs.findSourceForOutput(outputName)
133
+ if !ok {
134
+ return nil
135
+ }
136
+ if aliases, ok := rs.sortedAliasesByPath[srcPath]; ok {
137
+ return aliases
138
+ }
139
+ seen := rs.aliasesByPath[srcPath]
140
+ if len(seen) == 0 {
141
+ rs.sortedAliasesByPath[srcPath] = []string{}
142
+ return rs.sortedAliasesByPath[srcPath]
143
+ }
144
+ aliases := sortCleanupRuntimeAliases(seen)
145
+ rs.sortedAliasesByPath[srcPath] = aliases
146
+ return aliases
147
+ }
148
+
149
+ func nativeRewriteFirstIndex(text string, rewrite nativeRewrite) (int, bool) {
150
+ best := -1
151
+ for _, target := range candidateNativeTargets(rewrite) {
152
+ hit, ok := indexNativeFlexibleCall(text, target, rewrite, 0)
153
+ if ok == false {
154
+ continue
155
+ }
156
+ if best < 0 || hit.start < best {
157
+ best = hit.start
158
+ }
159
+ }
160
+ return best, best >= 0
161
+ }
162
+
163
+ func (rs *nativeRewriteSet) findSourceForOutput(outputName string) (string, bool) {
164
+ outSlash := strings.TrimSuffix(filepath.ToSlash(outputName), filepath.Ext(outputName))
165
+ for path := range rs.byPath {
166
+ srcStem := strings.TrimSuffix(filepath.ToSlash(path), filepath.Ext(path))
167
+ if OutputMatchesSourceStem(outSlash, srcStem) {
168
+ return path, true
169
+ }
170
+ }
171
+ return rs.findSourceByUniqueBase(outSlash)
172
+ }
173
+
174
+ func (rs *nativeRewriteSet) findSourceByUniqueBase(outputStem string) (string, bool) {
175
+ // Only fall back to basename matching when the output looks like an actual
176
+ // build product (sits inside lib/dist/bin/build). If the output lives in
177
+ // the source tree (e.g. ttsc's virtual filesystem mirroring src/), basename
178
+ // matching crosses unrelated files `src/index.ts` would silently absorb
179
+ // the rewrite intended for every `src/.../index.ts` sibling.
180
+ if outputHasBuildMarker(outputStem) == false {
181
+ return "", false
182
+ }
183
+ base := pathBase(outputStem)
184
+ if base == "" {
185
+ return "", false
186
+ }
187
+ matched := ""
188
+ count := 0
189
+ for path := range rs.byPath {
190
+ srcStem := strings.TrimSuffix(filepath.ToSlash(path), filepath.Ext(path))
191
+ if pathBase(srcStem) != base {
192
+ continue
193
+ }
194
+ matched = path
195
+ count++
196
+ if count > 1 {
197
+ return "", false
198
+ }
199
+ }
200
+ return matched, count == 1
201
+ }
202
+
203
+ func outputHasBuildMarker(stem string) bool {
204
+ for _, marker := range []string{"lib", "dist", "bin", "build"} {
205
+ if _, ok := suffixAfterPathMarker(stem, []string{marker}, false); ok {
206
+ return true
207
+ }
208
+ }
209
+ return false
210
+ }
211
+
212
+ func OutputMatchesSourceStem(outputStem string, sourceStem string) bool {
213
+ if outputStem == sourceStem {
214
+ return true
215
+ }
216
+ for _, sourceRel := range sourceOutputCandidates(sourceStem) {
217
+ for _, outputRel := range outputSourceCandidates(outputStem) {
218
+ if sourceRel == outputRel {
219
+ return true
220
+ }
221
+ }
222
+ // Suffix-only matching is a fallback for paths that don't share a
223
+ // recognized build marker (e.g. ttsc's virtual filesystem mirroring
224
+ // the source tree). Require the relative path to span at least two
225
+ // segments so a top-level source like `src/index.ts` does not match
226
+ // every output file that happens to end in `/index.js`.
227
+ if strings.Contains(sourceRel, "/") && strings.HasSuffix(outputStem, "/"+sourceRel) {
228
+ return true
229
+ }
230
+ }
231
+ return false
232
+ }
233
+
234
+ func pathBase(stem string) string {
235
+ stem = strings.TrimSuffix(filepath.ToSlash(stem), "/")
236
+ if stem == "" {
237
+ return ""
238
+ }
239
+ if idx := strings.LastIndexByte(stem, '/'); idx >= 0 {
240
+ return stem[idx+1:]
241
+ }
242
+ return stem
243
+ }
244
+
245
+ func isJavaScriptOutput(fileName string) bool {
246
+ switch strings.ToLower(filepath.Ext(fileName)) {
247
+ case ".js", ".mjs", ".cjs":
248
+ return true
249
+ default:
250
+ return false
251
+ }
252
+ }
253
+
254
+ func sourceOutputCandidates(stem string) []string {
255
+ candidates := []string{}
256
+ if rel, ok := suffixAfterPathMarker(stem, []string{"src", "api"}, false); ok {
257
+ candidates = append(candidates, rel)
258
+ }
259
+ if rel, ok := suffixAfterPathMarker(stem, []string{"src"}, false); ok {
260
+ candidates = append(candidates, rel)
261
+ }
262
+ if rel, ok := suffixAfterPathMarker(stem, []string{"test"}, true); ok {
263
+ candidates = append(candidates, rel)
264
+ }
265
+ return candidates
266
+ }
267
+
268
+ func outputSourceCandidates(stem string) []string {
269
+ candidates := []string{}
270
+ // Mirror sourceOutputCandidates so paths whose source and output trees
271
+ // share the same `src/` (or `test/`) root e.g. ttsc's virtual filesystem
272
+ // emits next to the source — can intersect on the same relative tail.
273
+ // We intentionally do NOT include the {"src","api"} marker here: that
274
+ // marker exists on the source side to let a source under `src/api/`
275
+ // pretend its rel is the leaf only (so it matches an output that lacks
276
+ // the `api/` segment); mirroring it on the output side would let two
277
+ // unrelated files (e.g. `src/index.ts` and `src/api/index.ts`) collide
278
+ // on the rel "index".
279
+ for _, marker := range [][]string{{"lib"}, {"bin"}, {"dist"}, {"build"}, {"src"}} {
280
+ if rel, ok := suffixAfterPathMarker(stem, marker, false); ok {
281
+ candidates = append(candidates, rel)
282
+ }
283
+ }
284
+ if rel, ok := suffixAfterPathMarker(stem, []string{"test"}, true); ok {
285
+ candidates = append(candidates, rel)
286
+ }
287
+ return append(candidates, stem)
288
+ }
289
+
290
+ func suffixAfterPathMarker(stem string, marker []string, includeMarker bool) (string, bool) {
291
+ segments := strings.Split(filepath.ToSlash(stem), "/")
292
+ for i := len(segments) - len(marker); i >= 0; i-- {
293
+ matched := true
294
+ for j := range marker {
295
+ if segments[i+j] != marker[j] {
296
+ matched = false
297
+ break
298
+ }
299
+ }
300
+ if !matched {
301
+ continue
302
+ }
303
+ start := i + len(marker)
304
+ if includeMarker {
305
+ start = i
306
+ }
307
+ if start >= len(segments) {
308
+ return "", false
309
+ }
310
+ return strings.Join(segments[start:], "/"), true
311
+ }
312
+ return "", false
313
+ }
314
+
315
+ func spliceNativeCall(text string, r nativeRewrite) (string, bool, error) {
316
+ for _, target := range candidateNativeTargets(r) {
317
+ searchFrom := 0
318
+ for {
319
+ hit, ok := indexNativeFlexibleCall(text, target, r, searchFrom)
320
+ if !ok {
321
+ break
322
+ }
323
+ closePos, ok := matchNativeParen(text, hit.paren)
324
+ if !ok {
325
+ searchFrom = advanceNativeSearch(searchFrom, hit)
326
+ continue
327
+ }
328
+ if r.ExpectedArgumentCount != nil &&
329
+ countNativeArguments(text[hit.paren+1:closePos]) != *r.ExpectedArgumentCount {
330
+ searchFrom = advanceNativeSearch(searchFrom, hit)
331
+ continue
332
+ }
333
+ current := strings.TrimSpace(text[hit.paren+1 : closePos])
334
+ if r.ExpectedArgumentsText != "" && compactNativeArgumentText(current) != compactNativeArgumentText(r.ExpectedArgumentsText) {
335
+ searchFrom = advanceNativeSearch(searchFrom, hit)
336
+ continue
337
+ }
338
+ if r.ReplaceArguments {
339
+ next := strings.Join(r.AppendArguments, ", ")
340
+ replaced := text[:hit.paren+1] + next + text[closePos:]
341
+ return replaced, true, nil
342
+ }
343
+ if len(r.AppendArguments) != 0 {
344
+ next := strings.Join(r.AppendArguments, ", ")
345
+ if current != "" {
346
+ next = current + ", " + next
347
+ }
348
+ replaced := text[:hit.paren+1] + next + text[closePos:]
349
+ return replaced, true, nil
350
+ }
351
+ if r.ConsumeParens {
352
+ replaced := text[:hit.start] + r.Replacement + text[closePos+1:]
353
+ return replaced, true, nil
354
+ }
355
+ replaced := text[:hit.start] + r.Replacement + text[hit.paren:]
356
+ return replaced, true, nil
357
+ }
358
+ }
359
+ return text, false, nil
360
+ }
361
+
362
+ func advanceNativeSearch(current int, hit nativeCallHit) int {
363
+ next := hit.paren + 1
364
+ if next <= current {
365
+ return current + 1
366
+ }
367
+ return next
368
+ }
369
+
370
+ func compactNativeArgumentText(text string) string {
371
+ var builder strings.Builder
372
+ inString := byte(0)
373
+ escaped := false
374
+ for i := 0; i < len(text); i++ {
375
+ ch := text[i]
376
+ if inString != 0 {
377
+ builder.WriteByte(ch)
378
+ if escaped {
379
+ escaped = false
380
+ } else if ch == '\\' {
381
+ escaped = true
382
+ } else if ch == inString {
383
+ inString = 0
384
+ }
385
+ continue
386
+ }
387
+ switch ch {
388
+ case '"', '\'', '`':
389
+ inString = ch
390
+ builder.WriteByte(ch)
391
+ case ' ', '\t', '\r', '\n':
392
+ continue
393
+ default:
394
+ builder.WriteByte(ch)
395
+ }
396
+ }
397
+ return builder.String()
398
+ }
399
+
400
+ type nativeCallHit struct {
401
+ start int
402
+ paren int
403
+ }
404
+
405
+ func indexNativeFlexibleCall(text string, target string, r nativeRewrite, searchFrom int) (nativeCallHit, bool) {
406
+ parts := strings.Split(target, ".")
407
+ if len(parts) == 0 || parts[0] == "" {
408
+ return nativeCallHit{}, false
409
+ }
410
+ root := parts[0]
411
+ parts = parts[1:]
412
+ start := searchFrom
413
+ if start < 0 {
414
+ start = 0
415
+ }
416
+ for {
417
+ hit := strings.Index(text[start:], root)
418
+ if hit < 0 {
419
+ return nativeCallHit{}, false
420
+ }
421
+ pos := start + hit
422
+ if pos > 0 && isNativeIdentifierPart(rune(text[pos-1])) {
423
+ start = pos + 1
424
+ continue
425
+ }
426
+ cursor := pos + len(root)
427
+ ok := true
428
+ for _, part := range parts {
429
+ cursor = skipNativeWhitespace(text, cursor)
430
+ if cursor >= len(text) || text[cursor] != '.' {
431
+ ok = false
432
+ break
433
+ }
434
+ cursor++
435
+ cursor = skipNativeWhitespace(text, cursor)
436
+ if !strings.HasPrefix(text[cursor:], part) {
437
+ ok = false
438
+ break
439
+ }
440
+ cursor += len(part)
441
+ }
442
+ cursor = skipNativeWhitespace(text, cursor)
443
+ if ok && cursor < len(text) && text[cursor] == '(' {
444
+ return nativeCallHit{start: pos, paren: cursor}, true
445
+ }
446
+ if ok {
447
+ if wrappedStart, paren, wrapped := matchNativeCommaWrappedCall(text, pos, cursor); wrapped {
448
+ return nativeCallHit{start: wrappedStart, paren: paren}, true
449
+ }
450
+ }
451
+ start = pos + 1
452
+ }
453
+ }
454
+
455
+ func skipNativeWhitespace(text string, pos int) int {
456
+ for pos < len(text) {
457
+ switch text[pos] {
458
+ case ' ', '\t', '\r', '\n':
459
+ pos++
460
+ default:
461
+ return pos
462
+ }
463
+ }
464
+ return pos
465
+ }
466
+
467
+ func countNativeArguments(text string) int {
468
+ text = strings.TrimSpace(text)
469
+ if text == "" {
470
+ return 0
471
+ }
472
+ count := 1
473
+ depth := 0
474
+ // templateDepths records the bracket depth at which each currently-open
475
+ // `${...}` substitution started. When depth drops back to that level on a
476
+ // '}' token, we pop and re-enter template-string mode.
477
+ templateDepths := []int{}
478
+ i := 0
479
+ for i < len(text) {
480
+ ch := text[i]
481
+ switch ch {
482
+ case '(', '[', '{':
483
+ depth++
484
+ case ')', ']':
485
+ if depth > 0 {
486
+ depth--
487
+ }
488
+ case '}':
489
+ if depth > 0 {
490
+ depth--
491
+ }
492
+ if n := len(templateDepths); n > 0 && templateDepths[n-1] == depth {
493
+ templateDepths = templateDepths[:n-1]
494
+ i++
495
+ skipTemplateLiteral(text, &i, &templateDepths, &depth)
496
+ continue
497
+ }
498
+ case '"', '\'':
499
+ i++
500
+ for i < len(text) && text[i] != ch {
501
+ if text[i] == '\\' && i+1 < len(text) {
502
+ i++
503
+ }
504
+ i++
505
+ }
506
+ case '`':
507
+ i++
508
+ skipTemplateLiteral(text, &i, &templateDepths, &depth)
509
+ continue
510
+ case ',':
511
+ if depth == 0 {
512
+ count++
513
+ }
514
+ }
515
+ i++
516
+ }
517
+ return count
518
+ }
519
+
520
+ // skipTemplateLiteral advances i through a template-literal body. It exits
521
+ // either at the matching closing backtick (consuming it) or at the opening
522
+ // of a `${...}` substitution, in which case the caller resumes regular
523
+ // expression parsing and templateDepths records that we owe a return to
524
+ // template-string mode on the matching '}'.
525
+ func skipTemplateLiteral(text string, i *int, templateDepths *[]int, depth *int) {
526
+ for *i < len(text) {
527
+ c := text[*i]
528
+ if c == '\\' && *i+1 < len(text) {
529
+ *i += 2
530
+ continue
531
+ }
532
+ if c == '`' {
533
+ *i++
534
+ return
535
+ }
536
+ if c == '$' && *i+1 < len(text) && text[*i+1] == '{' {
537
+ *templateDepths = append(*templateDepths, *depth)
538
+ *depth++
539
+ *i += 2
540
+ return
541
+ }
542
+ *i++
543
+ }
544
+ }
545
+
546
+ func candidateNativeRoots(root string) []string {
547
+ return []string{
548
+ root,
549
+ root + "_1.default",
550
+ root + "_2.default",
551
+ root + ".default",
552
+ root + "_1",
553
+ root + "_2",
554
+ }
555
+ }
556
+
557
+ func candidateNativeTargets(r nativeRewrite) []string {
558
+ seen := map[string]bool{}
559
+ output := []string{}
560
+ add := func(value string) {
561
+ value = strings.TrimSpace(value)
562
+ if value == "" || seen[value] {
563
+ return
564
+ }
565
+ seen[value] = true
566
+ output = append(output, value)
567
+ }
568
+ for _, candidate := range r.TargetExpressionCandidates {
569
+ add(candidate)
570
+ }
571
+ if r.Method == "" && len(r.Namespaces) == 0 {
572
+ for _, root := range candidateNativeRoots(r.RootName) {
573
+ add(root)
574
+ }
575
+ return output
576
+ }
577
+ for _, root := range candidateNativeRoots(r.RootName) {
578
+ parts := []string{root}
579
+ parts = append(parts, r.Namespaces...)
580
+ if r.Method != "" {
581
+ parts = append(parts, r.Method)
582
+ }
583
+ add(strings.Join(parts, "."))
584
+ }
585
+ return output
586
+ }
587
+
588
+ func matchNativeCommaWrappedCall(text string, exprStart int, exprEnd int) (int, int, bool) {
589
+ left := exprStart - 1
590
+ for left >= 0 && (text[left] == ' ' || text[left] == '\t' || text[left] == '\r' || text[left] == '\n') {
591
+ left--
592
+ }
593
+ if left < 0 || text[left] != ',' {
594
+ return 0, 0, false
595
+ }
596
+ left--
597
+ for left >= 0 && (text[left] == ' ' || text[left] == '\t' || text[left] == '\r' || text[left] == '\n') {
598
+ left--
599
+ }
600
+ if left < 0 || text[left] != '0' {
601
+ return 0, 0, false
602
+ }
603
+ left--
604
+ for left >= 0 && (text[left] == ' ' || text[left] == '\t' || text[left] == '\r' || text[left] == '\n') {
605
+ left--
606
+ }
607
+ if left < 0 || text[left] != '(' {
608
+ return 0, 0, false
609
+ }
610
+ right := skipNativeWhitespace(text, exprEnd)
611
+ if right >= len(text) || text[right] != ')' {
612
+ return 0, 0, false
613
+ }
614
+ paren := skipNativeWhitespace(text, right+1)
615
+ if paren >= len(text) || text[paren] != '(' {
616
+ return 0, 0, false
617
+ }
618
+ return left, paren, true
619
+ }
620
+
621
+ func joinNativeRootAndNamespaces(r nativeRewrite) string {
622
+ if len(r.Namespaces) == 0 {
623
+ return r.RootName
624
+ }
625
+ return r.RootName + "." + strings.Join(r.Namespaces, ".")
626
+ }
627
+
628
+ func matchNativeParen(text string, pos int) (int, bool) {
629
+ if pos >= len(text) || text[pos] != '(' {
630
+ return 0, false
631
+ }
632
+ depth := 1
633
+ for i := pos + 1; i < len(text); i++ {
634
+ switch text[i] {
635
+ case '(':
636
+ depth++
637
+ case ')':
638
+ depth--
639
+ if depth == 0 {
640
+ return i, true
641
+ }
642
+ case '"', '\'', '`':
643
+ q := text[i]
644
+ j := i + 1
645
+ for j < len(text) && text[j] != q {
646
+ if text[j] == '\\' {
647
+ j++
648
+ }
649
+ j++
650
+ }
651
+ i = j
652
+ }
653
+ }
654
+ return 0, false
655
+ }
656
+
657
+ func isNativeIdentifierPart(r rune) bool {
658
+ return r == '_' || r == '$' || unicode.IsLetter(r) || unicode.IsDigit(r)
659
+ }
660
+
661
+ func insertNativeRewriteSentinel(text string) string {
662
+ for _, prefix := range []string{"\"use strict\";\n", "'use strict';\n"} {
663
+ if strings.HasPrefix(text, prefix) {
664
+ return prefix + "/* @ttsc-rewritten */\n" + text[len(prefix):]
665
+ }
666
+ }
667
+ return "/* @ttsc-rewritten */\n" + text
668
+ }