@ttsc/lint 0.23.0 → 0.25.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.
@@ -1,51 +1,31 @@
1
- export const CONFIG_EVALUATOR_MAX_BUFFER = 16 * 1024 * 1024;
2
- export const CONFIG_EVALUATOR_TIMEOUT_MS = 60_000;
3
- export const CONFIG_EVALUATOR_TEARDOWN_GRACE_MS = 5_000;
4
- export const CONFIG_EVALUATOR_STATUS_FD = 3;
5
- export const CONFIG_EVALUATOR_PROCESS_OPTIONS = Object.freeze({
6
- killSignal: "SIGKILL" as const,
7
- maxBuffer: CONFIG_EVALUATOR_MAX_BUFFER,
8
- timeout: CONFIG_EVALUATOR_TIMEOUT_MS + CONFIG_EVALUATOR_TEARDOWN_GRACE_MS,
9
- });
1
+ import fs from "node:fs";
10
2
 
11
3
  interface ConfigEvaluatorProcessResult {
12
4
  error?: Error;
13
- output?: readonly (string | null)[] | null;
14
5
  signal: NodeJS.Signals | null;
15
6
  status: number | null;
16
- stderr: string | null | undefined;
17
7
  }
18
8
 
19
9
  /**
20
10
  * Classify the ways the isolated lint-config evaluator can stop.
21
11
  *
22
- * Node reports both timeout and max-buffer termination with the configured
23
- * signal, so the process error code must take precedence over the signal. The
24
- * evaluator uses `SIGKILL`: Node's synchronous process API otherwise keeps
25
- * waiting when a POSIX child handles the default `SIGTERM` without exiting. A
26
- * bare signal is an external termination and a non-zero status is an evaluator
27
- * failure whose stderr tail contains the useful user-config diagnostic.
12
+ * The evaluator writes the child's own output straight to this process's
13
+ * stderr, so a diagnostic has already reached the user by the time anything
14
+ * here runs. What is left to say is only how the process ended: it never
15
+ * launched, something outside killed it, or it exited non-zero after printing
16
+ * its own reason.
17
+ *
18
+ * Nothing is bounded here — not time, not output. Both were the compiler
19
+ * deciding, on numbers nobody chose for this machine, that a user's own config
20
+ * had run too long or said too much. A slow config is a slow build the user can
21
+ * watch and interrupt; a loud one is output they asked for. Neither is this
22
+ * process's memory to spend either, because the child's streams are no longer
23
+ * collected into it.
28
24
  */
29
25
  export function configEvaluatorProcessFailure(
30
26
  result: ConfigEvaluatorProcessResult,
31
27
  configPath: string,
32
28
  ): Error | undefined {
33
- const code = (result.error as NodeJS.ErrnoException | undefined)?.code;
34
- const nestedCode = result.output?.[CONFIG_EVALUATOR_STATUS_FD]?.trim() ?? "";
35
- if (code === "ETIMEDOUT" || nestedCode === "ETIMEDOUT") {
36
- return new Error(
37
- `@ttsc/lint: ttsx evaluation of ${configPath} timed out after ` +
38
- `${CONFIG_EVALUATOR_TIMEOUT_MS / 1_000} seconds. ` +
39
- "Simplify the config or move heavy work out of top-level.",
40
- );
41
- }
42
- if (code === "ENOBUFS" || nestedCode === "ENOBUFS") {
43
- return new Error(
44
- `@ttsc/lint: ttsx evaluation of ${configPath} exceeded the ` +
45
- `${CONFIG_EVALUATOR_MAX_BUFFER / (1024 * 1024)} MiB output limit. ` +
46
- "Reduce console output from the config and its dependencies.",
47
- );
48
- }
49
29
  if (result.error) {
50
30
  return new Error(
51
31
  `@ttsc/lint: failed to spawn ttsx for ${configPath}: ${result.error.message}`,
@@ -57,43 +37,34 @@ export function configEvaluatorProcessFailure(
57
37
  );
58
38
  }
59
39
  if (result.status !== 0) {
60
- const reason = configEvaluatorFailureReason(result.stderr);
61
40
  return new Error(
62
- `@ttsc/lint: lint config ${configPath} evaluation failed with exit code ${String(result.status)}` +
63
- (reason === "" ? "" : "\n" + reason),
41
+ `@ttsc/lint: lint config ${configPath} evaluation failed with exit code ${String(result.status)}`,
64
42
  );
65
43
  }
66
44
  return undefined;
67
45
  }
68
46
 
69
47
  /**
70
- * Pass the semantic deadline and private status pipe through the `ttsx` wrapper
71
- * to the runtime child that actually executes the config.
72
- */
73
- export function configEvaluatorBoundaryEnvironment(
74
- now: number = Date.now(),
75
- ): NodeJS.ProcessEnv {
76
- return {
77
- TTSC_TTSX_EVALUATOR_DEADLINE_MS: String(now + CONFIG_EVALUATOR_TIMEOUT_MS),
78
- TTSC_TTSX_EVALUATOR_MAX_BUFFER_BYTES: String(CONFIG_EVALUATOR_MAX_BUFFER),
79
- TTSC_TTSX_EVALUATOR_STATUS_FD: String(CONFIG_EVALUATOR_STATUS_FD),
80
- };
81
- }
82
-
83
- /**
84
- * Return the useful tail of evaluator stderr without turning an exception into
85
- * an unbounded duplicate of the already-forwarded child stream.
48
+ * Read the failure envelope the evaluator writes to its result file when it
49
+ * stops on an error it can name.
50
+ *
51
+ * This is the other half of classifying how the evaluation ended, which is why
52
+ * it lives beside {@link configEvaluatorProcessFailure} rather than at the call
53
+ * site: the status says that it failed, and this says why.
54
+ *
55
+ * Only a well-formed envelope is honoured. A real evaluation payload never
56
+ * carries this key, and every other shape — an absent file, a build that failed
57
+ * before the loader ran, a half-written result, a payload written before a
58
+ * later non-zero exit — leaves the process status to speak for itself.
86
59
  */
87
- function configEvaluatorFailureReason(
88
- stderr: string | null | undefined,
89
- ): string {
90
- const bounded = (stderr ?? "").slice(-CONFIG_EVALUATOR_REASON_MAX_CHARS);
91
- const lines = bounded
92
- .split(/\r?\n/)
93
- .map((line) => line.trimEnd())
94
- .filter((line) => line.trim() !== "");
95
- return lines.slice(-CONFIG_EVALUATOR_REASON_LINES).join("\n");
60
+ export function configEvaluatorFailureReason(outputPath: string): string {
61
+ try {
62
+ const parsed: unknown = JSON.parse(fs.readFileSync(outputPath, "utf8"));
63
+ if (typeof parsed !== "object" || parsed === null) return "";
64
+ const message = (parsed as { __ttscLoaderError?: unknown })
65
+ .__ttscLoaderError;
66
+ return typeof message === "string" ? message.trim() : "";
67
+ } catch {
68
+ return "";
69
+ }
96
70
  }
97
-
98
- const CONFIG_EVALUATOR_REASON_LINES = 5;
99
- const CONFIG_EVALUATOR_REASON_MAX_CHARS = 8 * 1024;