ttsc 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +39 -68
  2. package/cmd/ttsc/api_compile.go +109 -115
  3. package/cmd/ttsc/api_transform.go +54 -51
  4. package/cmd/ttsc/main.go +103 -121
  5. package/driver/program.go +30 -2
  6. package/driver/rewrite.go +126 -9
  7. package/go.mod +2 -0
  8. package/lib/TtscCompiler.js +28 -46
  9. package/lib/TtscCompiler.js.map +1 -1
  10. package/lib/compiler/internal/buildNativeCompiler.js +8 -38
  11. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  12. package/lib/compiler/internal/compileProjectInMemory.js +36 -63
  13. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  14. package/lib/compiler/internal/project/readProjectConfig.js +108 -60
  15. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  16. package/lib/compiler/internal/project/resolveProjectConfig.js +15 -45
  17. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  18. package/lib/compiler/internal/resolveBinary.js +10 -40
  19. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  20. package/lib/compiler/internal/resolveEmittedJavaScript.js +21 -48
  21. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  22. package/lib/compiler/internal/resolveTsgo.js +14 -44
  23. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  24. package/lib/compiler/internal/runBuild.d.ts +7 -0
  25. package/lib/compiler/internal/runBuild.js +47 -66
  26. package/lib/compiler/internal/runBuild.js.map +1 -1
  27. package/lib/compiler/internal/runSingleFileEmit.js +18 -48
  28. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  29. package/lib/compiler/internal/transformProjectInMemory.js +41 -75
  30. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  31. package/lib/launcher/internal/getCompilerVersionText.js +7 -37
  32. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  33. package/lib/launcher/internal/prepareExecution.js +48 -69
  34. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  35. package/lib/launcher/internal/runTtsc.js +53 -64
  36. package/lib/launcher/internal/runTtsc.js.map +1 -1
  37. package/lib/launcher/internal/runTtsx.js +328 -57
  38. package/lib/launcher/internal/runTtsx.js.map +1 -1
  39. package/lib/plugin/internal/buildSourcePlugin.d.ts +8 -0
  40. package/lib/plugin/internal/buildSourcePlugin.js +287 -139
  41. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  42. package/lib/plugin/internal/loadProjectPlugins.d.ts +2 -0
  43. package/lib/plugin/internal/loadProjectPlugins.js +197 -9
  44. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  45. package/lib/structures/ITtscCompilerContext.d.ts +16 -3
  46. package/lib/structures/ITtscCompilerResult.d.ts +4 -0
  47. package/lib/structures/ITtscCompilerTransformation.d.ts +2 -0
  48. package/lib/structures/ITtscPluginFactoryContext.d.ts +6 -3
  49. package/lib/structures/ITtscProjectPluginConfig.d.ts +8 -7
  50. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  51. package/lib/structures/internal/ITtscProjectLocatorOptions.d.ts +2 -0
  52. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -0
  53. package/package.json +9 -14
  54. package/shim/diagnosticwriter/lint.go +8 -0
  55. package/shim/printer/go.mod +5 -0
  56. package/shim/printer/shim.go +23 -0
  57. package/src/TtscCompiler.ts +20 -5
  58. package/src/compiler/internal/buildNativeCompiler.ts +2 -2
  59. package/src/compiler/internal/compileProjectInMemory.ts +21 -16
  60. package/src/compiler/internal/project/readProjectConfig.ts +134 -35
  61. package/src/compiler/internal/project/resolveProjectConfig.ts +2 -2
  62. package/src/compiler/internal/resolveBinary.ts +2 -2
  63. package/src/compiler/internal/resolveEmittedJavaScript.ts +11 -7
  64. package/src/compiler/internal/resolveTsgo.ts +2 -2
  65. package/src/compiler/internal/runBuild.ts +34 -21
  66. package/src/compiler/internal/runSingleFileEmit.ts +3 -3
  67. package/src/compiler/internal/transformProjectInMemory.ts +37 -37
  68. package/src/launcher/internal/getCompilerVersionText.ts +2 -2
  69. package/src/launcher/internal/prepareExecution.ts +14 -4
  70. package/src/launcher/internal/runTtsc.ts +30 -4
  71. package/src/launcher/internal/runTtsx.ts +377 -26
  72. package/src/plugin/internal/buildSourcePlugin.ts +321 -50
  73. package/src/plugin/internal/loadProjectPlugins.ts +266 -10
  74. package/src/structures/ITtscCompilerContext.ts +17 -3
  75. package/src/structures/ITtscCompilerResult.ts +5 -0
  76. package/src/structures/ITtscCompilerTransformation.ts +3 -0
  77. package/src/structures/ITtscPluginFactoryContext.ts +6 -3
  78. package/src/structures/ITtscProjectPluginConfig.ts +8 -7
  79. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  80. package/src/structures/internal/ITtscProjectLocatorOptions.ts +2 -0
  81. package/src/structures/internal/TtscCommonOptions.ts +2 -0
  82. package/utility/host.go +622 -20
  83. package/cmd/ttsc/main_test.go +0 -288
  84. package/test/compiler.test.ts +0 -422
  85. package/test/platform.test.ts +0 -12
  86. package/test/project.test.ts +0 -197
  87. package/test/source-build.test.ts +0 -45
  88. package/test/tsgo.test.ts +0 -69
package/utility/host.go CHANGED
@@ -5,6 +5,7 @@ import (
5
5
  "flag"
6
6
  "fmt"
7
7
  "os"
8
+ "os/exec"
8
9
  "path/filepath"
9
10
  "sort"
10
11
  "strings"
@@ -12,6 +13,7 @@ import (
12
13
  shimast "github.com/microsoft/typescript-go/shim/ast"
13
14
  shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
14
15
  shimcore "github.com/microsoft/typescript-go/shim/core"
16
+ shimprinter "github.com/microsoft/typescript-go/shim/printer"
15
17
 
16
18
  "github.com/samchon/ttsc/packages/ttsc/driver"
17
19
  )
@@ -43,6 +45,22 @@ type transformResult struct {
43
45
  TypeScript map[string]string `json:"typescript"`
44
46
  }
45
47
 
48
+ // RunCheck validates the project and first-party utility plugin configuration
49
+ // without emitting output.
50
+ func RunCheck(args []string) int {
51
+ opts, ok := parseHostOptions("check", args)
52
+ if !ok {
53
+ return 2
54
+ }
55
+ opts.noEmit = true
56
+ prog, _, _, _, ok := loadUtilityProgram(opts)
57
+ if !ok {
58
+ return 2
59
+ }
60
+ defer prog.Close()
61
+ return 0
62
+ }
63
+
46
64
  // RunBuild hosts first-party utility transform plugins inside one compiler emit.
47
65
  func RunBuild(args []string) int {
48
66
  opts, ok := parseHostOptions("build", args)
@@ -76,6 +94,9 @@ func RunBuild(args []string) int {
76
94
  for _, d := range eDiags {
77
95
  fmt.Fprintln(os.Stderr, " -", d.String())
78
96
  }
97
+ if driver.CountErrors(eDiags) > 0 {
98
+ return 2
99
+ }
79
100
  if res != nil && !opts.quiet {
80
101
  fmt.Fprintf(os.Stdout, "// ttsc utility: emitted=%d files\n", len(res.EmittedFiles))
81
102
  }
@@ -88,14 +109,19 @@ func RunTransform(args []string) int {
88
109
  if !ok {
89
110
  return 2
90
111
  }
91
- prog, _, _, _, ok := loadUtilityProgram(opts)
112
+ prog, _, _, sourcePreamble, ok := loadUtilityProgram(opts)
92
113
  if !ok {
93
114
  return 2
94
115
  }
95
116
  defer prog.Close()
117
+ printer := shimprinter.NewPrinter(shimprinter.PrinterOptions{}, shimprinter.PrintHandlers{}, nil)
96
118
  out := transformResult{TypeScript: map[string]string{}}
97
119
  for _, file := range prog.SourceFiles() {
98
- out.TypeScript[apiOutputKey(opts.cwd, file.FileName())] = file.Text()
120
+ text := shimprinter.EmitSourceFile(printer, file)
121
+ if sourcePreamble != "" && !shouldRemoveComments(prog) && !strings.Contains(text, sourcePreamble) {
122
+ text = driver.ApplySourcePreamble(text, sourcePreamble)
123
+ }
124
+ out.TypeScript[apiOutputKey(opts.cwd, file.FileName())] = text
99
125
  }
100
126
  data, err := json.Marshal(out)
101
127
  if err != nil {
@@ -117,7 +143,11 @@ func parseHostOptions(command string, args []string) (hostOptions, bool) {
117
143
  quiet := fs.Bool("quiet", true, "suppress summary")
118
144
  tsconfig := fs.String("tsconfig", "tsconfig.json", "project tsconfig")
119
145
  verbose := fs.Bool("verbose", false, "print summary")
120
- if err := fs.Parse(args); err != nil {
146
+ if err := fs.Parse(filterHostArgs(args)); err != nil {
147
+ return hostOptions{}, false
148
+ }
149
+ if *emit && *noEmit {
150
+ fmt.Fprintln(os.Stderr, "ttsc utility: --emit and --noEmit are mutually exclusive")
121
151
  return hostOptions{}, false
122
152
  }
123
153
  resolvedCwd := *cwd
@@ -149,13 +179,57 @@ func parseHostOptions(command string, args []string) (hostOptions, bool) {
149
179
  }, true
150
180
  }
151
181
 
182
+ func filterHostArgs(args []string) []string {
183
+ known := map[string]bool{
184
+ "cwd": true,
185
+ "emit": false,
186
+ "noEmit": false,
187
+ "outDir": true,
188
+ "plugins-json": true,
189
+ "quiet": false,
190
+ "tsconfig": true,
191
+ "verbose": false,
192
+ }
193
+ filtered := make([]string, 0, len(args))
194
+ for i := 0; i < len(args); i++ {
195
+ current := args[i]
196
+ if current == "--" {
197
+ break
198
+ }
199
+ if !strings.HasPrefix(current, "--") {
200
+ filtered = append(filtered, current)
201
+ continue
202
+ }
203
+ name, hasInlineValue := flagName(current)
204
+ takesValue, ok := known[name]
205
+ if ok {
206
+ filtered = append(filtered, current)
207
+ if takesValue && !hasInlineValue && i+1 < len(args) {
208
+ i++
209
+ filtered = append(filtered, args[i])
210
+ }
211
+ continue
212
+ }
213
+ if !hasInlineValue && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
214
+ i++
215
+ }
216
+ }
217
+ return filtered
218
+ }
219
+
220
+ func flagName(arg string) (string, bool) {
221
+ name := strings.TrimPrefix(arg, "--")
222
+ before, _, found := strings.Cut(name, "=")
223
+ return before, found
224
+ }
225
+
152
226
  func loadUtilityProgram(opts hostOptions) (*driver.Program, []pluginEntry, transformState, string, bool) {
153
227
  entries, err := parsePluginEntries(opts.pluginsJSON)
154
228
  if err != nil {
155
229
  fmt.Fprintln(os.Stderr, err)
156
230
  return nil, nil, transformState{}, "", false
157
231
  }
158
- sourcePreamble, err := prepareSourcePreamble(entries)
232
+ sourcePreamble, err := prepareSourcePreamble(entries, opts.cwd, opts.tsconfig)
159
233
  if err != nil {
160
234
  fmt.Fprintln(os.Stderr, err)
161
235
  return nil, nil, transformState{}, "", false
@@ -172,19 +246,25 @@ func loadUtilityProgram(opts hostOptions) (*driver.Program, []pluginEntry, trans
172
246
  }
173
247
  if len(diags) > 0 {
174
248
  driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
249
+ if prog != nil {
250
+ _ = prog.Close()
251
+ }
175
252
  return nil, nil, transformState{}, "", false
176
253
  }
177
254
  if diags := prog.Diagnostics(); len(diags) > 0 {
178
255
  driver.WritePrettyDiagnostics(os.Stderr, diags, opts.cwd)
256
+ _ = prog.Close()
179
257
  return nil, nil, transformState{}, "", false
180
258
  }
181
259
  state, err := prepareTransforms(prog, entries)
182
260
  if err != nil {
183
261
  fmt.Fprintln(os.Stderr, err)
262
+ _ = prog.Close()
184
263
  return nil, nil, transformState{}, "", false
185
264
  }
186
265
  if err := applySourceTransforms(prog, state); err != nil {
187
266
  fmt.Fprintln(os.Stderr, err)
267
+ _ = prog.Close()
188
268
  return nil, nil, transformState{}, "", false
189
269
  }
190
270
  return prog, entries, state, sourcePreamble, true
@@ -218,13 +298,13 @@ func prepareTransforms(prog *driver.Program, entries []pluginEntry) (transformSt
218
298
  return state, nil
219
299
  }
220
300
 
221
- func prepareSourcePreamble(entries []pluginEntry) (string, error) {
301
+ func prepareSourcePreamble(entries []pluginEntry, cwd, tsconfigPath string) (string, error) {
222
302
  var preamble strings.Builder
223
303
  for _, entry := range entries {
224
304
  if entry.Name != "@ttsc/banner" {
225
305
  continue
226
306
  }
227
- banner, err := parseBanner(entry.Config)
307
+ banner, err := parseBanner(entry.Config, cwd, tsconfigPath)
228
308
  if err != nil {
229
309
  return "", err
230
310
  }
@@ -239,7 +319,7 @@ func makeSourcePreambleWriteFile(prog *driver.Program, sourcePreamble string) sh
239
319
  }
240
320
  return func(fileName, text string, _ *shimcompiler.WriteFileData) error {
241
321
  if shouldEnsureSourcePreamble(fileName, text, sourcePreamble) {
242
- text = sourcePreamble + text
322
+ text = driver.ApplySourcePreamble(text, sourcePreamble)
243
323
  }
244
324
  return driver.DefaultWriteFile(fileName, text)
245
325
  }
@@ -280,20 +360,20 @@ func applySourceTransforms(prog *driver.Program, state transformState) error {
280
360
 
281
361
  func apiOutputKey(cwd, fileName string) string {
282
362
  rel, err := filepath.Rel(cwd, fileName)
283
- if err != nil || strings.HasPrefix(rel, "..") {
363
+ if err != nil || isOutsideRelativePath(rel) {
284
364
  return filepath.ToSlash(fileName)
285
365
  }
286
366
  return filepath.ToSlash(rel)
287
367
  }
288
368
 
289
- func parseBanner(config map[string]any) (string, error) {
290
- raw, ok := config["banner"]
291
- if !ok {
292
- return "", fmt.Errorf("@ttsc/banner: \"banner\" must be a non-empty string")
293
- }
294
- text, ok := raw.(string)
295
- if !ok || strings.TrimSpace(text) == "" {
296
- return "", fmt.Errorf("@ttsc/banner: \"banner\" must be a non-empty string")
369
+ func isOutsideRelativePath(rel string) bool {
370
+ return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
371
+ }
372
+
373
+ func parseBanner(config map[string]any, cwd, tsconfigPath string) (string, error) {
374
+ text, err := resolveBannerText(config, cwd, tsconfigPath)
375
+ if err != nil {
376
+ return "", err
297
377
  }
298
378
  lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
299
379
  for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
@@ -316,6 +396,437 @@ func parseBanner(config map[string]any) (string, error) {
316
396
  return b.String(), nil
317
397
  }
318
398
 
399
+ func resolveBannerText(config map[string]any, cwd, tsconfigPath string) (string, error) {
400
+ if text, ok, err := bannerTextFromConfigValue(config["text"], `"text"`); ok || err != nil {
401
+ return text, err
402
+ }
403
+ if rawConfigPath, ok := config["config"]; ok {
404
+ configPath, ok := rawConfigPath.(string)
405
+ if !ok || strings.TrimSpace(configPath) == "" {
406
+ return "", fmt.Errorf("@ttsc/banner: \"config\" must be a non-empty string path")
407
+ }
408
+ location := resolveBannerConfigPath(configPath, cwd, tsconfigPath)
409
+ raw, err := loadBannerConfigFile(location)
410
+ if err != nil {
411
+ return "", err
412
+ }
413
+ text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
414
+ if err != nil {
415
+ return "", err
416
+ }
417
+ if !ok {
418
+ return "", fmt.Errorf("@ttsc/banner: %s must export a non-empty string or an object with a non-empty \"text\" string", location)
419
+ }
420
+ return text, nil
421
+ }
422
+ location, err := findBannerConfigFile(cwd, tsconfigPath)
423
+ if err != nil {
424
+ return "", err
425
+ }
426
+ if location == "" {
427
+ return "", fmt.Errorf("@ttsc/banner: \"text\" must be a non-empty string or a banner.config.{js,cjs,mjs,ts,mts,cts} file must exist")
428
+ }
429
+ raw, err := loadBannerConfigFile(location)
430
+ if err != nil {
431
+ return "", err
432
+ }
433
+ text, ok, err := bannerTextFromConfigValue(raw, filepath.Base(location))
434
+ if err != nil {
435
+ return "", err
436
+ }
437
+ if !ok {
438
+ return "", fmt.Errorf("@ttsc/banner: %s must export a non-empty string or an object with a non-empty \"text\" string", location)
439
+ }
440
+ return text, nil
441
+ }
442
+
443
+ func bannerTextFromConfigValue(raw any, label string) (string, bool, error) {
444
+ if raw == nil {
445
+ return "", false, nil
446
+ }
447
+ text, ok := raw.(string)
448
+ if ok {
449
+ if strings.TrimSpace(text) == "" {
450
+ return "", true, fmt.Errorf("@ttsc/banner: %s must be a non-empty string", label)
451
+ }
452
+ return text, true, nil
453
+ }
454
+ object, ok := raw.(map[string]any)
455
+ if !ok {
456
+ return "", true, fmt.Errorf("@ttsc/banner: %s must be a string or an object with a non-empty \"text\" string", label)
457
+ }
458
+ rawText, ok := object["text"]
459
+ if !ok {
460
+ return "", false, nil
461
+ }
462
+ text, ok = rawText.(string)
463
+ if !ok || strings.TrimSpace(text) == "" {
464
+ return "", true, fmt.Errorf("@ttsc/banner: %s.text must be a non-empty string", label)
465
+ }
466
+ return text, true, nil
467
+ }
468
+
469
+ func findBannerConfigFile(cwd, tsconfigPath string) (string, error) {
470
+ dir := discoveryConfigBaseDir(cwd, tsconfigPath)
471
+ for {
472
+ matches := make([]string, 0, 1)
473
+ for _, name := range []string{
474
+ "banner.config.js",
475
+ "banner.config.cjs",
476
+ "banner.config.mjs",
477
+ "banner.config.ts",
478
+ "banner.config.cts",
479
+ "banner.config.mts",
480
+ } {
481
+ candidate := filepath.Join(dir, name)
482
+ if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
483
+ matches = append(matches, candidate)
484
+ }
485
+ }
486
+ if len(matches) > 1 {
487
+ return "", fmt.Errorf("@ttsc/banner: multiple banner.config.* files found in %s", dir)
488
+ }
489
+ if len(matches) == 1 {
490
+ return matches[0], nil
491
+ }
492
+ parent := filepath.Dir(dir)
493
+ if parent == dir {
494
+ return "", nil
495
+ }
496
+ dir = parent
497
+ }
498
+ }
499
+
500
+ func resolveBannerConfigPath(configPath, cwd, tsconfigPath string) string {
501
+ if filepath.IsAbs(configPath) {
502
+ return configPath
503
+ }
504
+ return filepath.Join(tsconfigBaseDir(cwd, tsconfigPath), configPath)
505
+ }
506
+
507
+ func tsconfigBaseDir(cwd, tsconfigPath string) string {
508
+ if tsconfigPath == "" {
509
+ return cwd
510
+ }
511
+ resolvedTsconfig := tsconfigPath
512
+ if !filepath.IsAbs(resolvedTsconfig) {
513
+ resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
514
+ }
515
+ return filepath.Dir(resolvedTsconfig)
516
+ }
517
+
518
+ func discoveryConfigBaseDir(cwd, tsconfigPath string) string {
519
+ if tsconfigPath != "" {
520
+ resolvedTsconfig := tsconfigPath
521
+ if !filepath.IsAbs(resolvedTsconfig) {
522
+ resolvedTsconfig = filepath.Join(cwd, resolvedTsconfig)
523
+ }
524
+ return filepath.Dir(resolvedTsconfig)
525
+ }
526
+ return cwd
527
+ }
528
+
529
+ func loadBannerConfigFile(location string) (any, error) {
530
+ if !isBannerConfigFileName(filepath.Base(location)) {
531
+ return nil, fmt.Errorf("@ttsc/banner: config file must be named banner.config.{js,cjs,mjs,ts,mts,cts}: %s", location)
532
+ }
533
+ ext := strings.ToLower(filepath.Ext(location))
534
+ switch ext {
535
+ case ".js", ".cjs", ".mjs":
536
+ return loadBannerScriptConfigFile(location)
537
+ case ".ts", ".cts", ".mts":
538
+ return loadBannerTypeScriptConfigFile(location)
539
+ default:
540
+ return nil, fmt.Errorf("@ttsc/banner: unsupported config file extension %q for %s", ext, location)
541
+ }
542
+ }
543
+
544
+ func isBannerConfigFileName(name string) bool {
545
+ switch name {
546
+ case "banner.config.js",
547
+ "banner.config.cjs",
548
+ "banner.config.mjs",
549
+ "banner.config.ts",
550
+ "banner.config.cts",
551
+ "banner.config.mts":
552
+ return true
553
+ default:
554
+ return false
555
+ }
556
+ }
557
+
558
+ func loadBannerScriptConfigFile(location string) (any, error) {
559
+ const script = `
560
+ const { pathToFileURL } = require("node:url");
561
+
562
+ (async () => {
563
+ const mod = await import(pathToFileURL(process.argv[1]).href);
564
+ const candidate = Object.prototype.hasOwnProperty.call(mod, "default") ? mod.default : mod;
565
+ const value = typeof candidate === "function" ? await candidate() : candidate;
566
+ process.stdout.write(JSON.stringify(toSerializableBanner(value)));
567
+ })().catch((error) => {
568
+ process.stderr.write(error && error.stack ? error.stack : String(error));
569
+ process.exit(1);
570
+ });
571
+
572
+ function toSerializableBanner(value) {
573
+ if (typeof value === "string") {
574
+ return value;
575
+ }
576
+ if (value !== null && typeof value === "object" && typeof value.text === "string") {
577
+ return { text: value.text };
578
+ }
579
+ throw new Error("config file must export a string or an object with a text string");
580
+ }
581
+ `
582
+ node := os.Getenv("TTSC_NODE_BINARY")
583
+ if node == "" {
584
+ node = "node"
585
+ }
586
+ cmd := exec.Command(node, "-e", script, location)
587
+ cmd.Env = nodeConfigLoaderEnv(location)
588
+ output, err := cmd.Output()
589
+ if err != nil {
590
+ stderr := ""
591
+ if exit, ok := err.(*exec.ExitError); ok {
592
+ stderr = strings.TrimSpace(string(exit.Stderr))
593
+ }
594
+ if stderr != "" {
595
+ return nil, fmt.Errorf("@ttsc/banner: load config file %s: %s", location, stderr)
596
+ }
597
+ return nil, fmt.Errorf("@ttsc/banner: load config file %s: %w", location, err)
598
+ }
599
+ var out any
600
+ if err := json.Unmarshal(output, &out); err != nil {
601
+ return nil, fmt.Errorf("@ttsc/banner: parse config file %s output: %w", location, err)
602
+ }
603
+ return out, nil
604
+ }
605
+
606
+ func loadBannerTypeScriptConfigFile(location string) (any, error) {
607
+ tempDir, err := os.MkdirTemp("", "ttsc-banner-config-")
608
+ if err != nil {
609
+ return nil, fmt.Errorf("@ttsc/banner: create config loader tempdir: %w", err)
610
+ }
611
+ defer os.RemoveAll(tempDir)
612
+
613
+ if err := linkNearestNodeModules(tempDir, filepath.Dir(location)); err != nil {
614
+ return nil, err
615
+ }
616
+
617
+ loader := filepath.Join(tempDir, "loader.mts")
618
+ tsconfig := filepath.Join(tempDir, "tsconfig.json")
619
+ importSpecifier, err := relativeImportSpecifier(tempDir, location)
620
+ if err != nil {
621
+ return nil, err
622
+ }
623
+ importLiteral, err := json.Marshal(importSpecifier)
624
+ if err != nil {
625
+ return nil, fmt.Errorf("@ttsc/banner: encode config import %s: %w", location, err)
626
+ }
627
+ if err := os.WriteFile(loader, []byte(bannerTypeScriptConfigLoaderSource(string(importLiteral))), 0o644); err != nil {
628
+ return nil, fmt.Errorf("@ttsc/banner: write config loader: %w", err)
629
+ }
630
+ if err := os.WriteFile(tsconfig, []byte(typeScriptConfigLoaderTsconfig(loader, location, tempDir)), 0o644); err != nil {
631
+ return nil, fmt.Errorf("@ttsc/banner: write config loader tsconfig: %w", err)
632
+ }
633
+
634
+ args := []string{
635
+ "--project", tsconfig,
636
+ "--cwd", tempDir,
637
+ "--cache-dir", filepath.Join(tempDir, "cache"),
638
+ }
639
+ if tsgo := os.Getenv("TTSC_TSGO_BINARY"); tsgo != "" {
640
+ args = append(args, "--binary", tsgo)
641
+ }
642
+ args = append(args, loader)
643
+
644
+ cmd := ttsxCommand(args...)
645
+ cmd.Env = nodeConfigLoaderEnv(location)
646
+ output, err := cmd.Output()
647
+ if err != nil {
648
+ stderr := ""
649
+ if exit, ok := err.(*exec.ExitError); ok {
650
+ stderr = strings.TrimSpace(string(exit.Stderr))
651
+ }
652
+ if stderr != "" {
653
+ return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %s", location, stderr)
654
+ }
655
+ return nil, fmt.Errorf("@ttsc/banner: load TypeScript config file %s: %w", location, err)
656
+ }
657
+ var out any
658
+ if err := json.Unmarshal(output, &out); err != nil {
659
+ return nil, fmt.Errorf("@ttsc/banner: parse TypeScript config file %s output: %w", location, err)
660
+ }
661
+ return out, nil
662
+ }
663
+
664
+ func relativeImportSpecifier(fromDir, location string) (string, error) {
665
+ relative, err := filepath.Rel(fromDir, location)
666
+ if err != nil {
667
+ return "", fmt.Errorf("@ttsc/banner: resolve relative config import %s: %w", location, err)
668
+ }
669
+ relative = filepath.ToSlash(relative)
670
+ if strings.HasPrefix(relative, "../") || strings.HasPrefix(relative, "./") {
671
+ return relative, nil
672
+ }
673
+ return "./" + relative, nil
674
+ }
675
+
676
+ func bannerTypeScriptConfigLoaderSource(importLiteral string) string {
677
+ return fmt.Sprintf(`import * as importedConfig from %s;
678
+
679
+ declare const process: {
680
+ stdout: { write(value: string): void };
681
+ stderr: { write(value: string): void };
682
+ exit(code?: number): never;
683
+ };
684
+
685
+ try {
686
+ const value = await resolveConfig(importedConfig);
687
+ process.stdout.write(JSON.stringify(toSerializableBanner(value)));
688
+ } catch (error) {
689
+ process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));
690
+ process.exit(1);
691
+ }
692
+
693
+ async function resolveConfig(value: unknown): Promise<unknown> {
694
+ let current = value;
695
+ for (let i = 0; i < 8; i++) {
696
+ if (isObject(current) && hasOwn(current, "default")) {
697
+ current = current.default;
698
+ continue;
699
+ }
700
+ break;
701
+ }
702
+ if (typeof current === "function") {
703
+ return await (current as () => unknown | Promise<unknown>)();
704
+ }
705
+ return current;
706
+ }
707
+
708
+ function isObject(value: unknown): value is Record<string, unknown> {
709
+ return value !== null && typeof value === "object";
710
+ }
711
+
712
+ function hasOwn(value: Record<string, unknown>, key: string): boolean {
713
+ return Object.prototype.hasOwnProperty.call(value, key);
714
+ }
715
+
716
+ function toSerializableBanner(value: unknown): unknown {
717
+ if (typeof value === "string") {
718
+ return value;
719
+ }
720
+ if (isObject(value) && typeof value.text === "string") {
721
+ return { text: value.text };
722
+ }
723
+ throw new Error("config file must export a string or an object with a text string");
724
+ }
725
+ `, importLiteral)
726
+ }
727
+
728
+ func typeScriptConfigLoaderTsconfig(loader, location, outDir string) string {
729
+ content := map[string]any{
730
+ "compilerOptions": map[string]any{
731
+ "allowImportingTsExtensions": true,
732
+ "module": "ESNext",
733
+ "moduleResolution": "bundler",
734
+ "outDir": filepath.ToSlash(filepath.Join(outDir, "out")),
735
+ "rewriteRelativeImportExtensions": true,
736
+ "rootDir": "/",
737
+ "skipLibCheck": true,
738
+ "strict": true,
739
+ "target": "ES2022",
740
+ },
741
+ "files": []string{
742
+ filepath.ToSlash(loader),
743
+ filepath.ToSlash(location),
744
+ },
745
+ }
746
+ body, err := json.MarshalIndent(content, "", " ")
747
+ if err != nil {
748
+ panic(err)
749
+ }
750
+ return string(body)
751
+ }
752
+
753
+ func ttsxCommand(args ...string) *exec.Cmd {
754
+ ttsx := os.Getenv("TTSC_TTSX_BINARY")
755
+ if ttsx == "" {
756
+ ttsx = "ttsx"
757
+ }
758
+ if shouldRunTtsxThroughNode(ttsx) {
759
+ node := os.Getenv("TTSC_NODE_BINARY")
760
+ if node == "" {
761
+ node = "node"
762
+ }
763
+ return exec.Command(node, append([]string{ttsx}, args...)...)
764
+ }
765
+ return exec.Command(ttsx, args...)
766
+ }
767
+
768
+ func shouldRunTtsxThroughNode(binary string) bool {
769
+ switch strings.ToLower(filepath.Ext(binary)) {
770
+ case ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts":
771
+ return true
772
+ default:
773
+ return false
774
+ }
775
+ }
776
+
777
+ func nodeConfigLoaderEnv(location string) []string {
778
+ env := os.Environ()
779
+ parts := make([]string, 0, 2)
780
+ if nodeModules := findNearestNodeModules(filepath.Dir(location)); nodeModules != "" {
781
+ parts = append(parts, nodeModules)
782
+ }
783
+ if existing := os.Getenv("NODE_PATH"); existing != "" {
784
+ parts = append(parts, existing)
785
+ }
786
+ if len(parts) == 0 {
787
+ return env
788
+ }
789
+ return setEnv(env, "NODE_PATH", strings.Join(parts, string(os.PathListSeparator)))
790
+ }
791
+
792
+ func linkNearestNodeModules(tempDir, sourceDir string) error {
793
+ nodeModules := findNearestNodeModules(sourceDir)
794
+ if nodeModules == "" {
795
+ return nil
796
+ }
797
+ link := filepath.Join(tempDir, "node_modules")
798
+ if err := os.Symlink(nodeModules, link); err != nil {
799
+ return fmt.Errorf("@ttsc/banner: link config node_modules %s: %w", nodeModules, err)
800
+ }
801
+ return nil
802
+ }
803
+
804
+ func findNearestNodeModules(start string) string {
805
+ dir := filepath.Clean(start)
806
+ for {
807
+ candidate := filepath.Join(dir, "node_modules")
808
+ if stat, err := os.Stat(candidate); err == nil && stat.IsDir() {
809
+ return candidate
810
+ }
811
+ parent := filepath.Dir(dir)
812
+ if parent == dir {
813
+ return ""
814
+ }
815
+ dir = parent
816
+ }
817
+ }
818
+
819
+ func setEnv(env []string, key, value string) []string {
820
+ prefix := key + "="
821
+ for i, entry := range env {
822
+ if strings.HasPrefix(entry, prefix) {
823
+ env[i] = prefix + value
824
+ return env
825
+ }
826
+ }
827
+ return append(env, prefix+value)
828
+ }
829
+
319
830
  func sanitizeJSDocLine(line string) string {
320
831
  return strings.ReplaceAll(line, "*/", "* /")
321
832
  }
@@ -402,9 +913,14 @@ func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
402
913
  if arg != nil && arg.Kind == shimast.KindLiteralType {
403
914
  visit(arg.AsLiteralTypeNode().Literal)
404
915
  }
916
+ case shimast.KindModuleDeclaration:
917
+ decl := node.AsModuleDeclaration()
918
+ if decl != nil {
919
+ visit(decl.Name())
920
+ }
405
921
  case shimast.KindCallExpression:
406
922
  call := node.AsCallExpression()
407
- if call.Expression != nil && call.Expression.Kind == shimast.KindImportKeyword && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
923
+ if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
408
924
  visit(call.Arguments.Nodes[0])
409
925
  }
410
926
  }
@@ -414,6 +930,20 @@ func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
414
930
  })
415
931
  }
416
932
 
933
+ func isModuleSpecifierCall(call *shimast.CallExpression) bool {
934
+ if call == nil || call.Expression == nil {
935
+ return false
936
+ }
937
+ switch call.Expression.Kind {
938
+ case shimast.KindImportKeyword:
939
+ return true
940
+ case shimast.KindIdentifier:
941
+ return call.Expression.Text() == "require"
942
+ default:
943
+ return false
944
+ }
945
+ }
946
+
417
947
  func (r *pathsRewriter) rewrite(fromSource string, specifier string) (string, bool) {
418
948
  if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
419
949
  return specifier, false
@@ -468,6 +998,11 @@ func (r *pathsRewriter) lookupSource(candidate string) (string, bool) {
468
998
  return source, true
469
999
  }
470
1000
  }
1001
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
1002
+ if source, ok := r.sourceFiles[normalizePath(filepath.Join(stem, "index"+ext))]; ok {
1003
+ return source, true
1004
+ }
1005
+ }
471
1006
  return "", false
472
1007
  }
473
1008
 
@@ -476,10 +1011,21 @@ func (r *pathsRewriter) outputPathForSource(source string) string {
476
1011
  return ""
477
1012
  }
478
1013
  rel, err := filepath.Rel(r.rootDir, source)
479
- if err != nil || strings.HasPrefix(rel, "..") {
1014
+ if err != nil || isOutsideRelativePath(rel) {
480
1015
  return ""
481
1016
  }
482
- return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, ".js")))
1017
+ return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, emittedJavaScriptExtension(rel))))
1018
+ }
1019
+
1020
+ func emittedJavaScriptExtension(source string) string {
1021
+ switch strings.ToLower(filepath.Ext(source)) {
1022
+ case ".mts":
1023
+ return ".mjs"
1024
+ case ".cts":
1025
+ return ".cjs"
1026
+ default:
1027
+ return ".js"
1028
+ }
483
1029
  }
484
1030
 
485
1031
  func matchPattern(pattern string, specifier string) (string, bool) {
@@ -557,6 +1103,14 @@ type callPattern struct {
557
1103
  }
558
1104
 
559
1105
  func parseStrip(config map[string]any) (*stripRewriter, error) {
1106
+ _, hasCalls := config["calls"]
1107
+ _, hasStatements := config["statements"]
1108
+ if !hasCalls && !hasStatements {
1109
+ config = map[string]any{
1110
+ "calls": []any{"console.log", "console.debug", "assert.*"},
1111
+ "statements": []any{"debugger"},
1112
+ }
1113
+ }
560
1114
  calls, err := stringArrayConfig(config, "calls")
561
1115
  if err != nil {
562
1116
  return nil, fmt.Errorf("@ttsc/strip: %w", err)
@@ -595,7 +1149,7 @@ func filterStatements(list *shimast.NodeList, strip *stripRewriter) {
595
1149
  if list == nil || len(list.Nodes) == 0 {
596
1150
  return
597
1151
  }
598
- out := list.Nodes[:0]
1152
+ out := make([]*shimast.Node, 0, len(list.Nodes))
599
1153
  for _, stmt := range list.Nodes {
600
1154
  if shouldStripStatement(stmt, strip) {
601
1155
  continue
@@ -610,6 +1164,7 @@ func filterChildStatements(node *shimast.Node, strip *stripRewriter) {
610
1164
  if node == nil {
611
1165
  return
612
1166
  }
1167
+ filterEmbeddedStatements(node, strip)
613
1168
  if node.CanHaveStatements() {
614
1169
  filterStatements(node.StatementList(), strip)
615
1170
  }
@@ -619,6 +1174,53 @@ func filterChildStatements(node *shimast.Node, strip *stripRewriter) {
619
1174
  })
620
1175
  }
621
1176
 
1177
+ func filterEmbeddedStatements(node *shimast.Node, strip *stripRewriter) {
1178
+ switch node.Kind {
1179
+ case shimast.KindIfStatement:
1180
+ stmt := node.AsIfStatement()
1181
+ stmt.ThenStatement = filterEmbeddedStatement(stmt.ThenStatement, strip)
1182
+ stmt.ElseStatement = filterEmbeddedStatement(stmt.ElseStatement, strip)
1183
+ case shimast.KindDoStatement:
1184
+ stmt := node.AsDoStatement()
1185
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1186
+ case shimast.KindWhileStatement:
1187
+ stmt := node.AsWhileStatement()
1188
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1189
+ case shimast.KindForStatement:
1190
+ stmt := node.AsForStatement()
1191
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1192
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
1193
+ stmt := node.AsForInOrOfStatement()
1194
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1195
+ case shimast.KindWithStatement:
1196
+ stmt := node.AsWithStatement()
1197
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1198
+ case shimast.KindLabeledStatement:
1199
+ stmt := node.AsLabeledStatement()
1200
+ stmt.Statement = filterEmbeddedStatement(stmt.Statement, strip)
1201
+ }
1202
+ }
1203
+
1204
+ func filterEmbeddedStatement(stmt *shimast.Statement, strip *stripRewriter) *shimast.Statement {
1205
+ if stmt == nil {
1206
+ return nil
1207
+ }
1208
+ if shouldStripStatement(stmt, strip) {
1209
+ return emptyStatement(stmt)
1210
+ }
1211
+ filterChildStatements(stmt, strip)
1212
+ return stmt
1213
+ }
1214
+
1215
+ func emptyStatement(original *shimast.Node) *shimast.Statement {
1216
+ empty := shimast.NewNodeFactory(shimast.NodeFactoryHooks{}).NewEmptyStatement()
1217
+ empty.Flags |= shimast.NodeFlagsSynthesized
1218
+ if original != nil {
1219
+ empty.Loc = original.Loc
1220
+ }
1221
+ return empty
1222
+ }
1223
+
622
1224
  func shouldStripStatement(node *shimast.Node, strip *stripRewriter) bool {
623
1225
  if node == nil {
624
1226
  return false