ast-mcp-server 0.16.0 → 0.18.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/CHANGELOG.md +43 -1
- package/README.md +10 -3
- package/cordis.patch.yml +1 -1
- package/dist/angular-template-typecheck-entry.js +19 -0
- package/dist/services/angular-package-resolution.js +62 -0
- package/dist/services/angular-template-typecheck-core.js +139 -0
- package/dist/services/angular-template-typecheck-protocol.js +75 -0
- package/dist/services/angular-template-typecheck.js +262 -0
- package/dist/services/angular-templates.js +110 -59
- package/dist/services/context-builder.js +21 -2
- package/dist/services/explore-presentation.js +1 -0
- package/dist/services/h03-timeout-fixture.js +2 -1
- package/dist/services/operation-block-policy.js +19 -0
- package/dist/services/operation-plan-file.js +38 -4
- package/dist/services/operations.js +88 -15
- package/dist/services/project-operation-scheduler.js +4 -0
- package/dist/services/references.js +53 -28
- package/dist/tools/explore.js +3 -0
- package/dist/tools/find_references.js +6 -22
- package/dist/tools/operation-schema.js +43 -0
- package/dist/tools/rename_symbol.js +9 -4
- package/dist/tools/template-evidence-schema.js +25 -0
- package/package.json +10 -4
- package/skills/structural-code-editing/SKILL.md +8 -3
- package/skills/structural-code-editing/references/operations.md +5 -1
- package/skills/structural-code-editing/releases.json +31 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,45 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [0.
|
|
9
|
+
## [0.18.0] — not yet published (2026-11-10)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `ast_explore` reports Angular template evidence in `evidence[].references` whenever it expands references: each item carries `origin` (`compiler`, `template_external`, `template_inline`) and the block adds `template_coverage` and `template_reference_count`, with the verdict `ast_find_references` gives for the same symbol. The fields are additive and absent when coverage is `not_applicable`, so output for non-Angular projects is unchanged (#380).
|
|
14
|
+
- New `ast_explore` omission reason `template_coverage_incomplete` (category `incomplete`, component `references`). Any coverage status other than `completed` raises it once per symbol and sets `evidence_complete: false`; the reason code is in `template_coverage`, and the omission is still counted when `max_bytes` drops the references block. A failure of the template step alone keeps the TypeScript references and reports `unfinished` with `template_unresolved` (#380).
|
|
15
|
+
- `yarn benchmark:explore-templates` measures the added latency on demand. The recorded run adds 1059 ms at p95 for `detail: "full"`, `limit: 20` on 501 components (threshold 2 s), and every request on 2001 components returns a typed result with `work_exhausted`; see `docs/spikes/0380-explore-template-latency.md` (#380).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- In `ast_explore`, `reference_limit` now bounds the merged list of TypeScript and template items, with the same ordering as `ast_find_references`. Its input description still says "compiler references", and `reference_count` counts TypeScript items only. Page the remaining items through `ast_find_references` (#380).
|
|
20
|
+
- The `tools/list` digest changed because the `ast_explore` output schema gained the optional fields and the new omission reason. Tool descriptions and input schemas are unchanged. `ast_get_impact`, `ast_find_test_candidates` and the `ast_explore` call spines stay compiler-only (#380).
|
|
21
|
+
- The `structural-code-editing` skill is `4.9.0`: it and the README describe the `ast_explore` template evidence instead of the former TypeScript-only limit. The published npm `0.17.0` bytes (skill `4.8.0`) are recorded as a predecessor (#380).
|
|
22
|
+
- `ast_explore` reads and parses each Angular template at most once per request: the selected symbols share the component list, the template reads and the parses through a cache that lives for that request only. Items, order, per-symbol scan budgets and `template_coverage` verdicts are the same as before, and `ast_find_references` is unaffected. In the recorded run the added p50 for `detail: "full"`, `limit: 20` falls from 1040 ms to 179 ms on 501 components and from 4238 ms to 698 ms on 2001 components, where every symbol still ends with `work_exhausted`; see `docs/spikes/0380-explore-template-latency.md` (#454).
|
|
23
|
+
|
|
24
|
+
## [0.17.0] — published to `next`, verification failed, not promoted (2026-11-10)
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `ast_rename_symbol` runs an Angular template type check for renames that edit templates and reports the result as `template_diagnostics` (`status`, `reason`, `added`, `removed`, `added_errors`). The check is on by default and best-effort: it runs ngtsc in a one-shot child process that never writes to the workspace, within a time budget, and `unavailable` or `unfinished` statuses mean unknown, so run the project's build or tests. The field is absent for non-Angular projects and renames that edit no template; the `diagnostics` field stays TypeScript-only (#421).
|
|
29
|
+
- New `ast_rename_symbol` input `block_on_template_errors` (default `false`). It blocks the plan only when a `completed` check adds template errors, and `allow_new_errors` waives it (#421).
|
|
30
|
+
- The check needs `@angular/compiler-cli` to resolve from the project (same version as `@angular/compiler`, TypeScript within its peer range); it is not a declared peer, and without it the status is `unavailable` with `compiler_cli_missing`. `template_diagnostics[].file` is relative to the directory of the tsconfig, while `diagnostics[].file` is relative to `project_root` (#421).
|
|
31
|
+
- Spike results and the design decision are in `docs/spikes/0421-template-type-check.md` and `docs/adr/0017-template-type-check-worker.md` (#421).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Plans from a template-editing rename persist as plan schema v3 (the hash adds `template_diagnostics` and `block_on_template_errors`), even when the status is `unavailable` or `unfinished`. Plans without a check keep schema v2 and the same hash. After a downgrade or revert, schema v3 plans are rejected on import (#421).
|
|
36
|
+
- The `ast_rename_symbol` and `ast_find_references` tool descriptions no longer say template errors are not diagnosed; `ast_find_references` still reports usages only and points to `template_diagnostics` (#421).
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- With `@angular/compiler` 18 or 19, template usage scans walked every subtree twice per nesting level, so `ast_find_references` on a `@Pipe` class or a component member reported `unfinished` with `work_exhausted` on deeply nested templates, and `ast_rename_symbol` refused the rename. Each template is now walked once on every supported major (#441).
|
|
41
|
+
- Upgrading the `structural-code-editing` skill over an unmodified install from npm `0.16.0` would have failed with a conflict: its content changed after `0.16.0` (#443) while the skill version stayed `4.7.0`, and the `0.16.0` byte set had no recorded predecessor. The published `0.16.0` bytes are now recorded, and the skill version is `4.8.0` (#447).
|
|
42
|
+
|
|
43
|
+
### Release status
|
|
44
|
+
|
|
45
|
+
- npm published these immutable bytes under `next` with provenance and the expected Git commit (`e49987bd1fd36432aa76ecb27ca8359eedb613ad`, integrity `sha512-NmjdENfdfLCSqdMJyazxHfx1XuFHgQeJlAB2XIpHGklqCKkwVw43xKMlXmDfdT7m1XT/b4mokx1MefFdj/AIyw==`, publish run `42753818346`). Public-registry verification (run `42754346795`) then failed for the same reason as `0.14.3`–`0.16.0`: the consumer install resolves `minimatch@10.2.6` through `ts-morph@28.0.0`, flagged by `npm audit` for GHSA-g3hv-75wg-m249 (#361). `0.17.0` supersedes `0.16.0` on `next` and was not promoted to `latest`, tagged in Git, or released on GitHub.
|
|
46
|
+
|
|
47
|
+
## [0.16.0] — published to `next`, verification failed, not promoted (2026-11-07)
|
|
10
48
|
|
|
11
49
|
### Added
|
|
12
50
|
|
|
@@ -26,6 +64,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
26
64
|
- `@Directive` members declared with `@Input`, `@Output`, `input()`, `output()` or `model()` reported no `template_coverage`, and members listed in the `inputs` / `outputs` metadata arrays of a `@Component` or `@Directive` were treated as plain members, so `ast_rename_symbol` planned their rename without blocking. They now report `unfinished` with reason `cross_component` (directives with `templates: 0`; a non-literal metadata array marks every member of the class), and `ast_rename_symbol` refuses them with `INCOMPLETE_EVIDENCE` (#410).
|
|
27
65
|
- Upgrading the `structural-code-editing` skill over an unmodified install from npm `0.11.0`–`0.15.1` failed with a conflict: `0.13.0`–`0.15.1` had no recorded predecessor, and the `0.11.0`–`0.12.0` predecessors did not list `references/operations.md`. Every published byte set is now recorded, and the skill version is `4.7.0` (#425).
|
|
28
66
|
|
|
67
|
+
### Release status
|
|
68
|
+
|
|
69
|
+
- npm published these immutable bytes under `next` with provenance and the expected Git commit (`0f5a3f137703fdd673640d00a04087db3b2808af`, integrity `sha512-eyKYMudp0DisVhVJctgwXwZERA2GGsNPP8eU+P/Zh7A9aSf/0mGG9PoGY6qxtBAMW+1FJd+nFLizqo8Go7NLZA==`, publish run `42329276499`). Public-registry verification (run `42331039791`) then failed for the same reason as `0.14.3`–`0.15.1`: the consumer install resolves `minimatch@10.2.6` through `ts-morph@28.0.0`, flagged by `npm audit` for GHSA-g3hv-75wg-m249 (#361). `0.16.0` supersedes `0.15.1` on `next` and was not promoted to `latest`, tagged in Git, or released on GitHub.
|
|
70
|
+
|
|
29
71
|
## [0.15.1] — published to `next`, verification failed, not promoted (2026-11-05)
|
|
30
72
|
|
|
31
73
|
### Changed
|
package/README.md
CHANGED
|
@@ -96,8 +96,15 @@ Template evidence is explicit about what it proves:
|
|
|
96
96
|
- For these members, `completed` means every component template inside the Project was read and every match resolved. `hostDirectives`, `@Directive` inputs and outputs, and non-literal scopes, aliases, metadata or pipe names stay `unfinished` with reason `cross_component`. Runtime bindings (`setInput`, router input binding, `createComponent`) and consumers outside the Project are never examined.
|
|
97
97
|
- Rename rewrites consumer bindings of a selector-bound member when coverage is `completed`, keeping the binding syntax, values and alias-keyed bindings. It returns `INCOMPLETE_EVIDENCE` when coverage is `unfinished` or `unsupported` (including `hostDirectives`, `@Directive` members and metadata-listed members), and `MUTATION_BLOCKED` for inline templates, a name collision with a template local or another binding of the element, `[(x)]` on a non-`model()` pair, `$any`, or a name in the selector. Inline templates are never rewritten.
|
|
98
98
|
- Renaming a `@Pipe` class applies TypeScript edits only and keeps the `name` literal and every `| name` usage; renaming `transform` returns `INCOMPLETE_EVIDENCE`.
|
|
99
|
-
- `ast_explore`
|
|
100
|
-
-
|
|
99
|
+
- `ast_explore` reports the same template evidence in `evidence[].references` whenever it expands references (`detail: "full"` or `include_references: true`): each item carries `origin`, and the block adds `template_coverage` and `template_reference_count` with the verdict `ast_find_references` gives for the same symbol. None of these keys appear when coverage is `not_applicable`.
|
|
100
|
+
- In `ast_explore`, `reference_limit` bounds the merged list of TypeScript and template items, even though its input description still says "compiler references". `reference_count` counts TypeScript items only, and a list cut by the limit raises the usual `reference_limit` omission. `ast_explore` has no reference offset: page the remaining items through `ast_find_references`.
|
|
101
|
+
- Any `template_coverage.status` other than `completed` adds one `template_coverage_incomplete` omission (category `incomplete`, component `references`) per symbol and sets `evidence_complete: false`. The reason code (for example `compiler_missing`) is in `template_coverage`. `omissions.details` lists the omission only when the detail limit leaves room; `omissions.counts` and `omissions.total` always count it, also when `max_bytes` drops the references block.
|
|
102
|
+
- `ast_get_impact`, `ast_find_test_candidates` and the `ast_explore` call spines stay compiler-only: they never contain a template usage.
|
|
103
|
+
- The `diagnostics` field stays TypeScript-only. When a rename edits a template, the plan also carries `template_diagnostics`: a best-effort Angular template type-check delta (`status`, `reason` when not `completed`, `added`, `removed`, `added_errors`) computed by the project's own `@angular/compiler-cli` in a separate child process that never writes to the workspace. It is absent for non-Angular projects and for renames that edit no template. `status` is `completed`, `unavailable` (`compiler_cli_missing`, `compiler_unsupported`, `typescript_incompatible`) or `unfinished` (`work_exhausted`, `deadline`, `check_failed`); the arrays are empty unless it is `completed`.
|
|
104
|
+
- Treat `unavailable` and `unfinished` as unknown, never as clean: run the project's Angular build or tests before applying. `block_on_template_errors` (default `false`) blocks the plan only when the status is `completed` and `added_errors` is not empty; `allow_new_errors` waives it. Without the input the delta is reported but never blocks.
|
|
105
|
+
- `template_diagnostics[].file` is relative to the directory of the tsconfig, while `diagnostics[].file` is relative to `project_root`; they differ when the tsconfig is not at the project root.
|
|
106
|
+
- `@angular/compiler-cli` is not a declared peer. It must resolve from the project, with the same version as `@angular/compiler` and a TypeScript inside its supported range; otherwise the status is `unavailable` with `compiler_cli_missing`.
|
|
107
|
+
- Any template-editing rename emits `template_diagnostics` and persists as plan schema v3, even when the status is `unavailable` or `unfinished`. Plans without the field stay schema v2. After a downgrade or revert, schema v3 plans are rejected on import. See [ADR 0017](docs/adr/0017-template-type-check-worker.md) and the [spike](docs/spikes/0421-template-type-check.md).
|
|
101
108
|
- Yarn PnP projects without a `node_modules` directory cannot resolve the compiler and report `unfinished`; use `nodeLinker: node-modules` for template coverage.
|
|
102
109
|
|
|
103
110
|
## Why this helps
|
|
@@ -374,7 +381,7 @@ For a local candidate, pack and install its tarball instead:
|
|
|
374
381
|
|
|
375
382
|
```bash
|
|
376
383
|
yarn pack --out ast-mcp-server-%v.tgz
|
|
377
|
-
dsh plugin --profile web add ./ast-mcp-server-0.
|
|
384
|
+
dsh plugin --profile web add ./ast-mcp-server-0.18.0.tgz
|
|
378
385
|
```
|
|
379
386
|
|
|
380
387
|
The first supported surface is **reads + prepare + preview**. Every apply path is
|
package/cordis.patch.yml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Mounts the packaged `ast-mcp-server` stdio command through the official
|
|
4
4
|
# `@deepseek-ai/dsh-mcp-client` bridge. Harness does not resolve MCP binaries
|
|
5
5
|
# from PATH, so the entrypoint is resolved relative to the profile directory
|
|
6
|
-
# (`baseUrl`), where `dsh plugin --profile <name> add ./ast-mcp-server-0.
|
|
6
|
+
# (`baseUrl`), where `dsh plugin --profile <name> add ./ast-mcp-server-0.18.0.tgz`
|
|
7
7
|
# installs a locally packed package as a dependency.
|
|
8
8
|
#
|
|
9
9
|
# First-slice surface: reads + prepare + preview. Every apply path is denied
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { runTemplateTypeCheck } from "./services/angular-template-typecheck-core.js";
|
|
4
|
+
import { RequestSchema } from "./services/angular-template-typecheck-protocol.js";
|
|
5
|
+
/** Validates one IPC request and runs the core; it never throws. */
|
|
6
|
+
export async function handleRequest(raw, run = runTemplateTypeCheck) {
|
|
7
|
+
const parsed = RequestSchema.safeParse(raw);
|
|
8
|
+
return parsed.success ? run(parsed.data) : { v: 1, ok: false, reason: "check_failed" };
|
|
9
|
+
}
|
|
10
|
+
// One-shot child: serve a single request, reply, exit. A lost parent ends the process too.
|
|
11
|
+
const entry = process.argv[1];
|
|
12
|
+
if (process.send &&
|
|
13
|
+
entry &&
|
|
14
|
+
fs.realpathSync(entry) === fs.realpathSync(fileURLToPath(import.meta.url))) {
|
|
15
|
+
process.once("disconnect", () => process.exit(1));
|
|
16
|
+
process.once("message", (raw) => {
|
|
17
|
+
void handleRequest(raw).then((reply) => process.send(reply, () => process.exit(0)));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const MIN_COMPILER_MAJOR = 18;
|
|
5
|
+
export const MAX_COMPILER_MAJOR = 22;
|
|
6
|
+
/** Resolves `request` the way the project's own tooling would, or `undefined` when absent. */
|
|
7
|
+
export function resolveFrom(tsConfigFilePath, request) {
|
|
8
|
+
try {
|
|
9
|
+
const resolved = createRequire(tsConfigFilePath).resolve(request);
|
|
10
|
+
return existsSync(resolved) ? resolved : undefined;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function isWithin(parent, child) {
|
|
17
|
+
const relative = path.relative(parent, child);
|
|
18
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
19
|
+
}
|
|
20
|
+
/** True when `packageDir` sits in a node_modules directory owned by `projectDir` or an ancestor. */
|
|
21
|
+
export function isUnderAncestorNodeModules(packageDir, projectDir) {
|
|
22
|
+
const segments = packageDir.split(path.sep);
|
|
23
|
+
return segments.some((segment, index) => segment === "node_modules" &&
|
|
24
|
+
isWithin(segments.slice(0, index).join(path.sep) || path.sep, projectDir));
|
|
25
|
+
}
|
|
26
|
+
export function isSupportedVersion(version) {
|
|
27
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+.*)?$/u.exec(String(version));
|
|
28
|
+
if (!match)
|
|
29
|
+
return false;
|
|
30
|
+
const [major, minor, patch] = [match[1], match[2], match[3]].map(Number);
|
|
31
|
+
const belowFloor = major === MIN_COMPILER_MAJOR && minor === 0 && patch === 0 && !!match[4];
|
|
32
|
+
return major >= MIN_COMPILER_MAJOR && major <= MAX_COMPILER_MAJOR && !belowFloor;
|
|
33
|
+
}
|
|
34
|
+
const MAX_COMPARATORS = 4;
|
|
35
|
+
const COMPARATOR = /^(>=|<)(\d+)\.(\d+)(?:\.(\d+))?$/u;
|
|
36
|
+
const PLAIN_VERSION = /^(\d+)\.(\d+)\.(\d+)$/u;
|
|
37
|
+
function compareTriples(left, right) {
|
|
38
|
+
return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Evaluates a plain `x.y.z` version against a space-joined list of `>=x.y[.z]` and `<x.y[.z]`
|
|
42
|
+
* comparators, the only grammar the Angular peer ranges need. Anything else (`||`, `^`, `~`,
|
|
43
|
+
* wildcards, prereleases, other operators) is `unsupported` so callers fail closed.
|
|
44
|
+
*/
|
|
45
|
+
export function rangeVerdict(version, range) {
|
|
46
|
+
const plain = PLAIN_VERSION.exec(version);
|
|
47
|
+
const tokens = range.trim().split(/\s+/u);
|
|
48
|
+
if (!plain || tokens.length === 0 || tokens.length > MAX_COMPARATORS)
|
|
49
|
+
return "unsupported";
|
|
50
|
+
const actual = [Number(plain[1]), Number(plain[2]), Number(plain[3])];
|
|
51
|
+
let satisfied = true;
|
|
52
|
+
for (const token of tokens) {
|
|
53
|
+
const match = COMPARATOR.exec(token);
|
|
54
|
+
if (!match)
|
|
55
|
+
return "unsupported";
|
|
56
|
+
const bound = [Number(match[2]), Number(match[3]), Number(match[4] ?? 0)];
|
|
57
|
+
const order = compareTriples(actual, bound);
|
|
58
|
+
if (match[1] === ">=" ? order < 0 : order >= 0)
|
|
59
|
+
satisfied = false;
|
|
60
|
+
}
|
|
61
|
+
return satisfied ? "satisfied" : "unsatisfied";
|
|
62
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { MAX_MESSAGE_CHARS, } from "./angular-template-typecheck-protocol.js";
|
|
5
|
+
/** Loads the TypeScript that ngtsc itself resolves, then the pre-validated compiler-cli entry. */
|
|
6
|
+
export async function loadToolchain(cliEntry) {
|
|
7
|
+
const tsEntry = createRequire(cliEntry).resolve("typescript");
|
|
8
|
+
const ts = (await import(pathToFileURL(tsEntry).href)).default;
|
|
9
|
+
const cli = (await import(pathToFileURL(cliEntry).href));
|
|
10
|
+
return { ts, cli };
|
|
11
|
+
}
|
|
12
|
+
/** True when the public surface this driver needs is present (never touches `private/*`). */
|
|
13
|
+
export function probeCompilerApi(cli) {
|
|
14
|
+
return (typeof cli.NgtscProgram === "function" &&
|
|
15
|
+
typeof cli.readConfiguration === "function" &&
|
|
16
|
+
typeof cli.createCompilerHost === "function" &&
|
|
17
|
+
typeof cli.NgtscProgram.prototype?.getNgSemanticDiagnostics === "function");
|
|
18
|
+
}
|
|
19
|
+
/** Forces the options that make an emit or a build-info write impossible. */
|
|
20
|
+
export function withNoEmit(options) {
|
|
21
|
+
return {
|
|
22
|
+
...options,
|
|
23
|
+
noEmit: true,
|
|
24
|
+
incremental: false,
|
|
25
|
+
composite: false,
|
|
26
|
+
tsBuildInfoFile: undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Scope B: an edited template or a scoped component file, never anything under node_modules. */
|
|
30
|
+
export function isInScope(fileName, scopeFiles) {
|
|
31
|
+
return scopeFiles.has(fileName) && !fileName.split(/[\\/]/u).includes("node_modules");
|
|
32
|
+
}
|
|
33
|
+
export function toWireDiagnostic(ts, root, diagnostic) {
|
|
34
|
+
const { file } = diagnostic;
|
|
35
|
+
const start = diagnostic.start ?? null;
|
|
36
|
+
const position = file && start !== null ? file.getLineAndCharacterOfPosition(start) : null;
|
|
37
|
+
return {
|
|
38
|
+
code: diagnostic.code,
|
|
39
|
+
category: ts.DiagnosticCategory[diagnostic.category] ?? String(diagnostic.category),
|
|
40
|
+
file: file ? path.relative(root, file.fileName) : "",
|
|
41
|
+
line: position ? position.line + 1 : null,
|
|
42
|
+
column: position ? position.character + 1 : null,
|
|
43
|
+
message: ts
|
|
44
|
+
.flattenDiagnosticMessageText(diagnostic.messageText, "\n")
|
|
45
|
+
.slice(0, MAX_MESSAGE_CHARS),
|
|
46
|
+
start,
|
|
47
|
+
length: diagnostic.length ?? null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Host over the disk with an optional in-memory overlay. Unchanged files come from one cache
|
|
52
|
+
* shared by both runs, so the after run reuses the identical SourceFile objects and only parses
|
|
53
|
+
* overlay files again.
|
|
54
|
+
*/
|
|
55
|
+
function createCheckHost({ ts, cli }, options, overlay, shared) {
|
|
56
|
+
const tsHost = ts.createCompilerHost(options, true);
|
|
57
|
+
const { readFile, fileExists, getSourceFile } = tsHost;
|
|
58
|
+
const fresh = new Map();
|
|
59
|
+
tsHost.writeFile = () => {
|
|
60
|
+
throw new Error("The template type check must not write files.");
|
|
61
|
+
};
|
|
62
|
+
tsHost.readFile = (name) => overlay?.get(name) ?? readFile.call(tsHost, name);
|
|
63
|
+
tsHost.fileExists = (name) => overlay?.has(name) === true || fileExists.call(tsHost, name);
|
|
64
|
+
tsHost.getSourceFile = (name, languageVersion, ...rest) => {
|
|
65
|
+
const text = overlay?.get(name);
|
|
66
|
+
if (text !== undefined) {
|
|
67
|
+
if (!fresh.has(name))
|
|
68
|
+
fresh.set(name, ts.createSourceFile(name, text, languageVersion, true));
|
|
69
|
+
return fresh.get(name);
|
|
70
|
+
}
|
|
71
|
+
if (!shared.has(name)) {
|
|
72
|
+
shared.set(name, getSourceFile.call(tsHost, name, languageVersion, ...rest));
|
|
73
|
+
}
|
|
74
|
+
return shared.get(name);
|
|
75
|
+
};
|
|
76
|
+
const ngHost = cli.createCompilerHost({ options, tsHost });
|
|
77
|
+
if (overlay) {
|
|
78
|
+
// With `oldProgram` reuse ngtsc re-analyzes a component only if it is told which resources changed.
|
|
79
|
+
const resources = new Set([...overlay.keys()].filter((name) => !/\.[cm]?tsx?$/u.test(name)));
|
|
80
|
+
ngHost.getModifiedResourceFiles = () => resources;
|
|
81
|
+
}
|
|
82
|
+
return ngHost;
|
|
83
|
+
}
|
|
84
|
+
async function checkSide(toolchain, config, request, overlay, shared, oldProgram) {
|
|
85
|
+
const host = createCheckHost(toolchain, config.options, overlay, shared);
|
|
86
|
+
const program = new toolchain.cli.NgtscProgram(config.rootNames, config.options, host, oldProgram);
|
|
87
|
+
await program.loadNgStructureAsync();
|
|
88
|
+
const compiler = program.compiler;
|
|
89
|
+
if (typeof compiler?.getComponentsWithTemplateFile !== "function")
|
|
90
|
+
return "unsupported";
|
|
91
|
+
const scope = new Set(request.templates);
|
|
92
|
+
for (const template of request.templates) {
|
|
93
|
+
for (const component of compiler.getComponentsWithTemplateFile(template)) {
|
|
94
|
+
scope.add(component.getSourceFile().fileName);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const root = path.dirname(request.tsconfig);
|
|
98
|
+
const diagnostics = program
|
|
99
|
+
.getNgSemanticDiagnostics()
|
|
100
|
+
.filter((diagnostic) => diagnostic.file && isInScope(diagnostic.file.fileName, scope))
|
|
101
|
+
.map((diagnostic) => toWireDiagnostic(toolchain.ts, root, diagnostic));
|
|
102
|
+
return { program, diagnostics };
|
|
103
|
+
}
|
|
104
|
+
const fail = (reason) => ({
|
|
105
|
+
v: 1,
|
|
106
|
+
ok: false,
|
|
107
|
+
reason,
|
|
108
|
+
});
|
|
109
|
+
/** Runs ngtsc on the disk state, then on disk plus overlay, and returns both scoped lists. */
|
|
110
|
+
export async function runTemplateTypeCheck(request, load = loadToolchain) {
|
|
111
|
+
try {
|
|
112
|
+
const toolchain = await load(request.cliEntry);
|
|
113
|
+
if (!probeCompilerApi(toolchain.cli))
|
|
114
|
+
return fail("compiler_unsupported");
|
|
115
|
+
const read = toolchain.cli.readConfiguration(request.tsconfig);
|
|
116
|
+
if (read.errors.length > 0)
|
|
117
|
+
return fail("check_failed");
|
|
118
|
+
const config = { options: withNoEmit(read.options), rootNames: read.rootNames };
|
|
119
|
+
const shared = new Map();
|
|
120
|
+
const overlay = new Map(request.overlay.map((file) => [file.absolutePath, file.text]));
|
|
121
|
+
const before = await checkSide(toolchain, config, request, undefined, shared, undefined);
|
|
122
|
+
if (before === "unsupported")
|
|
123
|
+
return fail("compiler_unsupported");
|
|
124
|
+
if (before.diagnostics.length > request.maxDiagnostics)
|
|
125
|
+
return fail("work_exhausted");
|
|
126
|
+
const after = await checkSide(toolchain, config, request, overlay, shared, before.program);
|
|
127
|
+
if (after === "unsupported")
|
|
128
|
+
return fail("compiler_unsupported");
|
|
129
|
+
if (after.diagnostics.length > request.maxDiagnostics)
|
|
130
|
+
return fail("work_exhausted");
|
|
131
|
+
return { v: 1, ok: true, before: before.diagnostics, after: after.diagnostics };
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const message = error instanceof Error ? error.message : "";
|
|
135
|
+
return fail(message.startsWith("The Angular Compiler requires TypeScript")
|
|
136
|
+
? "typescript_incompatible"
|
|
137
|
+
: "check_failed");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** Caps shared by both ends of the one-shot IPC channel (v1). */
|
|
4
|
+
export const MAX_WIRE_DIAGNOSTICS = 5000;
|
|
5
|
+
export const MAX_OVERLAY_BYTES = 32 * 1024 * 1024;
|
|
6
|
+
export const MAX_TEMPLATES = 2000;
|
|
7
|
+
export const MAX_OVERLAY_FILES = 10_000;
|
|
8
|
+
const MAX_PATH_CHARS = 4096;
|
|
9
|
+
export const MAX_MESSAGE_CHARS = 8192;
|
|
10
|
+
const absolutePath = z
|
|
11
|
+
.string()
|
|
12
|
+
.max(MAX_PATH_CHARS)
|
|
13
|
+
.refine((value) => path.isAbsolute(value), "must be an absolute path");
|
|
14
|
+
export const RequestSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
v: z.literal(1),
|
|
17
|
+
tsconfig: absolutePath,
|
|
18
|
+
cliEntry: absolutePath,
|
|
19
|
+
templates: z.array(absolutePath).max(MAX_TEMPLATES),
|
|
20
|
+
overlay: z
|
|
21
|
+
.array(z.object({ absolutePath, text: z.string() }).strict())
|
|
22
|
+
.max(MAX_OVERLAY_FILES)
|
|
23
|
+
.refine((files) => files.reduce((total, file) => total + Buffer.byteLength(file.text, "utf8"), 0) <=
|
|
24
|
+
MAX_OVERLAY_BYTES, "overlay exceeds the byte cap"),
|
|
25
|
+
maxDiagnostics: z.number().int().min(1).max(MAX_WIRE_DIAGNOSTICS),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
const position = z.number().int().nonnegative().nullable();
|
|
29
|
+
export const WireDiagnosticSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
code: z.number().int(),
|
|
32
|
+
category: z.string().max(32),
|
|
33
|
+
/** Project-relative path. */
|
|
34
|
+
file: z.string().max(MAX_PATH_CHARS),
|
|
35
|
+
line: position,
|
|
36
|
+
column: position,
|
|
37
|
+
message: z.string().max(MAX_MESSAGE_CHARS),
|
|
38
|
+
start: position,
|
|
39
|
+
length: position,
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
/** Only a successful reply carries diagnostics, so a non-completed status can never include any. */
|
|
43
|
+
export const ResponseSchema = z.discriminatedUnion("ok", [
|
|
44
|
+
z
|
|
45
|
+
.object({
|
|
46
|
+
v: z.literal(1),
|
|
47
|
+
ok: z.literal(true),
|
|
48
|
+
before: z.array(WireDiagnosticSchema).max(MAX_WIRE_DIAGNOSTICS),
|
|
49
|
+
after: z.array(WireDiagnosticSchema).max(MAX_WIRE_DIAGNOSTICS),
|
|
50
|
+
})
|
|
51
|
+
.strict(),
|
|
52
|
+
z
|
|
53
|
+
.object({
|
|
54
|
+
v: z.literal(1),
|
|
55
|
+
ok: z.literal(false),
|
|
56
|
+
reason: z.enum([
|
|
57
|
+
"compiler_unsupported",
|
|
58
|
+
"typescript_incompatible",
|
|
59
|
+
"work_exhausted",
|
|
60
|
+
"check_failed",
|
|
61
|
+
]),
|
|
62
|
+
})
|
|
63
|
+
.strict(),
|
|
64
|
+
]);
|
|
65
|
+
/** Separates an over-cap reply (work bound) from a malformed one (check failure). */
|
|
66
|
+
export function parseResponse(value) {
|
|
67
|
+
if (typeof value === "object" && value !== null) {
|
|
68
|
+
const { before, after } = value;
|
|
69
|
+
const oversizedSide = (side) => Array.isArray(side) && side.length > MAX_WIRE_DIAGNOSTICS;
|
|
70
|
+
if (oversizedSide(before) || oversizedSide(after))
|
|
71
|
+
return { status: "oversized" };
|
|
72
|
+
}
|
|
73
|
+
const parsed = ResponseSchema.safeParse(value);
|
|
74
|
+
return parsed.success ? { status: "valid", response: parsed.data } : { status: "malformed" };
|
|
75
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { isSupportedVersion, isUnderAncestorNodeModules, isWithin, rangeVerdict, resolveFrom, } from "./angular-package-resolution.js";
|
|
7
|
+
import { MAX_OVERLAY_BYTES, MAX_OVERLAY_FILES, MAX_TEMPLATES, MAX_WIRE_DIAGNOSTICS, parseResponse, } from "./angular-template-typecheck-protocol.js";
|
|
8
|
+
import { compareObservedDiagnostics, } from "./diagnostics.js";
|
|
9
|
+
import { NO_REQUEST_CONTEXT, RequestContextError } from "./request-context.js";
|
|
10
|
+
const UNSUPPORTED = { ok: false, reason: "compiler_unsupported" };
|
|
11
|
+
/** Locates an installed package through the shared guard, or reports why it cannot be trusted. */
|
|
12
|
+
function locatePackage(tsConfigFilePath, name, projectDir) {
|
|
13
|
+
const manifestPath = resolveFrom(tsConfigFilePath, `${name}/package.json`);
|
|
14
|
+
if (!manifestPath) {
|
|
15
|
+
try {
|
|
16
|
+
createRequire(tsConfigFilePath).resolve(`${name}/package.json`);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === "MODULE_NOT_FOUND")
|
|
20
|
+
return "missing";
|
|
21
|
+
}
|
|
22
|
+
return "unsupported";
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const dir = realpathSync(path.dirname(manifestPath));
|
|
26
|
+
if (!isUnderAncestorNodeModules(dir, projectDir))
|
|
27
|
+
return "unsupported";
|
|
28
|
+
const manifest = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
29
|
+
return manifest?.name === name ? { dir, manifest } : "unsupported";
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "unsupported";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function majorOf(version) {
|
|
36
|
+
return Number(String(version).split(".")[0]);
|
|
37
|
+
}
|
|
38
|
+
/** In-process checks before any spawn; TypeScript is resolved from the compiler-cli, as ngtsc does. */
|
|
39
|
+
export function preflightTemplateTypeCheck(tsConfigFilePath) {
|
|
40
|
+
let projectDir;
|
|
41
|
+
try {
|
|
42
|
+
projectDir = realpathSync(path.dirname(tsConfigFilePath));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return UNSUPPORTED;
|
|
46
|
+
}
|
|
47
|
+
const cli = locatePackage(tsConfigFilePath, "@angular/compiler-cli", projectDir);
|
|
48
|
+
if (cli === "missing")
|
|
49
|
+
return { ok: false, reason: "compiler_cli_missing" };
|
|
50
|
+
if (cli === "unsupported")
|
|
51
|
+
return UNSUPPORTED;
|
|
52
|
+
const compiler = locatePackage(tsConfigFilePath, "@angular/compiler", projectDir);
|
|
53
|
+
const core = locatePackage(tsConfigFilePath, "@angular/core", projectDir);
|
|
54
|
+
if (typeof compiler === "string" || typeof core === "string")
|
|
55
|
+
return UNSUPPORTED;
|
|
56
|
+
const version = cli.manifest.version;
|
|
57
|
+
if (!isSupportedVersion(version) || compiler.manifest.version !== version)
|
|
58
|
+
return UNSUPPORTED;
|
|
59
|
+
if (!isSupportedVersion(core.manifest.version) ||
|
|
60
|
+
majorOf(core.manifest.version) !== majorOf(version)) {
|
|
61
|
+
return UNSUPPORTED;
|
|
62
|
+
}
|
|
63
|
+
const entry = resolveFrom(tsConfigFilePath, "@angular/compiler-cli");
|
|
64
|
+
let cliEntry;
|
|
65
|
+
try {
|
|
66
|
+
cliEntry = entry && realpathSync(entry);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return UNSUPPORTED;
|
|
70
|
+
}
|
|
71
|
+
if (!cliEntry || !isWithin(cli.dir, cliEntry))
|
|
72
|
+
return UNSUPPORTED;
|
|
73
|
+
const peer = cli.manifest.peerDependencies?.typescript;
|
|
74
|
+
const typescript = resolveFrom(path.join(cli.dir, "package.json"), "typescript/package.json");
|
|
75
|
+
if (typeof peer !== "string")
|
|
76
|
+
return UNSUPPORTED;
|
|
77
|
+
if (!typescript)
|
|
78
|
+
return { ok: false, reason: "typescript_incompatible" };
|
|
79
|
+
let typescriptVersion;
|
|
80
|
+
try {
|
|
81
|
+
typescriptVersion = JSON.parse(readFileSync(typescript, "utf8")).version;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return UNSUPPORTED;
|
|
85
|
+
}
|
|
86
|
+
const verdict = rangeVerdict(String(typescriptVersion), peer);
|
|
87
|
+
if (verdict === "unsupported")
|
|
88
|
+
return UNSUPPORTED;
|
|
89
|
+
if (verdict === "unsatisfied")
|
|
90
|
+
return { ok: false, reason: "typescript_incompatible" };
|
|
91
|
+
return { ok: true, cliDir: cli.dir, cliEntry };
|
|
92
|
+
}
|
|
93
|
+
function emptyDelta(status, reason) {
|
|
94
|
+
return { status, reason, added: [], removed: [], addedErrors: [] };
|
|
95
|
+
}
|
|
96
|
+
const MAX_CHECK_MS = 30_000;
|
|
97
|
+
const MIN_CHECK_MS = 2_000;
|
|
98
|
+
const RESERVE_MS = 10_000;
|
|
99
|
+
const CHILD_HEAP_MB = 1024;
|
|
100
|
+
const EXIT_SETTLE_MS = 500;
|
|
101
|
+
const OOM_EXIT_CODE = 134;
|
|
102
|
+
const DEFAULT_ENTRY = fileURLToPath(new URL("../angular-template-typecheck-entry.js", import.meta.url));
|
|
103
|
+
const UNBOUNDED_CONTEXT = {
|
|
104
|
+
signal: NO_REQUEST_CONTEXT.signal,
|
|
105
|
+
checkpoint: () => NO_REQUEST_CONTEXT.checkpoint(),
|
|
106
|
+
remainingMs: () => MAX_CHECK_MS + RESERVE_MS,
|
|
107
|
+
};
|
|
108
|
+
/** Time the child may run: capped, and leaving a reserve for the rest of the operation. */
|
|
109
|
+
export function checkBudgetMs(remainingMs) {
|
|
110
|
+
return Math.min(MAX_CHECK_MS, remainingMs - RESERVE_MS);
|
|
111
|
+
}
|
|
112
|
+
function outcomeOf(code, signal, reply) {
|
|
113
|
+
if (signal === "SIGABRT" || code === OOM_EXIT_CODE) {
|
|
114
|
+
return { kind: "reason", reason: "work_exhausted" };
|
|
115
|
+
}
|
|
116
|
+
if (code !== 0)
|
|
117
|
+
return { kind: "reason", reason: "check_failed" };
|
|
118
|
+
const parsed = parseResponse(reply);
|
|
119
|
+
if (parsed.status === "valid")
|
|
120
|
+
return { kind: "reply", response: parsed.response };
|
|
121
|
+
return {
|
|
122
|
+
kind: "reason",
|
|
123
|
+
reason: parsed.status === "oversized" ? "work_exhausted" : "check_failed",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Runs one child to completion; it is always reaped before this settles. */
|
|
127
|
+
function runChild(request, budgetMs, ctx, deps) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
let child;
|
|
130
|
+
try {
|
|
131
|
+
child = (deps.spawn ?? fork)(deps.entry ?? DEFAULT_ENTRY, [], {
|
|
132
|
+
execPath: process.execPath,
|
|
133
|
+
execArgv: [`--max-old-space-size=${CHILD_HEAP_MB}`],
|
|
134
|
+
env: {},
|
|
135
|
+
serialization: "advanced",
|
|
136
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return resolve({ kind: "reason", reason: "check_failed" });
|
|
141
|
+
}
|
|
142
|
+
let reply;
|
|
143
|
+
let killedFor;
|
|
144
|
+
let failed = false;
|
|
145
|
+
let settled = false;
|
|
146
|
+
const kill = (why) => {
|
|
147
|
+
killedFor ??= why;
|
|
148
|
+
child.kill("SIGKILL");
|
|
149
|
+
};
|
|
150
|
+
const settle = (result) => {
|
|
151
|
+
if (settled)
|
|
152
|
+
return;
|
|
153
|
+
settled = true;
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
156
|
+
if (result instanceof Error)
|
|
157
|
+
reject(result);
|
|
158
|
+
else
|
|
159
|
+
resolve(result);
|
|
160
|
+
};
|
|
161
|
+
const onAbort = () => kill("abort");
|
|
162
|
+
const timer = setTimeout(() => kill("deadline"), budgetMs);
|
|
163
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
child.once("message", (message) => {
|
|
165
|
+
reply = message;
|
|
166
|
+
});
|
|
167
|
+
child.once("error", () => {
|
|
168
|
+
failed = true;
|
|
169
|
+
// No pid means the spawn itself failed and no exit will follow; otherwise reap first.
|
|
170
|
+
if (child.pid === undefined)
|
|
171
|
+
settle({ kind: "reason", reason: "check_failed" });
|
|
172
|
+
else
|
|
173
|
+
child.kill("SIGKILL");
|
|
174
|
+
});
|
|
175
|
+
child.once("exit", (code, signal) => {
|
|
176
|
+
const finish = () => {
|
|
177
|
+
if (killedFor === "abort")
|
|
178
|
+
settle(abortError(ctx));
|
|
179
|
+
else if (killedFor === "deadline")
|
|
180
|
+
settle({ kind: "deadline" });
|
|
181
|
+
else if (failed)
|
|
182
|
+
settle({ kind: "reason", reason: "check_failed" });
|
|
183
|
+
else
|
|
184
|
+
settle(outcomeOf(code, signal, reply));
|
|
185
|
+
};
|
|
186
|
+
// Pending IPC messages are delivered before the channel reports closed.
|
|
187
|
+
if (!child.connected)
|
|
188
|
+
return finish();
|
|
189
|
+
child.once("disconnect", finish);
|
|
190
|
+
setTimeout(finish, EXIT_SETTLE_MS).unref();
|
|
191
|
+
});
|
|
192
|
+
if (ctx.signal.aborted)
|
|
193
|
+
return onAbort();
|
|
194
|
+
child.send(request, () => undefined);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function abortError(ctx) {
|
|
198
|
+
try {
|
|
199
|
+
ctx.checkpoint();
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
return error;
|
|
203
|
+
}
|
|
204
|
+
return new RequestContextError("REQUEST_CANCELLED");
|
|
205
|
+
}
|
|
206
|
+
function observations(diagnostics) {
|
|
207
|
+
return diagnostics.map(({ start, length, ...shown }, ordinal) => ({
|
|
208
|
+
public: {
|
|
209
|
+
code: shown.code,
|
|
210
|
+
category: shown.category,
|
|
211
|
+
file: shown.file,
|
|
212
|
+
line: shown.line,
|
|
213
|
+
column: shown.column,
|
|
214
|
+
message: shown.message,
|
|
215
|
+
},
|
|
216
|
+
start,
|
|
217
|
+
length,
|
|
218
|
+
ordinal,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
function diagnosticsOf(response, changes, ctx) {
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
const reason = response.reason;
|
|
224
|
+
return emptyDelta(reason === "compiler_unsupported" || reason === "typescript_incompatible"
|
|
225
|
+
? "unavailable"
|
|
226
|
+
: "unfinished", reason);
|
|
227
|
+
}
|
|
228
|
+
const delta = compareObservedDiagnostics(observations(response.before), observations(response.after), changes, ctx);
|
|
229
|
+
return { status: "completed", ...delta };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Entry point of the template type check: preflight and limits in-process, then one forked child
|
|
233
|
+
* under a wall-clock budget. A client abort kills the child and rethrows `REQUEST_CANCELLED`.
|
|
234
|
+
*/
|
|
235
|
+
export async function checkTemplateTypes(input, ctx = UNBOUNDED_CONTEXT, deps = {}) {
|
|
236
|
+
ctx.checkpoint();
|
|
237
|
+
const preflight = preflightTemplateTypeCheck(input.tsConfigFilePath);
|
|
238
|
+
if (!preflight.ok)
|
|
239
|
+
return emptyDelta("unavailable", preflight.reason);
|
|
240
|
+
const overlayBytes = input.overlay.reduce((total, file) => total + Buffer.byteLength(file.text, "utf8"), 0);
|
|
241
|
+
if (input.templates.length > MAX_TEMPLATES ||
|
|
242
|
+
input.overlay.length > MAX_OVERLAY_FILES ||
|
|
243
|
+
overlayBytes > MAX_OVERLAY_BYTES) {
|
|
244
|
+
return emptyDelta("unfinished", "work_exhausted");
|
|
245
|
+
}
|
|
246
|
+
const budgetMs = checkBudgetMs(ctx.remainingMs());
|
|
247
|
+
if (budgetMs < MIN_CHECK_MS)
|
|
248
|
+
return emptyDelta("unfinished", "deadline");
|
|
249
|
+
const outcome = await runChild({
|
|
250
|
+
v: 1,
|
|
251
|
+
tsconfig: input.tsConfigFilePath,
|
|
252
|
+
cliEntry: preflight.cliEntry,
|
|
253
|
+
templates: [...input.templates],
|
|
254
|
+
overlay: input.overlay.map(({ absolutePath, text }) => ({ absolutePath, text })),
|
|
255
|
+
maxDiagnostics: MAX_WIRE_DIAGNOSTICS,
|
|
256
|
+
}, budgetMs, ctx, deps);
|
|
257
|
+
if (outcome.kind === "deadline")
|
|
258
|
+
return emptyDelta("unfinished", "deadline");
|
|
259
|
+
if (outcome.kind === "reason")
|
|
260
|
+
return emptyDelta("unfinished", outcome.reason);
|
|
261
|
+
return diagnosticsOf(outcome.response, input.changes, ctx);
|
|
262
|
+
}
|