Skip to content

refactor(compiler-cli): gate synchronous preprocessor APIs on synchronous hosts - #71919

Draft
clydin wants to merge 4 commits into
angular:mainfrom
clydin:ngp-host-h4-2
Draft

clydin wants to merge 4 commits into
angular:mainfrom
clydin:ngp-host-h4-2

Conversation

@clydin

@clydin clydin commented Nov 10, 2026

Copy link
Copy Markdown
Member

Asynchronous bundler hosts suspend module resolution and content loading across
pump turns, so synchronous *_blocking and *Sync entry points cannot run to
completion within a single call. Without explicit host gating, calling a
synchronous entry point against an asynchronous host either panics inside WASM
or starts an analysis stream that stalls mid-iteration.

Enforce AnalysisHost::is_synchronous inside QueryEngine::block_on_query and
add fallible ensure_synchronous_host checks to Analyzer's synchronous core
helpers. Convert WasmAnalyzer::get_metadata_for_file and read_resource to
return Result<Option<String>, String> and expose is_synchronous_host on
WASM bindings so synchronous WASM calls fail cleanly with
"synchronous APIs require a synchronous host" instead of panicking.

Surface isSynchronousHost on IAnalyzer and WasmAnalyzer, separating
internal WASM mutation/template helpers so asynchronous entry points continue
working while *Sync entry points across WasmAnalyzer, HybridCompiler, and
getIndexedComponentsSync immediately throw when isSynchronousHost is
false. Behavior under DefaultFsHost is unchanged.

@clydin clydin added state: blocked target: minor This PR is targeted for the next minor release labels Nov 10, 2026
@angular-robot angular-robot Bot added the area: compiler Issues related to `ngc`, Angular's template compiler label Nov 10, 2026
@ngbot ngbot Bot added this to the Backlog milestone Nov 10, 2026
@clydin
clydin force-pushed the ngp-host-h4-2 branch 4 times, most recently from b280b9d to fd507a4 Compare November 11, 2026 18:33
… imports in preprocessor

Previously, `QueryEngine` invoked `AnalysisHost::resolve` directly on every
syntactic dependency scan, cross-file constant evaluation, export-map walk,
and NgModule scope traversal without an engine-level cache. For asynchronous
bundler hosts, repeating identical `(importer, specifier, kind, type_only,
attributes)` lookups across queries and rebuilds would generate redundant
cross-boundary round-trips. In addition, failed module resolutions only
recorded `UNRESOLVED_IMPORTS_FILE_ID` during the Stage-1 static dependency
scan of `.ts` sources. That scan does not resolve the specifiers of `.d.ts`
files, so a failed lookup made from one, such as a re-export to a missing
target, was untracked and not re-run when the target was later created.
Deleting a resolved file also left importers holding stale cached dependency
edges.

Introduce `ResolutionCache` on `QueryEngine` to memoize completed module
resolution outcomes per importer `FileId` and request key while maintaining
both a target-to-importer reverse index and an unresolved-importer index.
Route query-time module lookups through `QueryContext::resolve_request`,
`resolve_from_id`, and `resolve_from_path` so every resolution records the
importer dependency and every failed lookup—including negative cache hits—
records `UNRESOLVED_IMPORTS_FILE_ID`. Classify pure re-exports in
`analyze_file_syntax` with `ImportKind::ReExport` so syntactic dependency
discovery shares cached resolutions with cross-file evaluation and export-map
queries.

On `invalidate_file_id`, drop only the edited importer's cached resolutions
so edits to a leaf module preserve cached resolutions across its importers.
On `FileUpdateType::Deleted`, evict queries derived from importers that had
resolved to the deleted target while keeping their `ParseFile` results
cached. On `invalidate_module_resolution`, stop serving every cached
resolution and re-run importers with previously unresolved specifiers. The
superseded entries are kept as stale rather than dropped, so the reverse
index still covers importers whose queries stay cached and that therefore do
not resolve again, and a lookup that the host answers after the change is
not memoized.

When a host resolution changes a module's `IdKind`, also evict the importers
whose cached resolutions report the previous kind. The resolution that
changed the kind is not memoized, so the query re-run that follows asks the
host again.
…or and HybridCompiler

Bundler integrations and workspace reconfigurations can change how module
specifiers resolve without editing any individual source file—for example
when alias tables, export conditions, or package layouts change between
builds. While `invalidate_module_resolution` re-runs previously failed
resolutions after file creation or deletion, callers also need a way to
flush positive cached resolutions without discarding parsed ASTs.

Add `ResolutionCache::clear_all` and `QueryEngine::clear_resolution_cache`
to clear both host-internal and engine-level resolution caches and evict
queries derived from any importer's resolutions while preserving cached
`ParseFile` results. Expose `clear_resolution_cache` on `Analyzer`,
`TestAnalyzer`, `WasmAnalyzer`, and the sidecar JSON-RPC server, returning
the deduplicated list of stale module IDs while preserving virtual module
IDs verbatim instead of normalizing backslashes.

Surface `clearResolutionCache` on `IAnalyzer`, `WasmAnalyzer`,
`NapiAnalyzer`, `SidecarAnalyzer`, and `HybridCompiler`. In `HybridCompiler`,
sequence `clearResolutionCache` through the serialized update queue so
evicted analyses are dropped from the TypeScript file cache, recorded for
incremental emit, and refreshed on the following delta pass.
…processor pump loop

Previously, `WasmAnalyzer.getMetadataForFile` called the synchronous
`getMetadataForFile` WASM export directly, stepping the query engine to
completion inside a single FFI call. When an asynchronous `AnalysisHost`
suspends module resolution or content loading across JavaScript round-trips,
blocking inside a single WASM export cannot yield back to the host.

Add `ModuleRef` and `QueryEngine::get_metadata_for_file_id` (tracking whether
`ParseFile` loaded a module on `ParsedFileOwner::loaded` so missing modules
return `None` under asynchronous hosts just as they do under `DefaultFsHost`).
Expose `Analyzer::request_metadata_core` and `WasmAnalyzer::request_metadata`,
emitting `metadataResult` events from `WasmAnalyzer::pump` as oneshot queries
settle.

Update the TypeScript `WasmAnalyzer` adapter so `getMetadataForFile` registers
a pending request via `request_metadata` and resolves through `pumpLoop`
alongside active analysis streams, while cleaning up in-flight requests when
`close()` is called.
…nous hosts

Asynchronous bundler hosts suspend module resolution and content loading across
pump turns, so synchronous `*_blocking` and FFI entry points cannot run to
completion within a single call. Without explicit host gating, calling a
synchronous entry point against an asynchronous host panics inside WASM or NAPI
instead of reporting the host constraint cleanly.

Enforce `AnalysisHost::is_synchronous` inside `QueryEngine::block_on_query` and
add fallible `ensure_synchronous_host` checks to `Analyzer`'s synchronous core
helpers. Convert `WasmAnalyzer::get_metadata_for_file` and `read_resource` to
return `Result<Option<String>, String>` and expose `is_synchronous_host` on
WASM bindings so synchronous WASM calls fail cleanly with
`"synchronous APIs require a synchronous host"` instead of panicking.

Expose `isSynchronousHost` (and `WasmAnalyzerOptions::isSynchronousHost`) on
`WasmAnalyzer` while keeping asynchronous WASM mutation and template-lookup
calls working when `isSynchronousHost` is `false`. Behavior under
`DefaultFsHost` is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: compiler Issues related to `ngc`, Angular's template compiler state: blocked target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant