ttsc 0.12.4 → 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 (52) hide show
  1. package/cmd/ttsc/api_compile.go +15 -2
  2. package/cmd/ttsc/api_transform.go +15 -2
  3. package/cmd/ttsc/build.go +34 -4
  4. package/cmd/ttsc/filter.go +70 -0
  5. package/cmd/ttsc/filter_strips_unknown_forwarded_flag_test.go +61 -0
  6. package/cmd/ttsc/flags_gen.go +30 -0
  7. package/driver/program.go +105 -4
  8. package/driver/rewrite.go +39 -4
  9. package/lib/compiler/internal/project/resolveProjectConfig.js +18 -0
  10. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  11. package/lib/compiler/internal/runBuild.d.ts +5 -4
  12. package/lib/compiler/internal/runBuild.js +243 -47
  13. package/lib/compiler/internal/runBuild.js.map +1 -1
  14. package/lib/flags/parser.d.ts +63 -0
  15. package/lib/flags/parser.js +254 -0
  16. package/lib/flags/parser.js.map +1 -0
  17. package/lib/flags/schema.d.ts +147 -0
  18. package/lib/flags/schema.js +405 -0
  19. package/lib/flags/schema.js.map +1 -0
  20. package/lib/launcher/internal/prepareExecution.js +3 -0
  21. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  22. package/lib/launcher/internal/runTtsc.js +71 -147
  23. package/lib/launcher/internal/runTtsc.js.map +1 -1
  24. package/lib/launcher/internal/runTtsx.js +104 -72
  25. package/lib/launcher/internal/runTtsx.js.map +1 -1
  26. package/lib/plugin/internal/loadProjectPlugins.js +58 -23
  27. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  28. package/lib/structures/ITtscPlugin.d.ts +24 -0
  29. package/lib/structures/ITtscPluginCapabilities.d.ts +29 -0
  30. package/lib/structures/ITtscPluginCapabilities.js +3 -0
  31. package/lib/structures/ITtscPluginCapabilities.js.map +1 -0
  32. package/lib/structures/index.d.ts +1 -0
  33. package/lib/structures/index.js +1 -0
  34. package/lib/structures/index.js.map +1 -1
  35. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
  36. package/lib/structures/internal/TtscCommonOptions.d.ts +19 -0
  37. package/package.json +9 -9
  38. package/src/compiler/internal/project/resolveProjectConfig.ts +20 -0
  39. package/src/compiler/internal/runBuild.ts +288 -57
  40. package/src/flags/parser.ts +367 -0
  41. package/src/flags/schema.ts +570 -0
  42. package/src/launcher/internal/prepareExecution.ts +3 -0
  43. package/src/launcher/internal/runTtsc.ts +78 -136
  44. package/src/launcher/internal/runTtsx.ts +116 -74
  45. package/src/plugin/internal/loadProjectPlugins.ts +74 -27
  46. package/src/structures/ITtscPlugin.ts +26 -0
  47. package/src/structures/ITtscPluginCapabilities.ts +29 -0
  48. package/src/structures/index.ts +1 -0
  49. package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
  50. package/src/structures/internal/TtscCommonOptions.ts +19 -0
  51. package/utility/flags_gen.go +30 -0
  52. package/utility/host.go +43 -33
@@ -1,12 +1,16 @@
1
1
  import path from "node:path";
2
2
 
3
- 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";
4
8
  import type { ITtscCompilerDiagnostic } from "../../structures/ITtscCompilerDiagnostic";
5
9
  import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
6
10
  import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
7
11
  import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult";
8
12
  import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
9
- import { resolveProjectConfig } from "./project/resolveProjectConfig";
13
+ import { readProjectConfig } from "./project/readProjectConfig";
10
14
  import { resolveBinary } from "./resolveBinary";
11
15
  import { resolveTsgo } from "./resolveTsgo";
12
16
  import {
@@ -16,6 +20,11 @@ import {
16
20
  } from "./sharedHostHelpers";
17
21
  import { outputText, spawnNative } from "./spawnNative";
18
22
 
23
+ type RunBuildOptions = TtscBuildOptions & {
24
+ skipDiagnosticsCheck?: boolean;
25
+ forceListEmittedFiles?: boolean;
26
+ };
27
+
19
28
  /**
20
29
  * Merge extra environment variables over `process.env`, always injecting
21
30
  * `TTSC_NODE_BINARY` so child processes can re-invoke the same Node.js binary
@@ -61,24 +70,20 @@ function nativePluginEnv(
61
70
  * Run `ttsc` against a tsconfig. Returns once the binary exits so the CLI can
62
71
  * decide how to surface diagnostics. Does not throw on non-zero exit.
63
72
  */
64
- export function runBuild(
65
- options: TtscBuildOptions & {
66
- skipDiagnosticsCheck?: boolean;
67
- forceListEmittedFiles?: boolean;
68
- } = {},
69
- ): TtscBuildResult {
73
+ export function runBuild(options: RunBuildOptions = {}): TtscBuildResult {
70
74
  const execution = resolveExecutionContext(options);
75
+ const buildOptions = applyProjectNoEmit(options, execution);
71
76
  if (execution.nativePlugins.length > 0) {
72
77
  const compilers = execution.nativePlugins.filter(
73
78
  (plugin) => plugin.stage === "transform",
74
79
  );
75
- const checked = runNativeCheckPlugins(options, execution);
80
+ const checked = runNativeCheckPlugins(buildOptions, execution);
76
81
  if (checked.status !== 0) {
77
82
  return checked;
78
83
  }
79
84
 
80
- if (options.emit === false) {
81
- if (options.format === true) {
85
+ if (buildOptions.emit === false) {
86
+ if (buildOptions.format === true) {
82
87
  // Format mode is write-only by contract: the lint sidecar
83
88
  // already rewrote source files and reported nothing. Running
84
89
  // tsgo --noEmit OR a transform compiler afterwards would either
@@ -97,12 +102,15 @@ export function runBuild(
97
102
  assertSharedHostCompatibility(compilers, "emit");
98
103
  return appendBuildOutput(
99
104
  checked,
100
- buildWithNativeCompilerPlugins(options, execution, compilers),
105
+ buildWithNativeCompilerPlugins(buildOptions, execution, compilers),
101
106
  );
102
107
  }
108
+ if (checkPluginsReportTypeScriptDiagnostics(execution.nativePlugins)) {
109
+ return checked;
110
+ }
103
111
  return appendBuildOutput(
104
112
  checked,
105
- runTsgo(execution, ["--noEmit"], options),
113
+ runTsgo(execution, ["--noEmit"], buildOptions),
106
114
  );
107
115
  }
108
116
 
@@ -111,26 +119,30 @@ export function runBuild(
111
119
  assertSharedHostCompatibility(compilers, "emit");
112
120
  result = appendBuildOutput(
113
121
  checked,
114
- buildWithNativeCompilerPlugins(options, execution, compilers),
122
+ buildWithNativeCompilerPlugins(buildOptions, execution, compilers),
115
123
  );
116
124
  } else {
117
- if (options.skipDiagnosticsCheck !== true) {
118
- 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);
119
131
  if (tsgoChecked.status !== 0) {
120
132
  return appendBuildOutput(checked, tsgoChecked);
121
133
  }
122
134
  }
123
- const args = createTsgoBuildArgs(execution, options, {
124
- listEmittedFiles: options.forceListEmittedFiles === true,
135
+ const args = createTsgoBuildArgs(execution, buildOptions, {
136
+ listEmittedFiles: buildOptions.forceListEmittedFiles === true,
125
137
  });
126
- const emitted = runTsgoBuild(execution, options, args);
138
+ const emitted = runTsgoBuild(execution, buildOptions, args);
127
139
  result = appendBuildOutput(checked, emitted);
128
140
  }
129
141
 
130
142
  return result;
131
143
  }
132
144
 
133
- if (options.format === true) {
145
+ if (buildOptions.format === true) {
134
146
  // Format mode is write-only by contract — see the matching
135
147
  // short-circuit in the with-native-plugins branch above. When no
136
148
  // native plugin is loaded there is nothing for `ttsc format` to
@@ -145,18 +157,100 @@ export function runBuild(
145
157
  };
146
158
  }
147
159
 
148
- if (options.emit !== false && options.skipDiagnosticsCheck !== true) {
149
- const checked = runTsgo(execution, ["--noEmit"], options);
150
- if (checked.status !== 0) {
151
- return checked;
152
- }
153
- }
154
-
155
- const args = createTsgoBuildArgs(execution, options, {
160
+ const args = createTsgoBuildArgs(execution, buildOptions, {
156
161
  listEmittedFiles:
157
- 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),
158
168
  });
159
- 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
+ );
224
+ }
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
+ );
160
254
  }
161
255
 
162
256
  /**
@@ -186,7 +280,7 @@ function buildWithNativeCompilerPlugins(
186
280
  function runTsgo(
187
281
  execution: ReturnType<typeof resolveExecutionContext>,
188
282
  extraArgs: readonly string[],
189
- options: NonNullable<Parameters<typeof runBuild>[0]>,
283
+ options: RunBuildOptions,
190
284
  ): TtscBuildResult {
191
285
  const res = spawnNative(
192
286
  execution.tsgo.binary,
@@ -195,6 +289,8 @@ function runTsgo(
195
289
  execution.tsconfig,
196
290
  ...extraArgs,
197
291
  ...createTsgoDiagnosticArgs(options),
292
+ ...createTsgoThreadingArgs(options),
293
+ ...(options.passthrough ?? []),
198
294
  ],
199
295
  {
200
296
  cwd: execution.projectRoot,
@@ -227,7 +323,7 @@ function runTsgo(
227
323
  */
228
324
  function runTsgoBuild(
229
325
  execution: ReturnType<typeof resolveExecutionContext>,
230
- options: NonNullable<Parameters<typeof runBuild>[0]>,
326
+ options: RunBuildOptions,
231
327
  args: readonly string[],
232
328
  ): TtscBuildResult {
233
329
  const res = spawnNative(execution.tsgo.binary, args, {
@@ -249,7 +345,18 @@ function runTsgoBuild(
249
345
  stderr: outputText(res.stderr),
250
346
  };
251
347
  const emittedFiles = parseEmittedFiles(result.stdout);
252
- 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) {
253
360
  result.stdout = stripEmittedFileLines(result.stdout);
254
361
  }
255
362
  return normalizeBuildOutput(
@@ -261,12 +368,15 @@ function runTsgoBuild(
261
368
  /** Build the argument list for a direct `tsgo` build invocation. */
262
369
  function createTsgoBuildArgs(
263
370
  execution: ReturnType<typeof resolveExecutionContext>,
264
- options: NonNullable<Parameters<typeof runBuild>[0]>,
265
- flags: { listEmittedFiles: boolean },
371
+ options: RunBuildOptions,
372
+ flags: { listEmittedFiles: boolean; noEmitOnError?: boolean },
266
373
  ): string[] {
267
374
  const args = ["-p", execution.tsconfig];
268
375
  if (options.emit === true) {
269
376
  args.push("--noEmit", "false", "--emitDeclarationOnly", "false");
377
+ if (execution.rewriteRelativeImportExtensionsForEmit) {
378
+ args.push("--rewriteRelativeImportExtensions");
379
+ }
270
380
  } else if (options.emit === false) {
271
381
  args.push("--noEmit");
272
382
  }
@@ -277,15 +387,49 @@ function createTsgoBuildArgs(
277
387
  args.push("--listEmittedFiles");
278
388
  }
279
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
+ }
280
395
  return args;
281
396
  }
282
397
 
283
398
  /**
284
399
  * Return `["--pretty", "false"]` when structured diagnostics are requested so
285
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.
286
411
  */
287
412
  function createTsgoDiagnosticArgs(options: TtscCommonOptions): string[] {
288
- return options.structuredDiagnostics === true ? ["--pretty", "false"] : [];
413
+ if (options.structuredDiagnostics !== true) return [];
414
+ if (forwardsInternalShadowFlag(options, "--pretty")) return [];
415
+ return ["--pretty", "false"];
416
+ }
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;
289
433
  }
290
434
 
291
435
  /** Build the argument list for a native plugin `build`/`check` invocation. */
@@ -302,17 +446,21 @@ function createNativeBuildArgs(
302
446
  ];
303
447
  if (options.emit === true) {
304
448
  args.push("--emit");
305
- } else if (options.emit === false) {
306
- args.push("--noEmit");
307
449
  }
308
450
  if (options.outDir) {
309
451
  args.push("--outDir=" + path.resolve(execution.cwd, options.outDir));
310
452
  }
311
- if (options.quiet === false) {
312
- args.push("--verbose");
313
- } else if (options.quiet === true) {
314
- 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
+ }
315
462
  }
463
+ args.push(...createNativeTsgoArgs(options));
316
464
  return args;
317
465
  }
318
466
 
@@ -320,6 +468,7 @@ function createNativeBuildArgs(
320
468
  function createNativeCheckArgs(
321
469
  execution: ReturnType<typeof resolveExecutionContext>,
322
470
  options: TtscBuildOptions,
471
+ plugin: ITtscLoadedNativePlugin,
323
472
  ): string[] {
324
473
  const args = [
325
474
  nativeCheckSubcommand(options),
@@ -335,9 +484,85 @@ function createNativeCheckArgs(
335
484
  } else if (options.quiet === true) {
336
485
  args.push("--quiet");
337
486
  }
487
+ args.push(...createNativeCheckThreadingArgs(options, plugin));
488
+ args.push(...createNativeTsgoArgs(options));
489
+ return args;
490
+ }
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
+ }
338
525
  return args;
339
526
  }
340
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
+
341
566
  /**
342
567
  * Decide which native plugin subcommand the lint sidecar should run for the
343
568
  * current `runBuild` invocation. The launcher selects exactly one of `fix` /
@@ -385,7 +610,7 @@ function runNativeCheckPlugins(
385
610
  )) {
386
611
  const result = runNativePluginCommand(
387
612
  plugin,
388
- createNativeCheckArgs(execution, options),
613
+ createNativeCheckArgs(execution, options, plugin),
389
614
  options,
390
615
  execution,
391
616
  "ttsc.check",
@@ -458,30 +683,36 @@ export function appendBuildOutput(
458
683
  * here so every code path in `runBuild` shares the same resolution logic.
459
684
  */
460
685
  function resolveExecutionContext(
461
- options: TtscCommonOptions & { tsconfig?: string },
686
+ options: TtscCommonOptions & { emit?: boolean; tsconfig?: string },
462
687
  ) {
463
688
  const cwd = path.resolve(options.cwd ?? process.cwd());
464
- const tsconfig = resolveProjectConfig({
689
+ const project = readProjectConfig({
465
690
  cwd,
691
+ projectRoot: options.projectRoot,
466
692
  tsconfig: options.tsconfig,
467
693
  });
468
- const projectRoot = options.projectRoot
469
- ? path.resolve(cwd, options.projectRoot)
470
- : path.dirname(tsconfig);
694
+ const tsconfig = project.path;
695
+ const projectRoot = project.root;
471
696
  const tsgo = resolveTsgo({ ...options, cwd: projectRoot });
472
- const fallbackBinary = resolveBinary(options);
473
- const loaded = loadProjectPlugins({
474
- binary: fallbackBinary ?? "",
475
- cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
476
- cwd,
477
- entries: options.plugins,
478
- projectRoot,
479
- tsconfig,
480
- });
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: [] };
481
708
  return {
482
709
  cwd,
483
710
  nativePlugins: loaded.nativePlugins,
711
+ projectNoEmit: project.compilerOptions.noEmit === true,
484
712
  projectRoot,
713
+ rewriteRelativeImportExtensionsForEmit:
714
+ options.emit === true &&
715
+ project.compilerOptions.allowImportingTsExtensions === true,
485
716
  tsgo,
486
717
  tsconfig,
487
718
  };