ttsc 0.20.1 → 0.22.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/cmd/ttscserver/main.go +79 -3
- package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
- package/driver/lsp.go +4 -0
- package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
- package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
- package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
- package/internal/lspserver/lsp_native_plugin_source.go +171 -55
- package/internal/lspserver/lsp_native_resident.go +110 -9
- package/internal/lspserver/lsp_plugin.go +6 -0
- package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
- package/internal/lspserver/lsp_project_diagnostics.go +235 -0
- package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
- package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
- package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
- package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
- package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
- package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_registration.go +504 -0
- package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
- package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
- package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
- package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
- package/internal/lspserver/lsp_project_inputs.go +944 -0
- package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
- package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
- package/internal/lspserver/lsp_proxy.go +591 -28
- package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
- package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
- package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
- package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
- package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
- package/lib/compiler/internal/project/readProjectConfig.js +46 -14
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
- package/lib/compiler/internal/residentCheckProcess.js +227 -0
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +64 -1
- package/lib/compiler/internal/runBuild.js +484 -7
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +1 -0
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/flags/parser.js +13 -17
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +10 -3
- package/lib/flags/schema.js +99 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +23 -0
- package/lib/internal/projectInputPathIdentity.js +222 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +121 -88
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +13 -1
- package/lib/launcher/internal/runTtscserver.js +415 -45
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
- package/lib/launcher/internal/singleFileOutput.js +103 -0
- package/lib/launcher/internal/singleFileOutput.js.map +1 -0
- package/lib/launcher/internal/watchTopology.d.ts +214 -2
- package/lib/launcher/internal/watchTopology.js +1560 -41
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +5 -0
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
- package/package.json +8 -8
- package/src/compiler/internal/project/readProjectConfig.ts +62 -8
- package/src/compiler/internal/residentCheckProcess.ts +279 -0
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
- package/src/compiler/internal/runBuild.ts +728 -12
- package/src/compiler/internal/runSingleFileEmit.ts +1 -0
- package/src/flags/parser.ts +17 -17
- package/src/flags/schema.ts +111 -3
- package/src/internal/projectInputPathIdentity.ts +273 -0
- package/src/launcher/internal/runTtsc.ts +141 -117
- package/src/launcher/internal/runTtscserver.ts +561 -54
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2008 -59
- package/src/plugin/internal/loadProjectPlugins.ts +8 -0
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
import { resolveFlagSpec } from "../../flags/schema";
|
|
5
|
+
import {
|
|
6
|
+
type ProjectInputPathIdentityContext,
|
|
7
|
+
createProjectInputPathIdentityContext,
|
|
8
|
+
} from "../../internal/projectInputPathIdentity";
|
|
4
9
|
import {
|
|
5
10
|
hasProjectPluginEntries,
|
|
6
11
|
loadProjectPlugins,
|
|
@@ -8,11 +13,16 @@ import {
|
|
|
8
13
|
import type { ITtscCompilerDiagnostic } from "../../structures/ITtscCompilerDiagnostic";
|
|
9
14
|
import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
|
|
10
15
|
import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscParsedProjectConfig";
|
|
16
|
+
import type { ITtscProjectInputSnapshot } from "../../structures/internal/ITtscProjectInputSnapshot";
|
|
11
17
|
import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions";
|
|
12
18
|
import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult";
|
|
13
19
|
import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
|
|
14
20
|
import { createNativeProjectContextArgs } from "./project/createNativeProjectContextArgs";
|
|
15
21
|
import { readProjectConfig } from "./project/readProjectConfig";
|
|
22
|
+
import {
|
|
23
|
+
ResidentCheckProcess,
|
|
24
|
+
type ResidentCheckRequest,
|
|
25
|
+
} from "./residentCheckProcess";
|
|
16
26
|
import { resolveBinary } from "./resolveBinary";
|
|
17
27
|
import { resolveTsgo } from "./resolveTsgo";
|
|
18
28
|
import {
|
|
@@ -23,15 +33,23 @@ import {
|
|
|
23
33
|
} from "./sharedHostHelpers";
|
|
24
34
|
import { outputText, spawnNative } from "./spawnNative";
|
|
25
35
|
|
|
26
|
-
type RunBuildOptions = TtscBuildOptions & {
|
|
36
|
+
export type RunBuildOptions = TtscBuildOptions & {
|
|
27
37
|
skipDiagnosticsCheck?: boolean;
|
|
28
38
|
forceListEmittedFiles?: boolean;
|
|
39
|
+
/** Keep every compiler-owned side product inside this private directory. */
|
|
40
|
+
isolateOutputsTo?: string;
|
|
29
41
|
/**
|
|
30
42
|
* Receives selected native-plugin source roots after the project resolves.
|
|
31
43
|
* The watch launcher uses these roots to invalidate a sidecar when its Go
|
|
32
44
|
* implementation changes between rebuilds.
|
|
33
45
|
*/
|
|
34
46
|
onWatchInputs?: (inputs: readonly string[]) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Receives the reconciled project-rule filesystem dependency snapshot. Called
|
|
49
|
+
* only by watch launchers; ordinary builds do not probe the optional sidecar
|
|
50
|
+
* command.
|
|
51
|
+
*/
|
|
52
|
+
onProjectInputs?: (inputs: ITtscProjectInputSnapshot) => void;
|
|
35
53
|
/**
|
|
36
54
|
* Emit an external source map from the direct tsgo build lane even when the
|
|
37
55
|
* project configures none. Set by the ttsx runtime builds so a served emit
|
|
@@ -192,6 +210,373 @@ export function runBuild(options: RunBuildOptions = {}): TtscBuildResult {
|
|
|
192
210
|
return appendTimingOutput(result, timing);
|
|
193
211
|
}
|
|
194
212
|
|
|
213
|
+
export type ResidentCheckWatchChange = {
|
|
214
|
+
/** Re-resolve project, plugin, contributor, and Program topology. */
|
|
215
|
+
reload?: boolean;
|
|
216
|
+
/** Retain the sidecar and execution selection but cold-load its Program. */
|
|
217
|
+
invalidate?: boolean;
|
|
218
|
+
/** Local compiler or data paths changed since the prior cycle. */
|
|
219
|
+
changed?: readonly string[];
|
|
220
|
+
/** Subset of changed paths declared by ProjectRules as external inputs. */
|
|
221
|
+
external?: readonly string[];
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Analysis-only watch coordinator.
|
|
226
|
+
*
|
|
227
|
+
* The selected project and compatible check-stage processes stay resident
|
|
228
|
+
* across ordinary source/data edits. A config, root-set, contributor, or plugin
|
|
229
|
+
* topology transition calls for a full reset before the next cycle. Emit and
|
|
230
|
+
* transform lanes can pass through the coordinator, but compatibility checks
|
|
231
|
+
* keep them on the established one-shot path without starting sidecars.
|
|
232
|
+
*/
|
|
233
|
+
export class ResidentCheckWatchSession {
|
|
234
|
+
private execution: ReturnType<typeof resolveExecutionContext> | undefined;
|
|
235
|
+
private readonly pendingChanges = new Map<number, ResidentCheckRequest>();
|
|
236
|
+
private projectInputs: ITtscProjectInputSnapshot | undefined;
|
|
237
|
+
private readonly processes = new Map<string, ResidentCheckProcess>();
|
|
238
|
+
|
|
239
|
+
public async run(
|
|
240
|
+
options: RunBuildOptions,
|
|
241
|
+
change: ResidentCheckWatchChange = {},
|
|
242
|
+
): Promise<TtscBuildResult> {
|
|
243
|
+
if (change.reload === true) this.reset();
|
|
244
|
+
|
|
245
|
+
const timing = createBuildTiming(options);
|
|
246
|
+
const projectFree = runProjectFreeTerminalFlag(options);
|
|
247
|
+
if (projectFree !== null) {
|
|
248
|
+
this.reset();
|
|
249
|
+
return appendTimingOutput(projectFree, timing);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let execution = this.execution;
|
|
253
|
+
const reusedExecution = execution !== undefined;
|
|
254
|
+
let buildOptions: RunBuildOptions;
|
|
255
|
+
if (execution === undefined) {
|
|
256
|
+
const setupStartedAt = process.hrtime.bigint();
|
|
257
|
+
execution = resolveExecutionContext(options);
|
|
258
|
+
const discoveryOptions = this.captureProjectInputs(options);
|
|
259
|
+
const prepared = prepareBuildExecution(
|
|
260
|
+
discoveryOptions,
|
|
261
|
+
timing,
|
|
262
|
+
execution,
|
|
263
|
+
setupStartedAt,
|
|
264
|
+
);
|
|
265
|
+
buildOptions = prepared.buildOptions;
|
|
266
|
+
if (prepared.result !== undefined) {
|
|
267
|
+
this.reset();
|
|
268
|
+
return appendTimingOutput(prepared.result, timing);
|
|
269
|
+
}
|
|
270
|
+
if (!residentCheckExecutionIsCompatible(buildOptions, execution)) {
|
|
271
|
+
this.reset();
|
|
272
|
+
return appendTimingOutput(
|
|
273
|
+
runPreparedBuild(options, timing, execution, buildOptions),
|
|
274
|
+
timing,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
this.execution = execution;
|
|
278
|
+
} else {
|
|
279
|
+
buildOptions = applyProjectNoEmit(options, execution);
|
|
280
|
+
}
|
|
281
|
+
if (
|
|
282
|
+
reusedExecution &&
|
|
283
|
+
this.refreshProjectInputTopology(options, execution)
|
|
284
|
+
) {
|
|
285
|
+
this.reset();
|
|
286
|
+
return this.run(options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const checked = await this.runCheckPlugins(
|
|
290
|
+
buildOptions,
|
|
291
|
+
execution,
|
|
292
|
+
timing,
|
|
293
|
+
change,
|
|
294
|
+
);
|
|
295
|
+
let result: TtscBuildResult;
|
|
296
|
+
if (checked.status !== 0) {
|
|
297
|
+
result = appendTypeScriptDiagnosticsAfterPluginFailure(
|
|
298
|
+
checked,
|
|
299
|
+
buildOptions,
|
|
300
|
+
execution,
|
|
301
|
+
);
|
|
302
|
+
} else if (
|
|
303
|
+
checkPluginsReportTypeScriptDiagnostics(execution.nativePlugins)
|
|
304
|
+
) {
|
|
305
|
+
result = checked;
|
|
306
|
+
} else {
|
|
307
|
+
result = appendBuildOutput(
|
|
308
|
+
checked,
|
|
309
|
+
runTsgo(execution, ["--noEmit"], buildOptions),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return appendTimingOutput(result, timing);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Terminate every sidecar and discard the cached selection context. */
|
|
316
|
+
public dispose(): void {
|
|
317
|
+
this.reset();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private reset(): void {
|
|
321
|
+
for (const process of this.processes.values()) process.dispose();
|
|
322
|
+
this.processes.clear();
|
|
323
|
+
this.pendingChanges.clear();
|
|
324
|
+
this.execution = undefined;
|
|
325
|
+
this.projectInputs = undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private captureProjectInputs(options: RunBuildOptions): RunBuildOptions {
|
|
329
|
+
const onProjectInputs = options.onProjectInputs;
|
|
330
|
+
if (onProjectInputs === undefined) return options;
|
|
331
|
+
return {
|
|
332
|
+
...options,
|
|
333
|
+
onProjectInputs: (snapshot) => {
|
|
334
|
+
this.projectInputs = snapshot;
|
|
335
|
+
onProjectInputs(snapshot);
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private refreshProjectInputTopology(
|
|
341
|
+
options: RunBuildOptions,
|
|
342
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
343
|
+
): boolean {
|
|
344
|
+
if (options.onProjectInputs === undefined) return false;
|
|
345
|
+
const next = discoverNativeProjectInputs(options, execution);
|
|
346
|
+
const changed =
|
|
347
|
+
this.projectInputs !== undefined &&
|
|
348
|
+
!projectInputSnapshotsEqual(this.projectInputs, next);
|
|
349
|
+
this.projectInputs = next;
|
|
350
|
+
options.onProjectInputs(next);
|
|
351
|
+
return changed;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private async runCheckPlugins(
|
|
355
|
+
options: RunBuildOptions,
|
|
356
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
357
|
+
timing: BuildTiming,
|
|
358
|
+
change: ResidentCheckWatchChange,
|
|
359
|
+
): Promise<TtscBuildResult> {
|
|
360
|
+
let out: TtscBuildResult = {
|
|
361
|
+
diagnostics: [],
|
|
362
|
+
status: 0,
|
|
363
|
+
stderr: "",
|
|
364
|
+
stdout: "",
|
|
365
|
+
};
|
|
366
|
+
const checks = planResidentCheckEntries(execution.nativePlugins, (plugin) =>
|
|
367
|
+
createNativeCheckArgs(execution, options, plugin),
|
|
368
|
+
);
|
|
369
|
+
const request = residentCheckRequest(change, execution.projectRoot);
|
|
370
|
+
// Buffer the cycle for every resident plugin before running any of them.
|
|
371
|
+
// An earlier plugin may fail and short-circuit diagnostics, but a later
|
|
372
|
+
// sidecar must still receive every filesystem transition when it resumes.
|
|
373
|
+
bufferResidentCheckEntryRequests(this.pendingChanges, checks, request);
|
|
374
|
+
|
|
375
|
+
for (const { args, entryIndex, key, plugin } of checks) {
|
|
376
|
+
let result: TtscBuildResult;
|
|
377
|
+
if (key === undefined) {
|
|
378
|
+
result = runNativePluginCommand(
|
|
379
|
+
plugin,
|
|
380
|
+
args,
|
|
381
|
+
options,
|
|
382
|
+
execution,
|
|
383
|
+
"ttsc.check",
|
|
384
|
+
timing,
|
|
385
|
+
`ttsc check plugin ${plugin.name} time`,
|
|
386
|
+
);
|
|
387
|
+
} else {
|
|
388
|
+
const startedAt = process.hrtime.bigint();
|
|
389
|
+
let resident = this.processes.get(key);
|
|
390
|
+
if (resident === undefined) {
|
|
391
|
+
resident = new ResidentCheckProcess({
|
|
392
|
+
args: ["check-serve", ...args.slice(1)],
|
|
393
|
+
binary: plugin.binary,
|
|
394
|
+
cwd: execution.projectRoot,
|
|
395
|
+
env: nativePluginEnv(options.env, execution, plugin),
|
|
396
|
+
});
|
|
397
|
+
this.processes.set(key, resident);
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const reply = await resident.request(
|
|
401
|
+
takeResidentCheckEntryRequest(this.pendingChanges, entryIndex),
|
|
402
|
+
);
|
|
403
|
+
result = normalizeBuildOutput(
|
|
404
|
+
{
|
|
405
|
+
status: reply.status,
|
|
406
|
+
stderr: reply.stderr,
|
|
407
|
+
stdout: reply.stdout,
|
|
408
|
+
},
|
|
409
|
+
execution.projectRoot,
|
|
410
|
+
);
|
|
411
|
+
} catch {
|
|
412
|
+
resident.dispose();
|
|
413
|
+
this.processes.delete(key);
|
|
414
|
+
// The one-shot fallback observes the complete current filesystem,
|
|
415
|
+
// and a later sidecar starts cold, so neither needs old deltas.
|
|
416
|
+
// A capability-aware host may still disappear or violate framing.
|
|
417
|
+
// Preserve correctness by running the established one-shot command
|
|
418
|
+
// for this cycle; the next cycle gets one clean respawn attempt.
|
|
419
|
+
result = runNativePluginCommand(
|
|
420
|
+
plugin,
|
|
421
|
+
args,
|
|
422
|
+
options,
|
|
423
|
+
execution,
|
|
424
|
+
"ttsc.check",
|
|
425
|
+
{ ...timing, enabled: false },
|
|
426
|
+
"",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
recordTiming(
|
|
430
|
+
timing,
|
|
431
|
+
`ttsc check plugin ${plugin.name} time`,
|
|
432
|
+
startedAt,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
out = appendBuildOutput(out, result);
|
|
436
|
+
if (result.status !== 0) return out;
|
|
437
|
+
}
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function projectInputSnapshotsEqual(
|
|
443
|
+
left: ITtscProjectInputSnapshot,
|
|
444
|
+
right: ITtscProjectInputSnapshot,
|
|
445
|
+
): boolean {
|
|
446
|
+
const leftReloadFiles = left.reloadFiles ?? [];
|
|
447
|
+
const rightReloadFiles = right.reloadFiles ?? [];
|
|
448
|
+
const leftReloadDirectories = left.reloadDirectories ?? [];
|
|
449
|
+
const rightReloadDirectories = right.reloadDirectories ?? [];
|
|
450
|
+
return (
|
|
451
|
+
left.root === right.root &&
|
|
452
|
+
left.files.length === right.files.length &&
|
|
453
|
+
left.globs.length === right.globs.length &&
|
|
454
|
+
leftReloadDirectories.length === rightReloadDirectories.length &&
|
|
455
|
+
leftReloadFiles.length === rightReloadFiles.length &&
|
|
456
|
+
left.files.every((value, index) => value === right.files[index]) &&
|
|
457
|
+
left.globs.every((value, index) => value === right.globs[index]) &&
|
|
458
|
+
leftReloadDirectories.every(
|
|
459
|
+
(value, index) => value === rightReloadDirectories[index],
|
|
460
|
+
) &&
|
|
461
|
+
leftReloadFiles.every((value, index) => value === rightReloadFiles[index])
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function residentCheckExecutionIsCompatible(
|
|
466
|
+
options: RunBuildOptions,
|
|
467
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
468
|
+
): boolean {
|
|
469
|
+
return (
|
|
470
|
+
options.emit === false &&
|
|
471
|
+
options.fix !== true &&
|
|
472
|
+
options.format !== true &&
|
|
473
|
+
forwardsTerminalTsgoFlag(options) === false &&
|
|
474
|
+
execution.nativePlugins.every((plugin) => plugin.stage === "check")
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function residentCheckProcessKey(
|
|
479
|
+
plugin: ITtscLoadedNativePlugin,
|
|
480
|
+
args: readonly string[],
|
|
481
|
+
): string {
|
|
482
|
+
return `${plugin.binary}\0${plugin.name}\0${JSON.stringify(args)}`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export type ResidentCheckEntryPlan = {
|
|
486
|
+
args: string[];
|
|
487
|
+
entryIndex: number;
|
|
488
|
+
key: string | undefined;
|
|
489
|
+
plugin: ITtscLoadedNativePlugin;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Plan every configured check entry while sharing resident processes only by
|
|
494
|
+
* binary/name/argument identity.
|
|
495
|
+
*/
|
|
496
|
+
export function planResidentCheckEntries(
|
|
497
|
+
plugins: readonly ITtscLoadedNativePlugin[],
|
|
498
|
+
createArgs: (plugin: ITtscLoadedNativePlugin) => string[],
|
|
499
|
+
): ResidentCheckEntryPlan[] {
|
|
500
|
+
return plugins
|
|
501
|
+
.filter((candidate) => candidate.stage === "check")
|
|
502
|
+
.map((plugin, entryIndex) => {
|
|
503
|
+
const args = createArgs(plugin);
|
|
504
|
+
return {
|
|
505
|
+
args,
|
|
506
|
+
entryIndex,
|
|
507
|
+
key:
|
|
508
|
+
plugin.capabilities?.residentCheck === true
|
|
509
|
+
? residentCheckProcessKey(plugin, args)
|
|
510
|
+
: undefined,
|
|
511
|
+
plugin,
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Retain one complete change stream per configured resident check entry. */
|
|
517
|
+
export function bufferResidentCheckEntryRequests(
|
|
518
|
+
pending: Map<number, ResidentCheckRequest>,
|
|
519
|
+
checks: readonly ResidentCheckEntryPlan[],
|
|
520
|
+
request: ResidentCheckRequest,
|
|
521
|
+
): void {
|
|
522
|
+
for (const check of checks) {
|
|
523
|
+
if (check.key === undefined) continue;
|
|
524
|
+
pending.set(
|
|
525
|
+
check.entryIndex,
|
|
526
|
+
mergeResidentCheckRequests(pending.get(check.entryIndex), request),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Consume exactly one configured entry's buffered request. */
|
|
532
|
+
export function takeResidentCheckEntryRequest(
|
|
533
|
+
pending: Map<number, ResidentCheckRequest>,
|
|
534
|
+
entryIndex: number,
|
|
535
|
+
): ResidentCheckRequest {
|
|
536
|
+
const request = pending.get(entryIndex);
|
|
537
|
+
if (request === undefined) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`ttsc: resident check entry ${String(entryIndex)} has no buffered request`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
pending.delete(entryIndex);
|
|
543
|
+
return request;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function residentCheckRequest(
|
|
547
|
+
change: ResidentCheckWatchChange,
|
|
548
|
+
cwd: string,
|
|
549
|
+
): ResidentCheckRequest {
|
|
550
|
+
const normalize = (values: readonly string[] | undefined): string[] =>
|
|
551
|
+
[...new Set(values?.map((value) => path.resolve(cwd, value)) ?? [])].sort();
|
|
552
|
+
const changed = normalize(change.changed);
|
|
553
|
+
const external = normalize(change.external);
|
|
554
|
+
return {
|
|
555
|
+
...(change.invalidate === true ? { invalidate: true } : {}),
|
|
556
|
+
...(changed.length === 0 ? {} : { changed }),
|
|
557
|
+
...(external.length === 0 ? {} : { external }),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function mergeResidentCheckRequests(
|
|
562
|
+
previous: ResidentCheckRequest | undefined,
|
|
563
|
+
current: ResidentCheckRequest,
|
|
564
|
+
): ResidentCheckRequest {
|
|
565
|
+
const merge = (
|
|
566
|
+
left: readonly string[] | undefined,
|
|
567
|
+
right: readonly string[] | undefined,
|
|
568
|
+
): string[] => [...new Set([...(left ?? []), ...(right ?? [])])].sort();
|
|
569
|
+
const changed = merge(previous?.changed, current.changed);
|
|
570
|
+
const external = merge(previous?.external, current.external);
|
|
571
|
+
return {
|
|
572
|
+
...(changed.length === 0 ? {} : { changed }),
|
|
573
|
+
...(external.length === 0 ? {} : { external }),
|
|
574
|
+
...(previous?.invalidate === true || current.invalidate === true
|
|
575
|
+
? { invalidate: true }
|
|
576
|
+
: {}),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
195
580
|
function runBuildTimed(
|
|
196
581
|
options: RunBuildOptions,
|
|
197
582
|
timing: BuildTiming,
|
|
@@ -200,12 +585,31 @@ function runBuildTimed(
|
|
|
200
585
|
if (projectFree !== null) return projectFree;
|
|
201
586
|
const setupStartedAt = process.hrtime.bigint();
|
|
202
587
|
const execution = resolveExecutionContext(options);
|
|
203
|
-
options
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
588
|
+
return runBuildWithExecution(options, timing, execution, setupStartedAt);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function runBuildWithExecution(
|
|
592
|
+
options: RunBuildOptions,
|
|
593
|
+
timing: BuildTiming,
|
|
594
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
595
|
+
setupStartedAt: bigint,
|
|
596
|
+
): TtscBuildResult {
|
|
597
|
+
const prepared = prepareBuildExecution(
|
|
598
|
+
options,
|
|
599
|
+
timing,
|
|
600
|
+
execution,
|
|
601
|
+
setupStartedAt,
|
|
208
602
|
);
|
|
603
|
+
if (prepared.result !== undefined) return prepared.result;
|
|
604
|
+
return runPreparedBuild(options, timing, execution, prepared.buildOptions);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function prepareBuildExecution(
|
|
608
|
+
options: RunBuildOptions,
|
|
609
|
+
timing: BuildTiming,
|
|
610
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
611
|
+
setupStartedAt: bigint,
|
|
612
|
+
): { buildOptions: RunBuildOptions; result?: TtscBuildResult } {
|
|
209
613
|
if (
|
|
210
614
|
execution.nativePlugins.length > 0 ||
|
|
211
615
|
execution.pluginSetupFailure !== undefined
|
|
@@ -214,12 +618,27 @@ function runBuildTimed(
|
|
|
214
618
|
}
|
|
215
619
|
const buildOptions = applyProjectNoEmit(options, execution);
|
|
216
620
|
if (execution.pluginSetupFailure !== undefined) {
|
|
217
|
-
return
|
|
218
|
-
execution.pluginSetupFailure,
|
|
621
|
+
return {
|
|
219
622
|
buildOptions,
|
|
220
|
-
|
|
221
|
-
|
|
623
|
+
result: appendTypeScriptDiagnosticsAfterPluginFailure(
|
|
624
|
+
execution.pluginSetupFailure,
|
|
625
|
+
buildOptions,
|
|
626
|
+
execution,
|
|
627
|
+
),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
if (options.onProjectInputs !== undefined) {
|
|
631
|
+
options.onProjectInputs(discoverNativeProjectInputs(options, execution));
|
|
222
632
|
}
|
|
633
|
+
return { buildOptions };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function runPreparedBuild(
|
|
637
|
+
options: RunBuildOptions,
|
|
638
|
+
timing: BuildTiming,
|
|
639
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
640
|
+
buildOptions: RunBuildOptions,
|
|
641
|
+
): TtscBuildResult {
|
|
223
642
|
if (execution.nativePlugins.length > 0) {
|
|
224
643
|
const compilers = execution.nativePlugins.filter(
|
|
225
644
|
(plugin) => plugin.stage === "transform",
|
|
@@ -838,6 +1257,7 @@ function createTsgoBuildArgs(
|
|
|
838
1257
|
args.push(...createTsgoDiagnosticArgs(options));
|
|
839
1258
|
args.push(...createTsgoThreadingArgs(options));
|
|
840
1259
|
args.push(...(options.passthrough ?? []));
|
|
1260
|
+
args.push(...isolatedTsgoOutputArgs(options));
|
|
841
1261
|
if (flags.noEmitOnError === true) {
|
|
842
1262
|
args.push("--noEmitOnError");
|
|
843
1263
|
}
|
|
@@ -957,6 +1377,27 @@ function createNativeCheckArgs(
|
|
|
957
1377
|
return args;
|
|
958
1378
|
}
|
|
959
1379
|
|
|
1380
|
+
function createNativeProjectInputsArgs(
|
|
1381
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
1382
|
+
plugin: ITtscLoadedNativePlugin,
|
|
1383
|
+
): string[] {
|
|
1384
|
+
const args = [
|
|
1385
|
+
"project-inputs",
|
|
1386
|
+
"--tsconfig=" + execution.tsconfig,
|
|
1387
|
+
"--plugins-json=" + serializeNativePlugins(execution.nativePlugins),
|
|
1388
|
+
"--cwd=" + execution.projectRoot,
|
|
1389
|
+
];
|
|
1390
|
+
if (plugin.capabilities?.projectContextArgs === true) {
|
|
1391
|
+
args.push(
|
|
1392
|
+
...createNativeProjectContextArgs(
|
|
1393
|
+
execution.project,
|
|
1394
|
+
execution.pluginConfigDir,
|
|
1395
|
+
),
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
return args;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
960
1401
|
// `--singleThreaded` / `--checkers` are forwarded to native check-stage hosts
|
|
961
1402
|
// only when the host is one ttsc itself owns (currently `@ttsc/lint`). #113
|
|
962
1403
|
// forwarded both flags as bare CLI tokens to every native sidecar, then
|
|
@@ -1048,13 +1489,35 @@ function transformHostTimingLabel(
|
|
|
1048
1489
|
* single token so the sidecars' unknown-flag filters keep it intact.
|
|
1049
1490
|
*/
|
|
1050
1491
|
function createNativeTsgoArgs(options: TtscCommonOptions): string[] {
|
|
1051
|
-
const passthrough =
|
|
1052
|
-
|
|
1492
|
+
const passthrough = [
|
|
1493
|
+
...(nativeTsgoPassthroughArgs(options) ?? []),
|
|
1494
|
+
...isolatedTsgoOutputArgs(options),
|
|
1495
|
+
];
|
|
1496
|
+
if (passthrough.length === 0) {
|
|
1053
1497
|
return [];
|
|
1054
1498
|
}
|
|
1055
1499
|
return ["--tsgo-args=" + JSON.stringify(passthrough)];
|
|
1056
1500
|
}
|
|
1057
1501
|
|
|
1502
|
+
function isolatedTsgoOutputArgs(options: TtscCommonOptions): string[] {
|
|
1503
|
+
const target =
|
|
1504
|
+
"isolateOutputsTo" in options &&
|
|
1505
|
+
typeof options.isolateOutputsTo === "string"
|
|
1506
|
+
? path.resolve(options.isolateOutputsTo)
|
|
1507
|
+
: undefined;
|
|
1508
|
+
if (target === undefined) return [];
|
|
1509
|
+
return [
|
|
1510
|
+
"--outFile",
|
|
1511
|
+
"null",
|
|
1512
|
+
"--declarationDir",
|
|
1513
|
+
"null",
|
|
1514
|
+
"--tsBuildInfoFile",
|
|
1515
|
+
"null",
|
|
1516
|
+
"--outDir",
|
|
1517
|
+
target,
|
|
1518
|
+
];
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1058
1521
|
function nativeTsgoPassthroughArgs(
|
|
1059
1522
|
options: TtscCommonOptions,
|
|
1060
1523
|
): readonly string[] | undefined {
|
|
@@ -1151,6 +1614,255 @@ function runNativeCheckPlugins(
|
|
|
1151
1614
|
return out;
|
|
1152
1615
|
}
|
|
1153
1616
|
|
|
1617
|
+
function discoverNativeProjectInputs(
|
|
1618
|
+
options: TtscBuildOptions,
|
|
1619
|
+
execution: ReturnType<typeof resolveExecutionContext>,
|
|
1620
|
+
): ITtscProjectInputSnapshot {
|
|
1621
|
+
const snapshots: ITtscProjectInputSnapshot[] = [];
|
|
1622
|
+
for (const plugin of execution.nativePlugins.filter(
|
|
1623
|
+
(candidate) =>
|
|
1624
|
+
candidate.stage === "check" &&
|
|
1625
|
+
candidate.capabilities?.projectInputs === true,
|
|
1626
|
+
)) {
|
|
1627
|
+
const res = spawnNative(
|
|
1628
|
+
plugin.binary,
|
|
1629
|
+
createNativeProjectInputsArgs(execution, plugin),
|
|
1630
|
+
{
|
|
1631
|
+
cwd: execution.projectRoot,
|
|
1632
|
+
env: nativePluginEnv(options.env, execution, plugin),
|
|
1633
|
+
encoding: "utf8",
|
|
1634
|
+
},
|
|
1635
|
+
);
|
|
1636
|
+
if (res.error) {
|
|
1637
|
+
throw new Error(
|
|
1638
|
+
`ttsc.project-inputs: failed to spawn ${plugin.binary}: ${res.error.message}`,
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
const stdout = outputText(res.stdout).trim();
|
|
1642
|
+
if (res.status !== 0) {
|
|
1643
|
+
const detail = outputText(res.stderr).trim() || stdout;
|
|
1644
|
+
throw new Error(
|
|
1645
|
+
`ttsc.project-inputs: ${plugin.name ?? plugin.binary} failed${detail ? `: ${detail}` : ""}`,
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
const snapshot = parseProjectInputSnapshot(stdout, plugin);
|
|
1649
|
+
snapshots.push(snapshot);
|
|
1650
|
+
}
|
|
1651
|
+
return mergeProjectInputSnapshots(execution.projectRoot, snapshots);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function arraysEqual(
|
|
1655
|
+
left: readonly string[],
|
|
1656
|
+
right: readonly string[],
|
|
1657
|
+
): boolean {
|
|
1658
|
+
return (
|
|
1659
|
+
left.length === right.length && left.every((item, i) => item === right[i])
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Keep every declared spelling of one identity, ordered without regard to which
|
|
1665
|
+
* producer ran first.
|
|
1666
|
+
*
|
|
1667
|
+
* Two contributors can name the same file through different aliases, and each
|
|
1668
|
+
* alias is a path a watcher has to observe: keeping only one of them would
|
|
1669
|
+
* discard the link whose retarget the other spelling exists to catch. Insertion
|
|
1670
|
+
* order is not canonical, so the caller sorts the flattened result.
|
|
1671
|
+
*/
|
|
1672
|
+
function retainDeclaredSpelling(
|
|
1673
|
+
target: Map<string, string[]>,
|
|
1674
|
+
key: string,
|
|
1675
|
+
declared: string,
|
|
1676
|
+
): void {
|
|
1677
|
+
const previous = target.get(key);
|
|
1678
|
+
if (previous === undefined) {
|
|
1679
|
+
target.set(key, [declared]);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (previous.includes(declared)) return;
|
|
1683
|
+
previous.push(declared);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
export function mergeProjectInputSnapshots(
|
|
1687
|
+
fallbackRoot: string,
|
|
1688
|
+
snapshots: readonly ITtscProjectInputSnapshot[],
|
|
1689
|
+
identities: ProjectInputPathIdentityContext = createProjectInputPathIdentityContext(),
|
|
1690
|
+
): ITtscProjectInputSnapshot {
|
|
1691
|
+
const files = new Map<string, string>();
|
|
1692
|
+
const globs = new Map<string, string>();
|
|
1693
|
+
const reloadDirectories = new Map<string, string>();
|
|
1694
|
+
const reloadFiles = new Map<string, string>();
|
|
1695
|
+
// Deduplicated on the same identity keys, but holding what the contributors
|
|
1696
|
+
// wrote. Normalization resolves a declaration through its symlinks, which is
|
|
1697
|
+
// right for every comparison and wrong for the watcher that has to observe
|
|
1698
|
+
// the link itself being retargeted.
|
|
1699
|
+
const declaredFiles = new Map<string, string[]>();
|
|
1700
|
+
const declaredGlobs = new Map<string, string[]>();
|
|
1701
|
+
const declaredReloadDirectories = new Map<string, string[]>();
|
|
1702
|
+
const declaredReloadFiles = new Map<string, string[]>();
|
|
1703
|
+
const rootIdentity = identities.resolve(fallbackRoot);
|
|
1704
|
+
for (const snapshot of snapshots) {
|
|
1705
|
+
const candidateRoot = identities.resolve(snapshot.root);
|
|
1706
|
+
if (rootIdentity.key !== candidateRoot.key) {
|
|
1707
|
+
throw new Error(
|
|
1708
|
+
`ttsc.project-inputs: plugin root ${candidateRoot.path} differs from the selected project root ${rootIdentity.path}`,
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
for (const file of snapshot.files) {
|
|
1712
|
+
const identity = identities.resolve(file);
|
|
1713
|
+
files.set(identity.key, identity.path);
|
|
1714
|
+
retainDeclaredSpelling(declaredFiles, identity.key, path.resolve(file));
|
|
1715
|
+
}
|
|
1716
|
+
for (const glob of snapshot.globs) {
|
|
1717
|
+
const identity = identities.resolve(glob);
|
|
1718
|
+
globs.set(identity.key, identity.path.split(path.sep).join("/"));
|
|
1719
|
+
retainDeclaredSpelling(
|
|
1720
|
+
declaredGlobs,
|
|
1721
|
+
identity.key,
|
|
1722
|
+
path.resolve(glob).split(path.sep).join("/"),
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
for (const reloadFile of snapshot.reloadFiles ?? []) {
|
|
1726
|
+
const identity = identities.resolve(reloadFile);
|
|
1727
|
+
reloadFiles.set(identity.key, identity.path);
|
|
1728
|
+
retainDeclaredSpelling(
|
|
1729
|
+
declaredReloadFiles,
|
|
1730
|
+
identity.key,
|
|
1731
|
+
path.resolve(reloadFile),
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
for (const reloadDirectory of snapshot.reloadDirectories ?? []) {
|
|
1735
|
+
const identity = identities.resolve(reloadDirectory);
|
|
1736
|
+
reloadDirectories.set(identity.key, identity.path);
|
|
1737
|
+
retainDeclaredSpelling(
|
|
1738
|
+
declaredReloadDirectories,
|
|
1739
|
+
identity.key,
|
|
1740
|
+
path.resolve(reloadDirectory),
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
const normalized = {
|
|
1745
|
+
root: rootIdentity.path,
|
|
1746
|
+
files: [...files.values()].sort(),
|
|
1747
|
+
globs: [...globs.values()].sort(),
|
|
1748
|
+
reloadDirectories: [...reloadDirectories.values()].sort(),
|
|
1749
|
+
reloadFiles: [...reloadFiles.values()].sort(),
|
|
1750
|
+
};
|
|
1751
|
+
const declared = {
|
|
1752
|
+
files: [...declaredFiles.values()].flat().sort(),
|
|
1753
|
+
globs: [...declaredGlobs.values()].flat().sort(),
|
|
1754
|
+
reloadDirectories: [...declaredReloadDirectories.values()].flat().sort(),
|
|
1755
|
+
reloadFiles: [...declaredReloadFiles.values()].flat().sort(),
|
|
1756
|
+
};
|
|
1757
|
+
// Carried only when it says something the normalized arrays do not. A tree
|
|
1758
|
+
// with no alias on any declaration produces the same four lists, and a
|
|
1759
|
+
// consumer that has to scan both would pay twice for one population.
|
|
1760
|
+
return arraysEqual(declared.files, normalized.files) &&
|
|
1761
|
+
arraysEqual(declared.globs, normalized.globs) &&
|
|
1762
|
+
arraysEqual(declared.reloadDirectories, normalized.reloadDirectories) &&
|
|
1763
|
+
arraysEqual(declared.reloadFiles, normalized.reloadFiles)
|
|
1764
|
+
? normalized
|
|
1765
|
+
: { ...normalized, declared };
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
export function parseProjectInputSnapshot(
|
|
1769
|
+
text: string,
|
|
1770
|
+
plugin: ITtscLoadedNativePlugin,
|
|
1771
|
+
): ITtscProjectInputSnapshot {
|
|
1772
|
+
let value: unknown;
|
|
1773
|
+
try {
|
|
1774
|
+
value = JSON.parse(text);
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`ttsc.project-inputs: ${plugin.name ?? plugin.binary} returned invalid JSON: ${
|
|
1778
|
+
error instanceof Error ? error.message : String(error)
|
|
1779
|
+
}`,
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
if (
|
|
1783
|
+
typeof value !== "object" ||
|
|
1784
|
+
value === null ||
|
|
1785
|
+
typeof (value as { root?: unknown }).root !== "string" ||
|
|
1786
|
+
!isStringArray((value as { files?: unknown }).files) ||
|
|
1787
|
+
!isStringArray((value as { globs?: unknown }).globs) ||
|
|
1788
|
+
((value as { reloadDirectories?: unknown }).reloadDirectories !==
|
|
1789
|
+
undefined &&
|
|
1790
|
+
!isStringArray(
|
|
1791
|
+
(value as { reloadDirectories?: unknown }).reloadDirectories,
|
|
1792
|
+
)) ||
|
|
1793
|
+
((value as { reloadFiles?: unknown }).reloadFiles !== undefined &&
|
|
1794
|
+
!isStringArray((value as { reloadFiles?: unknown }).reloadFiles))
|
|
1795
|
+
) {
|
|
1796
|
+
throw new Error(
|
|
1797
|
+
`ttsc.project-inputs: ${plugin.name ?? plugin.binary} returned an invalid snapshot`,
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
const snapshot = value as unknown as ITtscProjectInputSnapshot;
|
|
1801
|
+
const invalid = [
|
|
1802
|
+
["root", snapshot.root],
|
|
1803
|
+
...snapshot.files.map((file) => ["file", file] as const),
|
|
1804
|
+
...snapshot.globs.map((glob) => ["glob", glob] as const),
|
|
1805
|
+
...(snapshot.reloadDirectories ?? []).map(
|
|
1806
|
+
(directory) => ["reload directory", directory] as const,
|
|
1807
|
+
),
|
|
1808
|
+
...(snapshot.reloadFiles ?? []).map(
|
|
1809
|
+
(file) => ["reload file", file] as const,
|
|
1810
|
+
),
|
|
1811
|
+
].find(
|
|
1812
|
+
([, location]) =>
|
|
1813
|
+
location.length === 0 || !isAbsoluteLocalProjectInputPath(location),
|
|
1814
|
+
);
|
|
1815
|
+
if (invalid !== undefined) {
|
|
1816
|
+
throw new Error(
|
|
1817
|
+
`ttsc.project-inputs: ${plugin.name ?? plugin.binary} returned an invalid snapshot: ${invalid[0]} ${JSON.stringify(invalid[1])} is not an absolute local path`,
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
return {
|
|
1821
|
+
...snapshot,
|
|
1822
|
+
reloadDirectories: snapshot.reloadDirectories ?? [],
|
|
1823
|
+
reloadFiles: snapshot.reloadFiles ?? [],
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
export function isAbsoluteLocalProjectInputPath(
|
|
1828
|
+
location: string,
|
|
1829
|
+
platform: NodeJS.Platform = process.platform,
|
|
1830
|
+
): boolean {
|
|
1831
|
+
if (location.includes("\0")) return false;
|
|
1832
|
+
if (platform !== "win32") return path.posix.isAbsolute(location);
|
|
1833
|
+
const normalized = location.replaceAll("/", "\\");
|
|
1834
|
+
if (/^[A-Za-z]:\\/.test(normalized)) return true;
|
|
1835
|
+
if (normalized.startsWith("\\\\?\\")) {
|
|
1836
|
+
const extended = normalized.slice(4);
|
|
1837
|
+
if (/^[A-Za-z]:\\/.test(extended)) return true;
|
|
1838
|
+
if (extended.toLowerCase().startsWith("unc\\")) {
|
|
1839
|
+
return isWindowsUncProjectInputPath(`\\\\${extended.slice(4)}`);
|
|
1840
|
+
}
|
|
1841
|
+
return false;
|
|
1842
|
+
}
|
|
1843
|
+
if (normalized.startsWith("\\\\.\\")) return false;
|
|
1844
|
+
return isWindowsUncProjectInputPath(normalized);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function isWindowsUncProjectInputPath(location: string): boolean {
|
|
1848
|
+
const matched = /^\\\\([^\\]+)\\([^\\]+)(?:\\|$)/.exec(location);
|
|
1849
|
+
return (
|
|
1850
|
+
matched !== null &&
|
|
1851
|
+
isWindowsUncVolumeSegment(matched[1]!) &&
|
|
1852
|
+
isWindowsUncVolumeSegment(matched[2]!)
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function isWindowsUncVolumeSegment(segment: string): boolean {
|
|
1857
|
+
return segment !== "." && segment !== ".." && !/[\0<>:"/\\|?*]/.test(segment);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function isStringArray(value: unknown): value is string[] {
|
|
1861
|
+
return (
|
|
1862
|
+
Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1154
1866
|
/**
|
|
1155
1867
|
* Spawn a single native plugin binary with the given args and return the
|
|
1156
1868
|
* normalized build result. `label` is used in the thrown error message so the
|
|
@@ -1237,6 +1949,7 @@ export function createProcessDiagnostic(
|
|
|
1237
1949
|
function resolveExecutionContext(
|
|
1238
1950
|
options: TtscCommonOptions & {
|
|
1239
1951
|
emit?: boolean;
|
|
1952
|
+
onWatchInputs?: (inputs: readonly string[]) => void;
|
|
1240
1953
|
resolvedProject?: ITtscParsedProjectConfig;
|
|
1241
1954
|
tsconfig?: string;
|
|
1242
1955
|
},
|
|
@@ -1262,10 +1975,13 @@ function resolveExecutionContext(
|
|
|
1262
1975
|
cwd,
|
|
1263
1976
|
entries: options.plugins,
|
|
1264
1977
|
env: { ...process.env, ...options.env },
|
|
1978
|
+
onWatchInputs: options.onWatchInputs,
|
|
1265
1979
|
pluginConfigDir: options.pluginConfigDir,
|
|
1266
1980
|
projectRoot,
|
|
1267
1981
|
tsconfig,
|
|
1268
1982
|
}).nativePlugins;
|
|
1983
|
+
} else {
|
|
1984
|
+
options.onWatchInputs?.([]);
|
|
1269
1985
|
}
|
|
1270
1986
|
} catch (error) {
|
|
1271
1987
|
pluginSetupFailure = {
|