ttsc 0.19.2 → 0.20.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 +7 -5
- package/cmd/graphdump/main.go +2 -4
- package/cmd/ttsc/filter.go +12 -4
- package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
- package/cmd/ttsc/flags_gen.go +3 -3
- package/cmd/ttscgraph/dump.go +5 -7
- package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
- package/cmd/ttscgraph/serve.go +58 -308
- package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
- package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
- package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
- package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
- package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
- package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
- package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
- package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
- package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
- package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
- package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
- package/driver/graph.go +12 -6
- package/driver/lsp.go +30 -0
- package/driver/program.go +37 -1
- package/driver/resolution_candidates.go +819 -0
- package/driver/windowsjunction/windowsjunction.go +40 -0
- package/go.mod +2 -0
- package/internal/graph/build.go +45 -11
- package/internal/graph/dump.go +312 -108
- package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
- package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
- package/internal/graph/dump_paths.go +172 -0
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
- package/internal/graph/dump_schema_version_matches_the_typescript_client_test.go +1 -1
- package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
- package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
- package/internal/graph/edges.go +220 -5
- package/internal/graph/enum_members_carry_their_names_on_the_enum_node_test.go +131 -0
- package/internal/graph/graph.go +177 -5
- package/internal/graph/line_model.go +122 -0
- package/internal/graph/literals.go +175 -0
- package/internal/graph/literals_are_absent_when_the_type_admits_more_than_it_can_name_test.go +74 -0
- package/internal/graph/literals_cover_enum_members_the_graph_has_no_nodes_for_test.go +79 -0
- package/internal/graph/literals_do_not_depend_on_declaration_line_wrapping_test.go +84 -0
- package/internal/graph/literals_render_every_value_in_typescript_source_form_test.go +77 -0
- package/internal/graph/literals_resolve_through_alias_indirection_test.go +64 -0
- package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
- package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
- package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
- package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
- package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
- package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
- package/internal/graph/object_members.go +229 -0
- package/internal/graph/provenance.go +88 -30
- package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
- package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
- package/internal/graphsymbols/provider.go +26 -91
- package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
- package/internal/lspserver/lsp_completion.go +109 -0
- package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
- package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
- package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
- package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
- package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
- package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
- package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
- package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
- package/internal/lspserver/lsp_completion_request.go +455 -0
- package/internal/lspserver/lsp_completion_scope.go +530 -0
- package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
- package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
- package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
- package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
- package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
- package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
- package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
- package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
- package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
- package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
- package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
- package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
- package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
- package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
- package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
- package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
- package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
- package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
- package/internal/lspserver/lsp_native_plugin_source.go +315 -2
- package/internal/lspserver/lsp_native_resident.go +304 -0
- package/internal/lspserver/lsp_plugin.go +85 -8
- package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
- package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
- package/internal/lspserver/lsp_proxy.go +601 -9
- package/internal/lspserver/lsp_refresh.go +70 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
- package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
- package/internal/lspserver/lsp_server.go +13 -1
- package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
- package/lib/TtscCompiler.d.ts +3 -1
- package/lib/TtscCompiler.js +5 -1
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.d.ts +13 -3
- package/lib/TtscService.js +6 -6
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
- package/lib/compiler/internal/project/readConfigJson.js +208 -0
- package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -149
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
- package/lib/compiler/internal/residentTransformProcess.js +159 -39
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +6 -0
- package/lib/compiler/internal/runBuild.js +143 -44
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
- package/lib/compiler/internal/startResidentTransform.js +3 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +12 -2
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +19 -0
- package/lib/flags/parser.js +78 -36
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +49 -11
- package/lib/flags/schema.js +94 -22
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
- package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
- package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
- package/lib/launcher/internal/runTtsc.js +107 -69
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +28 -41
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
- package/lib/launcher/internal/runtimeHooks.js +18 -1
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/watchTopology.d.ts +43 -0
- package/lib/launcher/internal/watchTopology.js +360 -0
- package/lib/launcher/internal/watchTopology.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
- package/lib/plugin/internal/buildSourcePlugin.js +247 -61
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +63 -2
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
- package/package.json +8 -8
- package/shim/ast/shim.go +48 -3
- package/shim/astnav/go.mod +14 -0
- package/shim/astnav/go.sum +20 -0
- package/shim/astnav/shim.go +15 -0
- package/shim/checker/shim.go +113 -0
- package/shim/core/shim.go +47 -0
- package/shim/diagnosticwriter/lint.go +50 -0
- package/src/TtscCompiler.ts +5 -1
- package/src/TtscService.ts +37 -10
- package/src/compiler/internal/project/readConfigJson.ts +209 -0
- package/src/compiler/internal/project/readProjectConfig.ts +62 -158
- package/src/compiler/internal/residentTransformProcess.ts +197 -39
- package/src/compiler/internal/runBuild.ts +169 -47
- package/src/compiler/internal/startResidentTransform.ts +12 -1
- package/src/compiler/internal/transformProjectInMemory.ts +12 -1
- package/src/flags/parser.ts +113 -42
- package/src/flags/schema.ts +113 -31
- package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
- package/src/launcher/internal/runTtsc.ts +117 -68
- package/src/launcher/internal/runTtsx.ts +29 -44
- package/src/launcher/internal/runtimeHooks.ts +30 -2
- package/src/launcher/internal/watchTopology.ts +466 -0
- package/src/plugin/internal/buildSourcePlugin.ts +326 -64
- package/src/plugin/internal/loadProjectPlugins.ts +80 -2
- package/src/structures/ITtscCompilerTransformation.ts +17 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
- package/tools/shim_audit/baseline.json +0 -3
- package/tools/shim_audit/main.go +42 -14
- package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
- package/utility/flags_gen.go +3 -3
- package/utility/host.go +9 -3
|
@@ -7,6 +7,15 @@ const STDERR_TAIL_LIMIT = 64 * 1024;
|
|
|
7
7
|
/** Cap on how much of an offending line an error message echoes back. */
|
|
8
8
|
const REPLY_ECHO_LIMIT = 200;
|
|
9
9
|
|
|
10
|
+
/** Default deadline for one request to the resident transform host. */
|
|
11
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;
|
|
12
|
+
|
|
13
|
+
/** Node clamps timers beyond this signed 32-bit millisecond duration. */
|
|
14
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
15
|
+
|
|
16
|
+
/** Allow a cooperative host a short shutdown window before forcing it down. */
|
|
17
|
+
const TERMINATION_GRACE_MS = 1_000;
|
|
18
|
+
|
|
10
19
|
/**
|
|
11
20
|
* The operation a request expects a reply for. The host answers a transform
|
|
12
21
|
* request (`{"file":...}`) with `{"typescript":...,"found":...}` and an update
|
|
@@ -23,12 +32,24 @@ export interface ResidentTransformProcessOptions {
|
|
|
23
32
|
binary: string;
|
|
24
33
|
cwd?: string;
|
|
25
34
|
env?: NodeJS.ProcessEnv;
|
|
35
|
+
/** Maximum time one request may wait for the resident host's reply. */
|
|
36
|
+
requestTimeoutMs?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Per-request lifecycle controls for a resident transform host. */
|
|
40
|
+
export interface ResidentTransformRequestOptions {
|
|
41
|
+
/** Abort this request. An in-flight abort retires the FIFO host. */
|
|
42
|
+
signal?: AbortSignal;
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
interface PendingRequest {
|
|
46
|
+
abort?: () => void;
|
|
29
47
|
kind: ResidentReplyKind;
|
|
30
48
|
reject: (reason: Error) => void;
|
|
31
49
|
resolve: (reply: Record<string, unknown>) => void;
|
|
50
|
+
settled: boolean;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
timer: NodeJS.Timeout;
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
/**
|
|
@@ -49,10 +70,12 @@ export class ResidentTransformProcess {
|
|
|
49
70
|
private readonly child: ChildProcess;
|
|
50
71
|
private readonly reader: Interface;
|
|
51
72
|
private readonly pending: PendingRequest[] = [];
|
|
73
|
+
private readonly requestTimeoutMs: number;
|
|
52
74
|
private stderr = "";
|
|
53
75
|
private failure: Error | undefined;
|
|
54
76
|
|
|
55
77
|
public constructor(options: ResidentTransformProcessOptions) {
|
|
78
|
+
this.requestTimeoutMs = normalizeRequestTimeoutMs(options.requestTimeoutMs);
|
|
56
79
|
// Default stdio is "pipe" for stdin/stdout/stderr, which is exactly what the
|
|
57
80
|
// line protocol needs; spelling it out as a string[] would not narrow to
|
|
58
81
|
// StdioOptions, so it is left implicit.
|
|
@@ -102,25 +125,68 @@ export class ResidentTransformProcess {
|
|
|
102
125
|
public request(
|
|
103
126
|
payload: Record<string, unknown>,
|
|
104
127
|
kind: ResidentReplyKind,
|
|
128
|
+
options: ResidentTransformRequestOptions = {},
|
|
105
129
|
): Promise<Record<string, unknown>> {
|
|
106
130
|
if (this.failure !== undefined) {
|
|
107
131
|
return Promise.reject(this.failure);
|
|
108
132
|
}
|
|
133
|
+
if (options.signal?.aborted) {
|
|
134
|
+
return Promise.reject(cancelledError(options.signal));
|
|
135
|
+
}
|
|
109
136
|
const stdin = this.child.stdin;
|
|
110
137
|
if (stdin === null || stdin.destroyed) {
|
|
111
138
|
return Promise.reject(
|
|
112
139
|
new Error("ttsc: resident transform host stdin is closed"),
|
|
113
140
|
);
|
|
114
141
|
}
|
|
142
|
+
let line: string;
|
|
143
|
+
try {
|
|
144
|
+
line = `${JSON.stringify(payload)}\n`;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return Promise.reject(asError(error));
|
|
147
|
+
}
|
|
115
148
|
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
116
|
-
|
|
149
|
+
let pending!: PendingRequest;
|
|
150
|
+
pending = {
|
|
151
|
+
kind,
|
|
152
|
+
reject,
|
|
153
|
+
resolve,
|
|
154
|
+
settled: false,
|
|
155
|
+
signal: options.signal,
|
|
156
|
+
timer: setTimeout(() => this.timeout(pending), this.requestTimeoutMs),
|
|
157
|
+
};
|
|
158
|
+
pending.timer.unref();
|
|
159
|
+
if (options.signal !== undefined) {
|
|
160
|
+
pending.abort = () => this.cancel(pending, options.signal!);
|
|
161
|
+
options.signal.addEventListener("abort", pending.abort, { once: true });
|
|
162
|
+
}
|
|
117
163
|
this.pending.push(pending);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
if (options.signal?.aborted) {
|
|
165
|
+
pending.abort!();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
stdin.write(line, (error) => {
|
|
170
|
+
if (error === null || error === undefined || pending.settled) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.settlePending(pending, error);
|
|
174
|
+
this.fail(
|
|
175
|
+
new Error(
|
|
176
|
+
`ttsc: resident transform host could not write a request: ${error.message}`,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (pending.settled) return;
|
|
182
|
+
const reason = asError(error);
|
|
183
|
+
this.settlePending(pending, reason);
|
|
184
|
+
this.fail(
|
|
185
|
+
new Error(
|
|
186
|
+
`ttsc: resident transform host could not write a request: ${reason.message}`,
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
124
190
|
});
|
|
125
191
|
}
|
|
126
192
|
|
|
@@ -129,31 +195,23 @@ export class ResidentTransformProcess {
|
|
|
129
195
|
* call more than once.
|
|
130
196
|
*/
|
|
131
197
|
public dispose(): void {
|
|
132
|
-
if (this.failure
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const stdin = this.child.stdin;
|
|
141
|
-
if (stdin !== null && !stdin.destroyed) {
|
|
142
|
-
stdin.end();
|
|
143
|
-
}
|
|
144
|
-
this.reader.close();
|
|
145
|
-
this.rejectAll(this.failure);
|
|
146
|
-
if (this.child.exitCode === null && this.child.signalCode === null) {
|
|
147
|
-
this.child.kill();
|
|
148
|
-
}
|
|
198
|
+
if (this.failure !== undefined) return;
|
|
199
|
+
// If the host already died, reject with its real exit error (stderr + exit
|
|
200
|
+
// code) rather than a bland "disposed" message.
|
|
201
|
+
this.fail(
|
|
202
|
+
this.child.exitCode !== null || this.child.signalCode !== null
|
|
203
|
+
? this.exitError()
|
|
204
|
+
: new Error("ttsc: resident transform host disposed"),
|
|
205
|
+
);
|
|
149
206
|
}
|
|
150
207
|
|
|
151
208
|
private onLine(line: string): void {
|
|
209
|
+
if (this.failure !== undefined) return;
|
|
152
210
|
const trimmed = line.trim();
|
|
153
211
|
if (trimmed.length === 0) {
|
|
154
212
|
return;
|
|
155
213
|
}
|
|
156
|
-
const request = this.pending
|
|
214
|
+
const request = this.pending[0];
|
|
157
215
|
if (request === undefined) {
|
|
158
216
|
// The host must emit exactly one reply per request, so an unmatched line
|
|
159
217
|
// is a protocol violation that would desync every later reply into the
|
|
@@ -179,7 +237,7 @@ export class ResidentTransformProcess {
|
|
|
179
237
|
trimmed,
|
|
180
238
|
)}`,
|
|
181
239
|
);
|
|
182
|
-
|
|
240
|
+
this.settlePending(request, error);
|
|
183
241
|
this.fail(error);
|
|
184
242
|
return;
|
|
185
243
|
}
|
|
@@ -189,7 +247,8 @@ export class ResidentTransformProcess {
|
|
|
189
247
|
// line consumed exactly one slot — so only this request is corrupt; later
|
|
190
248
|
// replies still pair correctly. Reject just this request instead of
|
|
191
249
|
// failing the whole process.
|
|
192
|
-
|
|
250
|
+
this.settlePending(
|
|
251
|
+
request,
|
|
193
252
|
new Error(
|
|
194
253
|
`ttsc: resident transform host sent an invalid ${request.kind} reply: ${echoLine(
|
|
195
254
|
trimmed,
|
|
@@ -198,32 +257,93 @@ export class ResidentTransformProcess {
|
|
|
198
257
|
);
|
|
199
258
|
return;
|
|
200
259
|
}
|
|
201
|
-
|
|
260
|
+
this.settlePending(request, reply);
|
|
202
261
|
}
|
|
203
262
|
|
|
204
263
|
private onReaderClose(): void {
|
|
205
|
-
if (this.
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
this.rejectAll(this.failure ?? this.exitError());
|
|
264
|
+
if (this.failure === undefined) this.fail(this.exitError());
|
|
209
265
|
}
|
|
210
266
|
|
|
211
267
|
private fail(error: Error): void {
|
|
212
|
-
this.failure
|
|
213
|
-
this.
|
|
268
|
+
if (this.failure !== undefined) return;
|
|
269
|
+
this.failure = error;
|
|
270
|
+
this.rejectAll(error);
|
|
271
|
+
this.terminate();
|
|
214
272
|
}
|
|
215
273
|
|
|
216
274
|
private rejectAll(error: Error): void {
|
|
217
275
|
while (this.pending.length !== 0) {
|
|
218
|
-
this.pending
|
|
276
|
+
this.settlePending(this.pending[0]!, error);
|
|
219
277
|
}
|
|
220
278
|
}
|
|
221
279
|
|
|
222
|
-
private
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
280
|
+
private settlePending(
|
|
281
|
+
pending: PendingRequest,
|
|
282
|
+
result: Error | Record<string, unknown>,
|
|
283
|
+
): void {
|
|
284
|
+
if (pending.settled) return;
|
|
285
|
+
pending.settled = true;
|
|
286
|
+
const index = this.pending.indexOf(pending);
|
|
287
|
+
if (index !== -1) this.pending.splice(index, 1);
|
|
288
|
+
clearTimeout(pending.timer);
|
|
289
|
+
if (pending.signal !== undefined && pending.abort !== undefined) {
|
|
290
|
+
pending.signal.removeEventListener("abort", pending.abort);
|
|
226
291
|
}
|
|
292
|
+
if (result instanceof Error) pending.reject(result);
|
|
293
|
+
else pending.resolve(result);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private timeout(pending: PendingRequest): void {
|
|
297
|
+
if (pending.settled) return;
|
|
298
|
+
const timeout = new Error(
|
|
299
|
+
`ttsc: resident transform request timed out after ${String(
|
|
300
|
+
this.requestTimeoutMs,
|
|
301
|
+
)} ms${stderrSuffix(this.stderr)}`,
|
|
302
|
+
);
|
|
303
|
+
this.settlePending(pending, timeout);
|
|
304
|
+
this.fail(
|
|
305
|
+
new Error(
|
|
306
|
+
`ttsc: resident transform host retired after another request timed out${stderrSuffix(
|
|
307
|
+
this.stderr,
|
|
308
|
+
)}`,
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private cancel(pending: PendingRequest, signal: AbortSignal): void {
|
|
314
|
+
if (pending.settled) return;
|
|
315
|
+
this.settlePending(pending, cancelledError(signal));
|
|
316
|
+
this.fail(
|
|
317
|
+
new Error(
|
|
318
|
+
`ttsc: resident transform host retired after another request was cancelled${abortDetail(
|
|
319
|
+
signal,
|
|
320
|
+
)}${stderrSuffix(this.stderr)}`,
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private terminate(): void {
|
|
326
|
+
const stdin = this.child.stdin;
|
|
327
|
+
if (stdin !== null && !stdin.destroyed) stdin.destroy();
|
|
328
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null) return;
|
|
329
|
+
try {
|
|
330
|
+
this.child.kill();
|
|
331
|
+
} catch {
|
|
332
|
+
// The host exited between the liveness check and termination.
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const force = setTimeout(() => {
|
|
336
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
this.child.kill("SIGKILL");
|
|
341
|
+
} catch {
|
|
342
|
+
// The host exited between the liveness check and the forced signal.
|
|
343
|
+
}
|
|
344
|
+
}, TERMINATION_GRACE_MS);
|
|
345
|
+
force.unref();
|
|
346
|
+
this.child.once("exit", () => clearTimeout(force));
|
|
227
347
|
}
|
|
228
348
|
|
|
229
349
|
private exitError(): Error {
|
|
@@ -242,6 +362,44 @@ export class ResidentTransformProcess {
|
|
|
242
362
|
}
|
|
243
363
|
}
|
|
244
364
|
|
|
365
|
+
/** Validate and normalize the caller-visible resident request deadline. */
|
|
366
|
+
export function normalizeRequestTimeoutMs(requestTimeoutMs?: number): number {
|
|
367
|
+
const value = requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
368
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TIMER_MS) {
|
|
369
|
+
throw new TypeError(
|
|
370
|
+
`ttsc: requestTimeoutMs must be an integer between 1 and ${String(MAX_TIMER_MS)}`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function cancelledError(signal: AbortSignal): Error {
|
|
377
|
+
const error = new Error(
|
|
378
|
+
`ttsc: resident transform request cancelled${abortDetail(signal)}`,
|
|
379
|
+
);
|
|
380
|
+
error.name = "AbortError";
|
|
381
|
+
return error;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function abortDetail(signal: AbortSignal): string {
|
|
385
|
+
const reason = signal.reason;
|
|
386
|
+
if (reason === undefined) return "";
|
|
387
|
+
try {
|
|
388
|
+
return `: ${reason instanceof Error ? reason.message : String(reason)}`;
|
|
389
|
+
} catch {
|
|
390
|
+
return "";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function stderrSuffix(stderr: string): string {
|
|
395
|
+
const detail = stderr.trim();
|
|
396
|
+
return detail.length === 0 ? "" : `: ${detail}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function asError(error: unknown): Error {
|
|
400
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
401
|
+
}
|
|
402
|
+
|
|
245
403
|
/**
|
|
246
404
|
* Parse one reply line into a plain object, or `undefined` when the line is not
|
|
247
405
|
* a JSON object. Invalid JSON, arrays, primitives, and `null` all yield
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { resolveFlagSpec } from "../../flags/schema";
|
|
4
4
|
import {
|
|
5
5
|
hasProjectPluginEntries,
|
|
6
6
|
loadProjectPlugins,
|
|
@@ -26,6 +26,12 @@ import { outputText, spawnNative } from "./spawnNative";
|
|
|
26
26
|
type RunBuildOptions = TtscBuildOptions & {
|
|
27
27
|
skipDiagnosticsCheck?: boolean;
|
|
28
28
|
forceListEmittedFiles?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Receives selected native-plugin source roots after the project resolves.
|
|
31
|
+
* The watch launcher uses these roots to invalidate a sidecar when its Go
|
|
32
|
+
* implementation changes between rebuilds.
|
|
33
|
+
*/
|
|
34
|
+
onWatchInputs?: (inputs: readonly string[]) => void;
|
|
29
35
|
/**
|
|
30
36
|
* Emit an external source map from the direct tsgo build lane even when the
|
|
31
37
|
* project configures none. Set by the ttsx runtime builds so a served emit
|
|
@@ -80,15 +86,17 @@ function hasEnabledPassthroughFlag(
|
|
|
80
86
|
const passthrough = options.passthrough ?? [];
|
|
81
87
|
for (let i = 0; i < passthrough.length; i++) {
|
|
82
88
|
const token = passthrough[i]!;
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
// Identity, not spelling: the user forwards their own casing and ttsc must
|
|
90
|
+
// read `--DIAGNOSTICS` the way tsgo does.
|
|
91
|
+
if (resolveFlagSpec(token)?.name !== flag) continue;
|
|
92
|
+
const equalsIndex = token.indexOf("=");
|
|
93
|
+
if (equalsIndex !== -1) {
|
|
94
|
+
return token.slice(equalsIndex + 1).toLowerCase() !== "false";
|
|
85
95
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
return passthrough[i + 1]!.toLowerCase() !== "false";
|
|
89
|
-
}
|
|
90
|
-
return true;
|
|
96
|
+
if (i + 1 < passthrough.length && isBooleanLiteral(passthrough[i + 1]!)) {
|
|
97
|
+
return passthrough[i + 1]!.toLowerCase() !== "false";
|
|
91
98
|
}
|
|
99
|
+
return true;
|
|
92
100
|
}
|
|
93
101
|
return false;
|
|
94
102
|
}
|
|
@@ -188,8 +196,16 @@ function runBuildTimed(
|
|
|
188
196
|
options: RunBuildOptions,
|
|
189
197
|
timing: BuildTiming,
|
|
190
198
|
): TtscBuildResult {
|
|
199
|
+
const projectFree = runProjectFreeTerminalFlag(options);
|
|
200
|
+
if (projectFree !== null) return projectFree;
|
|
191
201
|
const setupStartedAt = process.hrtime.bigint();
|
|
192
202
|
const execution = resolveExecutionContext(options);
|
|
203
|
+
options.onWatchInputs?.(
|
|
204
|
+
execution.nativePlugins.flatMap((plugin) => [
|
|
205
|
+
plugin.source,
|
|
206
|
+
...(plugin.contributors?.map((contributor) => contributor.source) ?? []),
|
|
207
|
+
]),
|
|
208
|
+
);
|
|
193
209
|
if (
|
|
194
210
|
execution.nativePlugins.length > 0 ||
|
|
195
211
|
execution.pluginSetupFailure !== undefined
|
|
@@ -313,9 +329,14 @@ function runBuildTimed(
|
|
|
313
329
|
}
|
|
314
330
|
|
|
315
331
|
const args = createTsgoBuildArgs(execution, buildOptions, {
|
|
332
|
+
// `--verbose` promises the emitted-file list on every lane, and the list
|
|
333
|
+
// only exists if tsgo is asked for it. ttsc adds the flag for itself here
|
|
334
|
+
// exactly as it does for `forceListEmittedFiles`, and strips the raw
|
|
335
|
+
// `TSFILE:` lines back out below.
|
|
316
336
|
listEmittedFiles:
|
|
317
337
|
buildOptions.emit !== false &&
|
|
318
|
-
buildOptions.forceListEmittedFiles === true
|
|
338
|
+
(buildOptions.forceListEmittedFiles === true ||
|
|
339
|
+
buildOptions.quiet === false),
|
|
319
340
|
noEmitOnError:
|
|
320
341
|
buildOptions.emit !== false &&
|
|
321
342
|
buildOptions.skipDiagnosticsCheck !== true &&
|
|
@@ -324,6 +345,62 @@ function runBuildTimed(
|
|
|
324
345
|
return runTsgoBuild(execution, buildOptions, args);
|
|
325
346
|
}
|
|
326
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Answer a forwarded terminal flag whose meaning precedes a project, when no
|
|
350
|
+
* project can be resolved.
|
|
351
|
+
*
|
|
352
|
+
* `ttsc --init` exists to write the starter `tsconfig.json`, and `ttsc --all` /
|
|
353
|
+
* `ttsc -?` only print tsgo's help — none of them needs a project, yet all
|
|
354
|
+
* three died in project resolution because that layer ran first and
|
|
355
|
+
* unconditionally. The classification is `FLAG_SCHEMA`'s (`terminal` +
|
|
356
|
+
* `projectFree`), so marking a further flag project-free needs no edit here.
|
|
357
|
+
*
|
|
358
|
+
* A resolvable project keeps the established lane untouched: the build path
|
|
359
|
+
* still forwards the flag with `-p <tsconfig>` from the project root, so `ttsc
|
|
360
|
+
* --init` inside an existing project still reports tsgo's TS5054 instead of
|
|
361
|
+
* writing a second config into the current directory. Returns `null` when this
|
|
362
|
+
* lane does not apply.
|
|
363
|
+
*/
|
|
364
|
+
function runProjectFreeTerminalFlag(
|
|
365
|
+
options: RunBuildOptions,
|
|
366
|
+
): TtscBuildResult | null {
|
|
367
|
+
if (!forwardsProjectFreeTerminalTsgoFlag(options)) return null;
|
|
368
|
+
if (options.resolvedProject !== undefined) return null;
|
|
369
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
370
|
+
try {
|
|
371
|
+
readProjectConfig({
|
|
372
|
+
cwd,
|
|
373
|
+
projectRoot: options.projectRoot,
|
|
374
|
+
tsconfig: options.tsconfig,
|
|
375
|
+
});
|
|
376
|
+
return null;
|
|
377
|
+
} catch {
|
|
378
|
+
// Any resolution failure takes this branch, not only "not found": a
|
|
379
|
+
// malformed config and an explicitly named missing `-p` path are equally
|
|
380
|
+
// beside the point for a flag whose meaning does not presuppose a project.
|
|
381
|
+
// Forward it to tsgo from the invocation directory instead of failing.
|
|
382
|
+
}
|
|
383
|
+
const tsgo = resolveTsgo({ ...options, cwd });
|
|
384
|
+
const res = spawnNative(tsgo.binary, [...(options.passthrough ?? [])], {
|
|
385
|
+
cwd,
|
|
386
|
+
env: mergeEnv(options.env),
|
|
387
|
+
encoding: "utf8",
|
|
388
|
+
});
|
|
389
|
+
if (res.error) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`ttsc: failed to spawn ${tsgo.binary}: ${res.error.message}`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return normalizeBuildOutput(
|
|
395
|
+
{
|
|
396
|
+
status: res.status ?? 1,
|
|
397
|
+
stdout: outputText(res.stdout),
|
|
398
|
+
stderr: outputText(res.stderr),
|
|
399
|
+
},
|
|
400
|
+
cwd,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
327
404
|
/**
|
|
328
405
|
* A tsconfig-level `noEmit: true` is an analysis-only build unless the user
|
|
329
406
|
* explicitly asks `ttsc --emit` to override it. Treat it like CLI `--noEmit`
|
|
@@ -412,11 +489,17 @@ function createPluginFailureTypecheckOptions(
|
|
|
412
489
|
const passthrough: string[] = [];
|
|
413
490
|
for (let i = 0; i < (options.passthrough?.length ?? 0); i++) {
|
|
414
491
|
const token = options.passthrough![i]!;
|
|
415
|
-
if (token === "--pretty") {
|
|
416
|
-
|
|
492
|
+
if (resolveFlagSpec(token)?.name === "--pretty") {
|
|
493
|
+
// `--pretty` is boolean: it owns a following token only when that token
|
|
494
|
+
// is the literal `true`/`false`, and the inline form carries its own.
|
|
495
|
+
if (
|
|
496
|
+
!token.includes("=") &&
|
|
497
|
+
isBooleanLiteral(options.passthrough![i + 1] ?? "")
|
|
498
|
+
) {
|
|
499
|
+
i++;
|
|
500
|
+
}
|
|
417
501
|
continue;
|
|
418
502
|
}
|
|
419
|
-
if (token.startsWith("--pretty=")) continue;
|
|
420
503
|
passthrough.push(token);
|
|
421
504
|
}
|
|
422
505
|
return {
|
|
@@ -509,31 +592,42 @@ function diagnosticHeadline(message: string): string {
|
|
|
509
592
|
}
|
|
510
593
|
|
|
511
594
|
/**
|
|
512
|
-
*
|
|
513
|
-
* (`--showConfig`, `--listFilesOnly`, `--
|
|
514
|
-
*
|
|
515
|
-
* print something and exit instead of compiling the project.
|
|
595
|
+
* Report whether the caller forwarded a print-and-exit tsgo flag
|
|
596
|
+
* (`--showConfig`, `--listFilesOnly`, `--all`, `--init`, `-?`), so ttsc can
|
|
597
|
+
* avoid adding compile-only flags to a command that is not going to compile.
|
|
516
598
|
*
|
|
517
|
-
* Schema-derived
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
599
|
+
* Schema-derived, and resolved by flag identity rather than by exact spelling:
|
|
600
|
+
* `resolveFlagSpec` applies the one normalization the parsing engine and the
|
|
601
|
+
* generated Go allow-lists use, so `--showconfig` classifies exactly like
|
|
602
|
+
* `--showConfig`. Adding a new terminal flag means editing `schema.ts` and
|
|
603
|
+
* re-running `pnpm run gen:flags`; this predicate needs no edit, and it grows
|
|
604
|
+
* no normalization of its own for the next consumer to forget.
|
|
521
605
|
*/
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
606
|
+
function forwardsTerminalTsgoFlag(options: TtscCommonOptions): boolean {
|
|
607
|
+
return (
|
|
608
|
+
options.passthrough?.some(
|
|
609
|
+
(token) => resolveFlagSpec(token)?.terminal === true,
|
|
610
|
+
) ?? false
|
|
611
|
+
);
|
|
612
|
+
}
|
|
529
613
|
|
|
530
614
|
/**
|
|
531
|
-
* Report whether the caller forwarded a
|
|
532
|
-
*
|
|
615
|
+
* Report whether the caller forwarded a terminal flag whose meaning does not
|
|
616
|
+
* presuppose a resolved project (`--init`, `--all`, `-?`).
|
|
617
|
+
*
|
|
618
|
+
* Derived from `FLAG_SCHEMA[*].projectFree`, through the same identity
|
|
619
|
+
* resolution as every other classification — never a literal list of flag names
|
|
620
|
+
* beside this branch, which is the shape that let terminal-flag awareness exist
|
|
621
|
+
* in one layer and be missing from the layer above it.
|
|
533
622
|
*/
|
|
534
|
-
function
|
|
623
|
+
function forwardsProjectFreeTerminalTsgoFlag(
|
|
624
|
+
options: TtscCommonOptions,
|
|
625
|
+
): boolean {
|
|
535
626
|
return (
|
|
536
|
-
options.passthrough?.some((
|
|
627
|
+
options.passthrough?.some((token) => {
|
|
628
|
+
const flag = resolveFlagSpec(token);
|
|
629
|
+
return flag?.terminal === true && flag.projectFree === true;
|
|
630
|
+
}) ?? false
|
|
537
631
|
);
|
|
538
632
|
}
|
|
539
633
|
|
|
@@ -553,18 +647,16 @@ function forwardsInternalShadowFlag(
|
|
|
553
647
|
options: TtscCommonOptions,
|
|
554
648
|
flag: string,
|
|
555
649
|
): boolean {
|
|
556
|
-
if (!INTERNAL_SHADOW_FLAGS.has(flag)) return false;
|
|
557
650
|
const passthrough = options.passthrough;
|
|
558
651
|
if (passthrough === undefined) return false;
|
|
559
|
-
//
|
|
560
|
-
// (`--pretty=true`)
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
);
|
|
652
|
+
// Resolution covers the bare form (`--pretty`), the inline-value form
|
|
653
|
+
// (`--pretty=true`), and every casing tsgo accepts (`--PRETTY`) — the
|
|
654
|
+
// launcher forwards the user's own spelling verbatim, so comparing raw
|
|
655
|
+
// strings would miss a spelling tsgo honours.
|
|
656
|
+
return passthrough.some((token) => {
|
|
657
|
+
const spec = resolveFlagSpec(token);
|
|
658
|
+
return spec?.internalShadow === true && spec.name === flag;
|
|
659
|
+
});
|
|
568
660
|
}
|
|
569
661
|
|
|
570
662
|
/**
|
|
@@ -676,12 +768,46 @@ function runTsgoBuild(
|
|
|
676
768
|
if (emittedFiles.length !== 0 && !userListedEmitted) {
|
|
677
769
|
result.stdout = stripEmittedFileLines(result.stdout);
|
|
678
770
|
}
|
|
771
|
+
if (options.quiet === false) {
|
|
772
|
+
result.stdout += verboseBuildSummary(execution, options, emittedFiles);
|
|
773
|
+
}
|
|
679
774
|
return normalizeBuildOutput(
|
|
680
775
|
{ ...result, emittedFiles },
|
|
681
776
|
execution.projectRoot,
|
|
682
777
|
);
|
|
683
778
|
}
|
|
684
779
|
|
|
780
|
+
/**
|
|
781
|
+
* The `--verbose` summary for the direct-tsgo lane, in the shape
|
|
782
|
+
* `cmd/ttsc/build.go` already prints on the native-host lane.
|
|
783
|
+
*
|
|
784
|
+
* Verbosity is a launcher-owned presentation concern: which lane `runBuild`
|
|
785
|
+
* selects is an implementation detail the user cannot see, so a documented flag
|
|
786
|
+
* must not change meaning with it. This lane never consumed `quiet` at all,
|
|
787
|
+
* which is why the flag was silent on every project without a ttsc plugin.
|
|
788
|
+
*
|
|
789
|
+
* `sites=0` is a fact about this lane rather than a placeholder: it runs
|
|
790
|
+
* precisely when the project declares no native plugin. A build that emitted
|
|
791
|
+
* nothing prints the header and `emitted=0 files`, so the summary never claims
|
|
792
|
+
* files that were not written.
|
|
793
|
+
*/
|
|
794
|
+
function verboseBuildSummary(
|
|
795
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
796
|
+
options: RunBuildOptions,
|
|
797
|
+
emittedFiles: readonly string[],
|
|
798
|
+
): string {
|
|
799
|
+
const lines = [
|
|
800
|
+
`// ttsc: tsconfig=${execution.tsconfig} cwd=${execution.cwd} sites=0 emit=${options.emit !== false}`,
|
|
801
|
+
];
|
|
802
|
+
if (options.emit !== false) {
|
|
803
|
+
lines.push(`// ttsc: emitted=${emittedFiles.length} files`);
|
|
804
|
+
for (const file of emittedFiles) {
|
|
805
|
+
lines.push(` + ${path.relative(execution.cwd, file) || file}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return `${lines.join("\n")}\n`;
|
|
809
|
+
}
|
|
810
|
+
|
|
685
811
|
/** Build the argument list for a direct `tsgo` build invocation. */
|
|
686
812
|
function createTsgoBuildArgs(
|
|
687
813
|
execution: ReturnType<typeof resolveExecutionContext>,
|
|
@@ -953,12 +1079,8 @@ function nativeTsgoPassthroughArgs(
|
|
|
953
1079
|
}
|
|
954
1080
|
|
|
955
1081
|
function isDiagnosticsPassthroughFlag(token: string): boolean {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
token.startsWith("--diagnostics=") ||
|
|
959
|
-
token === "--extendedDiagnostics" ||
|
|
960
|
-
token.startsWith("--extendedDiagnostics=")
|
|
961
|
-
);
|
|
1082
|
+
const name = resolveFlagSpec(token)?.name;
|
|
1083
|
+
return name === "--diagnostics" || name === "--extendedDiagnostics";
|
|
962
1084
|
}
|
|
963
1085
|
|
|
964
1086
|
function isBooleanLiteral(token: string): boolean {
|