ttsc 0.12.3 → 0.13.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 (151) hide show
  1. package/cmd/platform/main.go +9 -0
  2. package/cmd/ttsc/api_compile.go +122 -101
  3. package/cmd/ttsc/api_compile_branches_test.go +55 -57
  4. package/cmd/ttsc/api_transform.go +67 -49
  5. package/cmd/ttsc/api_transform_branches_test.go +30 -32
  6. package/cmd/ttsc/build.go +141 -108
  7. package/cmd/ttsc/build_branches_test.go +95 -97
  8. package/cmd/ttsc/filter.go +70 -0
  9. package/cmd/ttsc/filter_strips_unknown_forwarded_flag_test.go +61 -0
  10. package/cmd/ttsc/flags_gen.go +30 -0
  11. package/cmd/ttsc/helpers_test.go +39 -28
  12. package/cmd/ttsc/main.go +111 -104
  13. package/cmd/ttsc/main_alias_branches_test.go +21 -23
  14. package/cmd/ttscserver/main.go +96 -90
  15. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +22 -22
  16. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +22 -22
  17. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +23 -23
  18. package/cmd/utility-host/main.go +30 -23
  19. package/driver/lsp.go +53 -7
  20. package/driver/plugins.go +88 -72
  21. package/driver/program.go +464 -340
  22. package/driver/rewrite.go +477 -380
  23. package/internal/lspserver/lsp_envelope.go +64 -64
  24. package/internal/lspserver/lsp_frame.go +60 -60
  25. package/internal/lspserver/lsp_plugin.go +57 -57
  26. package/internal/lspserver/lsp_proxy.go +265 -251
  27. package/internal/lspserver/lsp_server.go +155 -144
  28. package/lib/TtscCompiler.d.ts +7 -0
  29. package/lib/TtscCompiler.js +7 -0
  30. package/lib/TtscCompiler.js.map +1 -1
  31. package/lib/compiler/internal/buildNativeCompiler.d.ts +11 -1
  32. package/lib/compiler/internal/buildNativeCompiler.js +23 -3
  33. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  34. package/lib/compiler/internal/compileProjectInMemory.d.ts +9 -0
  35. package/lib/compiler/internal/compileProjectInMemory.js +36 -21
  36. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  37. package/lib/compiler/internal/paths.d.ts +27 -0
  38. package/lib/compiler/internal/paths.js +70 -0
  39. package/lib/compiler/internal/paths.js.map +1 -0
  40. package/lib/compiler/internal/project/readProjectConfig.d.ts +9 -1
  41. package/lib/compiler/internal/project/readProjectConfig.js +80 -1
  42. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  43. package/lib/compiler/internal/project/resolveProjectConfig.d.ts +13 -1
  44. package/lib/compiler/internal/project/resolveProjectConfig.js +42 -1
  45. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  46. package/lib/compiler/internal/resolveBinary.d.ts +9 -1
  47. package/lib/compiler/internal/resolveBinary.js +17 -1
  48. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  49. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +16 -1
  50. package/lib/compiler/internal/resolveEmittedJavaScript.js +42 -10
  51. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  52. package/lib/compiler/internal/resolveTsgo.d.ts +21 -1
  53. package/lib/compiler/internal/resolveTsgo.js +23 -1
  54. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  55. package/lib/compiler/internal/runBuild.d.ts +24 -4
  56. package/lib/compiler/internal/runBuild.js +355 -93
  57. package/lib/compiler/internal/runBuild.js.map +1 -1
  58. package/lib/compiler/internal/runSingleFileEmit.d.ts +10 -1
  59. package/lib/compiler/internal/runSingleFileEmit.js +14 -1
  60. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  61. package/lib/compiler/internal/sharedHostHelpers.d.ts +5 -0
  62. package/lib/compiler/internal/sharedHostHelpers.js +6 -2
  63. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  64. package/lib/compiler/internal/spawnNative.d.ts +21 -0
  65. package/lib/compiler/internal/spawnNative.js +59 -0
  66. package/lib/compiler/internal/spawnNative.js.map +1 -0
  67. package/lib/compiler/internal/transformProjectInMemory.d.ts +15 -1
  68. package/lib/compiler/internal/transformProjectInMemory.js +57 -61
  69. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  70. package/lib/flags/parser.d.ts +63 -0
  71. package/lib/flags/parser.js +254 -0
  72. package/lib/flags/parser.js.map +1 -0
  73. package/lib/flags/schema.d.ts +147 -0
  74. package/lib/flags/schema.js +405 -0
  75. package/lib/flags/schema.js.map +1 -0
  76. package/lib/launcher/internal/getCompilerVersionText.js +7 -0
  77. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  78. package/lib/launcher/internal/prepareExecution.js +35 -0
  79. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  80. package/lib/launcher/internal/resolveCacheDir.d.ts +9 -0
  81. package/lib/launcher/internal/resolveCacheDir.js +22 -0
  82. package/lib/launcher/internal/resolveCacheDir.js.map +1 -0
  83. package/lib/launcher/internal/resolveTtscserverBinary.js +2 -0
  84. package/lib/launcher/internal/resolveTtscserverBinary.js.map +1 -1
  85. package/lib/launcher/internal/runTtsc.d.ts +10 -0
  86. package/lib/launcher/internal/runTtsc.js +90 -153
  87. package/lib/launcher/internal/runTtsc.js.map +1 -1
  88. package/lib/launcher/internal/runTtscserver.js +10 -35
  89. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  90. package/lib/launcher/internal/runTtsx.d.ts +8 -0
  91. package/lib/launcher/internal/runTtsx.js +137 -79
  92. package/lib/launcher/internal/runTtsx.js.map +1 -1
  93. package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -0
  94. package/lib/plugin/internal/buildSourcePlugin.js +40 -17
  95. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  96. package/lib/plugin/internal/loadProjectPlugins.d.ts +26 -0
  97. package/lib/plugin/internal/loadProjectPlugins.js +90 -41
  98. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  99. package/lib/structures/ITtscCompilerTransformation.d.ts +15 -2
  100. package/lib/structures/ITtscPlugin.d.ts +24 -0
  101. package/lib/structures/ITtscPluginCapabilities.d.ts +29 -0
  102. package/lib/structures/ITtscPluginCapabilities.js +3 -0
  103. package/lib/structures/ITtscPluginCapabilities.js.map +1 -0
  104. package/lib/structures/index.d.ts +1 -0
  105. package/lib/structures/index.js +1 -0
  106. package/lib/structures/index.js.map +1 -1
  107. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
  108. package/lib/structures/internal/TtscCommonOptions.d.ts +19 -0
  109. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +0 -2
  110. package/package.json +9 -9
  111. package/shim/ast/shim.go +24 -0
  112. package/shim/checker/shim.go +35 -0
  113. package/shim/core/shim.go +17 -0
  114. package/shim/diagnosticwriter/shim.go +4 -0
  115. package/shim/lsp/shim.go +3 -3
  116. package/shim/printer/shim.go +20 -0
  117. package/src/TtscCompiler.ts +7 -0
  118. package/src/compiler/internal/buildNativeCompiler.ts +23 -3
  119. package/src/compiler/internal/compileProjectInMemory.ts +34 -25
  120. package/src/compiler/internal/paths.ts +68 -0
  121. package/src/compiler/internal/project/readProjectConfig.ts +86 -1
  122. package/src/compiler/internal/project/resolveProjectConfig.ts +44 -1
  123. package/src/compiler/internal/resolveBinary.ts +17 -1
  124. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -12
  125. package/src/compiler/internal/resolveTsgo.ts +29 -1
  126. package/src/compiler/internal/runBuild.ts +398 -112
  127. package/src/compiler/internal/runSingleFileEmit.ts +14 -1
  128. package/src/compiler/internal/sharedHostHelpers.ts +7 -7
  129. package/src/compiler/internal/spawnNative.ts +64 -0
  130. package/src/compiler/internal/transformProjectInMemory.ts +47 -67
  131. package/src/flags/parser.ts +367 -0
  132. package/src/flags/schema.ts +570 -0
  133. package/src/launcher/internal/getCompilerVersionText.ts +7 -0
  134. package/src/launcher/internal/prepareExecution.ts +35 -0
  135. package/src/launcher/internal/resolveCacheDir.ts +19 -0
  136. package/src/launcher/internal/resolveTtscserverBinary.ts +2 -0
  137. package/src/launcher/internal/runTtsc.ts +97 -143
  138. package/src/launcher/internal/runTtscserver.ts +9 -1
  139. package/src/launcher/internal/runTtsx.ts +149 -82
  140. package/src/plugin/internal/buildSourcePlugin.ts +40 -14
  141. package/src/plugin/internal/loadProjectPlugins.ts +116 -45
  142. package/src/structures/ITtscCompilerTransformation.ts +15 -2
  143. package/src/structures/ITtscPlugin.ts +26 -1
  144. package/src/structures/ITtscPluginCapabilities.ts +29 -0
  145. package/src/structures/index.ts +1 -0
  146. package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
  147. package/src/structures/internal/TtscCommonOptions.ts +19 -0
  148. package/src/structures/internal/TtscSingleFileEmitOptions.ts +0 -2
  149. package/tools/gen_shims/main.go +43 -8
  150. package/utility/flags_gen.go +30 -0
  151. package/utility/host.go +304 -245
@@ -2,9 +2,24 @@ import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
+ import {
6
+ getBoolean,
7
+ getNumber,
8
+ getString,
9
+ parseFlags,
10
+ } from "../../flags/parser";
5
11
  import { getCompilerVersionText } from "./getCompilerVersionText";
6
12
  import { prepareExecution } from "./prepareExecution";
7
-
13
+ import { resolveCacheDir } from "./resolveCacheDir";
14
+
15
+ /**
16
+ * CLI entry point for `ttsx`. Type-checks the owning project via tsgo, emits
17
+ * JavaScript to a PID-isolated temp directory, rewrites ESM specifiers when
18
+ * needed, and executes the compiled entry with the current Node.js runtime.
19
+ *
20
+ * @param argv - Command-line arguments (defaults to `process.argv.slice(2)`).
21
+ * @returns The child-process exit code, or `2` on a ttsx-level error.
22
+ */
8
23
  export function runTtsx(
9
24
  argv: readonly string[] = process.argv.slice(2),
10
25
  ): number {
@@ -39,8 +54,20 @@ function run(argv: readonly string[]): number {
39
54
  const prepared = prepareExecution(entry, {
40
55
  binary: parsed.binary,
41
56
  cacheDir: resolveCacheDir(cwd, parsed.cacheDir),
57
+ checkers: parsed.checkers,
42
58
  cwd,
59
+ passthrough: parsed.tsgoFlags,
60
+ // `--no-plugins` builds the entry's owning project with plugin
61
+ // discovery and loading disabled. ttsc's own config loaders use it
62
+ // when they evaluate a `*.config.ts` through ttsx: that build only
63
+ // needs to type-check and run the config file, so loading the host
64
+ // project's transform/check plugins (`@nestia/core`, `typia`, …)
65
+ // would be both wasteful and wrong — those plugins impose project
66
+ // requirements (e.g. `strict` mode) the ephemeral config-loader
67
+ // tsconfig deliberately does not satisfy.
68
+ plugins: parsed.noPlugins ? false : undefined,
43
69
  project: parsed.project,
70
+ singleThreaded: parsed.singleThreaded,
44
71
  });
45
72
  return runPreparedEntry(parsed, prepared, cwd);
46
73
  }
@@ -53,82 +80,108 @@ function formatError(error: unknown): string {
53
80
  }
54
81
 
55
82
  function parseCLI(argv: readonly string[]) {
56
- const preload: string[] = [];
57
- const passthroughIndex = argv.indexOf("--");
58
- const head =
59
- passthroughIndex === -1 ? [...argv] : [...argv.slice(0, passthroughIndex)];
60
- const passthrough =
61
- passthroughIndex === -1 ? [] : [...argv.slice(passthroughIndex + 1)];
62
-
63
- let binary: string | undefined;
64
- let cacheDir: string | undefined;
65
- let cwd: string | undefined;
66
- let entry: string | undefined;
67
- let project: string | undefined;
68
-
69
- while (head.length !== 0) {
70
- const current = head.shift()!;
71
- if (entry) {
72
- passthrough.push(current, ...head);
73
- break;
74
- }
75
- switch (current) {
76
- case "-h":
77
- case "--help":
78
- return "help";
79
- case "-v":
80
- case "--version":
81
- return "version";
82
- case "-P":
83
- case "--project":
84
- project = takeValue(current, head);
85
- break;
86
- case "--cwd":
87
- cwd = takeValue(current, head);
88
- break;
89
- case "--cache-dir":
90
- cacheDir = takeValue(current, head);
91
- break;
92
- case "-r":
93
- case "--require":
94
- preload.push(takeValue(current, head));
95
- break;
96
- case "--binary":
97
- binary = takeValue(current, head);
98
- break;
99
- default:
100
- if (current.startsWith("--project=")) {
101
- project = current.slice("--project=".length);
102
- } else if (current.startsWith("--cwd=")) {
103
- cwd = current.slice("--cwd=".length);
104
- } else if (current.startsWith("--cache-dir=")) {
105
- cacheDir = current.slice("--cache-dir=".length);
106
- } else if (current.startsWith("--binary=")) {
107
- binary = current.slice("--binary=".length);
108
- } else if (current.startsWith("-")) {
109
- throw new Error(`ttsx: unknown option ${current}`);
110
- } else {
111
- entry = current;
112
- }
113
- break;
114
- }
115
- }
83
+ // ttsx accepts ttsc-style flags plus its own `--no-plugins` / `--require`.
84
+ // The shared schema engine recognises both; the engine returns positional
85
+ // tokens (entry file + flag values that aren't `.ts`) and a passthrough
86
+ // list mirroring the pre-schema behaviour.
87
+ //
88
+ // Order pin: ttsx accepts `-P` as an alias for `--project`. The schema
89
+ // declares `-p` for ttsc; ttsx's lowercase shape would collide on `-p`
90
+ // → `--tsconfig`, so the legacy `-P` (uppercase) is treated as `--project`
91
+ // via a manual rewrite before the engine sees argv. We preserve the
92
+ // historical behaviour and emit a structural error otherwise.
93
+ const rewritten = argv.map((token) =>
94
+ token === "-P"
95
+ ? "--project"
96
+ : token.startsWith("-P=")
97
+ ? `--project=${token.slice("-P=".length)}`
98
+ : token,
99
+ );
100
+ // Terminal flags (--help / --version) short-circuit before parsing so
101
+ // ttsx prints help text even when the entry file is missing.
102
+ for (const token of rewritten) {
103
+ if (token === "-h" || token === "--help") return "help" as const;
104
+ if (token === "-v" || token === "--version") return "version" as const;
105
+ }
106
+ const result = parseFlags({
107
+ argv: rewritten,
108
+ errorPrefix: "ttsx:",
109
+ forwardAfterFirstPositional: true,
110
+ honorDoubleDashSeparator: true,
111
+ subcommand: "ttsx",
112
+ });
116
113
 
117
- if (!entry) {
114
+ const entry = result.positional.find(looksLikeEntryFile);
115
+ if (entry === undefined) {
118
116
  throw new Error("ttsx: entry file is required");
119
117
  }
118
+ // With `forwardAfterFirstPositional: true` the parser reports
119
+ // `result.positional` as just the entry, `result.passthrough` as flags
120
+ // arriving BEFORE the entry (tsgo-forwarded), and `result.tail` as every
121
+ // token AFTER the entry — those are the user program's argv (e.g. the
122
+ // `generate --input src/input` tail of `ttsx typia.ts generate
123
+ // --input src/input`) and MUST NOT reach tsgo. Anything in positional
124
+ // that is not the entry is a pre-entry flag value (e.g. `--target es2020`)
125
+ // that the parser stored positionally; forward those to tsgo with the
126
+ // rest of `passthrough`.
127
+ const preEntryValues: string[] = result.positional.filter(
128
+ (token) => token !== entry && !looksLikeEntryFile(token),
129
+ );
130
+ const postEntryArgs: string[] = [...result.tail];
131
+
132
+ const preload: string[] = [];
133
+ // `--require` accepts repeated values; the schema engine writes the
134
+ // LAST one into `values`, so reconstruct the full list by scanning the
135
+ // raw argv. Mirrors the legacy parser's `preload.push(takeValue(...))`
136
+ // behaviour.
137
+ //
138
+ // Stop the rescue scan at the first token that begins tail mode —
139
+ // either the entry file or the `--` separator. Without this guard,
140
+ // `ttsx entry.ts -r preload.cjs` would BOTH preload `preload.cjs` AND
141
+ // forward `-r preload.cjs` to the entry's argv, double-effecting the
142
+ // module load. The schema engine already routes post-entry tokens to
143
+ // `result.tail`; the rescue scan must respect the same boundary.
144
+ const scanEnd = rewritten.findIndex(
145
+ (token) => looksLikeEntryFile(token) || token === "--",
146
+ );
147
+ const scanLimit = scanEnd === -1 ? rewritten.length : scanEnd;
148
+ for (let i = 0; i < scanLimit; i += 1) {
149
+ const token = rewritten[i]!;
150
+ if (token === "-r" || token === "--require") {
151
+ const value = rewritten[i + 1];
152
+ if (value !== undefined && !value.startsWith("-")) {
153
+ preload.push(value);
154
+ i += 1;
155
+ }
156
+ } else if (token.startsWith("--require=")) {
157
+ preload.push(token.slice("--require=".length));
158
+ }
159
+ }
120
160
 
121
161
  return {
122
- binary,
123
- cacheDir,
124
- cwd,
162
+ binary: getString(result, "--binary"),
163
+ cacheDir: getString(result, "--cache-dir"),
164
+ checkers: getNumber(result, "--checkers"),
165
+ cwd: getString(result, "--cwd"),
125
166
  entry,
126
- passthrough,
167
+ noPlugins: getBoolean(result, "--no-plugins") === true,
168
+ passthrough: postEntryArgs,
127
169
  preload,
128
- project,
170
+ project: getString(result, "--tsconfig"),
171
+ singleThreaded: getBoolean(result, "--singleThreaded") === true,
172
+ tsgoFlags: [...result.passthrough, ...preEntryValues],
129
173
  };
130
174
  }
131
175
 
176
+ /**
177
+ * Report whether a bare CLI token is the TypeScript entry file rather than a
178
+ * forwarded flag's value. ttsx runs a TypeScript entrypoint, so only a token
179
+ * with a TypeScript source extension is treated as the entry.
180
+ */
181
+ function looksLikeEntryFile(token: string): boolean {
182
+ return [".ts", ".tsx", ".mts", ".cts"].some((ext) => token.endsWith(ext));
183
+ }
184
+
132
185
  function printHelp(): void {
133
186
  process.stdout.write(
134
187
  [
@@ -142,10 +195,16 @@ function printHelp(): void {
142
195
  " --cwd <dir> Resolve entry/project relative to this directory",
143
196
  " --cache-dir <dir> Override the runner and source-plugin cache root",
144
197
  " --binary <path> Use an explicit tsgo binary",
198
+ " --no-plugins Build the project without ttsc plugins",
145
199
  " -r, --require <module> Preload a module before the entrypoint",
200
+ " --singleThreaded Run TypeScript-Go single-threaded (one checker)",
201
+ " --checkers <n> Type-checker pool size (default: TypeScript-Go's)",
146
202
  " -h, --help Show this help",
147
203
  " -v, --version Print the runner version",
148
204
  "",
205
+ " Any other flag before the entry is forwarded to tsgo, so options like",
206
+ " --strict apply to the type-check (e.g. ttsx --strict src/index.ts).",
207
+ "",
149
208
  "Examples:",
150
209
  " ttsx src/index.ts",
151
210
  " ttsx --project tsconfig.json src/index.ts -- --port 3000",
@@ -172,21 +231,6 @@ function isRelativeSpecifier(specifier: string): boolean {
172
231
  );
173
232
  }
174
233
 
175
- function resolveCacheDir(cwd: string, cacheDir?: string): string | undefined {
176
- if (!cacheDir) {
177
- return undefined;
178
- }
179
- return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
180
- }
181
-
182
- function takeValue(flag: string, rest: string[]): string {
183
- const value = rest.shift();
184
- if (!value) {
185
- throw new Error(`ttsx: ${flag} requires a value`);
186
- }
187
- return value;
188
- }
189
-
190
234
  function runPreparedEntry(
191
235
  parsed: Exclude<ReturnType<typeof parseCLI>, "help" | "version">,
192
236
  execution: ReturnType<typeof prepareExecution>,
@@ -222,6 +266,12 @@ function runPreparedEntry(
222
266
  return result.status ?? 1;
223
267
  }
224
268
 
269
+ /**
270
+ * Walk every `.js`/`.mjs`/`.cjs` file in `root` and rewrite bare relative
271
+ * specifiers (e.g. `"./foo"`) to include the resolved file extension so Node
272
+ * can load them with `--input-type=module`. Files that did not change are not
273
+ * written back.
274
+ */
225
275
  function rewriteEsmSpecifiers(root: string): void {
226
276
  const stack = [root];
227
277
  while (stack.length !== 0) {
@@ -431,26 +481,37 @@ function findTemplateExpressionEnd(
431
481
  return null;
432
482
  }
433
483
 
484
+ /**
485
+ * Append or fix a file extension on a relative specifier so that Node's ESM
486
+ * loader can resolve it. The resolution order mirrors Node's own algorithm:
487
+ * exact file match → directory index → `.js` fallback. Non-relative specifiers
488
+ * and already-extensioned paths are returned unchanged.
489
+ */
434
490
  function withResolvableExtension(fromFile: string, specifier: string): string {
435
491
  if (!specifier.startsWith(".")) {
492
+ // Bare specifiers (packages, builtins) need no rewriting.
436
493
  return specifier;
437
494
  }
438
495
  if (/\.(?:[cm]?js|json|node)$/i.test(specifier)) {
496
+ // Already has a concrete extension the loader understands.
439
497
  return specifier;
440
498
  }
441
499
  const [pathname, suffix = ""] = splitSpecifierSuffix(specifier);
442
500
  const fromDir = path.dirname(fromFile);
501
+ // 1. Try the specifier as a file path with each JS extension.
443
502
  for (const extension of [".js", ".mjs", ".cjs"]) {
444
503
  if (fs.existsSync(path.resolve(fromDir, pathname + extension))) {
445
504
  return pathname + extension + suffix;
446
505
  }
447
506
  }
507
+ // 2. Try interpreting it as a directory with an index file.
448
508
  const directory = pathname.replace(/\/+$/, "");
449
509
  for (const extension of [".js", ".mjs", ".cjs"]) {
450
510
  if (fs.existsSync(path.resolve(fromDir, directory, "index" + extension))) {
451
511
  return `${directory}/index${extension}${suffix}`;
452
512
  }
453
513
  }
514
+ // 3. Last resort: append `.js` and let Node surface the error if it's wrong.
454
515
  return `${specifier}.js`;
455
516
  }
456
517
 
@@ -553,6 +614,12 @@ function skipRegex(input: string, start: number): number {
553
614
  return input.length;
554
615
  }
555
616
 
617
+ /**
618
+ * Determine whether the `/` at `start` opens a regex literal rather than a
619
+ * division operator. This is a conservative approximation: check the preceding
620
+ * non-whitespace token — if it is an operator character or a keyword that must
621
+ * be followed by an expression, we assume regex.
622
+ */
556
623
  function looksLikeRegexStart(input: string, start: number): boolean {
557
624
  let i = start - 1;
558
625
  while (i >= 0 && /\s/.test(input[i]!)) {
@@ -5,6 +5,8 @@ import { createRequire } from "node:module";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
 
8
+ import { findNearestGoMod } from "../../compiler/internal/paths";
9
+
8
10
  const GO_MOD_SEARCH_MAX_DEPTH = 3;
9
11
  const TTSC_GO_MODULE_PATH = "github.com/samchon/ttsc/packages/ttsc";
10
12
  const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
@@ -397,20 +399,6 @@ function resolveSourceBuildTarget(opts: {
397
399
  };
398
400
  }
399
401
 
400
- function findNearestGoMod(from: string, maxDepth: number): string | null {
401
- let current = path.resolve(from);
402
- let depth = 0;
403
- while (true) {
404
- const candidate = path.join(current, "go.mod");
405
- if (fs.existsSync(candidate)) return candidate;
406
- if (depth >= maxDepth) return null;
407
- const parent = path.dirname(current);
408
- if (parent === current) return null;
409
- current = parent;
410
- depth += 1;
411
- }
412
- }
413
-
414
402
  function materializeScratchDir(source: string, scratch: string): void {
415
403
  fs.mkdirSync(scratch, { recursive: true });
416
404
  fs.cpSync(source, scratch, {
@@ -768,6 +756,19 @@ function walkForGoMod(dir: string, out: string[]): void {
768
756
  }
769
757
  }
770
758
 
759
+ /**
760
+ * Resolve the directory where compiled plugin binaries are cached.
761
+ *
762
+ * Priority order:
763
+ *
764
+ * 1. `cacheDir` option (resolved relative to `projectRoot`).
765
+ * 2. `TTSC_CACHE_DIR` environment variable.
766
+ * 3. Platform-specific global user cache (XDG / AppData / Library/Caches).
767
+ *
768
+ * When falling back to the global cache, a GC pass is triggered
769
+ * opportunistically to evict old or oversized entries before the cache is
770
+ * used.
771
+ */
771
772
  export function resolvePluginCacheRoot(
772
773
  projectRoot: string,
773
774
  cacheDir?: string,
@@ -783,6 +784,13 @@ export function resolvePluginCacheRoot(
783
784
  return root;
784
785
  }
785
786
 
787
+ /**
788
+ * Return the absolute path to the global plugin binary cache root.
789
+ *
790
+ * The path follows platform conventions (XDG on Linux, AppData on Windows,
791
+ * Library/Caches on macOS) and does not depend on project-local config. Used by
792
+ * `ttsc cache clean` to enumerate cache locations.
793
+ */
786
794
  export function resolveGlobalPluginCacheRoot(): string {
787
795
  return path.join(
788
796
  resolveUserCacheRoot(),
@@ -791,6 +799,13 @@ export function resolveGlobalPluginCacheRoot(): string {
791
799
  );
792
800
  }
793
801
 
802
+ /**
803
+ * Return all directories that `ttsc cache clean` should wipe for a project.
804
+ *
805
+ * Covers three locations where plugin binaries may accumulate: the global user
806
+ * cache, the project-local `node_modules/.ttsc` cache (legacy), and the
807
+ * project-local `.ttsc` cache.
808
+ */
794
809
  export function defaultPluginCacheCleanTargets(projectRoot: string): string[] {
795
810
  return [
796
811
  resolveGlobalPluginCacheRoot(),
@@ -878,6 +893,17 @@ function resolveGoCompiler(): string {
878
893
  return "go";
879
894
  }
880
895
 
896
+ /**
897
+ * Compute a deterministic SHA-256 cache key for a plugin build.
898
+ *
899
+ * The key covers every input that can produce a different binary: ttsc/tsgo
900
+ * versions, platform, entry package, Go compiler identity, Go build environment
901
+ * variables, overlay module sources, plugin source files, and contributor
902
+ * source files. Contributors are sorted by name so declaration order does not
903
+ * affect the key.
904
+ *
905
+ * Exposed for testing and for the `ttsc cache` CLI command.
906
+ */
881
907
  export function computeCacheKey(inputs: {
882
908
  contributors?: readonly ITtscBuildContributor[];
883
909
  dir: string;
@@ -2,9 +2,10 @@ import fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
 
5
+ import { findNearestGoMod } from "../../compiler/internal/paths";
5
6
  import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
6
- import type { ITtscPluginContributor } from "../../structures/ITtscPluginContributor";
7
7
  import type { ITtscPlugin } from "../../structures/ITtscPlugin";
8
+ import type { ITtscPluginContributor } from "../../structures/ITtscPluginContributor";
8
9
  import type { ITtscPluginFactoryContext } from "../../structures/ITtscPluginFactoryContext";
9
10
  import type { ITtscProjectPluginConfig } from "../../structures/ITtscProjectPluginConfig";
10
11
  import type { TtscPluginStage } from "../../structures/TtscPluginStage";
@@ -30,6 +31,24 @@ type PackageManifest = {
30
31
  ttsc?: unknown;
31
32
  };
32
33
 
34
+ /**
35
+ * Resolve, load, and build all native plugin sidecars for a TypeScript project.
36
+ *
37
+ * Reads the project config, discovers plugin entries (from tsconfig and package
38
+ * auto-discovery), validates and composes their descriptors, then invokes
39
+ * `buildSourcePlugin` to compile each Go source package into a cached binary.
40
+ * Returns the ordered set of loaded native plugins alongside the parsed project
41
+ * config.
42
+ *
43
+ * @param options.binary - Absolute path to the ttsc native helper binary.
44
+ * @param options.cacheDir - Override the plugin binary cache directory.
45
+ * @param options.cwd - Working directory for resolving relative paths.
46
+ * @param options.entries - Explicit plugin entries; `false` disables all
47
+ * plugins (skips both tsconfig entries and package auto-discovery).
48
+ * @param options.file - Path to the tsconfig/jsconfig file.
49
+ * @param options.projectRoot - Override the project root directory.
50
+ * @param options.tsconfig - Alias for `file`.
51
+ */
33
52
  export function loadProjectPlugins(options: {
34
53
  binary: string;
35
54
  cacheDir?: string;
@@ -85,21 +104,31 @@ export function loadProjectPlugins(options: {
85
104
  validatePluginSource(plugin);
86
105
  const contributors = validatePluginContributors(plugin);
87
106
  const source = resolvePluginSource(plugin.source, context.projectRoot);
88
- const kind = resolveNativeSourceKind(source, plugin, entries[index]!.config, index);
107
+ const kind = resolveNativeSourceKind(
108
+ source,
109
+ plugin,
110
+ entries[index]!.config,
111
+ index,
112
+ );
89
113
  if (kind === "linked" && stage !== "transform") {
90
114
  throw new Error(
91
115
  `ttsc: plugin "${pluginLabel(plugin, entries[index]!.config, index)}" source is a linked Go package, but only transform-stage plugins can be linked into a compiler host`,
92
116
  );
93
117
  }
94
118
  const linkedContributorName =
95
- kind === "linked" ? `linked_${String(index).padStart(6, "0")}` : undefined;
119
+ kind === "linked"
120
+ ? `linked_${String(index).padStart(6, "0")}`
121
+ : undefined;
96
122
  return {
123
+ capabilities: plugin.capabilities,
97
124
  contributors,
98
125
  config: entries[index]!.config,
99
126
  kind,
100
127
  label: pluginLabel(plugin, entries[index]!.config, index),
101
128
  linkedContributorName,
102
129
  name: plugin.name,
130
+ reportsTypeScriptDiagnostics:
131
+ plugin.reportsTypeScriptDiagnostics === true,
103
132
  source,
104
133
  stage,
105
134
  };
@@ -170,10 +199,12 @@ export function loadProjectPlugins(options: {
170
199
  }
171
200
  return {
172
201
  binary,
202
+ capabilities: record.capabilities,
173
203
  config: record.config,
174
204
  contributors: record.contributors,
175
205
  kind: record.kind,
176
206
  name: record.name,
207
+ reportsTypeScriptDiagnostics: record.reportsTypeScriptDiagnostics,
177
208
  source: record.source,
178
209
  stage: record.stage,
179
210
  };
@@ -261,6 +292,15 @@ function composePluginSources(
261
292
  return {
262
293
  ...plugin,
263
294
  source: aggregate.plugin.source,
295
+ contributors: aggregate.plugin.contributors,
296
+ // The composed plugin's runtime BINARY is the aggregate's binary,
297
+ // so the CLI surface (which flags the sidecar parses) is the
298
+ // aggregate's. Inherit `capabilities` from the aggregate so a
299
+ // capability the aggregate declares — e.g. threadingArgs — does
300
+ // not get silently dropped just because the composed entry's own
301
+ // descriptor omitted it. If the aggregate did not set capabilities
302
+ // we keep the composed plugin's own as a fallback.
303
+ capabilities: aggregate.plugin.capabilities ?? plugin.capabilities,
264
304
  };
265
305
  });
266
306
  }
@@ -276,6 +316,14 @@ function matchesPluginAlias(
276
316
  );
277
317
  }
278
318
 
319
+ /**
320
+ * Return `true` when the project has at least one enabled plugin entry.
321
+ *
322
+ * Used by callers that need to skip plugin-specific work when no plugins are
323
+ * configured, without paying the full cost of `loadProjectPlugins`.
324
+ *
325
+ * @param entries - Explicit entries; `false` always returns `false`.
326
+ */
279
327
  export function hasProjectPluginEntries(
280
328
  project: ITtscParsedProjectConfig,
281
329
  entries?: readonly ITtscProjectPluginConfig[] | false,
@@ -510,38 +558,72 @@ function loadPluginEntry(
510
558
  context: ITtscPluginFactoryContext,
511
559
  baseDir: string,
512
560
  ): ITtscPlugin {
513
- const specifier = entry.transform;
514
- if (typeof specifier !== "string" || specifier.length === 0) {
515
- throw new Error(`ttsc: plugin entry is missing a string "transform" field`);
516
- }
517
-
518
- const request = resolvePluginRequest(specifier, baseDir);
519
- const mod = require(request) as {
520
- createTtscPlugin?: TtscPluginFactory;
521
- default?: ITtscPlugin | TtscPluginFactory;
522
- } & Partial<Record<"plugin", ITtscPlugin | TtscPluginFactory>>;
523
- const candidate =
524
- mod.createTtscPlugin ??
525
- mod.default ??
526
- mod.plugin ??
527
- (mod as unknown as ITtscPlugin | TtscPluginFactory);
528
- if (typeof candidate === "function") {
529
- const plugin = candidate(context);
530
- if (!isTtscPlugin(plugin)) {
561
+ return withPluginLoaderEnv(() => {
562
+ const specifier = entry.transform;
563
+ if (typeof specifier !== "string" || specifier.length === 0) {
531
564
  throw new Error(
532
- `ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
565
+ `ttsc: plugin entry is missing a string "transform" field`,
533
566
  );
534
567
  }
535
- rejectJsTransformFunctions(specifier, plugin);
536
- return plugin;
568
+
569
+ const request = resolvePluginRequest(specifier, baseDir);
570
+ const mod = require(request) as {
571
+ createTtscPlugin?: TtscPluginFactory;
572
+ default?: ITtscPlugin | TtscPluginFactory;
573
+ } & Partial<Record<"plugin", ITtscPlugin | TtscPluginFactory>>;
574
+ const candidate =
575
+ mod.createTtscPlugin ??
576
+ mod.default ??
577
+ mod.plugin ??
578
+ (mod as unknown as ITtscPlugin | TtscPluginFactory);
579
+ if (typeof candidate === "function") {
580
+ const plugin = candidate(context);
581
+ if (!isTtscPlugin(plugin)) {
582
+ throw new Error(
583
+ `ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
584
+ );
585
+ }
586
+ rejectJsTransformFunctions(specifier, plugin);
587
+ return plugin;
588
+ }
589
+ if (isTtscPlugin(candidate)) {
590
+ rejectJsTransformFunctions(specifier, candidate);
591
+ return candidate;
592
+ }
593
+ throw new Error(
594
+ `ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
595
+ );
596
+ });
597
+ }
598
+
599
+ function withPluginLoaderEnv<T>(run: () => T): T {
600
+ const previousNode = process.env.TTSC_NODE_BINARY;
601
+ const previousTtsx = process.env.TTSC_TTSX_BINARY;
602
+ process.env.TTSC_NODE_BINARY ??= process.execPath;
603
+ process.env.TTSC_TTSX_BINARY ??= path.join(
604
+ __dirname,
605
+ "..",
606
+ "..",
607
+ "launcher",
608
+ "ttsx.js",
609
+ );
610
+ try {
611
+ return run();
612
+ } finally {
613
+ restoreEnv("TTSC_NODE_BINARY", previousNode);
614
+ restoreEnv("TTSC_TTSX_BINARY", previousTtsx);
537
615
  }
538
- if (isTtscPlugin(candidate)) {
539
- rejectJsTransformFunctions(specifier, candidate);
540
- return candidate;
616
+ }
617
+
618
+ function restoreEnv(
619
+ key: "TTSC_NODE_BINARY" | "TTSC_TTSX_BINARY",
620
+ value: string | undefined,
621
+ ): void {
622
+ if (value === undefined) {
623
+ delete process.env[key];
624
+ } else {
625
+ process.env[key] = value;
541
626
  }
542
- throw new Error(
543
- `ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
544
- );
545
627
  }
546
628
 
547
629
  function isTtscPlugin(value: unknown): value is ITtscPlugin {
@@ -610,7 +692,10 @@ function resolveNativeSourceKind(
610
692
  config: ITtscProjectPluginConfig,
611
693
  index: number,
612
694
  ): "executable" | "linked" {
613
- const packageDir = resolveGoPackageDir(source, pluginLabel(plugin, config, index));
695
+ const packageDir = resolveGoPackageDir(
696
+ source,
697
+ pluginLabel(plugin, config, index),
698
+ );
614
699
  if (findNearestGoMod(packageDir, GO_MOD_SEARCH_MAX_DEPTH) === null) {
615
700
  throw new Error(
616
701
  `ttsc: plugin "${pluginLabel(plugin, config, index)}" source must be inside a Go module with go.mod within ${GO_MOD_SEARCH_MAX_DEPTH} parent directories: ${source}`,
@@ -625,20 +710,6 @@ function resolveNativeSourceKind(
625
710
  return packageName === "main" ? "executable" : "linked";
626
711
  }
627
712
 
628
- function findNearestGoMod(from: string, maxDepth: number): string | null {
629
- let current = path.resolve(from);
630
- let depth = 0;
631
- while (true) {
632
- const candidate = path.join(current, "go.mod");
633
- if (fs.existsSync(candidate)) return candidate;
634
- if (depth >= maxDepth) return null;
635
- const parent = path.dirname(current);
636
- if (parent === current) return null;
637
- current = parent;
638
- depth += 1;
639
- }
640
- }
641
-
642
713
  function resolveGoPackageDir(source: string, label: string): string {
643
714
  if (!fs.existsSync(source)) {
644
715
  throw new Error(`ttsc: plugin "${label}" source does not exist: ${source}`);