Skip to content

Commit 80e21d4

Browse files
Brooooooklynclaude
andcommitted
fix: always use Full template compilation mode, matching Angular's local compilation behavior
OXC is a single-file compiler equivalent to Angular's local compilation mode. In local mode, Angular always sets hasDirectiveDependencies=true (handler.ts:1257), so DomOnly mode is never used for component templates. Remove the conditional DomOnly/Full logic and always use Full mode. - Remove standalone_explicitly_set and has_directive_dependencies from ComponentMetadata - Remove has_any_non_pipe_import_elements() heuristic function - Remove useDomOnlyMode from NAPI bindings and TypeScript types - Remove shouldUseDomOnlyMode() from comparison tool - Fix build-test script to use comma-separated cargo features - Add test asserting standalone components use Full mode - Update existing DomOnly tests to assert Full mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a08f16e commit 80e21d4

File tree

10 files changed

+180
-386
lines changed

10 files changed

+180
-386
lines changed

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 1 addition & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ 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;
118117
}
119118
}
120119
"encapsulation" => {
@@ -128,17 +127,10 @@ pub fn extract_component_metadata<'a>(
128127
}
129128
"imports" => {
130129
// For standalone components, we need:
131-
// 1. The identifier list for local analysis and DomOnly mode detection
130+
// 1. The identifier list for local analysis
132131
metadata.imports = extract_identifier_array(allocator, &prop.value);
133132
// 2. The raw expression to pass to ɵɵgetComponentDepsFactory in RuntimeResolved mode
134133
metadata.raw_imports = convert_oxc_expression(allocator, &prop.value);
135-
// 3. Determine if the imports array has any non-pipe elements (directive deps).
136-
// Angular's ngtsc (handler.ts:1326-1339) only counts MetaKind.Directive
137-
// and MetaKind.NgModule — NOT MetaKind.Pipe. Without type info, we use
138-
// a naming convention heuristic: identifiers ending in "Pipe" are pipes.
139-
// See: angular/packages/compiler/src/render3/view/compiler.ts:229-232
140-
metadata.has_directive_dependencies =
141-
has_any_non_pipe_import_elements(&prop.value);
142134
}
143135
"exportAs" => {
144136
// exportAs can be comma-separated: "foo, bar"
@@ -418,28 +410,6 @@ fn extract_string_array<'a>(
418410
/// Returns `false` if:
419411
/// - The expression is an empty array literal (`imports: []`)
420412
/// - ALL elements in the array are identifiers ending in "Pipe"
421-
fn has_any_non_pipe_import_elements(expr: &Expression<'_>) -> bool {
422-
let Expression::ArrayExpression(arr) = expr else {
423-
// Not an array literal (e.g., variable reference like `imports: MY_IMPORTS`)
424-
// Conservatively assume it may contain directives
425-
return true;
426-
};
427-
for element in &arr.elements {
428-
match element {
429-
ArrayExpressionElement::Identifier(id) => {
430-
if !id.name.ends_with("Pipe") {
431-
return true;
432-
}
433-
}
434-
// Non-identifier elements (spread, call expressions, etc.)
435-
// conservatively treated as potential directives
436-
_ => return true,
437-
}
438-
}
439-
// All elements are identifiers ending in "Pipe", or the array is empty
440-
false
441-
}
442-
443413
/// Extract an array of identifiers (for imports).
444414
fn extract_identifier_array<'a>(
445415
allocator: &'a Allocator,
@@ -3283,146 +3253,4 @@ mod tests {
32833253
);
32843254
});
32853255
}
3286-
3287-
// =========================================================================
3288-
// Directive dependency detection tests (pipe vs directive imports)
3289-
// =========================================================================
3290-
//
3291-
// Angular's ngtsc (handler.ts:1326-1339) only counts MetaKind.Directive
3292-
// and MetaKind.NgModule as directive dependencies — NOT MetaKind.Pipe.
3293-
// Since OXC is a single-file compiler, we use a naming convention heuristic:
3294-
// identifiers ending in "Pipe" are assumed to be pipes.
3295-
3296-
#[test]
3297-
fn test_pipe_only_imports_no_directive_dependencies() {
3298-
let code = r#"
3299-
@Component({
3300-
selector: 'app-test',
3301-
standalone: true,
3302-
imports: [AsyncPipe],
3303-
template: ''
3304-
})
3305-
class TestComponent {}
3306-
"#;
3307-
assert_metadata(code, |meta| {
3308-
assert!(
3309-
!meta.has_directive_dependencies,
3310-
"Pipe-only imports should not set has_directive_dependencies"
3311-
);
3312-
});
3313-
}
3314-
3315-
#[test]
3316-
fn test_multiple_pipe_imports_no_directive_dependencies() {
3317-
let code = r#"
3318-
@Component({
3319-
selector: 'app-test',
3320-
standalone: true,
3321-
imports: [AsyncPipe, DatePipe, SlicePipe, KeyValuePipe],
3322-
template: ''
3323-
})
3324-
class TestComponent {}
3325-
"#;
3326-
assert_metadata(code, |meta| {
3327-
assert!(
3328-
!meta.has_directive_dependencies,
3329-
"Multiple pipe-only imports should not set has_directive_dependencies"
3330-
);
3331-
});
3332-
}
3333-
3334-
#[test]
3335-
fn test_mixed_pipe_and_directive_imports_has_directive_dependencies() {
3336-
let code = r#"
3337-
@Component({
3338-
selector: 'app-test',
3339-
standalone: true,
3340-
imports: [AsyncPipe, HighlightDirective],
3341-
template: ''
3342-
})
3343-
class TestComponent {}
3344-
"#;
3345-
assert_metadata(code, |meta| {
3346-
assert!(
3347-
meta.has_directive_dependencies,
3348-
"Mixed imports with non-pipe should set has_directive_dependencies"
3349-
);
3350-
});
3351-
}
3352-
3353-
#[test]
3354-
fn test_directive_only_imports_has_directive_dependencies() {
3355-
let code = r#"
3356-
@Component({
3357-
selector: 'app-test',
3358-
standalone: true,
3359-
imports: [HighlightDirective, RouterModule],
3360-
template: ''
3361-
})
3362-
class TestComponent {}
3363-
"#;
3364-
assert_metadata(code, |meta| {
3365-
assert!(
3366-
meta.has_directive_dependencies,
3367-
"Directive-only imports should set has_directive_dependencies"
3368-
);
3369-
});
3370-
}
3371-
3372-
#[test]
3373-
fn test_empty_imports_no_directive_dependencies() {
3374-
let code = r#"
3375-
@Component({
3376-
selector: 'app-test',
3377-
standalone: true,
3378-
imports: [],
3379-
template: ''
3380-
})
3381-
class TestComponent {}
3382-
"#;
3383-
assert_metadata(code, |meta| {
3384-
assert!(
3385-
!meta.has_directive_dependencies,
3386-
"Empty imports should not set has_directive_dependencies"
3387-
);
3388-
});
3389-
}
3390-
3391-
#[test]
3392-
fn test_variable_imports_has_directive_dependencies() {
3393-
let code = r#"
3394-
@Component({
3395-
selector: 'app-test',
3396-
standalone: true,
3397-
imports: MY_IMPORTS,
3398-
template: ''
3399-
})
3400-
class TestComponent {}
3401-
"#;
3402-
assert_metadata(code, |meta| {
3403-
assert!(
3404-
meta.has_directive_dependencies,
3405-
"Variable imports should conservatively set has_directive_dependencies"
3406-
);
3407-
});
3408-
}
3409-
3410-
#[test]
3411-
fn test_spread_in_imports_has_directive_dependencies() {
3412-
let code = r#"
3413-
@Component({
3414-
selector: 'app-test',
3415-
standalone: true,
3416-
imports: [...SHARED_IMPORTS, AsyncPipe],
3417-
template: ''
3418-
})
3419-
class TestComponent {}
3420-
"#;
3421-
assert_metadata(code, |meta| {
3422-
assert!(
3423-
meta.has_directive_dependencies,
3424-
"Spread in imports should conservatively set has_directive_dependencies"
3425-
);
3426-
});
3427-
}
34283256
}

crates/oxc_angular_compiler/src/component/metadata.rs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,6 @@ 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-
129118
/// View encapsulation mode.
130119
pub encapsulation: ViewEncapsulation,
131120

@@ -230,12 +219,6 @@ pub struct ComponentMetadata<'a> {
230219
/// a closure (for forward references), or resolved at runtime.
231220
pub declaration_list_emit_mode: DeclarationListEmitMode,
232221

233-
/// Whether any of the declarations are directives.
234-
///
235-
/// Used to determine compilation mode: DomOnly vs Full.
236-
/// When true, Full mode is used to enable directive dependency analysis.
237-
pub has_directive_dependencies: bool,
238-
239222
/// Raw imports expression for standalone components (local compilation).
240223
///
241224
/// Used with `RuntimeResolved` emit mode to pass the imports array
@@ -529,7 +512,6 @@ impl<'a> ComponentMetadata<'a> {
529512
styles: Vec::new_in(allocator),
530513
style_urls: Vec::new_in(allocator),
531514
standalone: implicit_standalone,
532-
standalone_explicitly_set: false,
533515
encapsulation: ViewEncapsulation::default(),
534516
change_detection: ChangeDetectionStrategy::default(),
535517
host: None,
@@ -549,7 +531,6 @@ impl<'a> ComponentMetadata<'a> {
549531
// Template dependency fields
550532
declarations: Vec::new_in(allocator),
551533
declaration_list_emit_mode: DeclarationListEmitMode::default(),
552-
has_directive_dependencies: false,
553534
raw_imports: None,
554535
animations: None,
555536
schemas: Vec::new_in(allocator),

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,6 @@ pub struct TransformOptions {
8282
/// When true, applies additional optimizations like constant folding.
8383
pub advanced_optimizations: bool,
8484

85-
/// Enable DomOnly compilation mode for standalone components.
86-
///
87-
/// When true, uses optimized DOM-only instructions (ɵɵdomElementStart, etc.)
88-
/// that skip directive matching. Only safe when the component has no
89-
/// directive dependencies.
90-
///
91-
/// This is a hint from the build tool's metadata resolver.
92-
pub use_dom_only_mode: bool,
93-
9485
/// i18n message ID strategy.
9586
///
9687
/// When true (default), uses external message IDs for Closure Compiler
@@ -208,7 +199,6 @@ impl Default for TransformOptions {
208199
jit: false,
209200
hmr: false,
210201
advanced_optimizations: false,
211-
use_dom_only_mode: false,
212202
i18n_use_external_ids: true, // Angular's JIT default
213203
angular_version: None, // None means assume latest (v19+ behavior)
214204
// Metadata overrides default to None (use extracted/default values)
@@ -1398,33 +1388,13 @@ fn compile_component_full<'a>(
13981388
// Build ingest options from metadata and transform options
13991389
let component_name_atom = Atom::from_in(metadata.class_name.as_str(), allocator);
14001390

1401-
// Determine compilation mode matching Angular's logic:
1402-
// meta.isStandalone && !meta.hasDirectiveDependencies → DomOnly
1403-
// otherwise → Full
1404-
// See: angular/packages/compiler/src/render3/view/compiler.ts:229-232
1391+
// OXC is a single-file compiler, equivalent to Angular's local compilation mode.
1392+
// In local compilation mode, Angular ALWAYS sets hasDirectiveDependencies=true,
1393+
// so DomOnly mode is never used for component templates.
1394+
// See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1257
14051395
//
1406-
// For full component compilation, we determine this from the parsed metadata
1407-
// rather than relying solely on the external use_dom_only_mode flag.
1408-
// The metadata has standalone (from decorator) and has_directive_dependencies
1409-
// (from analyzing the imports array).
1410-
//
1411-
// IMPORTANT: We only use DomOnly mode when `standalone: true` was EXPLICITLY
1412-
// set in the decorator. When standalone is implicitly defaulted (Angular v19+),
1413-
// we conservatively use Full mode because:
1414-
// 1. The component may be declared in an NgModule (OXC can't detect this)
1415-
// 2. Angular's ngtsc in local compilation mode always sets
1416-
// hasDirectiveDependencies=true for safety
1417-
// 3. Angular's ngtsc in global mode sets hasDirectiveDependencies=!isStandalone||...
1418-
// meaning non-standalone components ALWAYS use Full mode
1419-
// See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1326-1339
1420-
let mode = if metadata.standalone
1421-
&& metadata.standalone_explicitly_set
1422-
&& !metadata.has_directive_dependencies
1423-
{
1424-
TemplateCompilationMode::DomOnly
1425-
} else {
1426-
TemplateCompilationMode::Full
1427-
};
1396+
// Note: DomOnly mode is still used for host bindings (separate code path).
1397+
let mode = TemplateCompilationMode::Full;
14281398

14291399
// Determine defer block emit mode based on JIT setting
14301400
// In JIT mode, use PerComponent mode since the compiler doesn't have full dependency info
@@ -1842,11 +1812,8 @@ pub fn compile_template_to_js_with_options<'a>(
18421812
}
18431813

18441814
// Build IngestOptions from TransformOptions
1845-
let mode = if options.use_dom_only_mode {
1846-
TemplateCompilationMode::DomOnly
1847-
} else {
1848-
TemplateCompilationMode::Full
1849-
};
1815+
// OXC is a single-file compiler (local compilation mode): always use Full mode.
1816+
let mode = TemplateCompilationMode::Full;
18501817

18511818
let defer_block_deps_emit_mode = if options.jit {
18521819
DeferBlockDepsEmitMode::PerComponent
@@ -2009,11 +1976,8 @@ pub fn compile_template_for_hmr<'a>(
20091976
}
20101977

20111978
// Build IngestOptions from TransformOptions
2012-
let mode = if options.use_dom_only_mode {
2013-
TemplateCompilationMode::DomOnly
2014-
} else {
2015-
TemplateCompilationMode::Full
2016-
};
1979+
// OXC is a single-file compiler (local compilation mode): always use Full mode.
1980+
let mode = TemplateCompilationMode::Full;
20171981

20181982
let defer_block_deps_emit_mode = if options.jit {
20191983
DeferBlockDepsEmitMode::PerComponent

0 commit comments

Comments
 (0)