Skip to content

Commit b43c439

Browse files
Brooooooklynclaude
andauthored
fix: version-gate Angular 19 runtime instructions (#107) (#108)
Angular 19.2 runtime uses different instructions than Angular 20+: - Combined `propertyInterpolate*`/`attributeInterpolate*`/`stylePropInterpolate*`/ `styleMapInterpolate*`/`classMapInterpolate*` instead of nested `property(interpolate*())` calls - `hostProperty` instead of `domProperty` This adds `supports_value_interpolation()` (>= 20) and `supports_dom_property()` (>= 20) version gates so the compiler emits the correct instructions based on the target Angular version. Also refactors the chaining phase's CHAIN_COMPATIBILITY from LazyLock<FxHashMap> to a const fn match lookup, and fixes `reify_interpolation` to preserve trailing empty strings when extra positional args (sanitizer, namespace, unit) follow. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c04418c commit b43c439

19 files changed

+1388
-109
lines changed

crates/oxc_angular_compiler/src/component/metadata.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ impl AngularVersion {
4545
self.major >= 20
4646
}
4747

48+
/// Check if this version supports standalone `ɵɵinterpolate*` instructions (v20.0.0+).
49+
///
50+
/// Angular v20 introduced standalone `ɵɵinterpolate1`–`ɵɵinterpolateV` instructions
51+
/// used as nested calls within `ɵɵproperty`/`ɵɵattribute`. Earlier versions use
52+
/// combined `ɵɵpropertyInterpolate*`/`ɵɵattributeInterpolate*` instructions.
53+
pub fn supports_value_interpolation(&self) -> bool {
54+
self.major >= 20
55+
}
56+
57+
/// Check if this version supports `ɵɵdomProperty` (v20.0.0+).
58+
///
59+
/// Angular v20 introduced `ɵɵdomProperty` for host/DomOnly property bindings.
60+
/// Earlier versions use `ɵɵhostProperty` instead.
61+
pub fn supports_dom_property(&self) -> bool {
62+
self.major >= 20
63+
}
64+
4865
/// Parse a version string like "19.0.0" or "19.0.0-rc.1".
4966
///
5067
/// Returns `None` if the version string is invalid.

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ use crate::pipeline::emit::{
6363
};
6464
use crate::pipeline::ingest::{
6565
HostBindingInput, IngestOptions, ingest_component, ingest_component_with_options,
66-
ingest_host_binding,
66+
ingest_host_binding_with_version,
6767
};
6868
use crate::transform::HtmlToR3Transform;
6969
use crate::transform::html_to_r3::TransformOptions as R3TransformOptions;
@@ -2423,8 +2423,12 @@ fn compile_component_full<'a>(
24232423
// Pass the template pool's current index to ensure host binding constants
24242424
// continue from where template compilation left off (avoiding duplicate names)
24252425
let template_pool_index = job.pool.next_name_index();
2426-
let host_binding_output =
2427-
compile_component_host_bindings(allocator, metadata, template_pool_index);
2426+
let host_binding_output = compile_component_host_bindings(
2427+
allocator,
2428+
metadata,
2429+
template_pool_index,
2430+
options.angular_version,
2431+
);
24282432

24292433
// Extract the result and update pool index if host bindings were compiled
24302434
let (host_binding_result, host_binding_next_pool_index, host_binding_declarations) =
@@ -2848,6 +2852,7 @@ pub fn compile_template_to_js_with_options<'a>(
28482852
component_name,
28492853
options.selector.as_deref(),
28502854
host_pool_starting_index,
2855+
options.angular_version,
28512856
) {
28522857
// Add host binding pool declarations (pure functions, etc.)
28532858
for decl in host_result.declarations {
@@ -3111,6 +3116,7 @@ fn compile_component_host_bindings<'a>(
31113116
allocator: &'a Allocator,
31123117
metadata: &ComponentMetadata<'a>,
31133118
pool_starting_index: u32,
3119+
angular_version: Option<AngularVersion>,
31143120
) -> Option<HostBindingCompilationOutput<'a>> {
31153121
let host = metadata.host.as_ref()?;
31163122

@@ -3134,7 +3140,8 @@ fn compile_component_host_bindings<'a>(
31343140

31353141
// Ingest and compile the host bindings with the pool starting index
31363142
// This ensures constant names continue from where template compilation left off
3137-
let mut job = ingest_host_binding(allocator, input, pool_starting_index);
3143+
let mut job =
3144+
ingest_host_binding_with_version(allocator, input, pool_starting_index, angular_version);
31383145
let result = compile_host_bindings(&mut job);
31393146

31403147
// Get the next pool index after host binding compilation
@@ -3411,6 +3418,7 @@ fn compile_host_bindings_from_input<'a>(
34113418
component_name: &str,
34123419
selector: Option<&str>,
34133420
pool_starting_index: u32,
3421+
angular_version: Option<crate::AngularVersion>,
34143422
) -> Option<HostBindingCompilationResult<'a>> {
34153423
use oxc_allocator::FromIn;
34163424

@@ -3436,7 +3444,8 @@ fn compile_host_bindings_from_input<'a>(
34363444
// Convert to HostBindingInput and compile
34373445
let input =
34383446
convert_host_metadata_to_input(allocator, &host, component_name_atom, component_selector);
3439-
let mut job = ingest_host_binding(allocator, input, pool_starting_index);
3447+
let mut job =
3448+
ingest_host_binding_with_version(allocator, input, pool_starting_index, angular_version);
34403449
let result = compile_host_bindings(&mut job);
34413450

34423451
Some(result)
@@ -3471,6 +3480,7 @@ pub fn compile_host_bindings_for_linker(
34713480
component_name,
34723481
selector,
34733482
pool_starting_index,
3483+
None, // Linker always targets latest Angular version
34743484
)?;
34753485

34763486
let emitter = JsEmitter::new();

crates/oxc_angular_compiler/src/pipeline/compilation.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,23 @@ impl<'a> ComponentCompilationJob<'a> {
261261
self.angular_version.map_or(true, |v: AngularVersion| v.supports_conditional_create())
262262
}
263263

264+
/// Check if standalone `ɵɵinterpolate*` instructions are supported (Angular 20+).
265+
///
266+
/// Returns `true` for Angular 20+ or when version is unknown (None = latest).
267+
/// Returns `false` for Angular 19 and earlier, which use combined
268+
/// `ɵɵpropertyInterpolate*`/`ɵɵattributeInterpolate*` instructions.
269+
pub fn supports_value_interpolation(&self) -> bool {
270+
self.angular_version.map_or(true, |v: AngularVersion| v.supports_value_interpolation())
271+
}
272+
273+
/// Check if `ɵɵdomProperty` is supported (Angular 20+).
274+
///
275+
/// Returns `true` for Angular 20+ or when version is unknown (None = latest).
276+
/// Returns `false` for Angular 19 and earlier, which use `ɵɵhostProperty` instead.
277+
pub fn supports_dom_property(&self) -> bool {
278+
self.angular_version.map_or(true, |v: AngularVersion| v.supports_dom_property())
279+
}
280+
264281
/// Allocates a new cross-reference ID.
265282
pub fn allocate_xref_id(&mut self) -> XrefId {
266283
let id = XrefId::new(self.next_xref_id);
@@ -601,6 +618,8 @@ pub struct HostBindingCompilationJob<'a> {
601618
pub fn_suffix: Atom<'a>,
602619
/// Diagnostics collected during compilation.
603620
pub diagnostics: std::vec::Vec<OxcDiagnostic>,
621+
/// Angular version for version-gated instruction emission.
622+
pub angular_version: Option<AngularVersion>,
604623
}
605624

606625
impl<'a> HostBindingCompilationJob<'a> {
@@ -646,6 +665,7 @@ impl<'a> HostBindingCompilationJob<'a> {
646665
mode: TemplateCompilationMode::DomOnly, // Host bindings always use DomOnly
647666
fn_suffix: Atom::from("HostBindings"),
648667
diagnostics: std::vec::Vec::new(),
668+
angular_version: None,
649669
}
650670
}
651671

@@ -654,6 +674,16 @@ impl<'a> HostBindingCompilationJob<'a> {
654674
CompilationJobKind::Host
655675
}
656676

677+
/// Check if standalone `ɵɵinterpolate*` instructions are supported (Angular 20+).
678+
pub fn supports_value_interpolation(&self) -> bool {
679+
self.angular_version.map_or(true, |v| v.supports_value_interpolation())
680+
}
681+
682+
/// Check if `ɵɵdomProperty` is supported (Angular 20+).
683+
pub fn supports_dom_property(&self) -> bool {
684+
self.angular_version.map_or(true, |v| v.supports_dom_property())
685+
}
686+
657687
/// Allocates a new cross-reference ID.
658688
pub fn allocate_xref_id(&mut self) -> XrefId {
659689
let id = XrefId::new(self.next_xref_id);

crates/oxc_angular_compiler/src/pipeline/ingest.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3941,13 +3941,24 @@ pub fn ingest_host_binding<'a>(
39413941
allocator: &'a Allocator,
39423942
input: HostBindingInput<'a>,
39433943
pool_starting_index: u32,
3944+
) -> HostBindingCompilationJob<'a> {
3945+
ingest_host_binding_with_version(allocator, input, pool_starting_index, None)
3946+
}
3947+
3948+
/// Ingest host bindings into a `HostBindingCompilationJob` with a specific Angular version.
3949+
pub fn ingest_host_binding_with_version<'a>(
3950+
allocator: &'a Allocator,
3951+
input: HostBindingInput<'a>,
3952+
pool_starting_index: u32,
3953+
angular_version: Option<crate::AngularVersion>,
39443954
) -> HostBindingCompilationJob<'a> {
39453955
let mut job = HostBindingCompilationJob::with_pool_starting_index(
39463956
allocator,
39473957
input.component_name,
39483958
input.component_selector,
39493959
pool_starting_index,
39503960
);
3961+
job.angular_version = angular_version;
39513962

39523963
// Ingest host properties
39533964
for property in input.properties {

0 commit comments

Comments
 (0)