Skip to content

Commit c86d6f3

Browse files
committed
fix: ɵɵdomElement* / ɵɵdomListener instructions missing
1 parent 9d61f71 commit c86d6f3

7 files changed

Lines changed: 123 additions & 11 deletions

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ pub fn extract_component_metadata<'a>(
114114
// Only override the implicit value if an explicit boolean is provided
115115
if let Some(value) = extract_boolean_value(&prop.value) {
116116
metadata.standalone = value;
117+
metadata.standalone_explicitly_set = true;
117118
}
118119
}
119120
"encapsulation" => {

crates/oxc_angular_compiler/src/component/metadata.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ pub struct ComponentMetadata<'a> {
115115
/// Whether this is a standalone component.
116116
pub standalone: bool,
117117

118+
/// Whether `standalone` was explicitly set in the decorator.
119+
///
120+
/// When `false`, `standalone` was inherited from the implicit default (Angular v19+
121+
/// defaults to `true`). This distinction matters for DomOnly mode: only components
122+
/// with an explicit `standalone: true` should use DomOnly mode, because implicit
123+
/// standalone components may be declared in NgModules (which OXC can't detect in
124+
/// single-file compilation).
125+
///
126+
/// See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1326-1339
127+
pub standalone_explicitly_set: bool,
128+
118129
/// View encapsulation mode.
119130
pub encapsulation: ViewEncapsulation,
120131

@@ -518,6 +529,7 @@ impl<'a> ComponentMetadata<'a> {
518529
styles: Vec::new_in(allocator),
519530
style_urls: Vec::new_in(allocator),
520531
standalone: implicit_standalone,
532+
standalone_explicitly_set: false,
521533
encapsulation: ViewEncapsulation::default(),
522534
change_detection: ChangeDetectionStrategy::default(),
523535
host: None,

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,20 @@ fn compile_component_full<'a>(
13641364
// rather than relying solely on the external use_dom_only_mode flag.
13651365
// The metadata has standalone (from decorator) and has_directive_dependencies
13661366
// (from analyzing the imports array).
1367-
let mode = if metadata.standalone && !metadata.has_directive_dependencies {
1367+
//
1368+
// IMPORTANT: We only use DomOnly mode when `standalone: true` was EXPLICITLY
1369+
// set in the decorator. When standalone is implicitly defaulted (Angular v19+),
1370+
// we conservatively use Full mode because:
1371+
// 1. The component may be declared in an NgModule (OXC can't detect this)
1372+
// 2. Angular's ngtsc in local compilation mode always sets
1373+
// hasDirectiveDependencies=true for safety
1374+
// 3. Angular's ngtsc in global mode sets hasDirectiveDependencies=!isStandalone||...
1375+
// meaning non-standalone components ALWAYS use Full mode
1376+
// See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1326-1339
1377+
let mode = if metadata.standalone
1378+
&& metadata.standalone_explicitly_set
1379+
&& !metadata.has_directive_dependencies
1380+
{
13681381
TemplateCompilationMode::DomOnly
13691382
} else {
13701383
TemplateCompilationMode::Full

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use oxc_allocator::Allocator;
77
use oxc_angular_compiler::{
8-
ResolvedResources, TransformOptions as ComponentTransformOptions,
8+
AngularVersion, ResolvedResources, TransformOptions as ComponentTransformOptions,
99
output::ast::FunctionExpr,
1010
output::emitter::JsEmitter,
1111
parser::html::HtmlParser,
@@ -3325,6 +3325,95 @@ fn test_animation_in_for_with_listener_variable_naming() {
33253325
insta::assert_snapshot!("animation_in_for_with_listener", js);
33263326
}
33273327

3328+
/// Test that implicit standalone components (no `standalone` in decorator) use Full mode.
3329+
///
3330+
/// Angular 19+ defaults `standalone` to `true` when not specified. However, OXC performs
3331+
/// single-file compilation without NgModule context. Angular's ngtsc (in local compilation
3332+
/// mode) always sets `hasDirectiveDependencies = true` because it can't fully inspect
3333+
/// dependencies. OXC should do the same: only use DomOnly mode when `standalone: true`
3334+
/// is EXPLICITLY set in the decorator.
3335+
///
3336+
/// Components that rely on the implicit default may be declared in NgModules, which
3337+
/// Angular's global compilation handles by setting `hasDirectiveDependencies =
3338+
/// !isStandalone || ...`. Without NgModule context, OXC must be conservative.
3339+
///
3340+
/// See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1326-1339
3341+
#[test]
3342+
fn test_dom_only_mode_not_used_for_implicit_standalone() {
3343+
let allocator = Allocator::default();
3344+
let source = r"
3345+
import { Component } from '@angular/core';
3346+
3347+
@Component({
3348+
selector: 'app-test',
3349+
template: `
3350+
<div>Hello</div>
3351+
<span>World</span>
3352+
`
3353+
})
3354+
export class TestComponent {}
3355+
";
3356+
3357+
// Angular version 19+ defaults standalone to true, but implicit standalone
3358+
// should NOT trigger DomOnly mode because the component might be in an NgModule
3359+
let options = ComponentTransformOptions {
3360+
use_dom_only_mode: true,
3361+
angular_version: Some(AngularVersion::new(21, 0, 0)),
3362+
..Default::default()
3363+
};
3364+
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
3365+
3366+
// Should use Full mode (elementStart), NOT DomOnly (domElementStart)
3367+
assert!(
3368+
result.code.contains("ɵɵelementStart"),
3369+
"Implicit standalone component should use ɵɵelementStart (Full mode). Output:\n{}",
3370+
result.code
3371+
);
3372+
assert!(
3373+
!result.code.contains("ɵɵdomElementStart"),
3374+
"Implicit standalone component should NOT use ɵɵdomElementStart (DomOnly). Output:\n{}",
3375+
result.code
3376+
);
3377+
}
3378+
3379+
/// Test that implicit standalone components with empty imports also use Full mode.
3380+
///
3381+
/// Even with an empty `imports: []` array, if `standalone` is not explicitly set,
3382+
/// OXC should use Full mode to be safe.
3383+
#[test]
3384+
fn test_dom_only_mode_not_used_for_implicit_standalone_with_empty_imports() {
3385+
let allocator = Allocator::default();
3386+
let source = r"
3387+
import { Component } from '@angular/core';
3388+
3389+
@Component({
3390+
selector: 'app-test',
3391+
imports: [],
3392+
template: `<div>Hello</div>`
3393+
})
3394+
export class TestComponent {}
3395+
";
3396+
3397+
let options = ComponentTransformOptions {
3398+
use_dom_only_mode: true,
3399+
angular_version: Some(AngularVersion::new(21, 0, 0)),
3400+
..Default::default()
3401+
};
3402+
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
3403+
3404+
// Implicit standalone + empty imports should still use Full mode
3405+
assert!(
3406+
result.code.contains("ɵɵelementStart"),
3407+
"Implicit standalone with empty imports should use Full mode. Output:\n{}",
3408+
result.code
3409+
);
3410+
assert!(
3411+
!result.code.contains("ɵɵdomElementStart"),
3412+
"Implicit standalone with empty imports should NOT use DomOnly. Output:\n{}",
3413+
result.code
3414+
);
3415+
}
3416+
33283417
/// Test that standalone components with empty imports use DomOnly mode.
33293418
#[test]
33303419
fn test_dom_only_mode_used_for_standalone_with_empty_imports() {

crates/oxc_angular_compiler/tests/snapshots/integration_test__component_with_inline_styles.snap

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
source: crates/oxc_angular_compiler/tests/integration_test.rs
33
expression: result.code
44
---
5-
65
import { Component } from '@angular/core';
76
import * as i0 from '@angular/core';
87

@@ -14,9 +13,9 @@ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:StyledComponent,select
1413
decls:2,vars:0,consts:[[1,"container"]],template:function StyledComponent_Template(rf,
1514
ctx) {
1615
if ((rf & 1)) {
17-
i0.ɵɵdomElementStart(0,"div",0);
16+
i0.ɵɵelementStart(0,"div",0);
1817
i0.ɵɵtext(1,"Hello");
19-
i0.ɵɵdomElementEnd();
18+
i0.ɵɵelementEnd();
2019
}
2120
},styles:[".container[_ngcontent-%COMP%] { color: red; }"]});
2221
}

crates/oxc_angular_compiler/tests/snapshots/integration_test__component_with_multiple_styles.snap

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
source: crates/oxc_angular_compiler/tests/integration_test.rs
33
expression: result.code
44
---
5-
65
import { Component } from '@angular/core';
76
import * as i0 from '@angular/core';
87

@@ -13,9 +12,9 @@ static ɵfac = function MultiStyledComponent_Factory(__ngFactoryType__) {
1312
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:MultiStyledComponent,selectors:[["app-multi-styled"]],
1413
decls:2,vars:0,template:function MultiStyledComponent_Template(rf,ctx) {
1514
if ((rf & 1)) {
16-
i0.ɵɵdomElementStart(0,"div");
15+
i0.ɵɵelementStart(0,"div");
1716
i0.ɵɵtext(1,"Content");
18-
i0.ɵɵdomElementEnd();
17+
i0.ɵɵelementEnd();
1918
}
2019
},styles:[".first[_ngcontent-%COMP%] { color: blue; }",".second[_ngcontent-%COMP%] { background: white; }"]});
2120
}

crates/oxc_angular_compiler/tests/snapshots/integration_test__component_without_styles.snap

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
source: crates/oxc_angular_compiler/tests/integration_test.rs
33
expression: result.code
44
---
5-
65
import { Component } from '@angular/core';
76
import * as i0 from '@angular/core';
87

@@ -13,9 +12,9 @@ static ɵfac = function NoStylesComponent_Factory(__ngFactoryType__) {
1312
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:NoStylesComponent,selectors:[["app-no-styles"]],
1413
decls:2,vars:0,template:function NoStylesComponent_Template(rf,ctx) {
1514
if ((rf & 1)) {
16-
i0.ɵɵdomElementStart(0,"div");
15+
i0.ɵɵelementStart(0,"div");
1716
i0.ɵɵtext(1,"No styles");
18-
i0.ɵɵdomElementEnd();
17+
i0.ɵɵelementEnd();
1918
}
2019
},encapsulation:2});
2120
}

0 commit comments

Comments
 (0)