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