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 }}

', + 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