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
@@ -1,15 +1,16 @@
1
- import { spawnSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
1
  import path from "node:path";
5
2
 
6
- import { loadProjectPlugins } from "../../plugin/internal/loadProjectPlugins";
3
+ import { INTERNAL_SHADOW_FLAGS, TERMINAL_FLAGS } from "../../flags/schema";
4
+ import {
5
+ hasProjectPluginEntries,
6
+ loadProjectPlugins,
7
+ } from "../../plugin/internal/loadProjectPlugins";
7
8
  import type { ITtscCompilerDiagnostic } from "../../structures/ITtscCompilerDiagnostic";
8
9
  import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
9
10
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
10
11
  import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult";
11
12
  import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
12
- import { resolveProjectConfig } from "./project/resolveProjectConfig";
13
+ import { readProjectConfig } from "./project/readProjectConfig";
13
14
  import { resolveBinary } from "./resolveBinary";
14
15
  import { resolveTsgo } from "./resolveTsgo";
15
16
  import {
@@ -17,8 +18,18 @@ import {
17
18
  linkedTransformPlugins,
18
19
  selectSharedHostPlugin,
19
20
  } from "./sharedHostHelpers";
21
+ import { outputText, spawnNative } from "./spawnNative";
22
+
23
+ type RunBuildOptions = TtscBuildOptions & {
24
+ skipDiagnosticsCheck?: boolean;
25
+ forceListEmittedFiles?: boolean;
26
+ };
20
27
 
21
- /** Merge spawn env without clobbering unrelated vars. */
28
+ /**
29
+ * Merge extra environment variables over `process.env`, always injecting
30
+ * `TTSC_NODE_BINARY` so child processes can re-invoke the same Node.js binary
31
+ * without searching `PATH`.
32
+ */
22
33
  function mergeEnv(extra?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
23
34
  const base = {
24
35
  ...process.env,
@@ -28,6 +39,12 @@ function mergeEnv(extra?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
28
39
  return { ...base, ...extra };
29
40
  }
30
41
 
42
+ /**
43
+ * Build the environment for a native plugin spawn. Injects `TTSC_TSGO_BINARY`
44
+ * and `TTSC_TTSX_BINARY` alongside the base env from `mergeEnv`. For
45
+ * transform-stage plugins, also passes `TTSC_LINKED_PLUGINS_JSON` containing
46
+ * any linked sources so they run inside the same process as the host plugin.
47
+ */
31
48
  function nativePluginEnv(
32
49
  extra: NodeJS.ProcessEnv | undefined,
33
50
  execution: ReturnType<typeof resolveExecutionContext>,
@@ -49,76 +66,24 @@ function nativePluginEnv(
49
66
  return env;
50
67
  }
51
68
 
52
- function spawnBinary(
53
- binary: string,
54
- args: readonly string[],
55
- options: {
56
- cwd?: string;
57
- env?: NodeJS.ProcessEnv;
58
- encoding?: BufferEncoding;
59
- },
60
- ) {
61
- const viaNode = /\.(?:[cm]?js|ts)$/i.test(binary);
62
- if (!viaNode) {
63
- ensureExecutable(binary);
64
- }
65
- return spawnSync(
66
- viaNode ? process.execPath : binary,
67
- viaNode ? [binary, ...args] : [...args],
68
- {
69
- cwd: options.cwd,
70
- env: options.env,
71
- encoding: options.encoding,
72
- maxBuffer: 1024 * 1024 * 256,
73
- windowsHide: true,
74
- },
75
- );
76
- }
77
-
78
- function ensureExecutable(binary: string): void {
79
- if (process.platform === "win32") {
80
- return;
81
- }
82
- try {
83
- const mode = fs.statSync(binary).mode & 0o777;
84
- if ((mode & 0o111) !== 0) {
85
- return;
86
- }
87
- fs.chmodSync(binary, mode | 0o755);
88
- } catch {
89
- /* keep the original spawn error path */
90
- }
91
- }
92
-
93
- function outputText(value: string | Buffer | null | undefined): string {
94
- if (value == null) {
95
- return "";
96
- }
97
- return typeof value === "string" ? value : value.toString("utf8");
98
- }
99
-
100
69
  /**
101
70
  * Run `ttsc` against a tsconfig. Returns once the binary exits so the CLI can
102
71
  * decide how to surface diagnostics. Does not throw on non-zero exit.
103
72
  */
104
- export function runBuild(
105
- options: TtscBuildOptions & {
106
- skipDiagnosticsCheck?: boolean;
107
- forceListEmittedFiles?: boolean;
108
- } = {},
109
- ): TtscBuildResult {
73
+ export function runBuild(options: RunBuildOptions = {}): TtscBuildResult {
110
74
  const execution = resolveExecutionContext(options);
75
+ const buildOptions = applyProjectNoEmit(options, execution);
111
76
  if (execution.nativePlugins.length > 0) {
112
77
  const compilers = execution.nativePlugins.filter(
113
78
  (plugin) => plugin.stage === "transform",
114
79
  );
115
- const checked = runNativeCheckPlugins(options, execution);
80
+ const checked = runNativeCheckPlugins(buildOptions, execution);
116
81
  if (checked.status !== 0) {
117
82
  return checked;
118
83
  }
119
84
 
120
- if (options.emit === false) {
121
- if (options.format === true) {
85
+ if (buildOptions.emit === false) {
86
+ if (buildOptions.format === true) {
122
87
  // Format mode is write-only by contract: the lint sidecar
123
88
  // already rewrote source files and reported nothing. Running
124
89
  // tsgo --noEmit OR a transform compiler afterwards would either
@@ -137,12 +102,15 @@ export function runBuild(
137
102
  assertSharedHostCompatibility(compilers, "emit");
138
103
  return appendBuildOutput(
139
104
  checked,
140
- buildWithNativeCompilerPlugins(options, execution, compilers),
105
+ buildWithNativeCompilerPlugins(buildOptions, execution, compilers),
141
106
  );
142
107
  }
108
+ if (checkPluginsReportTypeScriptDiagnostics(execution.nativePlugins)) {
109
+ return checked;
110
+ }
143
111
  return appendBuildOutput(
144
112
  checked,
145
- runTsgo(execution, ["--noEmit"], options),
113
+ runTsgo(execution, ["--noEmit"], buildOptions),
146
114
  );
147
115
  }
148
116
 
@@ -151,26 +119,30 @@ export function runBuild(
151
119
  assertSharedHostCompatibility(compilers, "emit");
152
120
  result = appendBuildOutput(
153
121
  checked,
154
- buildWithNativeCompilerPlugins(options, execution, compilers),
122
+ buildWithNativeCompilerPlugins(buildOptions, execution, compilers),
155
123
  );
156
124
  } else {
157
- if (options.skipDiagnosticsCheck !== true) {
158
- const tsgoChecked = runTsgo(execution, ["--noEmit"], options);
125
+ if (
126
+ buildOptions.skipDiagnosticsCheck !== true &&
127
+ !checkPluginsReportTypeScriptDiagnostics(execution.nativePlugins) &&
128
+ !forwardsTerminalTsgoFlag(buildOptions)
129
+ ) {
130
+ const tsgoChecked = runTsgo(execution, ["--noEmit"], buildOptions);
159
131
  if (tsgoChecked.status !== 0) {
160
132
  return appendBuildOutput(checked, tsgoChecked);
161
133
  }
162
134
  }
163
- const args = createTsgoBuildArgs(execution, options, {
164
- listEmittedFiles: options.forceListEmittedFiles === true,
135
+ const args = createTsgoBuildArgs(execution, buildOptions, {
136
+ listEmittedFiles: buildOptions.forceListEmittedFiles === true,
165
137
  });
166
- const emitted = runTsgoBuild(execution, options, args);
138
+ const emitted = runTsgoBuild(execution, buildOptions, args);
167
139
  result = appendBuildOutput(checked, emitted);
168
140
  }
169
141
 
170
142
  return result;
171
143
  }
172
144
 
173
- if (options.format === true) {
145
+ if (buildOptions.format === true) {
174
146
  // Format mode is write-only by contract — see the matching
175
147
  // short-circuit in the with-native-plugins branch above. When no
176
148
  // native plugin is loaded there is nothing for `ttsc format` to
@@ -185,20 +157,107 @@ export function runBuild(
185
157
  };
186
158
  }
187
159
 
188
- if (options.emit !== false && options.skipDiagnosticsCheck !== true) {
189
- const checked = runTsgo(execution, ["--noEmit"], options);
190
- if (checked.status !== 0) {
191
- return checked;
192
- }
193
- }
194
-
195
- const args = createTsgoBuildArgs(execution, options, {
160
+ const args = createTsgoBuildArgs(execution, buildOptions, {
196
161
  listEmittedFiles:
197
- options.emit !== false && options.forceListEmittedFiles === true,
162
+ buildOptions.emit !== false &&
163
+ buildOptions.forceListEmittedFiles === true,
164
+ noEmitOnError:
165
+ buildOptions.emit !== false &&
166
+ buildOptions.skipDiagnosticsCheck !== true &&
167
+ !forwardsTerminalTsgoFlag(buildOptions),
198
168
  });
199
- return runTsgoBuild(execution, options, args);
169
+ return runTsgoBuild(execution, buildOptions, args);
170
+ }
171
+
172
+ /**
173
+ * A tsconfig-level `noEmit: true` is an analysis-only build unless the user
174
+ * explicitly asks `ttsc --emit` to override it. Treat it like CLI `--noEmit`
175
+ * before composing tsgo/native-host arguments so ttsc does not add emit-only
176
+ * guards around projects that cannot emit.
177
+ */
178
+ function applyProjectNoEmit(
179
+ options: RunBuildOptions,
180
+ execution: ReturnType<typeof resolveExecutionContext>,
181
+ ): RunBuildOptions {
182
+ if (options.emit !== undefined || execution.projectNoEmit !== true) {
183
+ return options;
184
+ }
185
+ return { ...options, emit: false };
186
+ }
187
+
188
+ function checkPluginsReportTypeScriptDiagnostics(
189
+ plugins: readonly ITtscLoadedNativePlugin[],
190
+ ): boolean {
191
+ return plugins.some(
192
+ (plugin) =>
193
+ plugin.stage === "check" && plugin.reportsTypeScriptDiagnostics === true,
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Tsgo CLI flags that make `tsgo` print something and exit instead of building
199
+ * (`--showConfig`, `--listFilesOnly`, `--help`, …). ttsc must not add
200
+ * build-only guard flags around these because the forwarded flag asks tsgo to
201
+ * print something and exit instead of compiling the project.
202
+ *
203
+ * Schema-derived: the set is computed from `FLAG_SCHEMA[*].terminal === true`,
204
+ * not hand-maintained next to runBuild. Adding a new terminal flag now means
205
+ * editing `schema.ts` and re-running `pnpm format` — every layer learns about
206
+ * it automatically.
207
+ */
208
+ const TERMINAL_TSGO_FLAGS: ReadonlySet<string> = (() => {
209
+ // Mirror the legacy hand-list: schema's terminal flags ∪ the `-?` alias
210
+ // tsgo accepts (kept here because it is a tsgo synonym, not a ttsc flag).
211
+ const out = new Set<string>(TERMINAL_FLAGS);
212
+ out.add("-?");
213
+ return out;
214
+ })();
215
+
216
+ /**
217
+ * Report whether the caller forwarded a print-and-exit tsgo flag, so ttsc can
218
+ * avoid adding compile-only flags to a command that is not going to compile.
219
+ */
220
+ function forwardsTerminalTsgoFlag(options: TtscCommonOptions): boolean {
221
+ return (
222
+ options.passthrough?.some((flag) => TERMINAL_TSGO_FLAGS.has(flag)) ?? false
223
+ );
200
224
  }
201
225
 
226
+ /**
227
+ * Report whether the caller forwarded a flag ttsc adds to tsgo internally —
228
+ * e.g. `--listEmittedFiles` (ttsc adds it to learn emitted paths) or
229
+ * `--noEmit` (ttsc adds it for the pre-emit type-check). When the user
230
+ * also forwards the same flag, post-processing must keep the user-visible
231
+ * effect intact instead of stripping it as ttsc-internal noise.
232
+ *
233
+ * Schema-derived: `FLAG_SCHEMA[*].internalShadow === true`. RC-2 from the
234
+ * RCA (RCA section 3, `--listEmittedFiles` / `--showConfig` swallowed):
235
+ * the per-flag `passthrough.includes("…")` check is now one structural
236
+ * lookup against the schema, not one bespoke `if` per shadow flag.
237
+ */
238
+ function forwardsInternalShadowFlag(
239
+ options: TtscCommonOptions,
240
+ flag: string,
241
+ ): boolean {
242
+ if (!INTERNAL_SHADOW_FLAGS.has(flag)) return false;
243
+ const passthrough = options.passthrough;
244
+ if (passthrough === undefined) return false;
245
+ // Match both the bare form (`--pretty`) and the inline-value form
246
+ // (`--pretty=true`). The launcher's parser preserves these shapes
247
+ // verbatim, so a tolerant prefix compare is what we want — `passthrough`
248
+ // never holds a stray substring that happens to start with the flag
249
+ // because parser positional/passthrough sinks split on whitespace.
250
+ const inlinePrefix = `${flag}=`;
251
+ return passthrough.some(
252
+ (token) => token === flag || token.startsWith(inlinePrefix),
253
+ );
254
+ }
255
+
256
+ /**
257
+ * Dispatch a build through the shared-host native plugin. The host plugin is
258
+ * the one that owns the process (non-linked); all other transform plugins ride
259
+ * inside it via the `--plugins-json` flag.
260
+ */
202
261
  function buildWithNativeCompilerPlugins(
203
262
  options: TtscBuildOptions,
204
263
  execution: ReturnType<typeof resolveExecutionContext>,
@@ -214,18 +273,24 @@ function buildWithNativeCompilerPlugins(
214
273
  );
215
274
  }
216
275
 
276
+ /**
277
+ * Run `tsgo -p <tsconfig> [extraArgs]` and return the normalized result. Used
278
+ * for the no-emit type-check pass that precedes file emission.
279
+ */
217
280
  function runTsgo(
218
281
  execution: ReturnType<typeof resolveExecutionContext>,
219
282
  extraArgs: readonly string[],
220
- options: NonNullable<Parameters<typeof runBuild>[0]>,
283
+ options: RunBuildOptions,
221
284
  ): TtscBuildResult {
222
- const res = spawnBinary(
285
+ const res = spawnNative(
223
286
  execution.tsgo.binary,
224
287
  [
225
288
  "-p",
226
289
  execution.tsconfig,
227
290
  ...extraArgs,
228
291
  ...createTsgoDiagnosticArgs(options),
292
+ ...createTsgoThreadingArgs(options),
293
+ ...(options.passthrough ?? []),
229
294
  ],
230
295
  {
231
296
  cwd: execution.projectRoot,
@@ -251,12 +316,17 @@ function runTsgo(
251
316
  );
252
317
  }
253
318
 
319
+ /**
320
+ * Run `tsgo` with the full emit arguments and parse `TSFILE:` lines from stdout
321
+ * into `emittedFiles`. The TSFILE lines are stripped before the result is
322
+ * returned so they do not appear in the user-facing output.
323
+ */
254
324
  function runTsgoBuild(
255
325
  execution: ReturnType<typeof resolveExecutionContext>,
256
- options: NonNullable<Parameters<typeof runBuild>[0]>,
326
+ options: RunBuildOptions,
257
327
  args: readonly string[],
258
328
  ): TtscBuildResult {
259
- const res = spawnBinary(execution.tsgo.binary, args, {
329
+ const res = spawnNative(execution.tsgo.binary, args, {
260
330
  cwd: execution.projectRoot,
261
331
  env: mergeEnv(options.env),
262
332
  encoding: "utf8",
@@ -275,7 +345,18 @@ function runTsgoBuild(
275
345
  stderr: outputText(res.stderr),
276
346
  };
277
347
  const emittedFiles = parseEmittedFiles(result.stdout);
278
- if (emittedFiles.length !== 0) {
348
+ // The `TSFILE:` lines are tsgo's `--listEmittedFiles` output. ttsc adds that
349
+ // flag internally to learn the emitted paths and strips the lines back out
350
+ // as noise — but when the user themselves forwarded `--listEmittedFiles`,
351
+ // the listing is what they asked for, so it must survive to stdout.
352
+ // The lookup is schema-driven (FLAG_SCHEMA marks `--listEmittedFiles` with
353
+ // `internalShadow: true`); see `forwardsInternalShadowFlag` for the RC-2
354
+ // background.
355
+ const userListedEmitted = forwardsInternalShadowFlag(
356
+ options,
357
+ "--listEmittedFiles",
358
+ );
359
+ if (emittedFiles.length !== 0 && !userListedEmitted) {
279
360
  result.stdout = stripEmittedFileLines(result.stdout);
280
361
  }
281
362
  return normalizeBuildOutput(
@@ -284,14 +365,18 @@ function runTsgoBuild(
284
365
  );
285
366
  }
286
367
 
368
+ /** Build the argument list for a direct `tsgo` build invocation. */
287
369
  function createTsgoBuildArgs(
288
370
  execution: ReturnType<typeof resolveExecutionContext>,
289
- options: NonNullable<Parameters<typeof runBuild>[0]>,
290
- flags: { listEmittedFiles: boolean },
371
+ options: RunBuildOptions,
372
+ flags: { listEmittedFiles: boolean; noEmitOnError?: boolean },
291
373
  ): string[] {
292
374
  const args = ["-p", execution.tsconfig];
293
375
  if (options.emit === true) {
294
376
  args.push("--noEmit", "false", "--emitDeclarationOnly", "false");
377
+ if (execution.rewriteRelativeImportExtensionsForEmit) {
378
+ args.push("--rewriteRelativeImportExtensions");
379
+ }
295
380
  } else if (options.emit === false) {
296
381
  args.push("--noEmit");
297
382
  }
@@ -302,13 +387,52 @@ function createTsgoBuildArgs(
302
387
  args.push("--listEmittedFiles");
303
388
  }
304
389
  args.push(...createTsgoDiagnosticArgs(options));
390
+ args.push(...createTsgoThreadingArgs(options));
391
+ args.push(...(options.passthrough ?? []));
392
+ if (flags.noEmitOnError === true) {
393
+ args.push("--noEmitOnError");
394
+ }
305
395
  return args;
306
396
  }
307
397
 
398
+ /**
399
+ * Return `["--pretty", "false"]` when structured diagnostics are requested so
400
+ * that the output can be parsed line-by-line, or an empty array otherwise.
401
+ *
402
+ * When the user explicitly forwarded `--pretty` (any value), the internal
403
+ * `--pretty false` shadow is dropped so the user wins on the surface. ttsc's
404
+ * own diagnostic parser will then see pretty-formatted output and fall back
405
+ * to surfacing it verbatim — the RC-2 contract that `--pretty`'s
406
+ * `internalShadow: true` flag in `FLAG_SCHEMA` declares. Without this guard
407
+ * the order in `runTsgo` (internal flags first, passthrough last) would
408
+ * still let the user's `--pretty true` override at the tsgo level, but ttsc
409
+ * would have already committed to a structured-diagnostics post-process
410
+ * that no longer matches the actual output.
411
+ */
308
412
  function createTsgoDiagnosticArgs(options: TtscCommonOptions): string[] {
309
- return options.structuredDiagnostics === true ? ["--pretty", "false"] : [];
413
+ if (options.structuredDiagnostics !== true) return [];
414
+ if (forwardsInternalShadowFlag(options, "--pretty")) return [];
415
+ return ["--pretty", "false"];
310
416
  }
311
417
 
418
+ /**
419
+ * Forward the `--singleThreaded` / `--checkers` knobs to a `tsgo` invocation.
420
+ * tsgo accepts both flags natively, so the no-plugin build lane only has to
421
+ * pass them through; the type-check and emit passes share this so the checker
422
+ * pool size stays consistent across both.
423
+ */
424
+ function createTsgoThreadingArgs(options: TtscCommonOptions): string[] {
425
+ const args: string[] = [];
426
+ if (options.singleThreaded === true) {
427
+ args.push("--singleThreaded");
428
+ }
429
+ if (options.checkers !== undefined) {
430
+ args.push("--checkers", String(options.checkers));
431
+ }
432
+ return args;
433
+ }
434
+
435
+ /** Build the argument list for a native plugin `build`/`check` invocation. */
312
436
  function createNativeBuildArgs(
313
437
  execution: ReturnType<typeof resolveExecutionContext>,
314
438
  options: TtscBuildOptions,
@@ -322,23 +446,29 @@ function createNativeBuildArgs(
322
446
  ];
323
447
  if (options.emit === true) {
324
448
  args.push("--emit");
325
- } else if (options.emit === false) {
326
- args.push("--noEmit");
327
449
  }
328
450
  if (options.outDir) {
329
451
  args.push("--outDir=" + path.resolve(execution.cwd, options.outDir));
330
452
  }
331
- if (options.quiet === false) {
332
- args.push("--verbose");
333
- } else if (options.quiet === true) {
334
- args.push("--quiet");
453
+ // Third-party transform hosts already treat the `check` subcommand as a
454
+ // quiet no-emit pass. Keep ttsc-owned build modifiers off that lane so older
455
+ // strict hosts do not reject unknown optional flags before analysis starts.
456
+ if (options.emit !== false) {
457
+ if (options.quiet === false) {
458
+ args.push("--verbose");
459
+ } else if (options.quiet === true) {
460
+ args.push("--quiet");
461
+ }
335
462
  }
463
+ args.push(...createNativeTsgoArgs(options));
336
464
  return args;
337
465
  }
338
466
 
467
+ /** Build the argument list for a native plugin check/fix/format invocation. */
339
468
  function createNativeCheckArgs(
340
469
  execution: ReturnType<typeof resolveExecutionContext>,
341
470
  options: TtscBuildOptions,
471
+ plugin: ITtscLoadedNativePlugin,
342
472
  ): string[] {
343
473
  const args = [
344
474
  nativeCheckSubcommand(options),
@@ -354,9 +484,85 @@ function createNativeCheckArgs(
354
484
  } else if (options.quiet === true) {
355
485
  args.push("--quiet");
356
486
  }
487
+ args.push(...createNativeCheckThreadingArgs(options, plugin));
488
+ args.push(...createNativeTsgoArgs(options));
357
489
  return args;
358
490
  }
359
491
 
492
+ // `--singleThreaded` / `--checkers` are forwarded to native check-stage hosts
493
+ // only when the host is one ttsc itself owns (currently `@ttsc/lint`). #113
494
+ // forwarded both flags as bare CLI tokens to every native sidecar, then
495
+ // commit ad3443a reverted that across the board because a third-party host
496
+ // built before #113 has no `singleThreaded` / `checkers` flag in its
497
+ // `flag.FlagSet` and would exit 2 on the unknown flag — so
498
+ // `ttsc --singleThreaded` failed deterministically on every typia/nestia
499
+ // transform-plugin project.
500
+ //
501
+ // The performance ceiling that caused, though, is real: format/check passes
502
+ // through the lint sidecar are dominated by parallel parse + parallel rule
503
+ // walk, and with the threading knob silently dropped, MT and ST runs of
504
+ // `ttsc format` produced identical wall-clock numbers — the benchmark cell
505
+ // became a non-measurement. The lint sidecar is built and shipped from this
506
+ // repo, accepts both flags via `parseSubcommandFlags`, and threads them down
507
+ // to `loadProgram` (parse phase) and `engine.SetSerial` (rule walk). The host
508
+ // is identified by name (`@ttsc/lint`) so a third-party check-stage plugin
509
+ // keeps the strict-host behavior from ad3443a; only the host we control gets
510
+ // the bare flag. Transform-stage hosts are never reached by this path
511
+ // (they go through `createNativeBuildArgs`), so the typia/nestia regression
512
+ // remains pinned by `test_plugin_corpus_single_threaded_flag_does_not_break_a_native_plugin_build`.
513
+ function createNativeCheckThreadingArgs(
514
+ options: TtscCommonOptions,
515
+ plugin: ITtscLoadedNativePlugin,
516
+ ): string[] {
517
+ if (!nativeHostAcceptsThreadingArgs(plugin)) return [];
518
+ const args: string[] = [];
519
+ if (options.singleThreaded === true) {
520
+ args.push("--singleThreaded");
521
+ }
522
+ if (options.checkers !== undefined) {
523
+ args.push("--checkers=" + String(options.checkers));
524
+ }
525
+ return args;
526
+ }
527
+
528
+ /**
529
+ * Return true when the loaded native check-stage host has declared
530
+ * `capabilities.threadingArgs` in its plugin descriptor.
531
+ *
532
+ * The lint sidecar (`packages/lint/src/index.ts::createTtscPlugin`) opts in
533
+ * because its `parseSubcommandFlags` handler accepts `--singleThreaded` and
534
+ * `--checkers` directly and threads them into `loadProgram` (parse phase)
535
+ * and `engine.SetSerial` (rule walk). Any other check-stage host that has
536
+ * not declared the capability is treated as a third-party binary whose flag
537
+ * set is unknown, matching the conservative default from commit ad3443a.
538
+ *
539
+ * The capability flag replaces the prior `plugin.name === "@ttsc/lint"`
540
+ * string check: routing on a descriptor field instead of the plugin name
541
+ * lets the next first-party check-stage plugin opt in without ttsc needing
542
+ * to learn its name. See `ITtscPluginCapabilities` and issue #125 for the
543
+ * broader CLI-parser cleanup this is the quick-win step of.
544
+ */
545
+ function nativeHostAcceptsThreadingArgs(
546
+ plugin: ITtscLoadedNativePlugin,
547
+ ): boolean {
548
+ return plugin.capabilities?.threadingArgs === true;
549
+ }
550
+
551
+ /**
552
+ * Forward the tsgo flags ttsc did not recognize to a native sidecar as one
553
+ * JSON-encoded `--tsgo-args` flag. The sidecar replays them through tsgo's own
554
+ * option parser onto `CompilerOptions`, so a flag like `ttsc --strict` reaches
555
+ * a plugin build the same way it reaches the plain tsgo lane. Encoded as a
556
+ * single token so the sidecars' unknown-flag filters keep it intact.
557
+ */
558
+ function createNativeTsgoArgs(options: TtscCommonOptions): string[] {
559
+ const passthrough = options.passthrough;
560
+ if (passthrough === undefined || passthrough.length === 0) {
561
+ return [];
562
+ }
563
+ return ["--tsgo-args=" + JSON.stringify(passthrough)];
564
+ }
565
+
360
566
  /**
361
567
  * Decide which native plugin subcommand the lint sidecar should run for the
362
568
  * current `runBuild` invocation. The launcher selects exactly one of `fix` /
@@ -369,6 +575,10 @@ function nativeCheckSubcommand(options: TtscBuildOptions): string {
369
575
  return "check";
370
576
  }
371
577
 
578
+ /**
579
+ * Serialize the plugin list to a compact JSON string for `--plugins-json=`.
580
+ * Only the fields the native binary protocol requires are included.
581
+ */
372
582
  function serializeNativePlugins(
373
583
  plugins: readonly ITtscLoadedNativePlugin[],
374
584
  ): string {
@@ -381,6 +591,10 @@ function serializeNativePlugins(
381
591
  );
382
592
  }
383
593
 
594
+ /**
595
+ * Run every check-stage plugin in order, short-circuiting on the first non-zero
596
+ * exit. Aggregates diagnostics and output across all check plugins.
597
+ */
384
598
  function runNativeCheckPlugins(
385
599
  options: TtscBuildOptions,
386
600
  execution: ReturnType<typeof resolveExecutionContext>,
@@ -396,7 +610,7 @@ function runNativeCheckPlugins(
396
610
  )) {
397
611
  const result = runNativePluginCommand(
398
612
  plugin,
399
- createNativeCheckArgs(execution, options),
613
+ createNativeCheckArgs(execution, options, plugin),
400
614
  options,
401
615
  execution,
402
616
  "ttsc.check",
@@ -409,6 +623,11 @@ function runNativeCheckPlugins(
409
623
  return out;
410
624
  }
411
625
 
626
+ /**
627
+ * Spawn a single native plugin binary with the given args and return the
628
+ * normalized build result. `label` is used in the thrown error message so the
629
+ * caller's context (e.g. `"ttsc.check"` or `"ttsc.build"`) is preserved.
630
+ */
412
631
  function runNativePluginCommand(
413
632
  plugin: ITtscLoadedNativePlugin,
414
633
  args: readonly string[],
@@ -416,7 +635,7 @@ function runNativePluginCommand(
416
635
  execution: ReturnType<typeof resolveExecutionContext>,
417
636
  label: string,
418
637
  ): TtscBuildResult {
419
- const res = spawnBinary(plugin.binary, args, {
638
+ const res = spawnNative(plugin.binary, args, {
420
639
  cwd: execution.projectRoot,
421
640
  env: nativePluginEnv(options.env, execution, plugin),
422
641
  encoding: "utf8",
@@ -436,6 +655,14 @@ function runNativePluginCommand(
436
655
  );
437
656
  }
438
657
 
658
+ /**
659
+ * Merge two `TtscBuildResult` values into one.
660
+ *
661
+ * - `status`: the right status wins unless it is 0 (failure propagates).
662
+ * - `diagnostics`: concatenated left then right.
663
+ * - `emittedFiles`: right wins when present, otherwise left is kept.
664
+ * - `stdout`/`stderr`: concatenated left then right.
665
+ */
439
666
  export function appendBuildOutput(
440
667
  left: TtscBuildResult,
441
668
  right: TtscBuildResult,
@@ -450,36 +677,51 @@ export function appendBuildOutput(
450
677
  });
451
678
  }
452
679
 
680
+ /**
681
+ * Resolve all runtime context needed for a build: cwd, tsconfig path, project
682
+ * root, tsgo binary location, and the loaded native plugin list. Centralised
683
+ * here so every code path in `runBuild` shares the same resolution logic.
684
+ */
453
685
  function resolveExecutionContext(
454
- options: TtscCommonOptions & { tsconfig?: string },
686
+ options: TtscCommonOptions & { emit?: boolean; tsconfig?: string },
455
687
  ) {
456
688
  const cwd = path.resolve(options.cwd ?? process.cwd());
457
- const tsconfig = resolveProjectConfig({
689
+ const project = readProjectConfig({
458
690
  cwd,
691
+ projectRoot: options.projectRoot,
459
692
  tsconfig: options.tsconfig,
460
693
  });
461
- const projectRoot = options.projectRoot
462
- ? path.resolve(cwd, options.projectRoot)
463
- : path.dirname(tsconfig);
694
+ const tsconfig = project.path;
695
+ const projectRoot = project.root;
464
696
  const tsgo = resolveTsgo({ ...options, cwd: projectRoot });
465
- const fallbackBinary = resolveBinary(options);
466
- const loaded = loadProjectPlugins({
467
- binary: fallbackBinary ?? "",
468
- cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
469
- cwd,
470
- entries: options.plugins,
471
- projectRoot,
472
- tsconfig,
473
- });
697
+ const hasPlugins = hasProjectPluginEntries(project, options.plugins);
698
+ const loaded = hasPlugins
699
+ ? loadProjectPlugins({
700
+ binary: resolveBinary(options) ?? "",
701
+ cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
702
+ cwd,
703
+ entries: options.plugins,
704
+ projectRoot,
705
+ tsconfig,
706
+ })
707
+ : { nativePlugins: [] };
474
708
  return {
475
709
  cwd,
476
710
  nativePlugins: loaded.nativePlugins,
711
+ projectNoEmit: project.compilerOptions.noEmit === true,
477
712
  projectRoot,
713
+ rewriteRelativeImportExtensionsForEmit:
714
+ options.emit === true &&
715
+ project.compilerOptions.allowImportingTsExtensions === true,
478
716
  tsgo,
479
717
  tsconfig,
480
718
  };
481
719
  }
482
720
 
721
+ /**
722
+ * Extract `TSFILE: <path>` lines from tsgo stdout and return the absolute
723
+ * paths. tsgo emits these when `--listEmittedFiles` is passed.
724
+ */
483
725
  function parseEmittedFiles(stdout: string): string[] {
484
726
  const out: string[] = [];
485
727
  for (const line of stdout.split(/\r?\n/)) {
@@ -491,6 +733,10 @@ function parseEmittedFiles(stdout: string): string[] {
491
733
  return out;
492
734
  }
493
735
 
736
+ /**
737
+ * Remove `TSFILE:` lines from tsgo stdout so they do not appear in the
738
+ * user-facing output after they have been parsed into `emittedFiles`.
739
+ */
494
740
  function stripEmittedFileLines(stdout: string): string {
495
741
  return stdout
496
742
  .split(/\r?\n/)
@@ -499,10 +745,21 @@ function stripEmittedFileLines(stdout: string): string {
499
745
  .replace(/\n+$/, "");
500
746
  }
501
747
 
748
+ /**
749
+ * `TtscBuildResult` with `diagnostics` made optional. Used internally when
750
+ * diagnostics are parsed lazily from stdout/stderr by `normalizeBuildOutput`.
751
+ */
502
752
  type PartialBuildResult = Omit<TtscBuildResult, "diagnostics"> & {
503
753
  diagnostics?: ITtscCompilerDiagnostic[];
504
754
  };
505
755
 
756
+ /**
757
+ * Normalise a raw spawn result into a `TtscBuildResult`.
758
+ *
759
+ * When `diagnostics` is absent they are parsed from the text output. When the
760
+ * process exited non-zero but stderr is empty and stdout is non-empty, stdout
761
+ * is moved to stderr so the error is visible to callers who only check stderr.
762
+ */
506
763
  export function normalizeBuildOutput(
507
764
  result: PartialBuildResult,
508
765
  cwd?: string,
@@ -524,6 +781,11 @@ export function normalizeBuildOutput(
524
781
  };
525
782
  }
526
783
 
784
+ /**
785
+ * Parse structured diagnostics from the combined stderr+stdout text when the
786
+ * caller did not supply pre-parsed diagnostics. ANSI escape codes are stripped
787
+ * and `TSFILE:` / `Found N errors` summary lines are skipped.
788
+ */
527
789
  function parseCompilerDiagnostics(
528
790
  result: Pick<TtscBuildResult, "stderr" | "stdout">,
529
791
  cwd: string | undefined,
@@ -553,6 +815,16 @@ function parseCompilerDiagnostics(
553
815
  return out;
554
816
  }
555
817
 
818
+ /**
819
+ * Try to parse a single line as a TypeScript compiler diagnostic in one of
820
+ * three formats:
821
+ *
822
+ * - `file:line:col - category TSxxxx: message` (colon-separated, tsgo style)
823
+ * - `file(line,col): category TSxxxx: message` (paren style, classic tsc)
824
+ * - `category TSxxxx: message` (global, no file)
825
+ *
826
+ * Returns `null` when the line does not match any format.
827
+ */
556
828
  function parseDiagnosticLine(
557
829
  line: string,
558
830
  cwd: string | undefined,
@@ -599,6 +871,10 @@ function parseDiagnosticLine(
599
871
  };
600
872
  }
601
873
 
874
+ /**
875
+ * Return an absolute file path. When `file` is already absolute it is returned
876
+ * unchanged; relative paths are resolved against `cwd` when available.
877
+ */
602
878
  function normalizeDiagnosticFile(
603
879
  file: string,
604
880
  cwd: string | undefined,
@@ -609,6 +885,11 @@ function normalizeDiagnosticFile(
609
885
  return path.resolve(cwd, file);
610
886
  }
611
887
 
888
+ /**
889
+ * Map a raw category string (`"error"`, `"warning"`, `"suggestion"`,
890
+ * `"message"`) to the canonical `ITtscCompilerDiagnostic.Category`. Any
891
+ * unrecognised value is coerced to `"error"`.
892
+ */
612
893
  function normalizeDiagnosticCategory(
613
894
  value: string,
614
895
  ): ITtscCompilerDiagnostic.Category {
@@ -620,10 +901,15 @@ function normalizeDiagnosticCategory(
620
901
  : "error";
621
902
  }
622
903
 
904
+ /**
905
+ * Parse a diagnostic code as a number when it is all digits (TS numeric codes
906
+ * like `2322`), or keep it as a string for plugin-defined alphanumeric codes.
907
+ */
623
908
  function normalizeDiagnosticCode(value: string): number | string {
624
909
  return /^\d+$/.test(value) ? Number(value) : value;
625
910
  }
626
911
 
912
+ /** Strip ANSI escape sequences from `text` for line-by-line diagnostic parsing. */
627
913
  function stripAnsi(text: string): string {
628
914
  return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
629
915
  }