Skip to content

Commit 8813cff

Browse files
committed
fix: ɵɵdomElementStart — nonexistent instruction
1 parent 81e25ad commit 8813cff

8 files changed

Lines changed: 193 additions & 19 deletions

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ pub fn extract_component_metadata<'a>(
131131
metadata.imports = extract_identifier_array(allocator, &prop.value);
132132
// 2. The raw expression to pass to ɵɵgetComponentDepsFactory in RuntimeResolved mode
133133
metadata.raw_imports = convert_oxc_expression(allocator, &prop.value);
134+
// 3. Determine if the imports array has any elements (directive dependencies).
135+
// Angular uses: meta.isStandalone && !meta.hasDirectiveDependencies → DomOnly
136+
// Without type info, we conservatively assume any non-empty import
137+
// could be a directive (not just a pipe).
138+
// See: angular/packages/compiler/src/render3/view/compiler.ts:229-232
139+
metadata.has_directive_dependencies = has_any_import_elements(&prop.value);
134140
}
135141
"exportAs" => {
136142
// exportAs can be comma-separated: "foo, bar"
@@ -391,6 +397,26 @@ fn extract_string_array<'a>(
391397
Some(result)
392398
}
393399

400+
/// Check if an imports expression has any elements that could be directive dependencies.
401+
///
402+
/// Returns `true` if:
403+
/// - The expression is a non-empty array literal (may contain directives)
404+
/// - The expression is not an array literal (e.g., a variable reference that could contain anything)
405+
///
406+
/// Returns `false` only for empty array literals (`imports: []`).
407+
///
408+
/// This is a conservative check: without type info, any non-empty import
409+
/// could be a directive. Angular's ngtsc has full type info to distinguish
410+
/// directives from pipes, but Oxc uses this heuristic.
411+
fn has_any_import_elements(expr: &Expression<'_>) -> bool {
412+
match expr {
413+
Expression::ArrayExpression(arr) => !arr.elements.is_empty(),
414+
// Not an array literal (e.g., variable reference like `imports: MY_IMPORTS`)
415+
// Conservatively assume it may contain directives
416+
_ => true,
417+
}
418+
}
419+
394420
/// Extract an array of identifiers (for imports).
395421
fn extract_identifier_array<'a>(
396422
allocator: &'a Allocator,

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,11 +1355,16 @@ fn compile_component_full<'a>(
13551355
// Build ingest options from metadata and transform options
13561356
let component_name_atom = Atom::from_in(metadata.class_name.as_str(), allocator);
13571357

1358-
// Determine compilation mode:
1359-
// - DomOnly is used when the component is standalone with no directive dependencies
1360-
// - The build tool's metadata resolver determines if DomOnly is safe
1361-
// - If use_dom_only_mode is true, we trust the build tool's analysis
1362-
let mode = if options.use_dom_only_mode {
1358+
// Determine compilation mode matching Angular's logic:
1359+
// meta.isStandalone && !meta.hasDirectiveDependencies → DomOnly
1360+
// otherwise → Full
1361+
// See: angular/packages/compiler/src/render3/view/compiler.ts:229-232
1362+
//
1363+
// For full component compilation, we determine this from the parsed metadata
1364+
// rather than relying solely on the external use_dom_only_mode flag.
1365+
// The metadata has standalone (from decorator) and has_directive_dependencies
1366+
// (from analyzing the imports array).
1367+
let mode = if metadata.standalone && !metadata.has_directive_dependencies {
13631368
TemplateCompilationMode::DomOnly
13641369
} else {
13651370
TemplateCompilationMode::Full

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3086,3 +3086,146 @@ fn test_parenthesized_safe_navigation_keyed_access() {
30863086
"Safe ternary should be wrapped in parentheses before keyed access. Output:\n{js}"
30873087
);
30883088
}
3089+
3090+
/// Test that standalone components WITH directive imports use Full mode (elementStart)
3091+
/// even when use_dom_only_mode is set to true.
3092+
///
3093+
/// Angular determines compilation mode from component metadata:
3094+
/// meta.isStandalone && !meta.hasDirectiveDependencies → DomOnly
3095+
/// otherwise → Full
3096+
///
3097+
/// See: angular/packages/compiler/src/render3/view/compiler.ts lines 229-232
3098+
#[test]
3099+
fn test_dom_only_mode_not_used_when_component_has_imports() {
3100+
let allocator = Allocator::default();
3101+
let source = r#"
3102+
import { Component, Directive } from '@angular/core';
3103+
3104+
@Directive({ selector: '[appHighlight]', standalone: true })
3105+
export class HighlightDirective {}
3106+
3107+
@Component({
3108+
selector: 'app-test',
3109+
standalone: true,
3110+
imports: [HighlightDirective],
3111+
template: `
3112+
<div>Hello</div>
3113+
@for (item of items; track item) {
3114+
<li>{{ item }}</li>
3115+
}
3116+
`
3117+
})
3118+
export class TestComponent {
3119+
items: string[] = [];
3120+
}
3121+
"#;
3122+
3123+
// Even with use_dom_only_mode: true, the compiler should detect directive dependencies
3124+
// from the imports array and use Full mode (elementStart, not domElementStart)
3125+
let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
3126+
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
3127+
3128+
// Should use elementStart (Full mode), NOT domElementStart (DomOnly mode)
3129+
assert!(
3130+
result.code.contains("ɵɵelementStart"),
3131+
"Component with imports should use ɵɵelementStart (Full mode), not domElementStart. Output:\n{}",
3132+
result.code
3133+
);
3134+
assert!(
3135+
!result.code.contains("ɵɵdomElementStart"),
3136+
"Component with imports should NOT use ɵɵdomElementStart. Output:\n{}",
3137+
result.code
3138+
);
3139+
}
3140+
3141+
/// Test that standalone components WITHOUT imports correctly use DomOnly mode.
3142+
#[test]
3143+
fn test_dom_only_mode_used_for_standalone_without_imports() {
3144+
let allocator = Allocator::default();
3145+
let source = r#"
3146+
import { Component } from '@angular/core';
3147+
3148+
@Component({
3149+
selector: 'app-test',
3150+
standalone: true,
3151+
template: `
3152+
<div>Hello</div>
3153+
<span>World</span>
3154+
`
3155+
})
3156+
export class TestComponent {}
3157+
"#;
3158+
3159+
let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
3160+
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
3161+
3162+
// Should use domElementStart (DomOnly mode) for standalone with no imports
3163+
assert!(
3164+
result.code.contains("ɵɵdomElementStart"),
3165+
"Standalone component without imports should use ɵɵdomElementStart. Output:\n{}",
3166+
result.code
3167+
);
3168+
assert!(
3169+
!result.code.contains("ɵɵelementStart"),
3170+
"Standalone component without imports should NOT use ɵɵelementStart. Output:\n{}",
3171+
result.code
3172+
);
3173+
}
3174+
3175+
/// Test that non-standalone components use Full mode even with use_dom_only_mode.
3176+
#[test]
3177+
fn test_dom_only_mode_not_used_for_non_standalone() {
3178+
let allocator = Allocator::default();
3179+
let source = r#"
3180+
import { Component } from '@angular/core';
3181+
3182+
@Component({
3183+
selector: 'app-test',
3184+
standalone: false,
3185+
template: `<div>Hello</div>`
3186+
})
3187+
export class TestComponent {}
3188+
"#;
3189+
3190+
let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
3191+
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
3192+
3193+
// Non-standalone should always use Full mode
3194+
assert!(
3195+
result.code.contains("ɵɵelementStart"),
3196+
"Non-standalone component should use ɵɵelementStart. Output:\n{}",
3197+
result.code
3198+
);
3199+
assert!(
3200+
!result.code.contains("ɵɵdomElementStart"),
3201+
"Non-standalone component should NOT use ɵɵdomElementStart. Output:\n{}",
3202+
result.code
3203+
);
3204+
}
3205+
3206+
/// Test that standalone components with empty imports use DomOnly mode.
3207+
#[test]
3208+
fn test_dom_only_mode_used_for_standalone_with_empty_imports() {
3209+
let allocator = Allocator::default();
3210+
let source = r#"
3211+
import { Component } from '@angular/core';
3212+
3213+
@Component({
3214+
selector: 'app-test',
3215+
standalone: true,
3216+
imports: [],
3217+
template: `<div>Hello</div>`
3218+
})
3219+
export class TestComponent {}
3220+
"#;
3221+
3222+
let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
3223+
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
3224+
3225+
// Empty imports means no directive dependencies → DomOnly mode
3226+
assert!(
3227+
result.code.contains("ɵɵdomElementStart"),
3228+
"Standalone with empty imports should use ɵɵdomElementStart. Output:\n{}",
3229+
result.code
3230+
);
3231+
}

crates/oxc_angular_compiler/tests/snapshots/integration_test__component_with_inline_styles.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:StyledComponent,select
1414
decls:2,vars:0,consts:[[1,"container"]],template:function StyledComponent_Template(rf,
1515
ctx) {
1616
if ((rf & 1)) {
17-
i0.ɵɵelementStart(0,"div",0);
17+
i0.ɵɵdomElementStart(0,"div",0);
1818
i0.ɵɵtext(1,"Hello");
19-
i0.ɵɵelementEnd();
19+
i0.ɵɵdomElementEnd();
2020
}
2121
},styles:[".container[_ngcontent-%COMP%] { color: red; }"]});
2222
}

crates/oxc_angular_compiler/tests/snapshots/integration_test__component_with_multiple_styles.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ static ɵfac = function MultiStyledComponent_Factory(__ngFactoryType__) {
1313
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:MultiStyledComponent,selectors:[["app-multi-styled"]],
1414
decls:2,vars:0,template:function MultiStyledComponent_Template(rf,ctx) {
1515
if ((rf & 1)) {
16-
i0.ɵɵelementStart(0,"div");
16+
i0.ɵɵdomElementStart(0,"div");
1717
i0.ɵɵtext(1,"Content");
18-
i0.ɵɵelementEnd();
18+
i0.ɵɵdomElementEnd();
1919
}
2020
},styles:[".first[_ngcontent-%COMP%] { color: blue; }",".second[_ngcontent-%COMP%] { background: white; }"]});
2121
}

crates/oxc_angular_compiler/tests/snapshots/integration_test__component_without_styles.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ static ɵfac = function NoStylesComponent_Factory(__ngFactoryType__) {
1313
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:NoStylesComponent,selectors:[["app-no-styles"]],
1414
decls:2,vars:0,template:function NoStylesComponent_Template(rf,ctx) {
1515
if ((rf & 1)) {
16-
i0.ɵɵelementStart(0,"div");
16+
i0.ɵɵdomElementStart(0,"div");
1717
i0.ɵɵtext(1,"No styles");
18-
i0.ɵɵelementEnd();
18+
i0.ɵɵdomElementEnd();
1919
}
2020
},encapsulation:2});
2121
}

crates/oxc_angular_compiler/tests/snapshots/integration_test__event_before_property_in_bindings.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:TestComponent,selector
1717
vars:1,consts:[[3,"click","disabled"]],template:function TestComponent_Template(rf,
1818
ctx) {
1919
if ((rf & 1)) {
20-
i0.ɵɵelementStart(0,"button",0);
21-
i0.ɵɵlistener("click",function TestComponent_Template_button_click_0_listener() {
20+
i0.ɵɵdomElementStart(0,"button",0);
21+
i0.ɵɵdomListener("click",function TestComponent_Template_button_click_0_listener() {
2222
return ctx.onClick();
2323
});
2424
i0.ɵɵtext(1,"Click");
25-
i0.ɵɵelementEnd();
25+
i0.ɵɵdomElementEnd();
2626
}
27-
if ((rf & 2)) { i0.ɵɵproperty("disabled",ctx.isDisabled); }
27+
if ((rf & 2)) { i0.ɵɵdomProperty("disabled",ctx.isDisabled); }
2828
},encapsulation:2});
2929
}
3030
(() =>{

crates/oxc_angular_compiler/tests/snapshots/integration_test__ngfor_attribute_ordering.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import * as i0 from '@angular/core';
88

99
function TestComponent_li_0_Template(rf,ctx) {
1010
if ((rf & 1)) {
11-
i0.ɵɵelementStart(0,"li");
11+
i0.ɵɵdomElementStart(0,"li");
1212
i0.ɵɵtext(1);
13-
i0.ɵɵelementEnd();
13+
i0.ɵɵdomElementEnd();
1414
}
1515
if ((rf & 2)) {
1616
const item_r1 = ctx.$implicit;
@@ -28,8 +28,8 @@ static ɵfac = function TestComponent_Factory(__ngFactoryType__) {
2828
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:TestComponent,selectors:[["test-comp"]],decls:1,
2929
vars:1,consts:[[4,"ngFor","ngForOf"]],template:function TestComponent_Template(rf,
3030
ctx) {
31-
if ((rf & 1)) { i0.ɵɵtemplate(0,TestComponent_li_0_Template,2,1,"li",0); }
32-
if ((rf & 2)) { i0.ɵɵproperty("ngForOf",ctx.items); }
31+
if ((rf & 1)) { i0.ɵɵdomTemplate(0,TestComponent_li_0_Template,2,1,"li",0); }
32+
if ((rf & 2)) { i0.ɵɵdomProperty("ngForOf",ctx.items); }
3333
},encapsulation:2});
3434
}
3535
(() =>{

0 commit comments

Comments
 (0)