Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 1 addition & 192 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" => {
Expand All @@ -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"
Expand Down Expand Up @@ -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 {
Comment thread
cursor[bot] marked this conversation as resolved.
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,
Expand Down Expand Up @@ -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"
);
});
}
}
19 changes: 0 additions & 19 deletions crates/oxc_angular_compiler/src/component/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand Down
56 changes: 10 additions & 46 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading