ttsc 0.12.4 → 0.13.1

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 (54) hide show
  1. package/README.md +3 -0
  2. package/cmd/ttsc/api_compile.go +15 -2
  3. package/cmd/ttsc/api_transform.go +15 -2
  4. package/cmd/ttsc/build.go +34 -4
  5. package/cmd/ttsc/filter.go +70 -0
  6. package/cmd/ttsc/filter_strips_unknown_forwarded_flag_test.go +61 -0
  7. package/cmd/ttsc/flags_gen.go +30 -0
  8. package/driver/program.go +105 -4
  9. package/driver/rewrite.go +39 -4
  10. package/lib/compiler/internal/project/resolveProjectConfig.js +18 -0
  11. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  12. package/lib/compiler/internal/runBuild.d.ts +5 -4
  13. package/lib/compiler/internal/runBuild.js +358 -51
  14. package/lib/compiler/internal/runBuild.js.map +1 -1
  15. package/lib/flags/parser.d.ts +63 -0
  16. package/lib/flags/parser.js +254 -0
  17. package/lib/flags/parser.js.map +1 -0
  18. package/lib/flags/schema.d.ts +146 -0
  19. package/lib/flags/schema.js +423 -0
  20. package/lib/flags/schema.js.map +1 -0
  21. package/lib/launcher/internal/prepareExecution.js +3 -0
  22. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  23. package/lib/launcher/internal/runTtsc.js +71 -147
  24. package/lib/launcher/internal/runTtsc.js.map +1 -1
  25. package/lib/launcher/internal/runTtsx.js +104 -72
  26. package/lib/launcher/internal/runTtsx.js.map +1 -1
  27. package/lib/plugin/internal/loadProjectPlugins.js +58 -23
  28. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  29. package/lib/structures/ITtscPlugin.d.ts +24 -0
  30. package/lib/structures/ITtscPluginCapabilities.d.ts +40 -0
  31. package/lib/structures/ITtscPluginCapabilities.js +3 -0
  32. package/lib/structures/ITtscPluginCapabilities.js.map +1 -0
  33. package/lib/structures/index.d.ts +1 -0
  34. package/lib/structures/index.js +1 -0
  35. package/lib/structures/index.js.map +1 -1
  36. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
  37. package/lib/structures/internal/TtscCommonOptions.d.ts +19 -0
  38. package/package.json +9 -9
  39. package/shim/ast/lint.go +6 -0
  40. package/src/compiler/internal/project/resolveProjectConfig.ts +20 -0
  41. package/src/compiler/internal/runBuild.ts +463 -56
  42. package/src/flags/parser.ts +367 -0
  43. package/src/flags/schema.ts +589 -0
  44. package/src/launcher/internal/prepareExecution.ts +3 -0
  45. package/src/launcher/internal/runTtsc.ts +78 -136
  46. package/src/launcher/internal/runTtsx.ts +116 -74
  47. package/src/plugin/internal/loadProjectPlugins.ts +74 -27
  48. package/src/structures/ITtscPlugin.ts +26 -0
  49. package/src/structures/ITtscPluginCapabilities.ts +41 -0
  50. package/src/structures/index.ts +1 -0
  51. package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
  52. package/src/structures/internal/TtscCommonOptions.ts +19 -0
  53. package/utility/flags_gen.go +30 -0
  54. package/utility/host.go +43 -33
@@ -0,0 +1,367 @@
1
+ // Schema-driven CLI parser used by both `runTtsc.ts` and `runTtsx.ts`.
2
+ //
3
+ // One engine, one schema. Each subcommand picks the subset of FLAG_SCHEMA it
4
+ // accepts and feeds it through `parseFlags`. The engine returns:
5
+ //
6
+ // * `values` — typed values for every consumed flag.
7
+ // * `passthrough` — flags ttsc did not consume but must forward verbatim
8
+ // to tsgo (or to native sidecars via `--tsgo-args`).
9
+ // * `positional` — bare non-flag tokens (file paths, entry files, …).
10
+ //
11
+ // The schema is the spec; behaviour at every layer boundary is determined by
12
+ // the FlagSpec attributes, not by ad-hoc `if (arg === "--foo")` branches.
13
+
14
+ import type {
15
+ AnySubcommand,
16
+ FlagSpec,
17
+ ValueValidator,
18
+ } from "./schema";
19
+ import { FLAG_BY_NAME, flagsForSubcommand } from "./schema";
20
+
21
+ /**
22
+ * Per-subcommand parse result. `values` is keyed by the canonical flag
23
+ * name (e.g. `"--singleThreaded"`); boolean flags resolve to `true`/`false`,
24
+ * value flags to the parsed value type. Callers narrow with the helpers
25
+ * below (`getBoolean`, `getString`, `getNumber`).
26
+ */
27
+ export interface ParseResult {
28
+ /** Canonical flag name → resolved value. */
29
+ readonly values: ReadonlyMap<string, string | boolean | number>;
30
+ /** Flags the engine did not consume — forwarded to tsgo. */
31
+ readonly passthrough: readonly string[];
32
+ /** Bare non-flag positional arguments, in original order. */
33
+ readonly positional: readonly string[];
34
+ /**
35
+ * Tokens that arrived after the `forwardAfterFirstPositional` sentinel.
36
+ * These are intended for the user's program (e.g. ttsx's entry-file argv);
37
+ * they are NOT forwarded to tsgo. Always empty when
38
+ * `forwardAfterFirstPositional` is false.
39
+ */
40
+ readonly tail: readonly string[];
41
+ }
42
+
43
+ /**
44
+ * Options controlling a single `parseFlags` invocation.
45
+ */
46
+ export interface ParseOptions {
47
+ /** Which subcommand's flag subset to accept. */
48
+ readonly subcommand: AnySubcommand;
49
+ /** Argv tail (the launcher has already split off the subcommand). */
50
+ readonly argv: readonly string[];
51
+ /**
52
+ * Error prefix used when the parser throws (`"ttsc:"` or `"ttsx:"`). The
53
+ * engine itself is product-neutral; the caller controls the brand.
54
+ */
55
+ readonly errorPrefix: string;
56
+ /**
57
+ * `true` to treat the FIRST positional token as a sentinel that switches
58
+ * the engine to "forward everything after" mode (ttsx's entry-file
59
+ * behaviour: tokens after the entry are runtime argv, not tsgo flags).
60
+ * The sentinel itself is still recorded as a positional argument.
61
+ */
62
+ readonly forwardAfterFirstPositional?: boolean;
63
+ /**
64
+ * Optional `"--"` separator handling: when present in argv, every token
65
+ * after `--` is appended to `passthrough` as-is (ttsx already does this).
66
+ */
67
+ readonly honorDoubleDashSeparator?: boolean;
68
+ }
69
+
70
+ /**
71
+ * Parse `argv` according to FLAG_SCHEMA filtered by `subcommand`. Returns a
72
+ * `ParseResult`. Throws `Error` (with the configured prefix) on invalid
73
+ * input — unknown subcommand-only flag, missing required value, value that
74
+ * fails its validator.
75
+ */
76
+ export function parseFlags(opts: ParseOptions): ParseResult {
77
+ const accepted = new Map<string, FlagSpec>();
78
+ for (const flag of flagsForSubcommand(opts.subcommand)) {
79
+ // A flag enters the launcher's `accepted` set only when its
80
+ // `consumedBy` includes `"launcher"`. Flags consumed solely by tsgo
81
+ // or by native sidecars (e.g. `--showConfig`, `--listFilesOnly`)
82
+ // must fall through to `forwardKnownButUnaccepted` so the launcher
83
+ // forwards them verbatim instead of storing them in `values` where
84
+ // no consumer reads them back out. The previous shape — filter on
85
+ // `subcommands` only — silently dropped every tsgo-only terminal
86
+ // flag at the launcher boundary (the RC-1 / RC-2 class the schema
87
+ // is meant to make impossible).
88
+ if (!flag.consumedBy.includes("launcher")) continue;
89
+ accepted.set(flag.name, flag);
90
+ for (const alias of flag.aliases ?? []) accepted.set(alias, flag);
91
+ }
92
+
93
+ const values = new Map<string, string | boolean | number>();
94
+ const passthrough: string[] = [];
95
+ const positional: string[] = [];
96
+ const tail: string[] = [];
97
+
98
+ let remainder: string[] | null = null;
99
+ if (opts.honorDoubleDashSeparator === true) {
100
+ const separator = opts.argv.indexOf("--");
101
+ if (separator !== -1) {
102
+ remainder = [...opts.argv.slice(separator + 1)];
103
+ }
104
+ }
105
+ const head: string[] = [
106
+ ...(remainder === null ? opts.argv : opts.argv.slice(0, indexOfSeparator(opts.argv))),
107
+ ];
108
+
109
+ let forwardingTail = false;
110
+ while (head.length !== 0) {
111
+ const current = head.shift()!;
112
+ if (forwardingTail) {
113
+ // Post-sentinel tokens belong to the user's program (e.g. the typia.ts
114
+ // entry's own argv: `ttsx typia.ts generate --input X`). They MUST NOT
115
+ // be forwarded to tsgo — the caller distinguishes `tail` from
116
+ // `passthrough` so script args never reach tsgo's option parser.
117
+ tail.push(current);
118
+ continue;
119
+ }
120
+
121
+ // `--foo=value` / `-p=value` form: split before resolving against the
122
+ // schema. Both long (`--foo`) and short (`-p`) aliases support the
123
+ // inline-value shape — without splitting the short form, `-p=value`
124
+ // would fall through as an unknown token and be forwarded to tsgo,
125
+ // bypassing the launcher's own consumer (e.g. plugin discovery against
126
+ // the wrong project root).
127
+ const equalsIndex = current.startsWith("-") ? current.indexOf("=") : -1;
128
+ const token = equalsIndex === -1 ? current : current.slice(0, equalsIndex);
129
+ const inlineValue =
130
+ equalsIndex === -1 ? undefined : current.slice(equalsIndex + 1);
131
+
132
+ const flag = accepted.get(token);
133
+ if (flag !== undefined) {
134
+ consumeFlag(values, flag, token, inlineValue, head, opts.errorPrefix);
135
+ continue;
136
+ }
137
+
138
+ // Token IS a known flag but is not accepted by THIS subcommand. The
139
+ // engine forwards it to tsgo just like an unknown flag — the same
140
+ // policy the bare-lane parser applied (RC-1 prevention).
141
+ const globalFlag = FLAG_BY_NAME.get(token);
142
+ if (globalFlag !== undefined) {
143
+ forwardKnownButUnaccepted(
144
+ passthrough,
145
+ globalFlag,
146
+ current,
147
+ inlineValue,
148
+ head,
149
+ );
150
+ continue;
151
+ }
152
+
153
+ // Truly unknown `-`-prefixed token: forward to tsgo verbatim. This
154
+ // is what makes `ttsc --strict file.ts` work — ttsc does not need to
155
+ // re-implement every tsgo flag.
156
+ if (current.startsWith("-")) {
157
+ passthrough.push(current);
158
+ continue;
159
+ }
160
+
161
+ // Bare token: positional argument (file / entry / project path).
162
+ positional.push(current);
163
+ if (
164
+ opts.forwardAfterFirstPositional === true &&
165
+ positional.length === 1
166
+ ) {
167
+ forwardingTail = true;
168
+ }
169
+ }
170
+
171
+ if (remainder !== null) {
172
+ // `--` separator: everything after goes to the user program when we are
173
+ // in tail mode (ttsx after the entry), otherwise to tsgo as passthrough.
174
+ const sink = forwardingTail ? tail : passthrough;
175
+ for (const token of remainder) sink.push(token);
176
+ }
177
+
178
+ return { values, passthrough, positional, tail };
179
+ }
180
+
181
+ /**
182
+ * Pull the value for `flag` from either `inlineValue` (`--flag=value`) or
183
+ * the next argv token (`--flag value`), validate it per the schema, and
184
+ * write it to the result map.
185
+ */
186
+ function consumeFlag(
187
+ values: Map<string, string | boolean | number>,
188
+ flag: FlagSpec,
189
+ token: string,
190
+ inlineValue: string | undefined,
191
+ rest: string[],
192
+ errorPrefix: string,
193
+ ): void {
194
+ if (flag.kind === "boolean") {
195
+ if (inlineValue !== undefined) {
196
+ // `--flag=false` / `--flag=true` inline form. Anything other than
197
+ // a recognised literal stays loud: `--singleThreaded=yes` silently
198
+ // becoming `true` is the kind of footgun the RCA's RC-4 class
199
+ // covers. Mirrors `validatePositiveInt`'s style.
200
+ const literal = parseBooleanLiteral(inlineValue);
201
+ if (literal === undefined) {
202
+ throw new Error(
203
+ `${errorPrefix} ${token} expects \`true\` or \`false\`, got ${JSON.stringify(
204
+ inlineValue,
205
+ )}`,
206
+ );
207
+ }
208
+ values.set(flag.name, literal);
209
+ return;
210
+ }
211
+ // Space form `--flag true` / `--flag false`: peek the next token and
212
+ // consume it only when it parses as a boolean literal. tsgo accepts
213
+ // this shape natively; the launcher must mirror it so `ttsc --noEmit
214
+ // false` does not corrupt argv (positional sink getting `false`,
215
+ // tsgo seeing it as a stray input file).
216
+ if (rest.length > 0) {
217
+ const peek = parseBooleanLiteral(rest[0]!);
218
+ if (peek !== undefined) {
219
+ rest.shift();
220
+ values.set(flag.name, peek);
221
+ return;
222
+ }
223
+ }
224
+ values.set(flag.name, true);
225
+ return;
226
+ }
227
+
228
+ const raw =
229
+ inlineValue !== undefined ? inlineValue : takeValueToken(token, rest, errorPrefix);
230
+ if (flag.validator === "positiveInt") {
231
+ values.set(flag.name, validatePositiveInt(token, raw, errorPrefix));
232
+ return;
233
+ }
234
+ values.set(flag.name, raw);
235
+ }
236
+
237
+ /**
238
+ * Read the value token that follows `flag`. Throws if argv ends here OR if
239
+ * the next token looks like another flag (`-` prefix). Without the
240
+ * "looks like a flag" guard `ttsc --cwd --strict src/main.ts` would silently
241
+ * consume `--strict` as the value of `--cwd`, leaving `--strict` lost and
242
+ * `cwd` set to a junk path. Mirrors the symmetric guard already in
243
+ * `forwardKnownButUnaccepted` (RC-1 fairness — the two value-resolution
244
+ * paths must agree on what counts as "a missing value").
245
+ */
246
+ function takeValueToken(
247
+ flag: string,
248
+ rest: string[],
249
+ errorPrefix: string,
250
+ ): string {
251
+ const value = rest.shift();
252
+ if (value === undefined) {
253
+ throw new Error(`${errorPrefix} ${flag} requires a value`);
254
+ }
255
+ if (value.startsWith("-")) {
256
+ // Put it back so the next loop iteration parses it as its own flag.
257
+ rest.unshift(value);
258
+ throw new Error(
259
+ `${errorPrefix} ${flag} requires a value (next token ${JSON.stringify(
260
+ value,
261
+ )} starts with "-")`,
262
+ );
263
+ }
264
+ return value;
265
+ }
266
+
267
+ /**
268
+ * Parse a CLI boolean literal — `true`/`false` only (case-sensitive to
269
+ * match tsgo's parser). Returns `undefined` for any other token so the
270
+ * caller knows to throw or treat as a non-value.
271
+ */
272
+ function parseBooleanLiteral(raw: string): boolean | undefined {
273
+ if (raw === "true") return true;
274
+ if (raw === "false") return false;
275
+ return undefined;
276
+ }
277
+
278
+ /**
279
+ * Validate a `positiveInt` value. Mirrors tsgo's `--checkers minValue:1`
280
+ * constraint so a typo fails loudly at the launcher rather than reaching
281
+ * tsgo with an invalid argument.
282
+ */
283
+ function validatePositiveInt(
284
+ flag: string,
285
+ raw: string,
286
+ errorPrefix: string,
287
+ ): number {
288
+ const value = Number(raw);
289
+ if (!Number.isInteger(value) || value < 1) {
290
+ throw new Error(
291
+ `${errorPrefix} ${flag} expects a positive integer, got ${JSON.stringify(raw)}`,
292
+ );
293
+ }
294
+ return value;
295
+ }
296
+
297
+ /**
298
+ * Forward a flag the schema knows about but the current subcommand does
299
+ * not accept. The launcher will hand it to tsgo (or to native sidecars
300
+ * via `--tsgo-args`); without this branch the parser would lose the
301
+ * value token of a `--flag value` pair.
302
+ */
303
+ function forwardKnownButUnaccepted(
304
+ passthrough: string[],
305
+ flag: FlagSpec,
306
+ original: string,
307
+ inlineValue: string | undefined,
308
+ rest: string[],
309
+ ): void {
310
+ passthrough.push(original);
311
+ // Boolean flags carry no value. `--foo=value` already encodes the value
312
+ // inline. Otherwise the next argv token is the flag's value: keep it
313
+ // adjacent so tsgo receives the pair intact.
314
+ if (flag.kind === "boolean" || inlineValue !== undefined) {
315
+ return;
316
+ }
317
+ if (rest.length === 0) return;
318
+ if (rest[0]!.startsWith("-")) return;
319
+ passthrough.push(rest.shift()!);
320
+ }
321
+
322
+ /**
323
+ * Return the index of the `--` separator in `argv`, or `argv.length` when
324
+ * absent. The parser slices on this index when honorDoubleDashSeparator.
325
+ */
326
+ function indexOfSeparator(argv: readonly string[]): number {
327
+ const index = argv.indexOf("--");
328
+ return index === -1 ? argv.length : index;
329
+ }
330
+
331
+ // -----------------------------------------------------------------------------
332
+ // Typed accessors. The parser stores raw values (string | boolean | number) so
333
+ // the engine stays untyped; callers pick the shape per flag.
334
+ // -----------------------------------------------------------------------------
335
+
336
+ /** Return the boolean value of `flag` or `undefined` if not present. */
337
+ export function getBoolean(
338
+ result: ParseResult,
339
+ flag: string,
340
+ ): boolean | undefined {
341
+ const value = result.values.get(flag);
342
+ if (typeof value === "boolean") return value;
343
+ return undefined;
344
+ }
345
+
346
+ /** Return the string value of `flag` or `undefined` if not present. */
347
+ export function getString(
348
+ result: ParseResult,
349
+ flag: string,
350
+ ): string | undefined {
351
+ const value = result.values.get(flag);
352
+ if (typeof value === "string") return value;
353
+ return undefined;
354
+ }
355
+
356
+ /** Return the numeric value of `flag` or `undefined` if not present. */
357
+ export function getNumber(
358
+ result: ParseResult,
359
+ flag: string,
360
+ ): number | undefined {
361
+ const value = result.values.get(flag);
362
+ if (typeof value === "number") return value;
363
+ return undefined;
364
+ }
365
+
366
+ /** Marker type so docs callers can name the validator without an import dance. */
367
+ export type { ValueValidator };