Skip to content

fix(linker): link with the application's angularVersion - #588

Merged
Brooooooklyn merged 4 commits into
voidzero-dev:mainfrom
edbzn:fix/linker-angular-version
Oct 20, 2026
Merged

Brooooooklyn merged 4 commits into
voidzero-dev:mainfrom
edbzn:fix/linker-angular-version

Conversation

@edbzn

@edbzn edbzn commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part of #586: this fixes item (1), "the linker can't be told the version". The linker ignored angularVersion: linkAngularPackage(code, filename) took no options, and compile_template_for_linker / compile_host_bindings_for_linker hard-coded angular_version: None. So every partially compiled library was linked with the latest security schema. Since #577, that schema omits ɵɵsanitizeUrl on img|src / video|src (angular/angular@439ff1bdf4, backported to 22.2.2 as 6fa2b083e6).

That emit works only with the matching runtime. The same Angular change added unwrapImgVideoSrcValue() to @angular/core 22.2.2. It unwraps SafeValues and maps null/undefined to '', two jobs the sanitizer used to do. On @angular/core ≤ 22.2.1, a linked library that binds a SafeUrl to [src] (e.g. ngx-image-cropper) assigns the raw SafeValue object. The image never loads, and nothing reports an error. Setting angularVersion fixed application code, but not libraries.

Angular's own linker compiles partial templates with the application's @angular/compiler, so the security schema follows the app's version. This PR threads angularVersion the same way:

Layer Change
linker New LinkOptions { angular_version } and link_with_options. link() keeps its signature and uses default options.
compile_template_for_linker / compile_host_bindings_for_linker Take angular_version and pass it to the R3 transform, ingest and host-binding jobs.
napi linkAngularPackage{,Sync}(code, filename, options?). The optional LinkOptions keeps existing callers working.
Vite angularLinkerPlugin({ angularVersion }), wired from PluginOptions.angularVersion for both the Rolldown dep-optimizer load hook and the transform hook.

Item (2) of #586 (use the installed @angular/core when angularVersion is unset) is not in this PR. With angularVersion unset, the app compiler and the linker both still assume the latest Angular.

Vite dep-optimizer cache

Vite's dep-optimizer cache key includes the names of the optimizeDeps.rolldownOptions plugins, but not their options. So after a version change, a warm cache kept serving deps linked for the old version until --force. The optimizer plugin is now named angular-linker@<version> when angularVersion is set. Unset keeps angular-linker, so existing caches stay valid. This holds for Vite 8.0.0 to 8.3.4. A real Vite 8.3.4 dev server re-optimizes on each version change and serves the right output.

Optional chaining is deliberately unchanged

Angular's linker takes ?. semantics from the declaration's own version (legacyOptionalChaining: major < 22 && version !== '0.0.0-PLACEHOLDER'), not from the app's version. Before this PR, the linker always resolved to legacy, because angular_version was None. If the app's version alone were passed, every linked library would switch to native ?. on v22+ apps. So the linker now pins legacy_optional_chaining: Some(true), which is today's behavior. Deriving it from the declaration version, as Angular does, is a separate change.

With angularVersion unset, output is byte-identical to before. Every gate still gets None, and the pinned Some(true) resolves to the same legacy result as the old None.

Checked against Angular's real linker

The @angular/compiler-cli linker was run with @angular/compiler 22.2.1 and 22.2.2 on 121 partial declarations (declaration versions 16.2 to 22.2):

  • With angularVersion 22.2.1, 37 files that differed now match. This covers img/video [src], [attr.src], src="{{…}}", and the :svg:/:math: root tag of @if/@for blocks.
  • No new mismatch. Every remaining difference also exists on main.
  • With angularVersion unset, output is byte-identical to main for 121/121 files, and for all 148 link() inputs in the crate tests.

Known differences that this PR does not change:

  • Declarations at version 22+ get native ?. from Angular, and legacy from OXC.
  • Angular emits ɵɵsanitizeUrlOrResourceUrl for host src bindings, because angular/angular@d06e3748b7 (concrete host security contexts) is not ported yet.
  • Angular enables block and @let syntax from the declaration version. OXC always enables them.

Test plan

  • linker_test.rs:
    • img/video src are sanitized for 21.2.0 and 22.2.1, and not for 22.2.2 or unset.
    • Directive host [src] on img[...] is sanitized at 22.2.1. The test does not assert which sanitizer, and does not assert 22.2.2, because Angular emits ɵɵsanitizeUrlOrResourceUrl there (see the d06e3748b7 note above).
    • Component host binding: ɵɵhostProperty at 19.2.0 and ɵɵdomProperty at 22.2.1, both with legacy ?..
    • @if block root tag: ":svg:rect" at 22.2.1 and "rect" at 22.2.2.
    • ?. stays legacy with a version set.
    • Each path the version takes (R3 transform, ingest, directive host, component template, component host, both ?. pins) has a test that fails if that path drops the version. All 12 mutants fail.
    • The component host and block root tag expectations match Angular's real linker for those compiler versions.
  • test/linker.test.ts:
    • napi sync API with and without angularVersion.
    • The Vite plugin forwards the version through the transform hook, the dep-optimizer load hook, and angular({ angularVersion }).
    • The optimizer plugin name per version (the cache key).
  • cargo test --all-features (3271 passed), cargo fmt --all -- --check, conformance (1264/1264, no snapshot diff).
  • pnpm test (447/447), pnpm test:e2e (43 passed; hmr-update-definition.spec.ts:72 fails its first try and passes on retry, the same on the base commit), pnpm --filter @oxc-angular/compare compare --fixtures (0 mismatched). Trial merge with main: all tests pass.
  • Checked in a real Angular 22.2.0 app's Cypress component tests (Vite 7, angularVersion: { major: 22, minor: 2, patch: 0 }): ngx-image-cropper loaded its image again, and the 4 tests that 0.0.41 broke (cropper and an upload-photo dialog built on it) pass.

🤖 Generated with Claude Code

edbzn and others added 4 commits October 19, 2026 15:50
The linker always compiled partial templates and host bindings for the latest
Angular, so since 22.2.2's security schema change it dropped `ɵɵsanitizeUrl`
from `img`/`video` `[src]` in every linked library. Runtimes before 22.2.2 lack
the shim that compensates for it, so a `SafeUrl` bound to `[src]` was assigned
as the raw `SafeValue` object.

Add `LinkOptions { angular_version }` (napi: optional `options` argument to
`linkAngularPackage{,Sync}`) and forward the Vite plugin's `angularVersion` to
both linker hooks. Optional chaining keeps its legacy semantics: Angular's
linker derives them from the declaration's own version, not the app's.

Fixes voidzero-dev#586

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- vite: name the dep optimizer plugin `angular-linker@<version>` when
  `angularVersion` is set (unset keeps `angular-linker`). Vite hashes
  optimizer plugin names, not their options, so a version change kept deps
  linked for the old version until `--force`.
- linker tests: component host path (`ɵɵhostProperty` at 19.2.0,
  `ɵɵdomProperty` at 22.2.1, legacy `?.`) and template ingest (`@if` svg
  root tag `:svg:rect` at 22.2.1, `rect` at 22.2.2), matching Angular's
  linker.
- Reword the directive host `img src` test: it checks OXC's host schema
  switch; Angular emits `ɵɵsanitizeUrlOrResourceUrl` there (d06e3748b7,
  not ported yet).
- napi tests: optimizer plugin name per version; `angular()` forwards
  `angularVersion` to the linker plugin.
- Comments: legacy `?.` pin, optional consumer version, and the
  `PluginOptions.angularVersion` JSDoc; `RustLinkOptions` idiom in napi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assert the exact name for 22.2.1, 22.2.2 and a prerelease, so a name that
drops the patch or prerelease (22.2.1 and 22.2.2 sharing one deps cache) fails.
Key an empty prerelease separately: the linker does not treat it as a stable
release. The `angular()` wiring test also checks 22.2.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The directive host `src` test required no sanitizer at 22.2.2, but Angular's
linker emits `ɵɵsanitizeUrlOrResourceUrl` there (angular/angular@d06e3748b7, not
ported yet). Assert only that 22.2.1 sanitizes. That still fails when the
directive host path drops the version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn
Brooooooklyn merged commit 1c6da96 into voidzero-dev:main Oct 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants