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.
- package/README.md +3 -0
- package/cmd/ttsc/api_compile.go +15 -2
- package/cmd/ttsc/api_transform.go +15 -2
- package/cmd/ttsc/build.go +34 -4
- package/cmd/ttsc/filter.go +70 -0
- package/cmd/ttsc/filter_strips_unknown_forwarded_flag_test.go +61 -0
- package/cmd/ttsc/flags_gen.go +30 -0
- package/driver/program.go +105 -4
- package/driver/rewrite.go +39 -4
- package/lib/compiler/internal/project/resolveProjectConfig.js +18 -0
- package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +5 -4
- package/lib/compiler/internal/runBuild.js +358 -51
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/flags/parser.d.ts +63 -0
- package/lib/flags/parser.js +254 -0
- package/lib/flags/parser.js.map +1 -0
- package/lib/flags/schema.d.ts +146 -0
- package/lib/flags/schema.js +423 -0
- package/lib/flags/schema.js.map +1 -0
- package/lib/launcher/internal/prepareExecution.js +3 -0
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +71 -147
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +104 -72
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +58 -23
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPlugin.d.ts +24 -0
- package/lib/structures/ITtscPluginCapabilities.d.ts +40 -0
- package/lib/structures/ITtscPluginCapabilities.js +3 -0
- package/lib/structures/ITtscPluginCapabilities.js.map +1 -0
- package/lib/structures/index.d.ts +1 -0
- package/lib/structures/index.js +1 -0
- package/lib/structures/index.js.map +1 -1
- package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +5 -0
- package/lib/structures/internal/TtscCommonOptions.d.ts +19 -0
- package/package.json +9 -9
- package/shim/ast/lint.go +6 -0
- package/src/compiler/internal/project/resolveProjectConfig.ts +20 -0
- package/src/compiler/internal/runBuild.ts +463 -56
- package/src/flags/parser.ts +367 -0
- package/src/flags/schema.ts +589 -0
- package/src/launcher/internal/prepareExecution.ts +3 -0
- package/src/launcher/internal/runTtsc.ts +78 -136
- package/src/launcher/internal/runTtsx.ts +116 -74
- package/src/plugin/internal/loadProjectPlugins.ts +74 -27
- package/src/structures/ITtscPlugin.ts +26 -0
- package/src/structures/ITtscPluginCapabilities.ts +41 -0
- package/src/structures/index.ts +1 -0
- package/src/structures/internal/ITtscLoadedNativePlugin.ts +5 -0
- package/src/structures/internal/TtscCommonOptions.ts +19 -0
- package/utility/flags_gen.go +30 -0
- package/utility/host.go +43 -33
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
13
|
+
import { readProjectConfig } from "./project/readProjectConfig";
|
|
10
14
|
import { resolveBinary } from "./resolveBinary";
|
|
11
15
|
import { resolveTsgo } from "./resolveTsgo";
|
|
12
16
|
import {
|
|
@@ -16,6 +20,17 @@ 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
|
+
|
|
28
|
+
type BuildTiming = {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
lines: string[];
|
|
31
|
+
startedAt: bigint;
|
|
32
|
+
};
|
|
33
|
+
|
|
19
34
|
/**
|
|
20
35
|
* Merge extra environment variables over `process.env`, always injecting
|
|
21
36
|
* `TTSC_NODE_BINARY` so child processes can re-invoke the same Node.js binary
|
|
@@ -30,6 +45,81 @@ function mergeEnv(extra?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
|
30
45
|
return { ...base, ...extra };
|
|
31
46
|
}
|
|
32
47
|
|
|
48
|
+
function createBuildTiming(options: TtscCommonOptions): BuildTiming {
|
|
49
|
+
return {
|
|
50
|
+
enabled: hasDiagnosticsFlag(options),
|
|
51
|
+
lines: [],
|
|
52
|
+
startedAt: process.hrtime.bigint(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasDiagnosticsFlag(options: TtscCommonOptions): boolean {
|
|
57
|
+
return (
|
|
58
|
+
hasEnabledPassthroughFlag(options, "--diagnostics") ||
|
|
59
|
+
hasEnabledPassthroughFlag(options, "--extendedDiagnostics")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasEnabledPassthroughFlag(
|
|
64
|
+
options: TtscCommonOptions,
|
|
65
|
+
flag: string,
|
|
66
|
+
): boolean {
|
|
67
|
+
const passthrough = options.passthrough ?? [];
|
|
68
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
69
|
+
const token = passthrough[i]!;
|
|
70
|
+
if (token.startsWith(`${flag}=`)) {
|
|
71
|
+
return token.slice(flag.length + 1).toLowerCase() !== "false";
|
|
72
|
+
}
|
|
73
|
+
if (token === flag) {
|
|
74
|
+
if (
|
|
75
|
+
i + 1 < passthrough.length &&
|
|
76
|
+
isBooleanLiteral(passthrough[i + 1]!)
|
|
77
|
+
) {
|
|
78
|
+
return passthrough[i + 1]!.toLowerCase() !== "false";
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function recordTiming(
|
|
87
|
+
timing: BuildTiming,
|
|
88
|
+
label: string,
|
|
89
|
+
startedAt: bigint,
|
|
90
|
+
): void {
|
|
91
|
+
if (!timing.enabled) return;
|
|
92
|
+
timing.lines.push(`${label}: ${formatTimingSeconds(hrtimeMs(startedAt))}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function appendTimingOutput(
|
|
96
|
+
result: TtscBuildResult,
|
|
97
|
+
timing: BuildTiming,
|
|
98
|
+
): TtscBuildResult {
|
|
99
|
+
if (!timing.enabled) return result;
|
|
100
|
+
const lines = [
|
|
101
|
+
...timing.lines,
|
|
102
|
+
`ttsc total time: ${formatTimingSeconds(hrtimeMs(timing.startedAt))}`,
|
|
103
|
+
];
|
|
104
|
+
return {
|
|
105
|
+
...result,
|
|
106
|
+
stdout: appendStdout(result.stdout, lines.join("\n") + "\n"),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function appendStdout(stdout: string, text: string): string {
|
|
111
|
+
if (stdout.length === 0 || stdout.endsWith("\n")) return stdout + text;
|
|
112
|
+
return `${stdout}\n${text}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hrtimeMs(startedAt: bigint): number {
|
|
116
|
+
return Number(process.hrtime.bigint() - startedAt) / 1e6;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatTimingSeconds(ms: number): string {
|
|
120
|
+
return `${(ms / 1000).toFixed(3)}s`;
|
|
121
|
+
}
|
|
122
|
+
|
|
33
123
|
/**
|
|
34
124
|
* Build the environment for a native plugin spawn. Injects `TTSC_TSGO_BINARY`
|
|
35
125
|
* and `TTSC_TTSX_BINARY` alongside the base env from `mergeEnv`. For
|
|
@@ -61,24 +151,33 @@ function nativePluginEnv(
|
|
|
61
151
|
* Run `ttsc` against a tsconfig. Returns once the binary exits so the CLI can
|
|
62
152
|
* decide how to surface diagnostics. Does not throw on non-zero exit.
|
|
63
153
|
*/
|
|
64
|
-
export function runBuild(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
154
|
+
export function runBuild(options: RunBuildOptions = {}): TtscBuildResult {
|
|
155
|
+
const timing = createBuildTiming(options);
|
|
156
|
+
const result = runBuildTimed(options, timing);
|
|
157
|
+
return appendTimingOutput(result, timing);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runBuildTimed(
|
|
161
|
+
options: RunBuildOptions,
|
|
162
|
+
timing: BuildTiming,
|
|
69
163
|
): TtscBuildResult {
|
|
164
|
+
const setupStartedAt = process.hrtime.bigint();
|
|
70
165
|
const execution = resolveExecutionContext(options);
|
|
166
|
+
if (execution.nativePlugins.length > 0) {
|
|
167
|
+
recordTiming(timing, "ttsc plugin setup time", setupStartedAt);
|
|
168
|
+
}
|
|
169
|
+
const buildOptions = applyProjectNoEmit(options, execution);
|
|
71
170
|
if (execution.nativePlugins.length > 0) {
|
|
72
171
|
const compilers = execution.nativePlugins.filter(
|
|
73
172
|
(plugin) => plugin.stage === "transform",
|
|
74
173
|
);
|
|
75
|
-
const checked = runNativeCheckPlugins(
|
|
174
|
+
const checked = runNativeCheckPlugins(buildOptions, execution, timing);
|
|
76
175
|
if (checked.status !== 0) {
|
|
77
176
|
return checked;
|
|
78
177
|
}
|
|
79
178
|
|
|
80
|
-
if (
|
|
81
|
-
if (
|
|
179
|
+
if (buildOptions.emit === false) {
|
|
180
|
+
if (buildOptions.format === true) {
|
|
82
181
|
// Format mode is write-only by contract: the lint sidecar
|
|
83
182
|
// already rewrote source files and reported nothing. Running
|
|
84
183
|
// tsgo --noEmit OR a transform compiler afterwards would either
|
|
@@ -97,12 +196,20 @@ export function runBuild(
|
|
|
97
196
|
assertSharedHostCompatibility(compilers, "emit");
|
|
98
197
|
return appendBuildOutput(
|
|
99
198
|
checked,
|
|
100
|
-
buildWithNativeCompilerPlugins(
|
|
199
|
+
buildWithNativeCompilerPlugins(
|
|
200
|
+
buildOptions,
|
|
201
|
+
execution,
|
|
202
|
+
compilers,
|
|
203
|
+
timing,
|
|
204
|
+
),
|
|
101
205
|
);
|
|
102
206
|
}
|
|
207
|
+
if (checkPluginsReportTypeScriptDiagnostics(execution.nativePlugins)) {
|
|
208
|
+
return checked;
|
|
209
|
+
}
|
|
103
210
|
return appendBuildOutput(
|
|
104
211
|
checked,
|
|
105
|
-
runTsgo(execution, ["--noEmit"],
|
|
212
|
+
runTsgo(execution, ["--noEmit"], buildOptions),
|
|
106
213
|
);
|
|
107
214
|
}
|
|
108
215
|
|
|
@@ -111,26 +218,35 @@ export function runBuild(
|
|
|
111
218
|
assertSharedHostCompatibility(compilers, "emit");
|
|
112
219
|
result = appendBuildOutput(
|
|
113
220
|
checked,
|
|
114
|
-
buildWithNativeCompilerPlugins(
|
|
221
|
+
buildWithNativeCompilerPlugins(
|
|
222
|
+
buildOptions,
|
|
223
|
+
execution,
|
|
224
|
+
compilers,
|
|
225
|
+
timing,
|
|
226
|
+
),
|
|
115
227
|
);
|
|
116
228
|
} else {
|
|
117
|
-
if (
|
|
118
|
-
|
|
229
|
+
if (
|
|
230
|
+
buildOptions.skipDiagnosticsCheck !== true &&
|
|
231
|
+
!checkPluginsReportTypeScriptDiagnostics(execution.nativePlugins) &&
|
|
232
|
+
!forwardsTerminalTsgoFlag(buildOptions)
|
|
233
|
+
) {
|
|
234
|
+
const tsgoChecked = runTsgo(execution, ["--noEmit"], buildOptions);
|
|
119
235
|
if (tsgoChecked.status !== 0) {
|
|
120
236
|
return appendBuildOutput(checked, tsgoChecked);
|
|
121
237
|
}
|
|
122
238
|
}
|
|
123
|
-
const args = createTsgoBuildArgs(execution,
|
|
124
|
-
listEmittedFiles:
|
|
239
|
+
const args = createTsgoBuildArgs(execution, buildOptions, {
|
|
240
|
+
listEmittedFiles: buildOptions.forceListEmittedFiles === true,
|
|
125
241
|
});
|
|
126
|
-
const emitted = runTsgoBuild(execution,
|
|
242
|
+
const emitted = runTsgoBuild(execution, buildOptions, args);
|
|
127
243
|
result = appendBuildOutput(checked, emitted);
|
|
128
244
|
}
|
|
129
245
|
|
|
130
246
|
return result;
|
|
131
247
|
}
|
|
132
248
|
|
|
133
|
-
if (
|
|
249
|
+
if (buildOptions.format === true) {
|
|
134
250
|
// Format mode is write-only by contract — see the matching
|
|
135
251
|
// short-circuit in the with-native-plugins branch above. When no
|
|
136
252
|
// native plugin is loaded there is nothing for `ttsc format` to
|
|
@@ -145,18 +261,100 @@ export function runBuild(
|
|
|
145
261
|
};
|
|
146
262
|
}
|
|
147
263
|
|
|
148
|
-
|
|
149
|
-
const checked = runTsgo(execution, ["--noEmit"], options);
|
|
150
|
-
if (checked.status !== 0) {
|
|
151
|
-
return checked;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const args = createTsgoBuildArgs(execution, options, {
|
|
264
|
+
const args = createTsgoBuildArgs(execution, buildOptions, {
|
|
156
265
|
listEmittedFiles:
|
|
157
|
-
|
|
266
|
+
buildOptions.emit !== false &&
|
|
267
|
+
buildOptions.forceListEmittedFiles === true,
|
|
268
|
+
noEmitOnError:
|
|
269
|
+
buildOptions.emit !== false &&
|
|
270
|
+
buildOptions.skipDiagnosticsCheck !== true &&
|
|
271
|
+
!forwardsTerminalTsgoFlag(buildOptions),
|
|
158
272
|
});
|
|
159
|
-
return runTsgoBuild(execution,
|
|
273
|
+
return runTsgoBuild(execution, buildOptions, args);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* A tsconfig-level `noEmit: true` is an analysis-only build unless the user
|
|
278
|
+
* explicitly asks `ttsc --emit` to override it. Treat it like CLI `--noEmit`
|
|
279
|
+
* before composing tsgo/native-host arguments so ttsc does not add emit-only
|
|
280
|
+
* guards around projects that cannot emit.
|
|
281
|
+
*/
|
|
282
|
+
function applyProjectNoEmit(
|
|
283
|
+
options: RunBuildOptions,
|
|
284
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
285
|
+
): RunBuildOptions {
|
|
286
|
+
if (options.emit !== undefined || execution.projectNoEmit !== true) {
|
|
287
|
+
return options;
|
|
288
|
+
}
|
|
289
|
+
return { ...options, emit: false };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function checkPluginsReportTypeScriptDiagnostics(
|
|
293
|
+
plugins: readonly ITtscLoadedNativePlugin[],
|
|
294
|
+
): boolean {
|
|
295
|
+
return plugins.some(
|
|
296
|
+
(plugin) =>
|
|
297
|
+
plugin.stage === "check" && plugin.reportsTypeScriptDiagnostics === true,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Tsgo CLI flags that make `tsgo` print something and exit instead of building
|
|
303
|
+
* (`--showConfig`, `--listFilesOnly`, `--help`, …). ttsc must not add
|
|
304
|
+
* build-only guard flags around these because the forwarded flag asks tsgo to
|
|
305
|
+
* print something and exit instead of compiling the project.
|
|
306
|
+
*
|
|
307
|
+
* Schema-derived: the set is computed from `FLAG_SCHEMA[*].terminal === true`,
|
|
308
|
+
* not hand-maintained next to runBuild. Adding a new terminal flag now means
|
|
309
|
+
* editing `schema.ts` and re-running `pnpm format` — every layer learns about
|
|
310
|
+
* it automatically.
|
|
311
|
+
*/
|
|
312
|
+
const TERMINAL_TSGO_FLAGS: ReadonlySet<string> = (() => {
|
|
313
|
+
// Mirror the legacy hand-list: schema's terminal flags ∪ the `-?` alias
|
|
314
|
+
// tsgo accepts (kept here because it is a tsgo synonym, not a ttsc flag).
|
|
315
|
+
const out = new Set<string>(TERMINAL_FLAGS);
|
|
316
|
+
out.add("-?");
|
|
317
|
+
return out;
|
|
318
|
+
})();
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Report whether the caller forwarded a print-and-exit tsgo flag, so ttsc can
|
|
322
|
+
* avoid adding compile-only flags to a command that is not going to compile.
|
|
323
|
+
*/
|
|
324
|
+
function forwardsTerminalTsgoFlag(options: TtscCommonOptions): boolean {
|
|
325
|
+
return (
|
|
326
|
+
options.passthrough?.some((flag) => TERMINAL_TSGO_FLAGS.has(flag)) ?? false
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Report whether the caller forwarded a flag ttsc adds to tsgo internally —
|
|
332
|
+
* e.g. `--listEmittedFiles` (ttsc adds it to learn emitted paths) or `--noEmit`
|
|
333
|
+
* (ttsc adds it for the pre-emit type-check). When the user also forwards the
|
|
334
|
+
* same flag, post-processing must keep the user-visible effect intact instead
|
|
335
|
+
* of stripping it as ttsc-internal noise.
|
|
336
|
+
*
|
|
337
|
+
* Schema-derived: `FLAG_SCHEMA[*].internalShadow === true`. RC-2 from the RCA
|
|
338
|
+
* (RCA section 3, `--listEmittedFiles` / `--showConfig` swallowed): the
|
|
339
|
+
* per-flag `passthrough.includes("…")` check is now one structural lookup
|
|
340
|
+
* against the schema, not one bespoke `if` per shadow flag.
|
|
341
|
+
*/
|
|
342
|
+
function forwardsInternalShadowFlag(
|
|
343
|
+
options: TtscCommonOptions,
|
|
344
|
+
flag: string,
|
|
345
|
+
): boolean {
|
|
346
|
+
if (!INTERNAL_SHADOW_FLAGS.has(flag)) return false;
|
|
347
|
+
const passthrough = options.passthrough;
|
|
348
|
+
if (passthrough === undefined) return false;
|
|
349
|
+
// Match both the bare form (`--pretty`) and the inline-value form
|
|
350
|
+
// (`--pretty=true`). The launcher's parser preserves these shapes
|
|
351
|
+
// verbatim, so a tolerant prefix compare is what we want — `passthrough`
|
|
352
|
+
// never holds a stray substring that happens to start with the flag
|
|
353
|
+
// because parser positional/passthrough sinks split on whitespace.
|
|
354
|
+
const inlinePrefix = `${flag}=`;
|
|
355
|
+
return passthrough.some(
|
|
356
|
+
(token) => token === flag || token.startsWith(inlinePrefix),
|
|
357
|
+
);
|
|
160
358
|
}
|
|
161
359
|
|
|
162
360
|
/**
|
|
@@ -168,6 +366,7 @@ function buildWithNativeCompilerPlugins(
|
|
|
168
366
|
options: TtscBuildOptions,
|
|
169
367
|
execution: ReturnType<typeof resolveExecutionContext>,
|
|
170
368
|
plugins: readonly ITtscLoadedNativePlugin[],
|
|
369
|
+
timing: BuildTiming,
|
|
171
370
|
): TtscBuildResult {
|
|
172
371
|
const host = selectSharedHostPlugin(plugins);
|
|
173
372
|
return runNativePluginCommand(
|
|
@@ -176,6 +375,8 @@ function buildWithNativeCompilerPlugins(
|
|
|
176
375
|
options,
|
|
177
376
|
execution,
|
|
178
377
|
"ttsc.build",
|
|
378
|
+
timing,
|
|
379
|
+
transformHostTimingLabel(plugins),
|
|
179
380
|
);
|
|
180
381
|
}
|
|
181
382
|
|
|
@@ -186,7 +387,7 @@ function buildWithNativeCompilerPlugins(
|
|
|
186
387
|
function runTsgo(
|
|
187
388
|
execution: ReturnType<typeof resolveExecutionContext>,
|
|
188
389
|
extraArgs: readonly string[],
|
|
189
|
-
options:
|
|
390
|
+
options: RunBuildOptions,
|
|
190
391
|
): TtscBuildResult {
|
|
191
392
|
const res = spawnNative(
|
|
192
393
|
execution.tsgo.binary,
|
|
@@ -195,6 +396,8 @@ function runTsgo(
|
|
|
195
396
|
execution.tsconfig,
|
|
196
397
|
...extraArgs,
|
|
197
398
|
...createTsgoDiagnosticArgs(options),
|
|
399
|
+
...createTsgoThreadingArgs(options),
|
|
400
|
+
...(options.passthrough ?? []),
|
|
198
401
|
],
|
|
199
402
|
{
|
|
200
403
|
cwd: execution.projectRoot,
|
|
@@ -227,7 +430,7 @@ function runTsgo(
|
|
|
227
430
|
*/
|
|
228
431
|
function runTsgoBuild(
|
|
229
432
|
execution: ReturnType<typeof resolveExecutionContext>,
|
|
230
|
-
options:
|
|
433
|
+
options: RunBuildOptions,
|
|
231
434
|
args: readonly string[],
|
|
232
435
|
): TtscBuildResult {
|
|
233
436
|
const res = spawnNative(execution.tsgo.binary, args, {
|
|
@@ -249,7 +452,18 @@ function runTsgoBuild(
|
|
|
249
452
|
stderr: outputText(res.stderr),
|
|
250
453
|
};
|
|
251
454
|
const emittedFiles = parseEmittedFiles(result.stdout);
|
|
252
|
-
|
|
455
|
+
// The `TSFILE:` lines are tsgo's `--listEmittedFiles` output. ttsc adds that
|
|
456
|
+
// flag internally to learn the emitted paths and strips the lines back out
|
|
457
|
+
// as noise — but when the user themselves forwarded `--listEmittedFiles`,
|
|
458
|
+
// the listing is what they asked for, so it must survive to stdout.
|
|
459
|
+
// The lookup is schema-driven (FLAG_SCHEMA marks `--listEmittedFiles` with
|
|
460
|
+
// `internalShadow: true`); see `forwardsInternalShadowFlag` for the RC-2
|
|
461
|
+
// background.
|
|
462
|
+
const userListedEmitted = forwardsInternalShadowFlag(
|
|
463
|
+
options,
|
|
464
|
+
"--listEmittedFiles",
|
|
465
|
+
);
|
|
466
|
+
if (emittedFiles.length !== 0 && !userListedEmitted) {
|
|
253
467
|
result.stdout = stripEmittedFileLines(result.stdout);
|
|
254
468
|
}
|
|
255
469
|
return normalizeBuildOutput(
|
|
@@ -261,12 +475,15 @@ function runTsgoBuild(
|
|
|
261
475
|
/** Build the argument list for a direct `tsgo` build invocation. */
|
|
262
476
|
function createTsgoBuildArgs(
|
|
263
477
|
execution: ReturnType<typeof resolveExecutionContext>,
|
|
264
|
-
options:
|
|
265
|
-
flags: { listEmittedFiles: boolean },
|
|
478
|
+
options: RunBuildOptions,
|
|
479
|
+
flags: { listEmittedFiles: boolean; noEmitOnError?: boolean },
|
|
266
480
|
): string[] {
|
|
267
481
|
const args = ["-p", execution.tsconfig];
|
|
268
482
|
if (options.emit === true) {
|
|
269
483
|
args.push("--noEmit", "false", "--emitDeclarationOnly", "false");
|
|
484
|
+
if (execution.rewriteRelativeImportExtensionsForEmit) {
|
|
485
|
+
args.push("--rewriteRelativeImportExtensions");
|
|
486
|
+
}
|
|
270
487
|
} else if (options.emit === false) {
|
|
271
488
|
args.push("--noEmit");
|
|
272
489
|
}
|
|
@@ -277,15 +494,49 @@ function createTsgoBuildArgs(
|
|
|
277
494
|
args.push("--listEmittedFiles");
|
|
278
495
|
}
|
|
279
496
|
args.push(...createTsgoDiagnosticArgs(options));
|
|
497
|
+
args.push(...createTsgoThreadingArgs(options));
|
|
498
|
+
args.push(...(options.passthrough ?? []));
|
|
499
|
+
if (flags.noEmitOnError === true) {
|
|
500
|
+
args.push("--noEmitOnError");
|
|
501
|
+
}
|
|
280
502
|
return args;
|
|
281
503
|
}
|
|
282
504
|
|
|
283
505
|
/**
|
|
284
506
|
* Return `["--pretty", "false"]` when structured diagnostics are requested so
|
|
285
507
|
* that the output can be parsed line-by-line, or an empty array otherwise.
|
|
508
|
+
*
|
|
509
|
+
* When the user explicitly forwarded `--pretty` (any value), the internal
|
|
510
|
+
* `--pretty false` shadow is dropped so the user wins on the surface. ttsc's
|
|
511
|
+
* own diagnostic parser will then see pretty-formatted output and fall back to
|
|
512
|
+
* surfacing it verbatim — the RC-2 contract that `--pretty`'s `internalShadow:
|
|
513
|
+
* true` flag in `FLAG_SCHEMA` declares. Without this guard the order in
|
|
514
|
+
* `runTsgo` (internal flags first, passthrough last) would still let the user's
|
|
515
|
+
* `--pretty true` override at the tsgo level, but ttsc would have already
|
|
516
|
+
* committed to a structured-diagnostics post-process that no longer matches the
|
|
517
|
+
* actual output.
|
|
286
518
|
*/
|
|
287
519
|
function createTsgoDiagnosticArgs(options: TtscCommonOptions): string[] {
|
|
288
|
-
|
|
520
|
+
if (options.structuredDiagnostics !== true) return [];
|
|
521
|
+
if (forwardsInternalShadowFlag(options, "--pretty")) return [];
|
|
522
|
+
return ["--pretty", "false"];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Forward the `--singleThreaded` / `--checkers` knobs to a `tsgo` invocation.
|
|
527
|
+
* tsgo accepts both flags natively, so the no-plugin build lane only has to
|
|
528
|
+
* pass them through; the type-check and emit passes share this so the checker
|
|
529
|
+
* pool size stays consistent across both.
|
|
530
|
+
*/
|
|
531
|
+
function createTsgoThreadingArgs(options: TtscCommonOptions): string[] {
|
|
532
|
+
const args: string[] = [];
|
|
533
|
+
if (options.singleThreaded === true) {
|
|
534
|
+
args.push("--singleThreaded");
|
|
535
|
+
}
|
|
536
|
+
if (options.checkers !== undefined) {
|
|
537
|
+
args.push("--checkers", String(options.checkers));
|
|
538
|
+
}
|
|
539
|
+
return args;
|
|
289
540
|
}
|
|
290
541
|
|
|
291
542
|
/** Build the argument list for a native plugin `build`/`check` invocation. */
|
|
@@ -302,17 +553,21 @@ function createNativeBuildArgs(
|
|
|
302
553
|
];
|
|
303
554
|
if (options.emit === true) {
|
|
304
555
|
args.push("--emit");
|
|
305
|
-
} else if (options.emit === false) {
|
|
306
|
-
args.push("--noEmit");
|
|
307
556
|
}
|
|
308
557
|
if (options.outDir) {
|
|
309
558
|
args.push("--outDir=" + path.resolve(execution.cwd, options.outDir));
|
|
310
559
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
560
|
+
// Third-party transform hosts already treat the `check` subcommand as a
|
|
561
|
+
// quiet no-emit pass. Keep ttsc-owned build modifiers off that lane so older
|
|
562
|
+
// strict hosts do not reject unknown optional flags before analysis starts.
|
|
563
|
+
if (options.emit !== false) {
|
|
564
|
+
if (options.quiet === false) {
|
|
565
|
+
args.push("--verbose");
|
|
566
|
+
} else if (options.quiet === true) {
|
|
567
|
+
args.push("--quiet");
|
|
568
|
+
}
|
|
315
569
|
}
|
|
570
|
+
args.push(...createNativeTsgoArgs(options));
|
|
316
571
|
return args;
|
|
317
572
|
}
|
|
318
573
|
|
|
@@ -320,6 +575,7 @@ function createNativeBuildArgs(
|
|
|
320
575
|
function createNativeCheckArgs(
|
|
321
576
|
execution: ReturnType<typeof resolveExecutionContext>,
|
|
322
577
|
options: TtscBuildOptions,
|
|
578
|
+
plugin: ITtscLoadedNativePlugin,
|
|
323
579
|
): string[] {
|
|
324
580
|
const args = [
|
|
325
581
|
nativeCheckSubcommand(options),
|
|
@@ -335,9 +591,147 @@ function createNativeCheckArgs(
|
|
|
335
591
|
} else if (options.quiet === true) {
|
|
336
592
|
args.push("--quiet");
|
|
337
593
|
}
|
|
594
|
+
args.push(...createNativeCheckThreadingArgs(options, plugin));
|
|
595
|
+
args.push(...createNativeCheckDiagnosticsArgs(options, plugin));
|
|
596
|
+
args.push(...createNativeTsgoArgs(options));
|
|
597
|
+
return args;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// `--singleThreaded` / `--checkers` are forwarded to native check-stage hosts
|
|
601
|
+
// only when the host is one ttsc itself owns (currently `@ttsc/lint`). #113
|
|
602
|
+
// forwarded both flags as bare CLI tokens to every native sidecar, then
|
|
603
|
+
// commit ad3443a reverted that across the board because a third-party host
|
|
604
|
+
// built before #113 has no `singleThreaded` / `checkers` flag in its
|
|
605
|
+
// `flag.FlagSet` and would exit 2 on the unknown flag — so
|
|
606
|
+
// `ttsc --singleThreaded` failed deterministically on every typia/nestia
|
|
607
|
+
// transform-plugin project.
|
|
608
|
+
//
|
|
609
|
+
// The performance ceiling that caused, though, is real: format/check passes
|
|
610
|
+
// through the lint sidecar are dominated by parallel parse + parallel rule
|
|
611
|
+
// walk, and with the threading knob silently dropped, MT and ST runs of
|
|
612
|
+
// `ttsc format` produced identical wall-clock numbers — the benchmark cell
|
|
613
|
+
// became a non-measurement. The lint sidecar is built and shipped from this
|
|
614
|
+
// repo, accepts both flags via `parseSubcommandFlags`, and threads them down
|
|
615
|
+
// to `loadProgram` (parse phase) and `engine.SetSerial` (rule walk). The host
|
|
616
|
+
// opts in through `capabilities.threadingArgs`, so a third-party check-stage
|
|
617
|
+
// plugin keeps the strict-host behavior from ad3443a unless it declares the
|
|
618
|
+
// same contract. Transform-stage hosts are never reached by this path (they go
|
|
619
|
+
// through `createNativeBuildArgs`), so the typia/nestia regression remains
|
|
620
|
+
// pinned by `test_plugin_corpus_single_threaded_flag_does_not_break_a_native_plugin_build`.
|
|
621
|
+
function createNativeCheckThreadingArgs(
|
|
622
|
+
options: TtscCommonOptions,
|
|
623
|
+
plugin: ITtscLoadedNativePlugin,
|
|
624
|
+
): string[] {
|
|
625
|
+
if (!nativeHostAcceptsThreadingArgs(plugin)) return [];
|
|
626
|
+
const args: string[] = [];
|
|
627
|
+
if (options.singleThreaded === true) {
|
|
628
|
+
args.push("--singleThreaded");
|
|
629
|
+
}
|
|
630
|
+
if (options.checkers !== undefined) {
|
|
631
|
+
args.push("--checkers=" + String(options.checkers));
|
|
632
|
+
}
|
|
338
633
|
return args;
|
|
339
634
|
}
|
|
340
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Return true when the loaded native check-stage host has declared
|
|
638
|
+
* `capabilities.threadingArgs` in its plugin descriptor.
|
|
639
|
+
*
|
|
640
|
+
* The lint sidecar (`packages/lint/src/index.ts::createTtscPlugin`) opts in
|
|
641
|
+
* because its `parseSubcommandFlags` handler accepts `--singleThreaded` and
|
|
642
|
+
* `--checkers` directly and threads them into `loadProgram` (parse phase) and
|
|
643
|
+
* `engine.SetSerial` (rule walk). Any other check-stage host that has not
|
|
644
|
+
* declared the capability is treated as a third-party binary whose flag set is
|
|
645
|
+
* unknown, matching the conservative default from commit ad3443a.
|
|
646
|
+
*
|
|
647
|
+
* The capability flag replaces the prior `plugin.name === "@ttsc/lint"` string
|
|
648
|
+
* check: routing on a descriptor field instead of the plugin name lets the next
|
|
649
|
+
* first-party check-stage plugin opt in without ttsc needing to learn its name.
|
|
650
|
+
* See `ITtscPluginCapabilities` and issue #125 for the broader CLI-parser
|
|
651
|
+
* cleanup this is the quick-win step of.
|
|
652
|
+
*/
|
|
653
|
+
function nativeHostAcceptsThreadingArgs(
|
|
654
|
+
plugin: ITtscLoadedNativePlugin,
|
|
655
|
+
): boolean {
|
|
656
|
+
return plugin.capabilities?.threadingArgs === true;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function createNativeCheckDiagnosticsArgs(
|
|
660
|
+
options: TtscCommonOptions,
|
|
661
|
+
plugin: ITtscLoadedNativePlugin,
|
|
662
|
+
): string[] {
|
|
663
|
+
if (!nativeHostAcceptsDiagnosticsTiming(plugin)) return [];
|
|
664
|
+
if (!hasDiagnosticsFlag(options)) return [];
|
|
665
|
+
return ["--diagnostics"];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function nativeHostAcceptsDiagnosticsTiming(
|
|
669
|
+
plugin: ITtscLoadedNativePlugin,
|
|
670
|
+
): boolean {
|
|
671
|
+
return plugin.capabilities?.diagnosticsTiming === true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function transformHostTimingLabel(
|
|
675
|
+
plugins: readonly ITtscLoadedNativePlugin[],
|
|
676
|
+
): string {
|
|
677
|
+
return (
|
|
678
|
+
`ttsc transform host [` +
|
|
679
|
+
`${plugins.map((plugin) => plugin.name).join(", ")}] time`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Forward the tsgo flags ttsc did not recognize to a native sidecar as one
|
|
685
|
+
* JSON-encoded `--tsgo-args` flag. The sidecar replays them through tsgo's own
|
|
686
|
+
* option parser onto `CompilerOptions`, so a flag like `ttsc --strict` reaches
|
|
687
|
+
* a plugin build the same way it reaches the plain tsgo lane. Encoded as a
|
|
688
|
+
* single token so the sidecars' unknown-flag filters keep it intact.
|
|
689
|
+
*/
|
|
690
|
+
function createNativeTsgoArgs(options: TtscCommonOptions): string[] {
|
|
691
|
+
const passthrough = nativeTsgoPassthroughArgs(options);
|
|
692
|
+
if (passthrough === undefined || passthrough.length === 0) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
return ["--tsgo-args=" + JSON.stringify(passthrough)];
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function nativeTsgoPassthroughArgs(
|
|
699
|
+
options: TtscCommonOptions,
|
|
700
|
+
): readonly string[] | undefined {
|
|
701
|
+
const passthrough = options.passthrough;
|
|
702
|
+
if (passthrough === undefined) return undefined;
|
|
703
|
+
const out: string[] = [];
|
|
704
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
705
|
+
const token = passthrough[i]!;
|
|
706
|
+
if (isDiagnosticsPassthroughFlag(token)) {
|
|
707
|
+
if (
|
|
708
|
+
!token.includes("=") &&
|
|
709
|
+
i + 1 < passthrough.length &&
|
|
710
|
+
isBooleanLiteral(passthrough[i + 1]!)
|
|
711
|
+
) {
|
|
712
|
+
i++;
|
|
713
|
+
}
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
out.push(token);
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function isDiagnosticsPassthroughFlag(token: string): boolean {
|
|
722
|
+
return (
|
|
723
|
+
token === "--diagnostics" ||
|
|
724
|
+
token.startsWith("--diagnostics=") ||
|
|
725
|
+
token === "--extendedDiagnostics" ||
|
|
726
|
+
token.startsWith("--extendedDiagnostics=")
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function isBooleanLiteral(token: string): boolean {
|
|
731
|
+
const normalized = token.toLowerCase();
|
|
732
|
+
return normalized === "true" || normalized === "false";
|
|
733
|
+
}
|
|
734
|
+
|
|
341
735
|
/**
|
|
342
736
|
* Decide which native plugin subcommand the lint sidecar should run for the
|
|
343
737
|
* current `runBuild` invocation. The launcher selects exactly one of `fix` /
|
|
@@ -373,6 +767,7 @@ function serializeNativePlugins(
|
|
|
373
767
|
function runNativeCheckPlugins(
|
|
374
768
|
options: TtscBuildOptions,
|
|
375
769
|
execution: ReturnType<typeof resolveExecutionContext>,
|
|
770
|
+
timing: BuildTiming,
|
|
376
771
|
): TtscBuildResult {
|
|
377
772
|
let out: TtscBuildResult = {
|
|
378
773
|
diagnostics: [],
|
|
@@ -385,10 +780,12 @@ function runNativeCheckPlugins(
|
|
|
385
780
|
)) {
|
|
386
781
|
const result = runNativePluginCommand(
|
|
387
782
|
plugin,
|
|
388
|
-
createNativeCheckArgs(execution, options),
|
|
783
|
+
createNativeCheckArgs(execution, options, plugin),
|
|
389
784
|
options,
|
|
390
785
|
execution,
|
|
391
786
|
"ttsc.check",
|
|
787
|
+
timing,
|
|
788
|
+
`ttsc check plugin ${plugin.name} time`,
|
|
392
789
|
);
|
|
393
790
|
out = appendBuildOutput(out, result);
|
|
394
791
|
if (result.status !== 0) {
|
|
@@ -409,12 +806,16 @@ function runNativePluginCommand(
|
|
|
409
806
|
options: TtscBuildOptions,
|
|
410
807
|
execution: ReturnType<typeof resolveExecutionContext>,
|
|
411
808
|
label: string,
|
|
809
|
+
timing: BuildTiming,
|
|
810
|
+
timingLabel: string,
|
|
412
811
|
): TtscBuildResult {
|
|
812
|
+
const startedAt = process.hrtime.bigint();
|
|
413
813
|
const res = spawnNative(plugin.binary, args, {
|
|
414
814
|
cwd: execution.projectRoot,
|
|
415
815
|
env: nativePluginEnv(options.env, execution, plugin),
|
|
416
816
|
encoding: "utf8",
|
|
417
817
|
});
|
|
818
|
+
recordTiming(timing, timingLabel, startedAt);
|
|
418
819
|
if (res.error) {
|
|
419
820
|
throw new Error(
|
|
420
821
|
`${label}: failed to spawn ${plugin.binary}: ${res.error.message}`,
|
|
@@ -458,30 +859,36 @@ export function appendBuildOutput(
|
|
|
458
859
|
* here so every code path in `runBuild` shares the same resolution logic.
|
|
459
860
|
*/
|
|
460
861
|
function resolveExecutionContext(
|
|
461
|
-
options: TtscCommonOptions & { tsconfig?: string },
|
|
862
|
+
options: TtscCommonOptions & { emit?: boolean; tsconfig?: string },
|
|
462
863
|
) {
|
|
463
864
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
464
|
-
const
|
|
865
|
+
const project = readProjectConfig({
|
|
465
866
|
cwd,
|
|
867
|
+
projectRoot: options.projectRoot,
|
|
466
868
|
tsconfig: options.tsconfig,
|
|
467
869
|
});
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
: path.dirname(tsconfig);
|
|
870
|
+
const tsconfig = project.path;
|
|
871
|
+
const projectRoot = project.root;
|
|
471
872
|
const tsgo = resolveTsgo({ ...options, cwd: projectRoot });
|
|
472
|
-
const
|
|
473
|
-
const loaded =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
873
|
+
const hasPlugins = hasProjectPluginEntries(project, options.plugins);
|
|
874
|
+
const loaded = hasPlugins
|
|
875
|
+
? loadProjectPlugins({
|
|
876
|
+
binary: resolveBinary(options) ?? "",
|
|
877
|
+
cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
|
|
878
|
+
cwd,
|
|
879
|
+
entries: options.plugins,
|
|
880
|
+
projectRoot,
|
|
881
|
+
tsconfig,
|
|
882
|
+
})
|
|
883
|
+
: { nativePlugins: [] };
|
|
481
884
|
return {
|
|
482
885
|
cwd,
|
|
483
886
|
nativePlugins: loaded.nativePlugins,
|
|
887
|
+
projectNoEmit: project.compilerOptions.noEmit === true,
|
|
484
888
|
projectRoot,
|
|
889
|
+
rewriteRelativeImportExtensionsForEmit:
|
|
890
|
+
options.emit === true &&
|
|
891
|
+
project.compilerOptions.allowImportingTsExtensions === true,
|
|
485
892
|
tsgo,
|
|
486
893
|
tsconfig,
|
|
487
894
|
};
|