ttsc 0.12.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/driver/rewrite.go +148 -56
- package/package.json +8 -8
package/driver/rewrite.go
CHANGED
|
@@ -19,7 +19,9 @@ import (
|
|
|
19
19
|
"fmt"
|
|
20
20
|
"os"
|
|
21
21
|
"path/filepath"
|
|
22
|
+
"regexp"
|
|
22
23
|
"strings"
|
|
24
|
+
"sync"
|
|
23
25
|
"unicode"
|
|
24
26
|
|
|
25
27
|
"github.com/microsoft/typescript-go/shim/ast"
|
|
@@ -195,51 +197,95 @@ func applyRewrites(outputName, text string, rs *RewriteSet, cursors map[string]i
|
|
|
195
197
|
return out, nil
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
// findSourceForOutput recovers which registered source file produced a given
|
|
201
|
+
// emitted output, using the source paths in rs.byPath as the universe.
|
|
202
|
+
//
|
|
203
|
+
// The match is anchored on the source's path relative to the common directory
|
|
204
|
+
// shared by all registered sources. The output's stem must end with that exact
|
|
205
|
+
// relative path (with a leading "/" boundary unless the source sits at the
|
|
206
|
+
// common directory root). This is stricter than a generic suffix match: a
|
|
207
|
+
// barrel file like `lib/api/x/index.js` will not accidentally collide with an
|
|
208
|
+
// unrelated `src/.../y/index.ts` that happens to share the basename. The bug
|
|
209
|
+
// surfaced when typia ran across shopping-backend's nestia-generated barrel
|
|
210
|
+
// files; the looser match steered the rewriter at the wrong source and threw
|
|
211
|
+
// `driver: could not locate typia.random(…) call in …`.
|
|
212
|
+
//
|
|
213
|
+
// Ambiguous matches (two or more registered sources with the same tail) return
|
|
214
|
+
// no match so the caller treats the output as having no rewrites.
|
|
198
215
|
func findSourceForOutput(outputName string, rs *RewriteSet) (string, bool) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
208
227
|
}
|
|
228
|
+
if outStem == tail || strings.HasSuffix(outStem, "/"+tail) {
|
|
229
|
+
matched = srcPath
|
|
230
|
+
hits++
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if hits != 1 {
|
|
234
|
+
return "", false
|
|
209
235
|
}
|
|
210
|
-
return
|
|
236
|
+
return matched, true
|
|
211
237
|
}
|
|
212
238
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
239
|
+
// commonSourceDirectoryFor returns the deepest directory (with trailing "/")
|
|
240
|
+
// shared by every source path in rs.byPath. When rs has a single source this is
|
|
241
|
+
// just that source's directory.
|
|
242
|
+
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 {
|
|
223
265
|
break
|
|
224
266
|
}
|
|
225
|
-
shared++
|
|
226
267
|
}
|
|
227
|
-
|
|
268
|
+
if len(common) == 0 {
|
|
269
|
+
return ""
|
|
270
|
+
}
|
|
271
|
+
return strings.Join(common, "/") + "/"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// sourceTail returns the source stem (extension dropped) without the common
|
|
275
|
+
// directory prefix. The leading "/" is also stripped so callers can match it as
|
|
276
|
+
// a suffix segment.
|
|
277
|
+
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, "/")
|
|
228
283
|
}
|
|
229
284
|
|
|
230
285
|
func spliceCall(text string, r Rewrite, searchFrom int) (string, int, bool, error) {
|
|
231
286
|
aliases := candidateRoots(r.RootName)
|
|
232
|
-
|
|
233
|
-
idx :=
|
|
234
|
-
needleLen := 0
|
|
235
|
-
for _, cand := range aliases {
|
|
236
|
-
candNeedle := cand + tail
|
|
237
|
-
if i := indexAtCallStart(text, candNeedle, searchFrom); i >= 0 {
|
|
238
|
-
idx = i
|
|
239
|
-
needleLen = len(candNeedle) - 1
|
|
240
|
-
break
|
|
241
|
-
}
|
|
242
|
-
}
|
|
287
|
+
pattern := callRegexFor(aliases, r.Namespaces, r.Method)
|
|
288
|
+
idx, needleLen := findCallMatch(text, pattern, searchFrom)
|
|
243
289
|
if idx < 0 {
|
|
244
290
|
return text, searchFrom, false, nil
|
|
245
291
|
}
|
|
@@ -256,6 +302,75 @@ func spliceCall(text string, r Rewrite, searchFrom int) (string, int, bool, erro
|
|
|
256
302
|
return replaced, idx + len(r.Replacement), true, nil
|
|
257
303
|
}
|
|
258
304
|
|
|
305
|
+
// callRegexFor compiles the loose-match needle pattern used by spliceCall.
|
|
306
|
+
//
|
|
307
|
+
// tsgo's emitter preserves source line breaks in property-access chains, so a
|
|
308
|
+
// source-side `typia.misc\n .literals<T>()` lands in the output as
|
|
309
|
+
// `typia_1.default.misc\n .literals()`. A literal needle would miss it; the
|
|
310
|
+
// pattern instead allows any whitespace (spaces, tabs, newlines) between
|
|
311
|
+
// segments, around the trailing dot before the method, and before the opening
|
|
312
|
+
// paren. Group 1 captures the trailing `(` so callers can compute the call
|
|
313
|
+
// site's text length precisely (regexes can't return per-byte segment widths
|
|
314
|
+
// otherwise).
|
|
315
|
+
//
|
|
316
|
+
// Results are cached by candidate-aliases × namespaces × method because the
|
|
317
|
+
// same rewrite descriptor is re-checked once per emitted file in incremental
|
|
318
|
+
// watch builds.
|
|
319
|
+
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
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
var callRegexCache sync.Map
|
|
347
|
+
|
|
348
|
+
// findCallMatch scans `text` from `searchFrom` for the next call expression
|
|
349
|
+
// matched by the loose-match `pattern`, applying the same "must start outside
|
|
350
|
+
// an identifier" rule as the old literal indexAtCallStart so generated locals
|
|
351
|
+
// like `mytypia.foo(` don't shadow `typia.foo(`. Returns the start byte of the
|
|
352
|
+
// match and the length up to (but not including) the captured `(`.
|
|
353
|
+
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
|
|
372
|
+
}
|
|
373
|
+
|
|
259
374
|
func candidateRoots(root string) []string {
|
|
260
375
|
return []string{
|
|
261
376
|
root,
|
|
@@ -281,29 +396,6 @@ func needleTail(r Rewrite) string {
|
|
|
281
396
|
return "." + strings.Join(r.Namespaces, ".") + "." + r.Method + "("
|
|
282
397
|
}
|
|
283
398
|
|
|
284
|
-
func indexAtCallStart(text, needle string, searchFrom int) int {
|
|
285
|
-
start := searchFrom
|
|
286
|
-
if start < 0 {
|
|
287
|
-
start = 0
|
|
288
|
-
}
|
|
289
|
-
for {
|
|
290
|
-
hit := strings.Index(text[start:], needle)
|
|
291
|
-
if hit < 0 {
|
|
292
|
-
return -1
|
|
293
|
-
}
|
|
294
|
-
pos := start + hit
|
|
295
|
-
if pos == 0 {
|
|
296
|
-
return pos
|
|
297
|
-
}
|
|
298
|
-
prev := rune(text[pos-1])
|
|
299
|
-
if isIdentifierPart(prev) {
|
|
300
|
-
start = pos + 1
|
|
301
|
-
continue
|
|
302
|
-
}
|
|
303
|
-
return pos
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
399
|
func isIdentifierPart(r rune) bool {
|
|
308
400
|
return r == '_' || r == '$' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
|
309
401
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ttsc",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "General-purpose TypeScript-Go compiler, plugin host, and runtime.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"rimraf": "^6.1.2"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@ttsc/linux-x64": "0.12.
|
|
36
|
-
"@ttsc/linux-
|
|
37
|
-
"@ttsc/
|
|
38
|
-
"@ttsc/
|
|
39
|
-
"@ttsc/
|
|
40
|
-
"@ttsc/
|
|
41
|
-
"@ttsc/win32-arm64": "0.12.
|
|
35
|
+
"@ttsc/linux-x64": "0.12.2",
|
|
36
|
+
"@ttsc/linux-arm": "0.12.2",
|
|
37
|
+
"@ttsc/linux-arm64": "0.12.2",
|
|
38
|
+
"@ttsc/darwin-arm64": "0.12.2",
|
|
39
|
+
"@ttsc/win32-x64": "0.12.2",
|
|
40
|
+
"@ttsc/darwin-x64": "0.12.2",
|
|
41
|
+
"@ttsc/win32-arm64": "0.12.2"
|
|
42
42
|
},
|
|
43
43
|
"sideEffects": false,
|
|
44
44
|
"files": [
|