ttsc 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/cmd/ttscgraph/main.go +18 -12
  2. package/cmd/ttscgraph/run_help_version_and_bad_flags_exit_codes_test.go +23 -5
  3. package/cmd/ttscgraph/serve.go +863 -0
  4. package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +43 -0
  5. package/cmd/ttscgraph/serve_protocol_retries_session_creation_test.go +83 -0
  6. package/cmd/ttscgraph/serve_protocol_reuses_session_test.go +35 -0
  7. package/cmd/ttscgraph/serve_session_applies_source_edit_incrementally_test.go +55 -0
  8. package/cmd/ttscgraph/serve_session_fails_closed_on_deleted_config_test.go +68 -0
  9. package/cmd/ttscgraph/serve_session_fails_closed_on_invalid_config_test.go +50 -0
  10. package/cmd/ttscgraph/serve_session_keeps_missing_literal_root_unchanged_test.go +46 -0
  11. package/cmd/ttscgraph/serve_session_rebuilds_import_graph_change_test.go +60 -0
  12. package/cmd/ttscgraph/serve_session_reloads_created_literal_root_test.go +44 -0
  13. package/cmd/ttscgraph/serve_session_reloads_extended_config_change_test.go +45 -0
  14. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_conditional_export_test.go +60 -0
  15. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_config_types_test.go +44 -0
  16. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_excluded_import_test.go +62 -0
  17. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_export_test.go +46 -0
  18. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_package_import_test.go +46 -0
  19. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_path_alias_test.go +47 -0
  20. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_reference_path_test.go +42 -0
  21. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_root_dirs_target_test.go +46 -0
  22. package/cmd/ttscgraph/serve_session_reloads_newly_resolved_type_reference_test.go +42 -0
  23. package/cmd/ttscgraph/serve_session_reloads_root_file_set_test.go +52 -0
  24. package/cmd/ttscgraph/serve_session_reloads_source_deleted_during_load_test.go +60 -0
  25. package/cmd/ttscgraph/serve_session_reloads_transitive_project_reference_test.go +55 -0
  26. package/cmd/ttscgraph/serve_session_reloads_when_program_plugin_linked_test.go +53 -0
  27. package/cmd/ttscgraph/serve_session_reuses_unchanged_preamble_source_test.go +45 -0
  28. package/cmd/ttscgraph/serve_session_reuses_unchanged_snapshot_test.go +52 -0
  29. package/cmd/ttscgraph/serve_session_revisits_source_edited_during_load_test.go +61 -0
  30. package/cmd/ttscgraph/serve_session_tolerates_os_invalid_resolution_candidate_test.go +56 -0
  31. package/cmd/ttscgraph/serve_session_tracks_all_module_specifier_forms_test.go +41 -0
  32. package/cmd/ttscgraph/serve_session_tracks_diamond_project_references_once_test.go +61 -0
  33. package/driver/emit_plugin.go +27 -12
  34. package/driver/plugins.go +21 -5
  35. package/driver/program.go +43 -14
  36. package/driver/session.go +11 -3
  37. package/lib/TtscCompiler.d.ts +3 -2
  38. package/lib/TtscCompiler.js +6 -14
  39. package/lib/TtscCompiler.js.map +1 -1
  40. package/lib/compiler/internal/compileProjectInMemory.js +14 -1
  41. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  42. package/lib/compiler/internal/runBuild.d.ts +7 -0
  43. package/lib/compiler/internal/runBuild.js +176 -16
  44. package/lib/compiler/internal/runBuild.js.map +1 -1
  45. package/lib/structures/ITtscCompilerResult.d.ts +8 -5
  46. package/package.json +8 -8
  47. package/shim/printer/shim.go +22 -4
  48. package/src/TtscCompiler.ts +4 -16
  49. package/src/compiler/internal/compileProjectInMemory.ts +17 -1
  50. package/src/compiler/internal/runBuild.ts +237 -23
  51. package/src/structures/ITtscCompilerResult.ts +8 -5
@@ -160,17 +160,31 @@ function runBuildTimed(
160
160
  ): TtscBuildResult {
161
161
  const setupStartedAt = process.hrtime.bigint();
162
162
  const execution = resolveExecutionContext(options);
163
- if (execution.nativePlugins.length > 0) {
163
+ if (
164
+ execution.nativePlugins.length > 0 ||
165
+ execution.pluginSetupFailure !== undefined
166
+ ) {
164
167
  recordTiming(timing, "ttsc plugin setup time", setupStartedAt);
165
168
  }
166
169
  const buildOptions = applyProjectNoEmit(options, execution);
170
+ if (execution.pluginSetupFailure !== undefined) {
171
+ return appendTypeScriptDiagnosticsAfterPluginFailure(
172
+ execution.pluginSetupFailure,
173
+ buildOptions,
174
+ execution,
175
+ );
176
+ }
167
177
  if (execution.nativePlugins.length > 0) {
168
178
  const compilers = execution.nativePlugins.filter(
169
179
  (plugin) => plugin.stage === "transform",
170
180
  );
171
181
  const checked = runNativeCheckPlugins(buildOptions, execution, timing);
172
182
  if (checked.status !== 0) {
173
- return checked;
183
+ return appendTypeScriptDiagnosticsAfterPluginFailure(
184
+ checked,
185
+ buildOptions,
186
+ execution,
187
+ );
174
188
  }
175
189
 
176
190
  if (buildOptions.emit === false) {
@@ -191,15 +205,20 @@ function runBuildTimed(
191
205
  }
192
206
  if (compilers.length !== 0) {
193
207
  assertSharedHostCompatibility(compilers, "emit");
194
- return appendBuildOutput(
195
- checked,
196
- buildWithNativeCompilerPlugins(
197
- buildOptions,
198
- execution,
199
- compilers,
200
- timing,
201
- ),
208
+ const compiled = buildWithNativeCompilerPlugins(
209
+ buildOptions,
210
+ execution,
211
+ compilers,
212
+ timing,
202
213
  );
214
+ const result = appendBuildOutput(checked, compiled);
215
+ return compiled.status === 0
216
+ ? result
217
+ : appendTypeScriptDiagnosticsAfterPluginFailure(
218
+ result,
219
+ buildOptions,
220
+ execution,
221
+ );
203
222
  }
204
223
  if (checkPluginsReportTypeScriptDiagnostics(execution.nativePlugins)) {
205
224
  return checked;
@@ -213,15 +232,20 @@ function runBuildTimed(
213
232
  let result: TtscBuildResult;
214
233
  if (compilers.length !== 0) {
215
234
  assertSharedHostCompatibility(compilers, "emit");
216
- result = appendBuildOutput(
217
- checked,
218
- buildWithNativeCompilerPlugins(
235
+ const compiled = buildWithNativeCompilerPlugins(
236
+ buildOptions,
237
+ execution,
238
+ compilers,
239
+ timing,
240
+ );
241
+ result = appendBuildOutput(checked, compiled);
242
+ if (compiled.status !== 0) {
243
+ result = appendTypeScriptDiagnosticsAfterPluginFailure(
244
+ result,
219
245
  buildOptions,
220
246
  execution,
221
- compilers,
222
- timing,
223
- ),
224
- );
247
+ );
248
+ }
225
249
  } else {
226
250
  if (
227
251
  buildOptions.skipDiagnosticsCheck !== true &&
@@ -295,6 +319,165 @@ function checkPluginsReportTypeScriptDiagnostics(
295
319
  );
296
320
  }
297
321
 
322
+ /**
323
+ * Preserve a failed plugin's output and status while collecting TypeScript
324
+ * diagnostics through an independent no-emit pass.
325
+ *
326
+ * A sidecar can fail before it loads the project Program, including when a Go
327
+ * panic or another runtime error terminates the process. The plugin failure
328
+ * must still block emit, but it must not hide unrelated errors in the user's
329
+ * TypeScript source. The fallback runs only after a plugin failure, skips modes
330
+ * whose contract intentionally omits diagnostics, and avoids appending a batch
331
+ * the plugin already reported itself.
332
+ */
333
+ function appendTypeScriptDiagnosticsAfterPluginFailure(
334
+ failure: TtscBuildResult,
335
+ options: RunBuildOptions,
336
+ execution: ReturnType<typeof resolveExecutionContext>,
337
+ ): TtscBuildResult {
338
+ if (
339
+ options.format === true ||
340
+ options.skipDiagnosticsCheck === true ||
341
+ forwardsTerminalTsgoFlag(options)
342
+ ) {
343
+ return failure;
344
+ }
345
+ const typechecked = runTsgo(
346
+ execution,
347
+ ["--noEmit"],
348
+ createPluginFailureTypecheckOptions(options),
349
+ );
350
+ const fallback = filterReportedTypeScriptDiagnostics(
351
+ failure,
352
+ typechecked,
353
+ execution.projectRoot,
354
+ );
355
+ if (fallback === null) {
356
+ return failure;
357
+ }
358
+ // Structured consumers (the public API's `IFailure.diagnostics`) never see
359
+ // stdout/stderr, so a plugin failure that reported no parsable diagnostics
360
+ // must be seeded as one before recovered TypeScript diagnostics are appended
361
+ // — otherwise the recovery would replace the plugin error with unrelated
362
+ // type errors instead of surfacing both.
363
+ const seeded =
364
+ failure.diagnostics.length === 0
365
+ ? { ...failure, diagnostics: [createProcessDiagnostic(failure)] }
366
+ : failure;
367
+ const status = failure.status;
368
+ return {
369
+ ...appendBuildOutput(seeded, fallback),
370
+ status,
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Make the recovery pass parseable regardless of the user's display flags. This
376
+ * is an internal second pass, so plain output is required to remove only
377
+ * diagnostics that the failed plugin already printed.
378
+ */
379
+ function createPluginFailureTypecheckOptions(
380
+ options: RunBuildOptions,
381
+ ): RunBuildOptions {
382
+ const passthrough: string[] = [];
383
+ for (let i = 0; i < (options.passthrough?.length ?? 0); i++) {
384
+ const token = options.passthrough![i]!;
385
+ if (token === "--pretty") {
386
+ if (isBooleanLiteral(options.passthrough![i + 1] ?? "")) i++;
387
+ continue;
388
+ }
389
+ if (token.startsWith("--pretty=")) continue;
390
+ passthrough.push(token);
391
+ }
392
+ return {
393
+ ...options,
394
+ passthrough,
395
+ structuredDiagnostics: true,
396
+ };
397
+ }
398
+
399
+ /** Return only fallback diagnostics the failed plugin did not already report. */
400
+ function filterReportedTypeScriptDiagnostics(
401
+ failure: TtscBuildResult,
402
+ typechecked: TtscBuildResult,
403
+ cwd: string,
404
+ ): TtscBuildResult | null {
405
+ if (typechecked.diagnostics.length === 0) {
406
+ return typechecked.status === 0 ? null : typechecked;
407
+ }
408
+ const diagnostics = typechecked.diagnostics.filter(
409
+ (diagnostic) =>
410
+ !failure.diagnostics.some((existing) =>
411
+ compilerDiagnosticsEqual(existing, diagnostic),
412
+ ),
413
+ );
414
+ if (diagnostics.length === 0) return null;
415
+ if (diagnostics.length === typechecked.diagnostics.length) return typechecked;
416
+ return {
417
+ ...typechecked,
418
+ diagnostics,
419
+ stderr: filterCompilerDiagnosticText(typechecked.stderr, diagnostics, cwd),
420
+ stdout: filterCompilerDiagnosticText(typechecked.stdout, diagnostics, cwd),
421
+ };
422
+ }
423
+
424
+ /** Remove diagnostic lines absent from the selected structured result. */
425
+ function filterCompilerDiagnosticText(
426
+ text: string,
427
+ diagnostics: readonly ITtscCompilerDiagnostic[],
428
+ cwd: string,
429
+ ): string {
430
+ const out: string[] = [];
431
+ let keepContinuation = true;
432
+ for (const line of text.split(/\r?\n/)) {
433
+ const plain = stripAnsi(line);
434
+ const diagnostic = parseDiagnosticLine(plain, cwd);
435
+ if (diagnostic !== null) {
436
+ keepContinuation = diagnostics.some((selected) =>
437
+ compilerDiagnosticsEqual(selected, diagnostic),
438
+ );
439
+ if (keepContinuation) out.push(line);
440
+ continue;
441
+ }
442
+ if (/^Found\s+\d+\s+errors?/i.test(plain)) continue;
443
+ if (!keepContinuation && /^\s+/.test(line)) continue;
444
+ keepContinuation = true;
445
+ out.push(line);
446
+ }
447
+ return out.join("\n");
448
+ }
449
+
450
+ /** Compare normalized compiler diagnostics before appending fallback output. */
451
+ function compilerDiagnosticsEqual(
452
+ left: ITtscCompilerDiagnostic,
453
+ right: ITtscCompilerDiagnostic,
454
+ ): boolean {
455
+ return (
456
+ left.category === right.category &&
457
+ left.code === right.code &&
458
+ left.file === right.file &&
459
+ diagnosticPositionsEqual(left, right) &&
460
+ diagnosticHeadline(left.messageText) ===
461
+ diagnosticHeadline(right.messageText)
462
+ );
463
+ }
464
+
465
+ /** Compare offsets when available, otherwise compare rendered line/column. */
466
+ function diagnosticPositionsEqual(
467
+ left: ITtscCompilerDiagnostic,
468
+ right: ITtscCompilerDiagnostic,
469
+ ): boolean {
470
+ if (left.start !== undefined && right.start !== undefined) {
471
+ return left.start === right.start;
472
+ }
473
+ return left.line === right.line && left.character === right.character;
474
+ }
475
+
476
+ /** Remove pretty-rendered source context from a diagnostic message. */
477
+ function diagnosticHeadline(message: string): string {
478
+ return message.split(/\r?\n/, 1)[0]!.trim();
479
+ }
480
+
298
481
  /**
299
482
  * Tsgo CLI flags that make `tsgo` print something and exit instead of building
300
483
  * (`--showConfig`, `--listFilesOnly`, `--help`, …). ttsc must not add
@@ -850,6 +1033,26 @@ export function appendBuildOutput(
850
1033
  });
851
1034
  }
852
1035
 
1036
+ /**
1037
+ * Synthesize one structured diagnostic from a non-zero process result that
1038
+ * produced no parsable diagnostics, carrying the captured stderr/stdout as the
1039
+ * message. Shared by the public API result mapping and the plugin-failure
1040
+ * recovery pass so the failure text is never dropped from structured output.
1041
+ */
1042
+ export function createProcessDiagnostic(
1043
+ result: TtscBuildResult,
1044
+ ): ITtscCompilerDiagnostic {
1045
+ const messageText =
1046
+ (result.stderr || result.stdout).trim() ||
1047
+ `ttsc exited with status ${result.status}`;
1048
+ return {
1049
+ category: "error",
1050
+ code: "TTSC_PROCESS",
1051
+ file: null,
1052
+ messageText,
1053
+ };
1054
+ }
1055
+
853
1056
  /**
854
1057
  * Resolve all runtime context needed for a build: cwd, tsconfig path, project
855
1058
  * root, tsgo binary location, and the loaded native plugin list. Centralised
@@ -867,20 +1070,31 @@ function resolveExecutionContext(
867
1070
  const tsconfig = project.path;
868
1071
  const projectRoot = project.root;
869
1072
  const tsgo = resolveTsgo({ ...options, cwd: projectRoot });
870
- const hasPlugins = hasProjectPluginEntries(project, options.plugins);
871
- const loaded = hasPlugins
872
- ? loadProjectPlugins({
1073
+ let pluginSetupFailure: TtscBuildResult | undefined;
1074
+ let nativePlugins: ITtscLoadedNativePlugin[] = [];
1075
+ try {
1076
+ if (hasProjectPluginEntries(project, options.plugins)) {
1077
+ nativePlugins = loadProjectPlugins({
873
1078
  binary: resolveBinary(options) ?? "",
874
1079
  cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
875
1080
  cwd,
876
1081
  entries: options.plugins,
877
1082
  projectRoot,
878
1083
  tsconfig,
879
- })
880
- : { nativePlugins: [] };
1084
+ }).nativePlugins;
1085
+ }
1086
+ } catch (error) {
1087
+ pluginSetupFailure = {
1088
+ diagnostics: [],
1089
+ status: 2,
1090
+ stdout: "",
1091
+ stderr: `${error instanceof Error ? error.message : String(error)}\n`,
1092
+ };
1093
+ }
881
1094
  return {
882
1095
  cwd,
883
- nativePlugins: loaded.nativePlugins,
1096
+ nativePlugins,
1097
+ pluginSetupFailure,
884
1098
  projectNoEmit: project.compilerOptions.noEmit === true,
885
1099
  projectRoot,
886
1100
  rewriteRelativeImportExtensionsForEmit:
@@ -47,12 +47,14 @@ export namespace ITtscCompilerResult {
47
47
  }
48
48
 
49
49
  /**
50
- * Represents a ttsc compilation that completed but had diagnostics.
50
+ * Represents a ttsc compilation that completed with diagnostics or a
51
+ * recoverable plugin failure.
51
52
  *
52
53
  * This interface is returned when the compiler reports errors, warnings, or
53
- * other diagnostics during normal compilation. It contains both structured
54
- * diagnostic information and any output that was still generated despite the
55
- * diagnostics.
54
+ * other diagnostics during normal compilation, or when a plugin failure can
55
+ * be retained as a build result while an independent TypeScript check runs.
56
+ * It contains both structured diagnostic information and any output that was
57
+ * still generated despite the failure.
56
58
  */
57
59
  export interface IFailure {
58
60
  /** Indicates that compilation completed with diagnostics. */
@@ -86,7 +88,8 @@ export namespace ITtscCompilerResult {
86
88
  * pattern-matching error messages. Omitted when ttsc cannot determine the
87
89
  * origin. Treat as `"unknown"` when missing.
88
90
  *
89
- * - `"plugin"`: a native plugin sidecar crashed or exited non-zero.
91
+ * - `"plugin"`: plugin preparation failed before a build result could be
92
+ * recovered.
90
93
  * - `"host"`: the TypeScript-Go host could not start (missing binary, cache
91
94
  * lock, invalid config).
92
95
  * - `"unknown"`: any other host-level failure.