Skip to content

Commit a5ebb8c

Browse files
Brooooooklynclaude
andcommitted
fix: version-gate conditionalCreate/conditionalBranchCreate for Angular 19 support
Angular 19 does not have ɵɵconditionalCreate/ɵɵconditionalBranchCreate runtime instructions (introduced in Angular 20). When angularVersion < 20, emit ɵɵtemplate instead for @if/@switch blocks. Also wire angularVersion from PluginOptions through to the compiler pipeline. Closes #105 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c0a2432 commit a5ebb8c

File tree

10 files changed

+326
-24
lines changed

10 files changed

+326
-24
lines changed

crates/oxc_angular_compiler/src/component/metadata.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ impl AngularVersion {
3737
self.major >= 19
3838
}
3939

40+
/// Check if this version supports `ɵɵconditionalCreate`/`ɵɵconditionalBranchCreate` (v20.0.0+).
41+
///
42+
/// Angular v20 introduced `ɵɵconditionalCreate` and `ɵɵconditionalBranchCreate`
43+
/// instructions for `@if`/`@switch` blocks. Earlier versions use `ɵɵtemplate` instead.
44+
pub fn supports_conditional_create(&self) -> bool {
45+
self.major >= 20
46+
}
47+
4048
/// Parse a version string like "19.0.0" or "19.0.0-rc.1".
4149
///
4250
/// Returns `None` if the version string is invalid.

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2371,6 +2371,8 @@ fn compile_component_full<'a>(
23712371
// Use the shared pool starting index to avoid duplicate constant names
23722372
// when compiling multiple components in the same file
23732373
pool_starting_index,
2374+
// Pass Angular version for feature-gated instruction selection
2375+
angular_version: options.angular_version,
23742376
};
23752377

23762378
let mut job = ingest_component_with_options(
@@ -2792,6 +2794,7 @@ pub fn compile_template_to_js_with_options<'a>(
27922794
template_source: Some(template),
27932795
all_deferrable_deps_fn: None,
27942796
pool_starting_index: 0, // Standalone template compilation starts from 0
2797+
angular_version: options.angular_version,
27952798
};
27962799

27972800
// Stage 3-5: Ingest and compile
@@ -2963,6 +2966,7 @@ pub fn compile_template_for_hmr<'a>(
29632966
template_source: Some(template),
29642967
all_deferrable_deps_fn: None,
29652968
pool_starting_index: 0, // HMR template compilation starts from 0
2969+
angular_version: options.angular_version,
29662970
};
29672971

29682972
// Stage 3-5: Ingest and compile
@@ -3585,6 +3589,7 @@ pub fn compile_template_for_linker<'a>(
35853589
template_source: Some(template),
35863590
all_deferrable_deps_fn: None,
35873591
pool_starting_index: 0,
3592+
angular_version: None,
35883593
};
35893594

35903595
let component_name_atom = Atom::from_in(component_name, allocator);

crates/oxc_angular_compiler/src/pipeline/compilation.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use oxc_diagnostics::OxcDiagnostic;
1212
use oxc_span::{Atom, Span};
1313
use rustc_hash::{FxBuildHasher, FxHashMap};
1414

15+
use crate::AngularVersion;
1516
use crate::ir::enums::CompatibilityMode;
1617
use crate::ir::list::{CreateOpList, UpdateOpList};
1718
use crate::ir::ops::XrefId;
@@ -183,6 +184,12 @@ pub struct ComponentCompilationJob<'a> {
183184
/// Causes `ngContentSelectors` to be emitted in the component definition.
184185
/// This is populated by the `generate_projection_def` phase.
185186
pub content_selectors: Option<crate::output::ast::OutputExpression<'a>>,
187+
/// Angular version for feature-gated instruction selection.
188+
///
189+
/// When set to a version < 20, the compiler emits `ɵɵtemplate` instead of
190+
/// `ɵɵconditionalCreate`/`ɵɵconditionalBranchCreate` for `@if`/`@switch` blocks.
191+
/// When `None`, assumes latest Angular version (v20+ behavior).
192+
pub angular_version: Option<AngularVersion>,
186193
/// Diagnostics collected during compilation.
187194
pub diagnostics: std::vec::Vec<OxcDiagnostic>,
188195
}
@@ -232,6 +239,7 @@ impl<'a> ComponentCompilationJob<'a> {
232239
defer_meta: DeferMetadata::PerBlock { blocks: FxHashMap::default() },
233240
all_deferrable_deps_fn: None,
234241
content_selectors: None,
242+
angular_version: None,
235243
diagnostics: std::vec::Vec::new(),
236244
}
237245
}
@@ -245,6 +253,14 @@ impl<'a> ComponentCompilationJob<'a> {
245253
self
246254
}
247255

256+
/// Check if `ɵɵconditionalCreate` is supported (Angular 20+).
257+
///
258+
/// Returns `true` for Angular 20+ or when version is unknown (None = latest).
259+
/// Returns `false` for Angular 19 and earlier, which use `ɵɵtemplate` instead.
260+
pub fn supports_conditional_create(&self) -> bool {
261+
self.angular_version.map_or(true, |v: AngularVersion| v.supports_conditional_create())
262+
}
263+
248264
/// Allocates a new cross-reference ID.
249265
pub fn allocate_xref_id(&mut self) -> XrefId {
250266
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
@@ -115,6 +115,13 @@ pub struct IngestOptions<'a> {
115115
///
116116
/// Default is 0 (start from _c0).
117117
pub pool_starting_index: u32,
118+
119+
/// Angular version for feature-gated instruction selection.
120+
///
121+
/// When set to a version < 20, the compiler emits `ɵɵtemplate` instead of
122+
/// `ɵɵconditionalCreate`/`ɵɵconditionalBranchCreate` for `@if`/`@switch` blocks.
123+
/// When `None`, assumes latest Angular version (v20+ behavior).
124+
pub angular_version: Option<crate::AngularVersion>,
118125
}
119126

120127
impl Default for IngestOptions<'_> {
@@ -129,6 +136,7 @@ impl Default for IngestOptions<'_> {
129136
template_source: None,
130137
all_deferrable_deps_fn: None,
131138
pool_starting_index: 0,
139+
angular_version: None,
132140
}
133141
}
134142
}
@@ -732,6 +740,9 @@ pub fn ingest_component_with_options<'a>(
732740
// This is used when DeferBlockDepsEmitMode::PerComponent to reference the shared deps function
733741
job.all_deferrable_deps_fn = options.all_deferrable_deps_fn;
734742

743+
// Set Angular version for feature-gated instruction selection
744+
job.angular_version = options.angular_version;
745+
735746
let root_xref = job.root.xref;
736747

737748
for node in template {

crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ struct ReifyContext<'a> {
110110
view_vars: FxHashMap<XrefId, u32>,
111111
/// Template compilation mode (Full or DomOnly).
112112
mode: TemplateCompilationMode,
113+
/// Whether to use `ɵɵconditionalCreate` (Angular 20+) or `ɵɵtemplate` (Angular 19-).
114+
supports_conditional_create: bool,
113115
}
114116

115117
/// Reifies IR expressions to Output AST.
@@ -138,7 +140,9 @@ pub fn reify(job: &mut ComponentCompilationJob<'_>) {
138140
view_vars.insert(view.xref, vars);
139141
}
140142
}
141-
let ctx = ReifyContext { view_fn_names, view_decls, view_vars, mode };
143+
let supports_conditional_create = job.supports_conditional_create();
144+
let ctx =
145+
ReifyContext { view_fn_names, view_decls, view_vars, mode, supports_conditional_create };
142146

143147
// Collect xrefs of embedded views (excluding root) before splitting borrows
144148
let embedded_xrefs: std::vec::Vec<XrefId> =
@@ -447,20 +451,34 @@ fn reify_create_op<'a>(
447451
Some(create_declare_let_stmt(allocator, slot))
448452
}
449453
CreateOp::Conditional(cond) => {
450-
// Emit ɵɵconditionalCreate instruction for the first branch in @if/@switch
451454
// Look up the function name for this branch's view
452455
let fn_name = ctx.view_fn_names.get(&cond.xref).cloned();
453456
let slot = cond.slot.map(|s| s.0).unwrap_or(0);
454-
Some(create_conditional_create_stmt(
455-
allocator,
456-
slot,
457-
fn_name,
458-
cond.decls,
459-
cond.vars,
460-
cond.tag.as_ref(),
461-
cond.attributes,
462-
cond.local_refs_index,
463-
))
457+
if ctx.supports_conditional_create {
458+
// Angular 20+: Emit ɵɵconditionalCreate for the first branch in @if/@switch
459+
Some(create_conditional_create_stmt(
460+
allocator,
461+
slot,
462+
fn_name,
463+
cond.decls,
464+
cond.vars,
465+
cond.tag.as_ref(),
466+
cond.attributes,
467+
cond.local_refs_index,
468+
))
469+
} else {
470+
// Angular 19: Emit ɵɵtemplate instead (conditionalCreate doesn't exist)
471+
Some(create_template_stmt(
472+
allocator,
473+
slot,
474+
fn_name,
475+
cond.decls,
476+
cond.vars,
477+
cond.tag.as_ref(),
478+
cond.attributes,
479+
cond.local_refs_index,
480+
))
481+
}
464482
}
465483
CreateOp::RepeaterCreate(repeater) => {
466484
// Emit repeaterCreate instruction for @for
@@ -708,20 +726,34 @@ fn reify_create_op<'a>(
708726
}
709727
}
710728
CreateOp::ConditionalBranch(branch) => {
711-
// Emit ɵɵconditionalBranchCreate instruction for branches after the first in @if/@switch
712729
// Look up the function name for this branch's view
713730
let fn_name = ctx.view_fn_names.get(&branch.xref).cloned();
714731
let slot = branch.slot.map(|s| s.0).unwrap_or(0);
715-
Some(create_conditional_branch_create_stmt(
716-
allocator,
717-
slot,
718-
fn_name,
719-
branch.decls,
720-
branch.vars,
721-
branch.tag.as_ref(),
722-
branch.attributes,
723-
branch.local_refs_index,
724-
))
732+
if ctx.supports_conditional_create {
733+
// Angular 20+: Emit ɵɵconditionalBranchCreate for branches after the first
734+
Some(create_conditional_branch_create_stmt(
735+
allocator,
736+
slot,
737+
fn_name,
738+
branch.decls,
739+
branch.vars,
740+
branch.tag.as_ref(),
741+
branch.attributes,
742+
branch.local_refs_index,
743+
))
744+
} else {
745+
// Angular 19: Emit ɵɵtemplate instead (conditionalBranchCreate doesn't exist)
746+
Some(create_template_stmt(
747+
allocator,
748+
slot,
749+
fn_name,
750+
branch.decls,
751+
branch.vars,
752+
branch.tag.as_ref(),
753+
branch.attributes,
754+
branch.local_refs_index,
755+
))
756+
}
725757
}
726758
CreateOp::ControlCreate(_) => {
727759
// Emit ɵɵcontrolCreate instruction for control binding initialization

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ use oxc_span::Atom;
1717

1818
/// Compiles an Angular template to JavaScript.
1919
fn compile_template_to_js(template: &str, component_name: &str) -> String {
20+
compile_template_to_js_with_version(template, component_name, None)
21+
}
22+
23+
/// Compiles an Angular template to JavaScript targeting a specific Angular version.
24+
fn compile_template_to_js_with_version(
25+
template: &str,
26+
component_name: &str,
27+
angular_version: Option<AngularVersion>,
28+
) -> String {
29+
use oxc_angular_compiler::pipeline::ingest::{IngestOptions, ingest_component_with_options};
30+
2031
let allocator = Allocator::default();
2132

2233
// Stage 1: Parse HTML (with expansion forms enabled for ICU/plural support)
@@ -40,7 +51,17 @@ fn compile_template_to_js(template: &str, component_name: &str) -> String {
4051
}
4152

4253
// Stage 3: Ingest R3 AST into IR
43-
let mut job = ingest_component(&allocator, Atom::from(component_name), r3_result.nodes);
54+
let mut job = if let Some(version) = angular_version {
55+
let options = IngestOptions { angular_version: Some(version), ..Default::default() };
56+
ingest_component_with_options(
57+
&allocator,
58+
Atom::from(component_name),
59+
r3_result.nodes,
60+
options,
61+
)
62+
} else {
63+
ingest_component(&allocator, Atom::from(component_name), r3_result.nodes)
64+
};
4465

4566
// Stage 4-5: Transform and emit
4667
let result = compile_template(&mut job);
@@ -7398,3 +7419,111 @@ export class TestComponent {
73987419
decl.members
73997420
);
74007421
}
7422+
7423+
// ============================================================================
7424+
// Angular Version Gating Tests (Issue #105)
7425+
// ============================================================================
7426+
// These tests verify that when targeting Angular 19, the compiler emits
7427+
// ɵɵtemplate instead of ɵɵconditionalCreate/ɵɵconditionalBranchCreate
7428+
// for @if/@switch blocks, since those instructions don't exist in Angular 19.
7429+
7430+
#[test]
7431+
fn test_if_block_angular_v19() {
7432+
let v19 = AngularVersion::new(19, 0, 0);
7433+
let js = compile_template_to_js_with_version(
7434+
r"@if (condition) { <div>Visible</div> }",
7435+
"TestComponent",
7436+
Some(v19),
7437+
);
7438+
// Angular 19 should use ɵɵtemplate, NOT ɵɵconditionalCreate
7439+
assert!(
7440+
js.contains("ɵɵtemplate("),
7441+
"Angular 19 should emit ɵɵtemplate for @if blocks. Got:\n{js}"
7442+
);
7443+
assert!(
7444+
!js.contains("ɵɵconditionalCreate("),
7445+
"Angular 19 should NOT emit ɵɵconditionalCreate. Got:\n{js}"
7446+
);
7447+
// Update instruction (ɵɵconditional) should still be emitted
7448+
assert!(
7449+
js.contains("ɵɵconditional("),
7450+
"Angular 19 should still emit ɵɵconditional for update. Got:\n{js}"
7451+
);
7452+
insta::assert_snapshot!("if_block_angular_v19", js);
7453+
}
7454+
7455+
#[test]
7456+
fn test_if_else_block_angular_v19() {
7457+
let v19 = AngularVersion::new(19, 2, 0);
7458+
let js = compile_template_to_js_with_version(
7459+
r"@if (condition) { <div>True</div> } @else { <div>False</div> }",
7460+
"TestComponent",
7461+
Some(v19),
7462+
);
7463+
// Angular 19 should use ɵɵtemplate for all branches, NOT conditionalCreate/conditionalBranchCreate
7464+
assert!(
7465+
js.contains("ɵɵtemplate("),
7466+
"Angular 19 should emit ɵɵtemplate for @if/@else blocks. Got:\n{js}"
7467+
);
7468+
assert!(
7469+
!js.contains("ɵɵconditionalCreate("),
7470+
"Angular 19 should NOT emit ɵɵconditionalCreate. Got:\n{js}"
7471+
);
7472+
assert!(
7473+
!js.contains("ɵɵconditionalBranchCreate("),
7474+
"Angular 19 should NOT emit ɵɵconditionalBranchCreate. Got:\n{js}"
7475+
);
7476+
insta::assert_snapshot!("if_else_block_angular_v19", js);
7477+
}
7478+
7479+
#[test]
7480+
fn test_switch_block_angular_v19() {
7481+
let v19 = AngularVersion::new(19, 0, 0);
7482+
let js = compile_template_to_js_with_version(
7483+
r"@switch (value) { @case (1) { <div>One</div> } @case (2) { <div>Two</div> } @default { <div>Other</div> } }",
7484+
"TestComponent",
7485+
Some(v19),
7486+
);
7487+
// Angular 19 should use ɵɵtemplate for all @switch cases
7488+
assert!(
7489+
js.contains("ɵɵtemplate("),
7490+
"Angular 19 should emit ɵɵtemplate for @switch blocks. Got:\n{js}"
7491+
);
7492+
assert!(
7493+
!js.contains("ɵɵconditionalCreate("),
7494+
"Angular 19 should NOT emit ɵɵconditionalCreate for @switch. Got:\n{js}"
7495+
);
7496+
assert!(
7497+
!js.contains("ɵɵconditionalBranchCreate("),
7498+
"Angular 19 should NOT emit ɵɵconditionalBranchCreate for @switch. Got:\n{js}"
7499+
);
7500+
insta::assert_snapshot!("switch_block_angular_v19", js);
7501+
}
7502+
7503+
#[test]
7504+
fn test_if_block_angular_v20_default() {
7505+
// Default (no version set) should emit conditionalCreate (Angular 20+ behavior)
7506+
let js = compile_template_to_js_with_version(
7507+
r"@if (condition) { <div>Visible</div> }",
7508+
"TestComponent",
7509+
None,
7510+
);
7511+
assert!(
7512+
js.contains("ɵɵconditionalCreate("),
7513+
"Default (latest) should emit ɵɵconditionalCreate. Got:\n{js}"
7514+
);
7515+
}
7516+
7517+
#[test]
7518+
fn test_if_block_angular_v20_explicit() {
7519+
let v20 = AngularVersion::new(20, 0, 0);
7520+
let js = compile_template_to_js_with_version(
7521+
r"@if (condition) { <div>Visible</div> }",
7522+
"TestComponent",
7523+
Some(v20),
7524+
);
7525+
assert!(
7526+
js.contains("ɵɵconditionalCreate("),
7527+
"Angular 20 should emit ɵɵconditionalCreate. Got:\n{js}"
7528+
);
7529+
}

0 commit comments

Comments
 (0)