Repository navigation
fix(linker): link with the application's angularVersion - #588
Merged
Brooooooklyn merged 4 commits intoOct 20, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, andcompile_template_for_linker/compile_host_bindings_for_linkerhard-codedangular_version: None. So every partially compiled library was linked with the latest security schema. Since #577, that schema omitsɵɵsanitizeUrlonimg|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/core22.2.2. It unwrapsSafeValues and mapsnull/undefinedto'', two jobs the sanitizer used to do. On@angular/core≤ 22.2.1, a linked library that binds aSafeUrlto[src](e.g.ngx-image-cropper) assigns the rawSafeValueobject. The image never loads, and nothing reports an error. SettingangularVersionfixed 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 threadsangularVersionthe same way:linkerLinkOptions { angular_version }andlink_with_options.link()keeps its signature and uses default options.compile_template_for_linker/compile_host_bindings_for_linkerangular_versionand pass it to the R3 transform, ingest and host-binding jobs.linkAngularPackage{,Sync}(code, filename, options?). The optionalLinkOptionskeeps existing callers working.angularLinkerPlugin({ angularVersion }), wired fromPluginOptions.angularVersionfor both the Rolldown dep-optimizerloadhook and thetransformhook.Item (2) of #586 (use the installed
@angular/corewhenangularVersionis unset) is not in this PR. WithangularVersionunset, 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.rolldownOptionsplugins, 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 namedangular-linker@<version>whenangularVersionis set. Unset keepsangular-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 ownversion(legacyOptionalChaining: major < 22 && version !== '0.0.0-PLACEHOLDER'), not from the app's version. Before this PR, the linker always resolved to legacy, becauseangular_versionwasNone. If the app's version alone were passed, every linked library would switch to native?.on v22+ apps. So the linker now pinslegacy_optional_chaining: Some(true), which is today's behavior. Deriving it from the declaration version, as Angular does, is a separate change.With
angularVersionunset, output is byte-identical to before. Every gate still getsNone, and the pinnedSome(true)resolves to the same legacy result as the oldNone.Checked against Angular's real linker
The
@angular/compiler-clilinker was run with@angular/compiler22.2.1 and 22.2.2 on 121 partial declarations (declaration versions 16.2 to 22.2):angularVersion22.2.1, 37 files that differed now match. This coversimg/video[src],[attr.src],src="{{…}}", and the:svg:/:math:root tag of@if/@forblocks.main.angularVersionunset, output is byte-identical tomainfor 121/121 files, and for all 148link()inputs in the crate tests.Known differences that this PR does not change:
?.from Angular, and legacy from OXC.ɵɵsanitizeUrlOrResourceUrlfor hostsrcbindings, because angular/angular@d06e3748b7 (concrete host security contexts) is not ported yet.@letsyntax from the declaration version. OXC always enables them.Test plan
linker_test.rs:img/videosrcare sanitized for 21.2.0 and 22.2.1, and not for 22.2.2 or unset.[src]onimg[...]is sanitized at 22.2.1. The test does not assert which sanitizer, and does not assert 22.2.2, because Angular emitsɵɵsanitizeUrlOrResourceUrlthere (see the d06e3748b7 note above).ɵɵhostPropertyat 19.2.0 andɵɵdomPropertyat 22.2.1, both with legacy?..@ifblock root tag:":svg:rect"at 22.2.1 and"rect"at 22.2.2.?.stays legacy with a version set.?.pins) has a test that fails if that path drops the version. All 12 mutants fail.test/linker.test.ts:angularVersion.transformhook, the dep-optimizerloadhook, andangular({ angularVersion }).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:72fails its first try and passes on retry, the same on the base commit),pnpm --filter @oxc-angular/compare compare --fixtures(0 mismatched). Trial merge withmain: all tests pass.angularVersion: { major: 22, minor: 2, patch: 0 }):ngx-image-cropperloaded 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