ttsc 0.14.0-dev.20260529.2 → 0.14.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.
- package/README.md +5 -5
- package/internal/lspserver/lsp_proxy.go +130 -21
- package/lib/TtscCompiler.js +14 -14
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/runBuild.js +1 -2
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/flags/parser.d.ts +18 -20
- package/lib/flags/parser.js +29 -38
- package/lib/flags/parser.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +65 -21
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.js +38 -0
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/structures/ITtscCompilerResult.d.ts +2 -2
- package/lib/structures/ITtscCompilerTransformation.d.ts +2 -2
- package/package.json +9 -9
- package/src/TtscCompiler.ts +15 -17
- package/src/compiler/internal/runBuild.ts +1 -4
- package/src/flags/parser.ts +45 -51
- package/src/launcher/internal/runTtsc.ts +71 -28
- package/src/plugin/internal/buildSourcePlugin.ts +43 -0
- package/src/structures/ITtscCompilerResult.ts +2 -2
- package/src/structures/ITtscCompilerTransformation.ts +2 -2
package/src/flags/parser.ts
CHANGED
|
@@ -10,19 +10,14 @@
|
|
|
10
10
|
//
|
|
11
11
|
// The schema is the spec; behaviour at every layer boundary is determined by
|
|
12
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";
|
|
13
|
+
import type { AnySubcommand, FlagSpec, ValueValidator } from "./schema";
|
|
19
14
|
import { FLAG_BY_NAME, flagsForSubcommand } from "./schema";
|
|
20
15
|
|
|
21
16
|
/**
|
|
22
|
-
* Per-subcommand parse result. `values` is keyed by the canonical flag
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
17
|
+
* Per-subcommand parse result. `values` is keyed by the canonical flag name
|
|
18
|
+
* (e.g. `"--singleThreaded"`); boolean flags resolve to `true`/`false`, value
|
|
19
|
+
* flags to the parsed value type. Callers narrow with the helpers below
|
|
20
|
+
* (`getBoolean`, `getString`, `getNumber`).
|
|
26
21
|
*/
|
|
27
22
|
export interface ParseResult {
|
|
28
23
|
/** Canonical flag name → resolved value. */
|
|
@@ -32,17 +27,15 @@ export interface ParseResult {
|
|
|
32
27
|
/** Bare non-flag positional arguments, in original order. */
|
|
33
28
|
readonly positional: readonly string[];
|
|
34
29
|
/**
|
|
35
|
-
* Tokens that arrived after the `forwardAfterFirstPositional` sentinel.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
30
|
+
* Tokens that arrived after the `forwardAfterFirstPositional` sentinel. These
|
|
31
|
+
* are intended for the user's program (e.g. ttsx's entry-file argv); they are
|
|
32
|
+
* NOT forwarded to tsgo. Always empty when `forwardAfterFirstPositional` is
|
|
33
|
+
* false.
|
|
39
34
|
*/
|
|
40
35
|
readonly tail: readonly string[];
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
/**
|
|
44
|
-
* Options controlling a single `parseFlags` invocation.
|
|
45
|
-
*/
|
|
38
|
+
/** Options controlling a single `parseFlags` invocation. */
|
|
46
39
|
export interface ParseOptions {
|
|
47
40
|
/** Which subcommand's flag subset to accept. */
|
|
48
41
|
readonly subcommand: AnySubcommand;
|
|
@@ -54,24 +47,24 @@ export interface ParseOptions {
|
|
|
54
47
|
*/
|
|
55
48
|
readonly errorPrefix: string;
|
|
56
49
|
/**
|
|
57
|
-
* `true` to treat the FIRST positional token as a sentinel that switches
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
50
|
+
* `true` to treat the FIRST positional token as a sentinel that switches the
|
|
51
|
+
* engine to "forward everything after" mode (ttsx's entry-file behaviour:
|
|
52
|
+
* tokens after the entry are runtime argv, not tsgo flags). The sentinel
|
|
53
|
+
* itself is still recorded as a positional argument.
|
|
61
54
|
*/
|
|
62
55
|
readonly forwardAfterFirstPositional?: boolean;
|
|
63
56
|
/**
|
|
64
|
-
* Optional `"--"` separator handling: when present in argv, every token
|
|
65
|
-
*
|
|
57
|
+
* Optional `"--"` separator handling: when present in argv, every token after
|
|
58
|
+
* `--` is appended to `passthrough` as-is (ttsx already does this).
|
|
66
59
|
*/
|
|
67
60
|
readonly honorDoubleDashSeparator?: boolean;
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
/**
|
|
71
64
|
* Parse `argv` according to FLAG_SCHEMA filtered by `subcommand`. Returns a
|
|
72
|
-
* `ParseResult`. Throws `Error` (with the configured prefix) on invalid
|
|
73
|
-
*
|
|
74
|
-
*
|
|
65
|
+
* `ParseResult`. Throws `Error` (with the configured prefix) on invalid input —
|
|
66
|
+
* unknown subcommand-only flag, missing required value, value that fails its
|
|
67
|
+
* validator.
|
|
75
68
|
*/
|
|
76
69
|
export function parseFlags(opts: ParseOptions): ParseResult {
|
|
77
70
|
const accepted = new Map<string, FlagSpec>();
|
|
@@ -103,7 +96,9 @@ export function parseFlags(opts: ParseOptions): ParseResult {
|
|
|
103
96
|
}
|
|
104
97
|
}
|
|
105
98
|
const head: string[] = [
|
|
106
|
-
...(remainder === null
|
|
99
|
+
...(remainder === null
|
|
100
|
+
? opts.argv
|
|
101
|
+
: opts.argv.slice(0, indexOfSeparator(opts.argv))),
|
|
107
102
|
];
|
|
108
103
|
|
|
109
104
|
let forwardingTail = false;
|
|
@@ -160,10 +155,7 @@ export function parseFlags(opts: ParseOptions): ParseResult {
|
|
|
160
155
|
|
|
161
156
|
// Bare token: positional argument (file / entry / project path).
|
|
162
157
|
positional.push(current);
|
|
163
|
-
if (
|
|
164
|
-
opts.forwardAfterFirstPositional === true &&
|
|
165
|
-
positional.length === 1
|
|
166
|
-
) {
|
|
158
|
+
if (opts.forwardAfterFirstPositional === true && positional.length === 1) {
|
|
167
159
|
forwardingTail = true;
|
|
168
160
|
}
|
|
169
161
|
}
|
|
@@ -179,9 +171,9 @@ export function parseFlags(opts: ParseOptions): ParseResult {
|
|
|
179
171
|
}
|
|
180
172
|
|
|
181
173
|
/**
|
|
182
|
-
* Pull the value for `flag` from either `inlineValue` (`--flag=value`) or
|
|
183
|
-
*
|
|
184
|
-
*
|
|
174
|
+
* Pull the value for `flag` from either `inlineValue` (`--flag=value`) or the
|
|
175
|
+
* next argv token (`--flag value`), validate it per the schema, and write it to
|
|
176
|
+
* the result map.
|
|
185
177
|
*/
|
|
186
178
|
function consumeFlag(
|
|
187
179
|
values: Map<string, string | boolean | number>,
|
|
@@ -226,7 +218,9 @@ function consumeFlag(
|
|
|
226
218
|
}
|
|
227
219
|
|
|
228
220
|
const raw =
|
|
229
|
-
inlineValue !== undefined
|
|
221
|
+
inlineValue !== undefined
|
|
222
|
+
? inlineValue
|
|
223
|
+
: takeValueToken(token, rest, errorPrefix);
|
|
230
224
|
if (flag.validator === "positiveInt") {
|
|
231
225
|
values.set(flag.name, validatePositiveInt(token, raw, errorPrefix));
|
|
232
226
|
return;
|
|
@@ -235,13 +229,13 @@ function consumeFlag(
|
|
|
235
229
|
}
|
|
236
230
|
|
|
237
231
|
/**
|
|
238
|
-
* Read the value token that follows `flag`. Throws if argv ends here OR if
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* `forwardKnownButUnaccepted` (RC-1 fairness — the two value-resolution
|
|
244
|
-
*
|
|
232
|
+
* Read the value token that follows `flag`. Throws if argv ends here OR if the
|
|
233
|
+
* next token looks like another flag (`-` prefix). Without the "looks like a
|
|
234
|
+
* flag" guard `ttsc --cwd --strict src/main.ts` would silently consume
|
|
235
|
+
* `--strict` as the value of `--cwd`, leaving `--strict` lost and `cwd` set to
|
|
236
|
+
* a junk path. Mirrors the symmetric guard already in
|
|
237
|
+
* `forwardKnownButUnaccepted` (RC-1 fairness — the two value-resolution paths
|
|
238
|
+
* must agree on what counts as "a missing value").
|
|
245
239
|
*/
|
|
246
240
|
function takeValueToken(
|
|
247
241
|
flag: string,
|
|
@@ -265,9 +259,9 @@ function takeValueToken(
|
|
|
265
259
|
}
|
|
266
260
|
|
|
267
261
|
/**
|
|
268
|
-
* Parse a CLI boolean literal — `true`/`false` only (case-sensitive to
|
|
269
|
-
*
|
|
270
|
-
*
|
|
262
|
+
* Parse a CLI boolean literal — `true`/`false` only (case-sensitive to match
|
|
263
|
+
* tsgo's parser). Returns `undefined` for any other token so the caller knows
|
|
264
|
+
* to throw or treat as a non-value.
|
|
271
265
|
*/
|
|
272
266
|
function parseBooleanLiteral(raw: string): boolean | undefined {
|
|
273
267
|
if (raw === "true") return true;
|
|
@@ -277,8 +271,8 @@ function parseBooleanLiteral(raw: string): boolean | undefined {
|
|
|
277
271
|
|
|
278
272
|
/**
|
|
279
273
|
* Validate a `positiveInt` value. Mirrors tsgo's `--checkers minValue:1`
|
|
280
|
-
* constraint so a typo fails loudly at the launcher rather than reaching
|
|
281
|
-
*
|
|
274
|
+
* constraint so a typo fails loudly at the launcher rather than reaching tsgo
|
|
275
|
+
* with an invalid argument.
|
|
282
276
|
*/
|
|
283
277
|
function validatePositiveInt(
|
|
284
278
|
flag: string,
|
|
@@ -295,10 +289,10 @@ function validatePositiveInt(
|
|
|
295
289
|
}
|
|
296
290
|
|
|
297
291
|
/**
|
|
298
|
-
* Forward a flag the schema knows about but the current subcommand does
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
292
|
+
* Forward a flag the schema knows about but the current subcommand does not
|
|
293
|
+
* accept. The launcher will hand it to tsgo (or to native sidecars via
|
|
294
|
+
* `--tsgo-args`); without this branch the parser would lose the value token of
|
|
295
|
+
* a `--flag value` pair.
|
|
302
296
|
*/
|
|
303
297
|
function forwardKnownButUnaccepted(
|
|
304
298
|
passthrough: string[],
|
|
@@ -558,37 +558,50 @@ function runWatch(
|
|
|
558
558
|
tsconfig: options.tsconfig,
|
|
559
559
|
}),
|
|
560
560
|
);
|
|
561
|
-
const directories = collectWatchDirectories(root);
|
|
562
|
-
const watchers = directories.map((dir) =>
|
|
563
|
-
fs.watch(dir, { persistent: true }, () => trigger()),
|
|
564
|
-
);
|
|
565
561
|
let running = false;
|
|
566
562
|
let rerun = false;
|
|
567
563
|
let timer: NodeJS.Timeout | null = null;
|
|
564
|
+
// Tracks the most recent build's exit code so the watch session can exit
|
|
565
|
+
// non-zero when its latest rebuild failed, instead of always reporting 0.
|
|
566
|
+
let lastStatus = 0;
|
|
568
567
|
|
|
569
568
|
const runOnce = () => {
|
|
570
569
|
running = true;
|
|
571
|
-
|
|
572
|
-
|
|
570
|
+
try {
|
|
571
|
+
if (!options.preserveWatchOutput) {
|
|
572
|
+
process.stdout.write("\x1bc");
|
|
573
|
+
}
|
|
574
|
+
process.stdout.write(
|
|
575
|
+
`[ttsc] rebuilding at ${new Date().toLocaleTimeString()}\n`,
|
|
576
|
+
);
|
|
577
|
+
// The debounced rebuild fires from setTimeout, outside runTtsc's
|
|
578
|
+
// top-level try/catch. runBuild/runSingleFile/loadProjectPlugins/
|
|
579
|
+
// readProjectConfig can throw (plugin go-build failure, tsconfig edited
|
|
580
|
+
// to invalid JSON, single-file invariant). Catch here so a throwing
|
|
581
|
+
// rebuild reports a clear failure and the watcher keeps running instead
|
|
582
|
+
// of crashing the process with an uncaught exception.
|
|
583
|
+
const status =
|
|
584
|
+
invocation.files.length !== 0
|
|
585
|
+
? runSingleFile(invocation)
|
|
586
|
+
: (() => {
|
|
587
|
+
const result = runBuild(
|
|
588
|
+
checkOnly ? { ...invocation, emit: false } : invocation,
|
|
589
|
+
);
|
|
590
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
591
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
592
|
+
return result.status;
|
|
593
|
+
})();
|
|
594
|
+
lastStatus = status;
|
|
595
|
+
process.stdout.write(
|
|
596
|
+
`[ttsc] ${status === 0 ? "watch build complete" : "watch build failed"}\n`,
|
|
597
|
+
);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
process.stderr.write(`${formatError(error)}\n`);
|
|
600
|
+
lastStatus = lastStatus === 0 ? 2 : lastStatus;
|
|
601
|
+
process.stdout.write(`[ttsc] watch build failed\n`);
|
|
602
|
+
} finally {
|
|
603
|
+
running = false;
|
|
573
604
|
}
|
|
574
|
-
process.stdout.write(
|
|
575
|
-
`[ttsc] rebuilding at ${new Date().toLocaleTimeString()}\n`,
|
|
576
|
-
);
|
|
577
|
-
const status =
|
|
578
|
-
invocation.files.length !== 0
|
|
579
|
-
? runSingleFile(invocation)
|
|
580
|
-
: (() => {
|
|
581
|
-
const result = runBuild(
|
|
582
|
-
checkOnly ? { ...invocation, emit: false } : invocation,
|
|
583
|
-
);
|
|
584
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
585
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
586
|
-
return result.status;
|
|
587
|
-
})();
|
|
588
|
-
process.stdout.write(
|
|
589
|
-
`[ttsc] ${status === 0 ? "watch build complete" : "watch build failed"}\n`,
|
|
590
|
-
);
|
|
591
|
-
running = false;
|
|
592
605
|
if (rerun) {
|
|
593
606
|
rerun = false;
|
|
594
607
|
trigger();
|
|
@@ -603,22 +616,52 @@ function runWatch(
|
|
|
603
616
|
timer = setTimeout(runOnce, 60);
|
|
604
617
|
};
|
|
605
618
|
|
|
619
|
+
const directories = collectWatchDirectories(root);
|
|
620
|
+
const watchers = directories.map((dir) => {
|
|
621
|
+
const watcher = fs.watch(dir, { persistent: true }, () => trigger());
|
|
622
|
+
// Without an error handler Node rethrows watcher errors as uncaught
|
|
623
|
+
// exceptions, which would crash the session. inotify limits (ENOSPC) and
|
|
624
|
+
// transient FS errors should be logged while the session stays alive.
|
|
625
|
+
watcher.on("error", (err) => {
|
|
626
|
+
process.stderr.write(
|
|
627
|
+
`[ttsc] watch error on ${path.relative(cwd, dir) || "."}: ${formatError(err)}\n`,
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
return watcher;
|
|
631
|
+
});
|
|
632
|
+
|
|
606
633
|
const close = () => {
|
|
607
634
|
if (timer) clearTimeout(timer);
|
|
608
635
|
for (const watcher of watchers) watcher.close();
|
|
609
636
|
};
|
|
610
637
|
process.on("SIGINT", () => {
|
|
611
638
|
close();
|
|
612
|
-
process.exit(
|
|
639
|
+
process.exit(toExitCode(lastStatus));
|
|
613
640
|
});
|
|
614
641
|
process.on("SIGTERM", () => {
|
|
615
642
|
close();
|
|
616
|
-
process.exit(
|
|
643
|
+
process.exit(toExitCode(lastStatus));
|
|
617
644
|
});
|
|
618
645
|
|
|
619
646
|
process.stdout.write(`[ttsc] watching ${path.relative(cwd, root) || "."}\n`);
|
|
620
|
-
|
|
621
|
-
|
|
647
|
+
try {
|
|
648
|
+
runOnce();
|
|
649
|
+
} catch (error) {
|
|
650
|
+
// runOnce already swallows build throws, but guard against any unforeseen
|
|
651
|
+
// throw escaping the first pass: tear down the persistent watchers so the
|
|
652
|
+
// event loop drains and the process exits cleanly with a non-zero code
|
|
653
|
+
// instead of hanging on live fs.watch handles.
|
|
654
|
+
close();
|
|
655
|
+
process.stderr.write(`${formatError(error)}\n`);
|
|
656
|
+
return toExitCode(lastStatus === 0 ? 2 : lastStatus);
|
|
657
|
+
}
|
|
658
|
+
return toExitCode(lastStatus);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Coerces a build status into a valid process exit code: 0 stays 0, any
|
|
662
|
+
// non-zero (or non-finite) status collapses to 1 so the session signals failure.
|
|
663
|
+
function toExitCode(status: number): number {
|
|
664
|
+
return Number.isInteger(status) && status >= 0 && status <= 255 ? status : 1;
|
|
622
665
|
}
|
|
623
666
|
|
|
624
667
|
function collectWatchDirectories(root: string): string[] {
|
|
@@ -1004,8 +1004,51 @@ function isHashableFile(name: string): boolean {
|
|
|
1004
1004
|
return !name.endsWith("~");
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
|
+
// Per-process memo for the Go compiler identity. `computeCacheKey` runs once
|
|
1008
|
+
// per source plugin, so an N-plugin project that points every plugin at the
|
|
1009
|
+
// same toolchain would otherwise pay N `go version` spawns plus N ~150MB
|
|
1010
|
+
// binary hashes for a value that does not change between plugins. The result
|
|
1011
|
+
// is a pure function of the go binary's resolved real path plus its on-disk
|
|
1012
|
+
// content; the memo key therefore mixes the resolved real path with a cheap
|
|
1013
|
+
// content signature (byte size + nanosecond mtime). That signature changes if
|
|
1014
|
+
// a long-lived host rewrites the binary in place between calls, so the memo
|
|
1015
|
+
// re-derives the identity exactly as the un-memoized code would and the
|
|
1016
|
+
// cache-key bytes stay byte-for-byte identical to today. `go version` reads no
|
|
1017
|
+
// cwd/custom env (the spawn passes neither), so cwd is not part of the key.
|
|
1018
|
+
const goCompilerIdentityCache = new Map<string, string>();
|
|
1019
|
+
|
|
1007
1020
|
function resolveGoCompilerIdentity(goBinary: string): string {
|
|
1008
1021
|
const resolved = resolveExecutableIdentityPath(goBinary);
|
|
1022
|
+
const memoKey = goCompilerIdentityMemoKey(goBinary, resolved);
|
|
1023
|
+
if (memoKey !== null) {
|
|
1024
|
+
const cached = goCompilerIdentityCache.get(memoKey);
|
|
1025
|
+
if (cached !== undefined) {
|
|
1026
|
+
return cached;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const identity = computeGoCompilerIdentity(goBinary, resolved);
|
|
1030
|
+
if (memoKey !== null) {
|
|
1031
|
+
goCompilerIdentityCache.set(memoKey, identity);
|
|
1032
|
+
}
|
|
1033
|
+
return identity;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Build a memo key that pins both the resolved binary path and its current
|
|
1037
|
+
// content. Returns null (skip caching, recompute) when the binary cannot be
|
|
1038
|
+
// stat-ed, so the rare unstattable case never serves a stale identity.
|
|
1039
|
+
function goCompilerIdentityMemoKey(
|
|
1040
|
+
goBinary: string,
|
|
1041
|
+
resolved: string,
|
|
1042
|
+
): string | null {
|
|
1043
|
+
try {
|
|
1044
|
+
const stat = fs.statSync(resolved);
|
|
1045
|
+
return `${goBinary}\0${resolved}\0${stat.size}\0${stat.mtimeMs}`;
|
|
1046
|
+
} catch {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function computeGoCompilerIdentity(goBinary: string, resolved: string): string {
|
|
1009
1052
|
if (!fs.existsSync(resolved)) {
|
|
1010
1053
|
return "missing";
|
|
1011
1054
|
}
|
|
@@ -87,8 +87,8 @@ export namespace ITtscCompilerResult {
|
|
|
87
87
|
* origin. Treat as `"unknown"` when missing.
|
|
88
88
|
*
|
|
89
89
|
* - `"plugin"`: a native plugin sidecar crashed or exited non-zero.
|
|
90
|
-
* - `"host"`: the TypeScript-Go host could not start (missing binary,
|
|
91
|
-
*
|
|
90
|
+
* - `"host"`: the TypeScript-Go host could not start (missing binary, cache
|
|
91
|
+
* lock, invalid config).
|
|
92
92
|
* - `"unknown"`: any other host-level failure.
|
|
93
93
|
*/
|
|
94
94
|
kind?: "plugin" | "host" | "unknown";
|
|
@@ -62,8 +62,8 @@ export namespace ITtscCompilerTransformation {
|
|
|
62
62
|
* origin. Treat as `"unknown"` when missing.
|
|
63
63
|
*
|
|
64
64
|
* - `"plugin"`: a native plugin sidecar crashed or exited non-zero.
|
|
65
|
-
* - `"host"`: the TypeScript-Go host could not start (missing binary,
|
|
66
|
-
*
|
|
65
|
+
* - `"host"`: the TypeScript-Go host could not start (missing binary, cache
|
|
66
|
+
* lock, invalid config).
|
|
67
67
|
* - `"unknown"`: any other host-level failure.
|
|
68
68
|
*/
|
|
69
69
|
kind?: "plugin" | "host" | "unknown";
|