diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs
index 8456c08f2..58fe52f26 100644
--- a/crates/oxc_angular_compiler/src/component/decorator.rs
+++ b/crates/oxc_angular_compiler/src/component/decorator.rs
@@ -114,7 +114,6 @@ pub fn extract_component_metadata<'a>(
// Only override the implicit value if an explicit boolean is provided
if let Some(value) = extract_boolean_value(&prop.value) {
metadata.standalone = value;
- metadata.standalone_explicitly_set = true;
}
}
"encapsulation" => {
@@ -128,17 +127,10 @@ pub fn extract_component_metadata<'a>(
}
"imports" => {
// For standalone components, we need:
- // 1. The identifier list for local analysis and DomOnly mode detection
+ // 1. The identifier list for local analysis
metadata.imports = extract_identifier_array(allocator, &prop.value);
// 2. The raw expression to pass to ɵɵgetComponentDepsFactory in RuntimeResolved mode
metadata.raw_imports = convert_oxc_expression(allocator, &prop.value);
- // 3. Determine if the imports array has any non-pipe elements (directive deps).
- // Angular's ngtsc (handler.ts:1326-1339) only counts MetaKind.Directive
- // and MetaKind.NgModule — NOT MetaKind.Pipe. Without type info, we use
- // a naming convention heuristic: identifiers ending in "Pipe" are pipes.
- // See: angular/packages/compiler/src/render3/view/compiler.ts:229-232
- metadata.has_directive_dependencies =
- has_any_non_pipe_import_elements(&prop.value);
}
"exportAs" => {
// exportAs can be comma-separated: "foo, bar"
@@ -399,47 +391,6 @@ fn extract_string_array<'a>(
Some(result)
}
-/// Check if an imports expression has any non-pipe elements (directive dependencies).
-///
-/// Angular's ngtsc (handler.ts:1326-1339) only counts `MetaKind.Directive` and
-/// `MetaKind.NgModule` as directive dependencies — NOT `MetaKind.Pipe`.
-///
-/// Since OXC is a single-file compiler without type information, we use a naming
-/// convention heuristic: identifiers ending in "Pipe" are assumed to be pipes
-/// and do NOT count as directive dependencies. This matches the universal Angular
-/// convention where all pipe classes are named `*Pipe` (AsyncPipe, DatePipe, etc.).
-///
-/// Returns `true` (has directive dependencies) if:
-/// - The expression is not an array literal (e.g., variable reference — conservatively
-/// assumed to potentially contain directives)
-/// - The array contains any non-identifier element (spread, function call, etc.)
-/// - The array contains any identifier that does NOT end in "Pipe"
-///
-/// Returns `false` if:
-/// - The expression is an empty array literal (`imports: []`)
-/// - ALL elements in the array are identifiers ending in "Pipe"
-fn has_any_non_pipe_import_elements(expr: &Expression<'_>) -> bool {
- let Expression::ArrayExpression(arr) = expr else {
- // Not an array literal (e.g., variable reference like `imports: MY_IMPORTS`)
- // Conservatively assume it may contain directives
- return true;
- };
- for element in &arr.elements {
- match element {
- ArrayExpressionElement::Identifier(id) => {
- if !id.name.ends_with("Pipe") {
- return true;
- }
- }
- // Non-identifier elements (spread, call expressions, etc.)
- // conservatively treated as potential directives
- _ => return true,
- }
- }
- // All elements are identifiers ending in "Pipe", or the array is empty
- false
-}
-
/// Extract an array of identifiers (for imports).
fn extract_identifier_array<'a>(
allocator: &'a Allocator,
@@ -3283,146 +3234,4 @@ mod tests {
);
});
}
-
- // =========================================================================
- // Directive dependency detection tests (pipe vs directive imports)
- // =========================================================================
- //
- // Angular's ngtsc (handler.ts:1326-1339) only counts MetaKind.Directive
- // and MetaKind.NgModule as directive dependencies — NOT MetaKind.Pipe.
- // Since OXC is a single-file compiler, we use a naming convention heuristic:
- // identifiers ending in "Pipe" are assumed to be pipes.
-
- #[test]
- fn test_pipe_only_imports_no_directive_dependencies() {
- let code = r#"
- @Component({
- selector: 'app-test',
- standalone: true,
- imports: [AsyncPipe],
- template: ''
- })
- class TestComponent {}
- "#;
- assert_metadata(code, |meta| {
- assert!(
- !meta.has_directive_dependencies,
- "Pipe-only imports should not set has_directive_dependencies"
- );
- });
- }
-
- #[test]
- fn test_multiple_pipe_imports_no_directive_dependencies() {
- let code = r#"
- @Component({
- selector: 'app-test',
- standalone: true,
- imports: [AsyncPipe, DatePipe, SlicePipe, KeyValuePipe],
- template: ''
- })
- class TestComponent {}
- "#;
- assert_metadata(code, |meta| {
- assert!(
- !meta.has_directive_dependencies,
- "Multiple pipe-only imports should not set has_directive_dependencies"
- );
- });
- }
-
- #[test]
- fn test_mixed_pipe_and_directive_imports_has_directive_dependencies() {
- let code = r#"
- @Component({
- selector: 'app-test',
- standalone: true,
- imports: [AsyncPipe, HighlightDirective],
- template: ''
- })
- class TestComponent {}
- "#;
- assert_metadata(code, |meta| {
- assert!(
- meta.has_directive_dependencies,
- "Mixed imports with non-pipe should set has_directive_dependencies"
- );
- });
- }
-
- #[test]
- fn test_directive_only_imports_has_directive_dependencies() {
- let code = r#"
- @Component({
- selector: 'app-test',
- standalone: true,
- imports: [HighlightDirective, RouterModule],
- template: ''
- })
- class TestComponent {}
- "#;
- assert_metadata(code, |meta| {
- assert!(
- meta.has_directive_dependencies,
- "Directive-only imports should set has_directive_dependencies"
- );
- });
- }
-
- #[test]
- fn test_empty_imports_no_directive_dependencies() {
- let code = r#"
- @Component({
- selector: 'app-test',
- standalone: true,
- imports: [],
- template: ''
- })
- class TestComponent {}
- "#;
- assert_metadata(code, |meta| {
- assert!(
- !meta.has_directive_dependencies,
- "Empty imports should not set has_directive_dependencies"
- );
- });
- }
-
- #[test]
- fn test_variable_imports_has_directive_dependencies() {
- let code = r#"
- @Component({
- selector: 'app-test',
- standalone: true,
- imports: MY_IMPORTS,
- template: ''
- })
- class TestComponent {}
- "#;
- assert_metadata(code, |meta| {
- assert!(
- meta.has_directive_dependencies,
- "Variable imports should conservatively set has_directive_dependencies"
- );
- });
- }
-
- #[test]
- fn test_spread_in_imports_has_directive_dependencies() {
- let code = r#"
- @Component({
- selector: 'app-test',
- standalone: true,
- imports: [...SHARED_IMPORTS, AsyncPipe],
- template: ''
- })
- class TestComponent {}
- "#;
- assert_metadata(code, |meta| {
- assert!(
- meta.has_directive_dependencies,
- "Spread in imports should conservatively set has_directive_dependencies"
- );
- });
- }
}
diff --git a/crates/oxc_angular_compiler/src/component/metadata.rs b/crates/oxc_angular_compiler/src/component/metadata.rs
index d517e5b77..514410169 100644
--- a/crates/oxc_angular_compiler/src/component/metadata.rs
+++ b/crates/oxc_angular_compiler/src/component/metadata.rs
@@ -115,17 +115,6 @@ pub struct ComponentMetadata<'a> {
/// Whether this is a standalone component.
pub standalone: bool,
- /// Whether `standalone` was explicitly set in the decorator.
- ///
- /// When `false`, `standalone` was inherited from the implicit default (Angular v19+
- /// defaults to `true`). This distinction matters for DomOnly mode: only components
- /// with an explicit `standalone: true` should use DomOnly mode, because implicit
- /// standalone components may be declared in NgModules (which OXC can't detect in
- /// single-file compilation).
- ///
- /// See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1326-1339
- pub standalone_explicitly_set: bool,
-
/// View encapsulation mode.
pub encapsulation: ViewEncapsulation,
@@ -230,12 +219,6 @@ pub struct ComponentMetadata<'a> {
/// a closure (for forward references), or resolved at runtime.
pub declaration_list_emit_mode: DeclarationListEmitMode,
- /// Whether any of the declarations are directives.
- ///
- /// Used to determine compilation mode: DomOnly vs Full.
- /// When true, Full mode is used to enable directive dependency analysis.
- pub has_directive_dependencies: bool,
-
/// Raw imports expression for standalone components (local compilation).
///
/// Used with `RuntimeResolved` emit mode to pass the imports array
@@ -529,7 +512,6 @@ impl<'a> ComponentMetadata<'a> {
styles: Vec::new_in(allocator),
style_urls: Vec::new_in(allocator),
standalone: implicit_standalone,
- standalone_explicitly_set: false,
encapsulation: ViewEncapsulation::default(),
change_detection: ChangeDetectionStrategy::default(),
host: None,
@@ -549,7 +531,6 @@ impl<'a> ComponentMetadata<'a> {
// Template dependency fields
declarations: Vec::new_in(allocator),
declaration_list_emit_mode: DeclarationListEmitMode::default(),
- has_directive_dependencies: false,
raw_imports: None,
animations: None,
schemas: Vec::new_in(allocator),
diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs
index b51ee375b..95472b5b3 100644
--- a/crates/oxc_angular_compiler/src/component/transform.rs
+++ b/crates/oxc_angular_compiler/src/component/transform.rs
@@ -82,15 +82,6 @@ pub struct TransformOptions {
/// When true, applies additional optimizations like constant folding.
pub advanced_optimizations: bool,
- /// Enable DomOnly compilation mode for standalone components.
- ///
- /// When true, uses optimized DOM-only instructions (ɵɵdomElementStart, etc.)
- /// that skip directive matching. Only safe when the component has no
- /// directive dependencies.
- ///
- /// This is a hint from the build tool's metadata resolver.
- pub use_dom_only_mode: bool,
-
/// i18n message ID strategy.
///
/// When true (default), uses external message IDs for Closure Compiler
@@ -208,7 +199,6 @@ impl Default for TransformOptions {
jit: false,
hmr: false,
advanced_optimizations: false,
- use_dom_only_mode: false,
i18n_use_external_ids: true, // Angular's JIT default
angular_version: None, // None means assume latest (v19+ behavior)
// Metadata overrides default to None (use extracted/default values)
@@ -1398,33 +1388,13 @@ fn compile_component_full<'a>(
// Build ingest options from metadata and transform options
let component_name_atom = Atom::from_in(metadata.class_name.as_str(), allocator);
- // Determine compilation mode matching Angular's logic:
- // meta.isStandalone && !meta.hasDirectiveDependencies → DomOnly
- // otherwise → Full
- // See: angular/packages/compiler/src/render3/view/compiler.ts:229-232
+ // OXC is a single-file compiler, equivalent to Angular's local compilation mode.
+ // In local compilation mode, Angular ALWAYS sets hasDirectiveDependencies=true,
+ // so DomOnly mode is never used for component templates.
+ // See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1257
//
- // For full component compilation, we determine this from the parsed metadata
- // rather than relying solely on the external use_dom_only_mode flag.
- // The metadata has standalone (from decorator) and has_directive_dependencies
- // (from analyzing the imports array).
- //
- // IMPORTANT: We only use DomOnly mode when `standalone: true` was EXPLICITLY
- // set in the decorator. When standalone is implicitly defaulted (Angular v19+),
- // we conservatively use Full mode because:
- // 1. The component may be declared in an NgModule (OXC can't detect this)
- // 2. Angular's ngtsc in local compilation mode always sets
- // hasDirectiveDependencies=true for safety
- // 3. Angular's ngtsc in global mode sets hasDirectiveDependencies=!isStandalone||...
- // meaning non-standalone components ALWAYS use Full mode
- // See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1326-1339
- let mode = if metadata.standalone
- && metadata.standalone_explicitly_set
- && !metadata.has_directive_dependencies
- {
- TemplateCompilationMode::DomOnly
- } else {
- TemplateCompilationMode::Full
- };
+ // Note: DomOnly mode is still used for host bindings (separate code path).
+ let mode = TemplateCompilationMode::Full;
// Determine defer block emit mode based on JIT setting
// 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>(
}
// Build IngestOptions from TransformOptions
- let mode = if options.use_dom_only_mode {
- TemplateCompilationMode::DomOnly
- } else {
- TemplateCompilationMode::Full
- };
+ // OXC is a single-file compiler (local compilation mode): always use Full mode.
+ let mode = TemplateCompilationMode::Full;
let defer_block_deps_emit_mode = if options.jit {
DeferBlockDepsEmitMode::PerComponent
@@ -2009,11 +1976,8 @@ pub fn compile_template_for_hmr<'a>(
}
// Build IngestOptions from TransformOptions
- let mode = if options.use_dom_only_mode {
- TemplateCompilationMode::DomOnly
- } else {
- TemplateCompilationMode::Full
- };
+ // OXC is a single-file compiler (local compilation mode): always use Full mode.
+ let mode = TemplateCompilationMode::Full;
let defer_block_deps_emit_mode = if options.jit {
DeferBlockDepsEmitMode::PerComponent
diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs
index 23ea8fec9..cdd1a0cb8 100644
--- a/crates/oxc_angular_compiler/tests/integration_test.rs
+++ b/crates/oxc_angular_compiler/tests/integration_test.rs
@@ -942,6 +942,61 @@ export class TestComponent {
insta::assert_snapshot!("event_before_property_in_bindings", result.code);
}
+// ============================================================================
+// Compilation Mode Tests (Full vs DomOnly)
+// ============================================================================
+
+#[test]
+fn test_standalone_component_uses_full_mode() {
+ // OXC operates as a single-file compiler, equivalent to Angular's local compilation mode.
+ // In local compilation mode, Angular ALWAYS sets hasDirectiveDependencies=true,
+ // which means DomOnly mode is never used for component templates.
+ // See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1257
+ //
+ // This test ensures standalone components with no imports use Full mode instructions
+ // (ɵɵelementStart) NOT DomOnly mode instructions (ɵɵdomElementStart).
+ let allocator = Allocator::default();
+ let source = r#"
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-external',
+ template: '
{{ title }}
@for (item of items; track item) { - {{ item }}
}
',
+ standalone: true,
+})
+export class ExternalComponent {
+ title = 'External Component';
+ items = ['Apple', 'Banana', 'Cherry'];
+}
+"#;
+
+ let result = transform_angular_file(
+ &allocator,
+ "test.component.ts",
+ source,
+ &ComponentTransformOptions::default(),
+ None,
+ );
+
+ assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
+
+ // Must use Full mode instructions (elementStart), not DomOnly (domElementStart)
+ assert!(
+ !result.code.contains("domElementStart"),
+ "Standalone component should use Full mode (elementStart), not DomOnly mode (domElementStart).\n\
+ OXC operates in local compilation mode where hasDirectiveDependencies is always true.\n\
+ Output:\n{}",
+ result.code
+ );
+ assert!(
+ result.code.contains("elementStart"),
+ "Expected Full mode instruction ɵɵelementStart in output.\nOutput:\n{}",
+ result.code
+ );
+
+ insta::assert_snapshot!("standalone_component_uses_full_mode", result.code);
+}
+
// ============================================================================
// Nested Control Flow Tests
// ============================================================================
@@ -2741,8 +2796,7 @@ fn test_svg_namespace_in_switch_case_inside_for_domonly_mode() {
}
"#;
- // Use DomOnly mode like the fixture tests do for standalone components
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
+ let options = ComponentTransformOptions::default();
let result = compile_template_to_js_with_options(
&allocator,
template,
@@ -2799,9 +2853,7 @@ import { Component } from '@angular/core';
export class SvgInSwitchCaseComponent {}
"#;
- // Use DomOnly mode like the fixture tests do for standalone components
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
-
+ let options = ComponentTransformOptions::default();
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
// Verify that conditionalCreate uses ":svg:svg" not just "svg"
@@ -3333,8 +3385,7 @@ fn test_parenthesized_safe_navigation_keyed_access() {
);
}
-/// Test that standalone components WITH directive imports use Full mode (elementStart)
-/// even when use_dom_only_mode is set to true.
+/// Test that standalone components WITH directive imports use Full mode (elementStart).
///
/// Angular determines compilation mode from component metadata:
/// meta.isStandalone && !meta.hasDirectiveDependencies → DomOnly
@@ -3366,9 +3417,8 @@ export class TestComponent {
}
";
- // Even with use_dom_only_mode: true, the compiler should detect directive dependencies
- // from the imports array and use Full mode (elementStart, not domElementStart)
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
+ // OXC always uses Full mode (elementStart, not domElementStart)
+ let options = ComponentTransformOptions::default();
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
// Should use elementStart (Full mode), NOT domElementStart (DomOnly mode)
@@ -3384,9 +3434,14 @@ export class TestComponent {
);
}
-/// Test that standalone components WITHOUT imports correctly use DomOnly mode.
+/// Test that standalone components WITHOUT imports use Full mode (local compilation).
+///
+/// OXC is a single-file compiler, equivalent to Angular's local compilation mode.
+/// In local compilation mode, Angular ALWAYS sets hasDirectiveDependencies=true,
+/// so DomOnly mode is never used for component templates.
+/// See: angular/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1257
#[test]
-fn test_dom_only_mode_used_for_standalone_without_imports() {
+fn test_dom_only_mode_not_used_for_standalone_without_imports() {
let allocator = Allocator::default();
let source = r"
import { Component } from '@angular/core';
@@ -3402,23 +3457,23 @@ import { Component } from '@angular/core';
export class TestComponent {}
";
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
+ let options = ComponentTransformOptions::default();
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
- // Should use domElementStart (DomOnly mode) for standalone with no imports
+ // OXC in local compilation mode: always Full mode for component templates
assert!(
- result.code.contains("ɵɵdomElementStart"),
- "Standalone component without imports should use ɵɵdomElementStart. Output:\n{}",
+ result.code.contains("ɵɵelementStart"),
+ "Standalone component without imports should use ɵɵelementStart (Full mode). Output:\n{}",
result.code
);
assert!(
- !result.code.contains("ɵɵelementStart"),
- "Standalone component without imports should NOT use ɵɵelementStart. Output:\n{}",
+ !result.code.contains("ɵɵdomElementStart"),
+ "Standalone component without imports should NOT use ɵɵdomElementStart. Output:\n{}",
result.code
);
}
-/// Test that non-standalone components use Full mode even with use_dom_only_mode.
+/// Test that non-standalone components use Full mode.
#[test]
fn test_dom_only_mode_not_used_for_non_standalone() {
let allocator = Allocator::default();
@@ -3433,7 +3488,7 @@ import { Component } from '@angular/core';
export class TestComponent {}
";
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
+ let options = ComponentTransformOptions::default();
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
// Non-standalone should always use Full mode
@@ -3653,7 +3708,6 @@ export class TestComponent {}
// Angular version 19+ defaults standalone to true, but implicit standalone
// should NOT trigger DomOnly mode because the component might be in an NgModule
let options = ComponentTransformOptions {
- use_dom_only_mode: true,
angular_version: Some(AngularVersion::new(21, 0, 0)),
..Default::default()
};
@@ -3691,7 +3745,6 @@ export class TestComponent {}
";
let options = ComponentTransformOptions {
- use_dom_only_mode: true,
angular_version: Some(AngularVersion::new(21, 0, 0)),
..Default::default()
};
@@ -3710,9 +3763,11 @@ export class TestComponent {}
);
}
-/// Test that standalone components with empty imports use DomOnly mode.
+/// Test that standalone components with empty imports use Full mode (local compilation).
+///
+/// OXC always uses Full mode for component templates, matching Angular's local compilation.
#[test]
-fn test_dom_only_mode_used_for_standalone_with_empty_imports() {
+fn test_dom_only_mode_not_used_for_standalone_with_empty_imports() {
let allocator = Allocator::default();
let source = r"
import { Component } from '@angular/core';
@@ -3726,25 +3781,27 @@ import { Component } from '@angular/core';
export class TestComponent {}
";
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
+ let options = ComponentTransformOptions::default();
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
- // Empty imports means no directive dependencies → DomOnly mode
+ // OXC in local compilation mode: always Full mode for component templates
+ assert!(
+ result.code.contains("ɵɵelementStart"),
+ "Standalone with empty imports should use ɵɵelementStart (Full mode). Output:\n{}",
+ result.code
+ );
assert!(
- result.code.contains("ɵɵdomElementStart"),
- "Standalone with empty imports should use ɵɵdomElementStart. Output:\n{}",
+ !result.code.contains("ɵɵdomElementStart"),
+ "Standalone with empty imports should NOT use ɵɵdomElementStart. Output:\n{}",
result.code
);
}
-/// Test that standalone components with ONLY pipe imports use DomOnly mode.
+/// Test that standalone components with ONLY pipe imports use Full mode (local compilation).
///
-/// Angular's ngtsc (handler.ts:1326-1339) only counts MetaKind.Directive and
-/// MetaKind.NgModule as directive dependencies — NOT MetaKind.Pipe. Since OXC
-/// is a single-file compiler, we use the naming convention (ending in "Pipe")
-/// to identify pipes.
+/// OXC always uses Full mode for component templates, matching Angular's local compilation.
#[test]
-fn test_dom_only_mode_used_for_standalone_with_pipe_only_imports() {
+fn test_dom_only_mode_not_used_for_standalone_with_pipe_only_imports() {
let allocator = Allocator::default();
let source = r#"
import { Component } from '@angular/core';
@@ -3761,24 +3818,27 @@ export class TestComponent {
}
"#;
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
+ let options = ComponentTransformOptions::default();
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
+ // OXC in local compilation mode: always Full mode for component templates
assert!(
- result.code.contains("ɵɵdomElementStart"),
- "Standalone with pipe-only imports should use ɵɵdomElementStart (DomOnly). Output:\n{}",
+ result.code.contains("ɵɵelementStart"),
+ "Standalone with pipe-only imports should use ɵɵelementStart (Full mode). Output:\n{}",
result.code
);
assert!(
- !result.code.contains("ɵɵelementStart"),
- "Standalone with pipe-only imports should NOT use ɵɵelementStart. Output:\n{}",
+ !result.code.contains("ɵɵdomElementStart"),
+ "Standalone with pipe-only imports should NOT use ɵɵdomElementStart. Output:\n{}",
result.code
);
}
-/// Test that multiple pipe-only imports also use DomOnly mode.
+/// Test that multiple pipe-only imports also use Full mode (local compilation).
+///
+/// OXC always uses Full mode for component templates, matching Angular's local compilation.
#[test]
-fn test_dom_only_mode_used_for_standalone_with_multiple_pipe_imports() {
+fn test_dom_only_mode_not_used_for_standalone_with_multiple_pipe_imports() {
let allocator = Allocator::default();
let source = r#"
import { Component } from '@angular/core';
@@ -3793,12 +3853,18 @@ import { AsyncPipe, DatePipe, SlicePipe } from '@angular/common';
export class TestComponent {}
"#;
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
+ let options = ComponentTransformOptions::default();
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
+ // OXC in local compilation mode: always Full mode for component templates
assert!(
- result.code.contains("ɵɵdomElementStart"),
- "Multiple pipe-only imports should use DomOnly mode. Output:\n{}",
+ result.code.contains("ɵɵelementStart"),
+ "Multiple pipe-only imports should use Full mode. Output:\n{}",
+ result.code
+ );
+ assert!(
+ !result.code.contains("ɵɵdomElementStart"),
+ "Multiple pipe-only imports should NOT use DomOnly mode. Output:\n{}",
result.code
);
}
@@ -3823,7 +3889,7 @@ export class HighlightDirective {}
export class TestComponent {}
"#;
- let options = ComponentTransformOptions { use_dom_only_mode: true, ..Default::default() };
+ let options = ComponentTransformOptions::default();
let result = transform_angular_file(&allocator, "test.ts", source, &options, None);
assert!(
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__event_before_property_in_bindings.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__event_before_property_in_bindings.snap
index 3f0a882e0..9228d9f0a 100644
--- a/crates/oxc_angular_compiler/tests/snapshots/integration_test__event_before_property_in_bindings.snap
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__event_before_property_in_bindings.snap
@@ -2,7 +2,6 @@
source: crates/oxc_angular_compiler/tests/integration_test.rs
expression: result.code
---
-
import { Component } from '@angular/core';
import * as i0 from '@angular/core';
@@ -17,14 +16,14 @@ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:TestComponent,selector
vars:1,consts:[[3,"click","disabled"]],template:function TestComponent_Template(rf,
ctx) {
if ((rf & 1)) {
- i0.ɵɵdomElementStart(0,"button",0);
- i0.ɵɵdomListener("click",function TestComponent_Template_button_click_0_listener() {
+ i0.ɵɵelementStart(0,"button",0);
+ i0.ɵɵlistener("click",function TestComponent_Template_button_click_0_listener() {
return ctx.onClick();
});
i0.ɵɵtext(1,"Click");
- i0.ɵɵdomElementEnd();
+ i0.ɵɵelementEnd();
}
- if ((rf & 2)) { i0.ɵɵdomProperty("disabled",ctx.isDisabled); }
+ if ((rf & 2)) { i0.ɵɵproperty("disabled",ctx.isDisabled); }
},encapsulation:2});
}
(() =>{
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__ngfor_attribute_ordering.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__ngfor_attribute_ordering.snap
index 389ebfdd1..a93c4681a 100644
--- a/crates/oxc_angular_compiler/tests/snapshots/integration_test__ngfor_attribute_ordering.snap
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__ngfor_attribute_ordering.snap
@@ -2,15 +2,14 @@
source: crates/oxc_angular_compiler/tests/integration_test.rs
expression: result.code
---
-
import { Component } from '@angular/core';
import * as i0 from '@angular/core';
function TestComponent_li_0_Template(rf,ctx) {
if ((rf & 1)) {
- i0.ɵɵdomElementStart(0,"li");
+ i0.ɵɵelementStart(0,"li");
i0.ɵɵtext(1);
- i0.ɵɵdomElementEnd();
+ i0.ɵɵelementEnd();
}
if ((rf & 2)) {
const item_r1 = ctx.$implicit;
@@ -28,8 +27,8 @@ static ɵfac = function TestComponent_Factory(__ngFactoryType__) {
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:TestComponent,selectors:[["test-comp"]],decls:1,
vars:1,consts:[[4,"ngFor","ngForOf"]],template:function TestComponent_Template(rf,
ctx) {
- if ((rf & 1)) { i0.ɵɵdomTemplate(0,TestComponent_li_0_Template,2,1,"li",0); }
- if ((rf & 2)) { i0.ɵɵdomProperty("ngForOf",ctx.items); }
+ if ((rf & 1)) { i0.ɵɵtemplate(0,TestComponent_li_0_Template,2,1,"li",0); }
+ if ((rf & 2)) { i0.ɵɵproperty("ngForOf",ctx.items); }
},encapsulation:2});
}
(() =>{
diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__standalone_component_uses_full_mode.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__standalone_component_uses_full_mode.snap
new file mode 100644
index 000000000..e75c7179d
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__standalone_component_uses_full_mode.snap
@@ -0,0 +1,50 @@
+---
+source: crates/oxc_angular_compiler/tests/integration_test.rs
+expression: result.code
+---
+import { Component } from '@angular/core';
+import * as i0 from '@angular/core';
+
+function ExternalComponent_For_5_Template(rf,ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵelementStart(0,"li");
+ i0.ɵɵtext(1);
+ i0.ɵɵelementEnd();
+ }
+ if ((rf & 2)) {
+ const item_r1 = ctx.$implicit;
+ i0.ɵɵadvance();
+ i0.ɵɵtextInterpolate(item_r1);
+ }
+}
+
+export class ExternalComponent {
+ title = 'External Component';
+ items = ['Apple', 'Banana', 'Cherry'];
+
+static ɵfac = function ExternalComponent_Factory(__ngFactoryType__) {
+ return new (__ngFactoryType__ || ExternalComponent)();
+};
+static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:ExternalComponent,selectors:[["app-external"]],
+ decls:6,vars:1,consts:[[1,"container"]],template:function ExternalComponent_Template(rf,
+ ctx) {
+ if ((rf & 1)) {
+ i0.ɵɵelementStart(0,"div",0)(1,"h2");
+ i0.ɵɵtext(2);
+ i0.ɵɵelementEnd();
+ i0.ɵɵelementStart(3,"ul");
+ i0.ɵɵrepeaterCreate(4,ExternalComponent_For_5_Template,2,1,"li",null,i0.ɵɵrepeaterTrackByIdentity);
+ i0.ɵɵelementEnd()();
+ }
+ if ((rf & 2)) {
+ i0.ɵɵadvance(2);
+ i0.ɵɵtextInterpolate(ctx.title);
+ i0.ɵɵadvance(2);
+ i0.ɵɵrepeater(ctx.items);
+ }
+ },encapsulation:2});
+}
+(() =>{
+ (((typeof ngDevMode === "undefined") || ngDevMode) && i0.ɵsetClassDebugInfo(ExternalComponent,
+ {className:"ExternalComponent",filePath:"test.component.ts",lineNumber:1}));
+})();
diff --git a/napi/angular-compiler/e2e/compare/src/compilers/angular-ngtsc.ts b/napi/angular-compiler/e2e/compare/src/compilers/angular-ngtsc.ts
index 9af612c58..369a1c72b 100644
--- a/napi/angular-compiler/e2e/compare/src/compilers/angular-ngtsc.ts
+++ b/napi/angular-compiler/e2e/compare/src/compilers/angular-ngtsc.ts
@@ -445,7 +445,9 @@ function getOrCreateCachedConfig(tsconfigPath: string): CachedProgramEntry {
const tsconfigHash = hashString(rawConfig)
// Merge with Angular options first (needed for host creation)
- const angularOptions = createAngularOptions()
+ // Use skipTypeChecking=true to get experimental-local compilation mode,
+ // which matches OXC's single-file compilation behavior (always Full template mode).
+ const angularOptions = createAngularOptions(true)
const mergedOptions = {
...compilerOptions,
...angularOptions,
@@ -693,7 +695,9 @@ export async function compileWithNgtsc(
if (!virtualModeConfig) {
// Create compiler options (only once)
const compilerOptions = createCompilerOptions(rootDir)
- const angularOptions = createAngularOptions()
+ // Use skipTypeChecking=true to get experimental-local compilation mode,
+ // which matches OXC's single-file compilation behavior (always Full template mode).
+ const angularOptions = createAngularOptions(true)
const mergedCompilerOptions = {
...compilerOptions,
...angularOptions,
@@ -1614,29 +1618,3 @@ export class ${className} {
${classBody.length > 0 ? classBody.join('\n') + '\n' : ''}}
`
}
-
-/**
- * Compile a template using NgtscProgram by generating a full component source.
- *
- * This function provides the same interface as compileWithAngular but uses
- * NgtscProgram internally for compilation, ensuring consistent behavior
- * with real Angular CLI builds.
- *
- * @param template - The template HTML to compile
- * @param className - The component class name
- * @param filePath - The file path for error reporting
- * @param metadata - Optional component metadata
- * @returns The compiled JavaScript output
- */
-export async function compileTemplateWithNgtsc(
- template: string,
- className: string,
- filePath: string,
- metadata?: ComponentMetadata,
-): Promise {
- // Generate the full component source
- const source = generateComponentSource(template, className, metadata)
-
- // Compile with NgtscProgram
- return compileWithNgtsc(source, filePath)
-}
diff --git a/napi/angular-compiler/e2e/compare/src/compilers/oxc.ts b/napi/angular-compiler/e2e/compare/src/compilers/oxc.ts
index 143a93b8d..151c8d7bc 100644
--- a/napi/angular-compiler/e2e/compare/src/compilers/oxc.ts
+++ b/napi/angular-compiler/e2e/compare/src/compilers/oxc.ts
@@ -24,81 +24,6 @@ export type PlainResolvedResources = {
styles: Record
}
-/**
- * Known Angular built-in pipes that are NOT directive dependencies.
- * These pipes don't affect whether DomOnly mode should be used.
- */
-const KNOWN_PIPES = new Set([
- // @angular/common pipes
- 'AsyncPipe',
- 'CurrencyPipe',
- 'DatePipe',
- 'DecimalPipe',
- 'I18nPluralPipe',
- 'I18nSelectPipe',
- 'JsonPipe',
- 'KeyValuePipe',
- 'LowerCasePipe',
- 'PercentPipe',
- 'SlicePipe',
- 'TitleCasePipe',
- 'UpperCasePipe',
-])
-
-/**
- * Detect if a component should use DomOnly mode using regex.
- *
- * DomOnly mode is used by Angular's NgtscProgram for standalone components
- * without directive dependencies. This produces optimized instructions like
- * `ɵɵdomElementStart` instead of `ɵɵelementStart`.
- *
- * A component should use DomOnly mode when:
- * 1. It is standalone (explicit or implicit via imports)
- * 2. It has no directive dependencies in its imports array (pipes don't count)
- *
- * @param source - The TypeScript source code
- * @returns true if the component should use DomOnly mode
- */
-function shouldUseDomOnlyMode(source: string): boolean {
- // Check if component is standalone
- const standaloneMatch = source.match(/standalone\s*:\s*(true|false)/)
- const isStandalone = standaloneMatch ? standaloneMatch[1] === 'true' : false
-
- if (!isStandalone) {
- // Non-standalone components use Full mode
- return false
- }
-
- // Check if component has imports property (directive dependencies)
- const importsMatch = source.match(/imports\s*:\s*\[([^\]]*)\]/)
- if (!importsMatch) {
- // Standalone without imports = DomOnly mode
- return true
- }
-
- const importsContent = importsMatch[1].trim()
- if (importsContent === '') {
- // Empty imports array = DomOnly mode
- return true
- }
-
- // Parse imports to check if any are directive dependencies (not pipes)
- // Extract identifiers from the imports array
- const identifierPattern = /\b([A-Z][a-zA-Z0-9]*)\b/g
- let match
- while ((match = identifierPattern.exec(importsContent)) !== null) {
- const identifier = match[1]
- // Skip known pipes - they don't affect DomOnly mode
- if (!KNOWN_PIPES.has(identifier)) {
- // Found a non-pipe import (likely a directive/component)
- return false
- }
- }
-
- // All imports are pipes, use DomOnly mode
- return true
-}
-
/**
* Output from raw full-file compilation.
*/
@@ -133,15 +58,11 @@ export function compileWithOxcFullFileRaw(
): OxcFullFileRawOutput {
const startTime = performance.now()
- // Detect if component should use DomOnly mode
- const useDomOnlyMode = shouldUseDomOnlyMode(source)
-
const transformOptions: TransformOptions = {
sourcemap: false,
jit: false,
hmr: false,
advancedOptimizations: false,
- useDomOnlyMode,
// Enable cross-file analysis for barrel export tracing
crossFileElision: true,
baseDir: path.dirname(filePath),
diff --git a/napi/angular-compiler/index.d.ts b/napi/angular-compiler/index.d.ts
index 655edd3df..4e4344a23 100644
--- a/napi/angular-compiler/index.d.ts
+++ b/napi/angular-compiler/index.d.ts
@@ -696,13 +696,6 @@ export interface TransformOptions {
hmr?: boolean
/** Enable advanced optimizations. */
advancedOptimizations?: boolean
- /**
- * Enable DomOnly compilation mode for standalone components.
- *
- * When true, uses optimized DOM-only instructions that skip directive matching.
- * Only safe when the component has no directive dependencies.
- */
- useDomOnlyMode?: boolean
/**
* i18n message ID strategy.
*
diff --git a/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs
index c875ebbe8..a443c986f 100644
--- a/napi/angular-compiler/src/lib.rs
+++ b/napi/angular-compiler/src/lib.rs
@@ -124,12 +124,6 @@ pub struct TransformOptions {
/// Enable advanced optimizations.
pub advanced_optimizations: Option,
- /// Enable DomOnly compilation mode for standalone components.
- ///
- /// When true, uses optimized DOM-only instructions that skip directive matching.
- /// Only safe when the component has no directive dependencies.
- pub use_dom_only_mode: Option,
-
/// i18n message ID strategy.
///
/// When true (default), uses external message IDs (MSG_EXTERNAL_abc123$$SUFFIX).
@@ -215,7 +209,6 @@ impl From for RustTransformOptions {
jit: options.jit.unwrap_or(false),
hmr: options.hmr.unwrap_or(false),
advanced_optimizations: options.advanced_optimizations.unwrap_or(false),
- use_dom_only_mode: options.use_dom_only_mode.unwrap_or(false),
i18n_use_external_ids: options.i18n_use_external_ids.unwrap_or(true),
angular_version: options.angular_version.map(Into::into),
// Component metadata overrides