Skip to content

Commit 5cdd256

Browse files
committed
fix
1 parent 38955a2 commit 5cdd256

12 files changed

Lines changed: 432 additions & 296 deletions

File tree

crates/oxc_angular_compiler/src/component/definition.rs

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,43 +1130,36 @@ fn generate_host_directives_feature<'a>(
11301130

11311131
/// Create a directive reference expression.
11321132
///
1133-
/// Angular uses different rules for host directive references:
1134-
/// - External packages (e.g., `@angular/cdk`): use bare type name
1135-
/// - Relative imports (e.g., `./my.directive`): use namespace reference (i1.DirectiveName)
1133+
/// For imported directives (those with a `source_module`), generates namespaced
1134+
/// references like `i1.MyDirective`. For local directives, generates bare
1135+
/// variable references like `MyDirective`.
11361136
///
1137-
/// This matches Angular's behavior where external modules are imported as named imports
1138-
/// but relative imports are compiled to namespace imports.
1137+
/// See: packages/compiler/src/render3/view/compiler.ts:689-692
11391138
fn create_directive_reference<'a>(
11401139
allocator: &'a Allocator,
11411140
directive: &HostDirectiveMetadata<'a>,
11421141
namespace_registry: &mut NamespaceRegistry<'a>,
11431142
) -> OutputExpression<'a> {
11441143
if let Some(ref source_module) = directive.source_module {
1145-
// Check if it's a relative import (starts with . or ..)
1146-
let is_relative = source_module.starts_with('.') || source_module.starts_with("..");
1147-
1148-
if is_relative {
1149-
// Relative import - use namespace.DirectiveName (e.g., i1.LocalDir)
1150-
let namespace = namespace_registry.get_or_assign(source_module);
1151-
return OutputExpression::ReadProp(Box::new_in(
1152-
ReadPropExpr {
1153-
receiver: Box::new_in(
1154-
OutputExpression::ReadVar(Box::new_in(
1155-
ReadVarExpr { name: namespace, source_span: None },
1156-
allocator,
1157-
)),
1144+
// Imported directive - use namespace.DirectiveName (e.g., i1.ExternalDir)
1145+
let namespace = namespace_registry.get_or_assign(source_module);
1146+
return OutputExpression::ReadProp(Box::new_in(
1147+
ReadPropExpr {
1148+
receiver: Box::new_in(
1149+
OutputExpression::ReadVar(Box::new_in(
1150+
ReadVarExpr { name: namespace, source_span: None },
11581151
allocator,
1159-
),
1160-
name: directive.directive.clone(),
1161-
optional: false,
1162-
source_span: None,
1163-
},
1164-
allocator,
1165-
));
1166-
}
1167-
// External package import - use bare type name
1152+
)),
1153+
allocator,
1154+
),
1155+
name: directive.directive.clone(),
1156+
optional: false,
1157+
source_span: None,
1158+
},
1159+
allocator,
1160+
));
11681161
}
1169-
// No source module or external package - use bare type name
1162+
// No source module - use bare type name (local directive)
11701163
OutputExpression::ReadVar(Box::new_in(
11711164
ReadVarExpr { name: directive.directive.clone(), source_span: None },
11721165
allocator,
@@ -2215,8 +2208,8 @@ mod tests {
22152208
let emitter = JsEmitter::new();
22162209
let js = emitter.emit_expression(&result);
22172210

2218-
// Should have trailing comma inside the array
2219-
assert!(js.contains("[A,B,C,]"), "Array should have trailing comma: {}", js);
2211+
// Should contain the array with dependencies
2212+
assert!(js.contains("[A,B,C]"), "Array should contain dependencies: {}", js);
22202213
}
22212214

22222215
#[test]

crates/oxc_angular_compiler/src/directive/compiler.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,10 +1015,10 @@ mod tests {
10151015
let emitter = JsEmitter::new();
10161016
let output = result.map(|e| emitter.emit_expression(&e)).unwrap_or_default();
10171017

1018-
// Expected: {count:[0,"itemCount","count",],} - array format with flags=0
1018+
// Expected: {count:[0,"itemCount","count"]} - array format with flags=0
10191019
// Key format: [flags, publicName, declaredName]
10201020
assert!(
1021-
output.contains(r#"count:[0,"itemCount","count","#),
1021+
output.contains(r#"count:[0,"itemCount","count"]"#),
10221022
"Aliased input should be array [flags, publicName, declaredName]: {}",
10231023
output
10241024
);
@@ -1045,9 +1045,9 @@ mod tests {
10451045
let emitter = JsEmitter::new();
10461046
let output = result.map(|e| emitter.emit_expression(&e)).unwrap_or_default();
10471047

1048-
// Expected: {disabled:[2,"disabled","disabled",booleanAttribute,],} - array with flags=2 (transform)
1048+
// Expected: {disabled:[2,"disabled","disabled",booleanAttribute]} - array with flags=2 (transform)
10491049
assert!(
1050-
output.contains(r#"disabled:[2,"disabled","disabled",booleanAttribute,"#),
1050+
output.contains(r#"disabled:[2,"disabled","disabled",booleanAttribute]"#),
10511051
"Input with transform should be array [flags, publicName, declaredName, transform]: {}",
10521052
output
10531053
);
@@ -1070,10 +1070,10 @@ mod tests {
10701070
let emitter = JsEmitter::new();
10711071
let output = result.map(|e| emitter.emit_expression(&e)).unwrap_or_default();
10721072

1073-
// Expected: {border:[1,"border",],} - array format with flags=1 (signal)
1073+
// Expected: {border:[1,"border"]} - array format with flags=1 (signal)
10741074
// For signal inputs with same name, we only need [flags, publicName]
10751075
assert!(
1076-
output.contains(r#"border:[1,"border","#),
1076+
output.contains(r#"border:[1,"border"]"#),
10771077
"Signal input should be array [flags, publicName]: {}",
10781078
output
10791079
);
@@ -1102,9 +1102,9 @@ mod tests {
11021102
let emitter = JsEmitter::new();
11031103
let output = result.map(|e| emitter.emit_expression(&e)).unwrap_or_default();
11041104

1105-
// Expected: {borderWidth:[1,"border","borderWidth",],} - array with flags=1 and both names
1105+
// Expected: {borderWidth:[1,"border","borderWidth"]} - array with flags=1 and both names
11061106
assert!(
1107-
output.contains(r#"borderWidth:[1,"border","borderWidth","#),
1107+
output.contains(r#"borderWidth:[1,"border","borderWidth"]"#),
11081108
"Signal input with alias should be array [flags, publicName, declaredName]: {}",
11091109
output
11101110
);
@@ -1132,9 +1132,9 @@ mod tests {
11321132
let emitter = JsEmitter::new();
11331133
let output = result.map(|e| emitter.emit_expression(&e)).unwrap_or_default();
11341134

1135-
// Expected: {count:[3,"count","count",toNumber,],} - array with flags=3 (signal + transform)
1135+
// Expected: {count:[3,"count","count",toNumber]} - array with flags=3 (signal + transform)
11361136
assert!(
1137-
output.contains(r#"count:[3,"count","count",toNumber,"#),
1137+
output.contains(r#"count:[3,"count","count",toNumber]"#),
11381138
"Signal input with transform should have flags=3: {}",
11391139
output
11401140
);
@@ -1180,7 +1180,7 @@ mod tests {
11801180

11811181
// Signal input: [1, "signalInput"]
11821182
assert!(
1183-
output.contains(r#"signalInput:[1,"signalInput","#),
1183+
output.contains(r#"signalInput:[1,"signalInput"]"#),
11841184
"Signal input should have flags=1: {}",
11851185
output
11861186
);
@@ -1189,7 +1189,7 @@ mod tests {
11891189
// Note: The emitter may add newlines in the output, so we check for key parts
11901190
assert!(
11911191
output.contains(r#"boolInput:[2,"boolInput","boolInput","#)
1192-
&& output.contains("booleanAttribute"),
1192+
&& output.contains("booleanAttribute]"),
11931193
"Transform input should have flags=2: {}",
11941194
output
11951195
);

crates/oxc_angular_compiler/src/pipeline/ingest.rs

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2400,17 +2400,35 @@ fn ingest_if_block<'a>(
24002400
let tag_name =
24012401
ingest_control_flow_insertion_point(job, view_xref, branch_view_xref, &branch.children);
24022402

2403+
// Extract namespace from tag_name (e.g., `:svg:foreignObject` -> Namespace::Svg)
2404+
// Matches TypeScript's ingestIfBlock behavior for SVG/MathML namespace handling.
2405+
let (namespace_key, tag_name_without_namespace) =
2406+
tag_name.as_ref().map_or((None, None), |tag| {
2407+
let (ns, stripped) = split_ns_name(tag.as_str());
2408+
(ns, Some(stripped))
2409+
});
2410+
let namespace = namespace_for_key(namespace_key);
2411+
2412+
// Compute fn_name_suffix with namespace prefix (e.g., `:svg:Conditional` for SVG)
2413+
let fn_name_suffix = {
2414+
let suffix = prefix_with_namespace("Conditional", namespace);
2415+
Atom::from(allocator.alloc_str(&suffix))
2416+
};
2417+
2418+
// Build the tag atom from the stripped tag name (without namespace prefix)
2419+
let tag = tag_name_without_namespace.map(|s| Atom::from(allocator.alloc_str(s)));
2420+
24032421
// Create the appropriate CREATE op
24042422
let create_op = if i == 0 {
24052423
// First branch uses ConditionalOp (ConditionalCreate)
24062424
CreateOp::Conditional(ConditionalOp {
24072425
base: CreateOpBase { source_span: Some(branch.source_span), ..Default::default() },
24082426
xref: branch_view_xref,
24092427
slot: None,
2410-
namespace: Namespace::Html,
2428+
namespace,
24112429
template_kind: TemplateKind::Block,
2412-
fn_name_suffix: Atom::from("Conditional"),
2413-
tag: tag_name.clone(),
2430+
fn_name_suffix: fn_name_suffix.clone(),
2431+
tag: tag.clone(),
24142432
decls: None,
24152433
vars: None,
24162434
local_refs: Vec::new_in(allocator),
@@ -2425,10 +2443,10 @@ fn ingest_if_block<'a>(
24252443
base: CreateOpBase { source_span: Some(branch.source_span), ..Default::default() },
24262444
xref: branch_view_xref,
24272445
slot: None,
2428-
namespace: Namespace::Html,
2446+
namespace,
24292447
template_kind: TemplateKind::Block,
2430-
fn_name_suffix: Atom::from("Conditional"),
2431-
tag: tag_name.clone(),
2448+
fn_name_suffix: fn_name_suffix.clone(),
2449+
tag: tag.clone(),
24322450
decls: None,
24332451
vars: None,
24342452
local_refs: Vec::new_in(allocator),
@@ -2949,17 +2967,35 @@ fn ingest_switch_block<'a>(
29492967
let tag_name =
29502968
ingest_control_flow_insertion_point(job, view_xref, case_view_xref, &case.children);
29512969

2970+
// Extract namespace from tag_name (e.g., `:svg:foreignObject` -> Namespace::Svg)
2971+
// Matches TypeScript's ingestSwitchBlock behavior for SVG/MathML namespace handling.
2972+
let (namespace_key, tag_name_without_namespace) =
2973+
tag_name.as_ref().map_or((None, None), |tag| {
2974+
let (ns, stripped) = split_ns_name(tag.as_str());
2975+
(ns, Some(stripped))
2976+
});
2977+
let namespace = namespace_for_key(namespace_key);
2978+
2979+
// Compute fn_name_suffix with namespace prefix (e.g., `:svg:Case` for SVG)
2980+
let fn_name_suffix = {
2981+
let suffix = prefix_with_namespace("Case", namespace);
2982+
Atom::from(allocator.alloc_str(&suffix))
2983+
};
2984+
2985+
// Build the tag atom from the stripped tag name (without namespace prefix)
2986+
let tag = tag_name_without_namespace.map(|s| Atom::from(allocator.alloc_str(s)));
2987+
29522988
// Create the appropriate CREATE op
29532989
let create_op = if i == 0 {
29542990
// First case uses ConditionalOp (ConditionalCreate)
29552991
CreateOp::Conditional(ConditionalOp {
29562992
base: CreateOpBase { source_span: Some(case.source_span), ..Default::default() },
29572993
xref: case_view_xref,
29582994
slot: None,
2959-
namespace: Namespace::Html,
2995+
namespace,
29602996
template_kind: TemplateKind::Block,
2961-
fn_name_suffix: Atom::from("Case"),
2962-
tag: tag_name,
2997+
fn_name_suffix: fn_name_suffix.clone(),
2998+
tag: tag.clone(),
29632999
decls: None,
29643000
vars: None,
29653001
local_refs: Vec::new_in(allocator),
@@ -2974,10 +3010,10 @@ fn ingest_switch_block<'a>(
29743010
base: CreateOpBase { source_span: Some(case.source_span), ..Default::default() },
29753011
xref: case_view_xref,
29763012
slot: None,
2977-
namespace: Namespace::Html,
3013+
namespace,
29783014
template_kind: TemplateKind::Block,
2979-
fn_name_suffix: Atom::from("Case"),
2980-
tag: tag_name,
3015+
fn_name_suffix: fn_name_suffix.clone(),
3016+
tag: tag.clone(),
29813017
decls: None,
29823018
vars: None,
29833019
local_refs: Vec::new_in(allocator),

crates/oxc_angular_compiler/src/pipeline/phases/const_collection.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,27 @@ fn serialize_attributes<'a>(
346346
}
347347
}
348348

349+
// Add ProjectAs marker and parsed CSS selector if ngProjectAs is present
350+
// This must come after the static attributes and before classes/styles/bindings
351+
// Reference: Angular's const_collection.ts lines 251-258
352+
if let Some(ref project_as) = attrs.project_as {
353+
// Parse the ngProjectAs value as a CSS selector
354+
// We only take the first selector (Angular doesn't support multiple selectors in ngProjectAs)
355+
let r3_selectors = parse_selector_to_r3_selector(project_as.as_str());
356+
if let Some(first_selector) = r3_selectors.first() {
357+
// Add the ProjectAs marker (value 5)
358+
elements.push(ConstValue::Number(AttributeMarker::ProjectAs as i32 as f64));
359+
360+
// Add the parsed selector as an array
361+
let selector_elements = r3_selector_to_output_expr(allocator, first_selector);
362+
let selector_array = OutputExpression::LiteralArray(Box::new_in(
363+
LiteralArrayExpr { entries: selector_elements, source_span: None },
364+
allocator,
365+
));
366+
elements.push(ConstValue::Expression(selector_array));
367+
}
368+
}
369+
349370
// Add classes marker and class names
350371
if !attrs.classes.is_empty() {
351372
elements.push(ConstValue::Number(AttributeMarker::Classes as i32 as f64));

crates/oxc_angular_compiler/src/pipeline/phases/naming.rs

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,7 @@ fn collect_child_views_with_indices<'a>(
513513
}
514514

515515
/// Process create ops in a view with interleaved child view recursion.
516-
/// This matches TypeScript's unit.ops() iteration order where child views
517-
/// are processed immediately when encountered.
516+
/// This matches TypeScript's unit.ops() iteration order.
518517
///
519518
/// TypeScript's ops() generator yields ops in this order:
520519
/// 1. Create op
@@ -525,16 +524,13 @@ fn collect_child_views_with_indices<'a>(
525524
///
526525
/// The naming phase switch handles ops as they come:
527526
/// - For Listener ops: names the handler function, then handler_ops (Variable) are processed
528-
/// - For RepeaterCreate: recurses into child views FIRST, then track_by_ops are yielded
529-
///
530-
/// So the key insight is:
531-
/// - For RepeaterCreate, track_by_ops are processed AFTER child view recursion
532-
/// - For Listeners, handler_ops can be processed inline (no child view recursion)
527+
/// - For RepeaterCreate: track_by_ops are yielded inline (processed as Variable ops),
528+
/// then the switch case recurses into child views
533529
///
534530
/// The order for a RepeaterCreate at index N is:
535531
/// 1. Process RepeaterCreate op (without track_by_ops)
536-
/// 2. Recurse into child views (empty view, then body view)
537-
/// 3. Process track_by_ops
532+
/// 2. Process track_by_ops (yielded inline by the generator)
533+
/// 3. Recurse into child views (empty view, then body view)
538534
/// 4. Move to next create op at index N+1
539535
#[allow(clippy::too_many_arguments)]
540536
fn process_create_ops_with_child_recursion<'a>(
@@ -605,25 +601,11 @@ fn process_create_ops_with_child_recursion<'a>(
605601
}
606602
} // Borrow of job ends here
607603

608-
// After processing this create op, immediately recurse into any child views at this index
609-
// This is the key to depth-first processing!
610-
if let Some(children) = child_views_by_index.remove(&index) {
611-
for (child_xref, child_base_name) in children {
612-
add_names_to_child_view(
613-
job,
614-
child_xref,
615-
&child_base_name,
616-
allocator,
617-
state,
618-
var_names,
619-
semantic_var_names,
620-
);
621-
}
622-
}
623-
624-
// Process track_by_ops IMMEDIATELY after child view recursion for this RepeaterCreate
625-
// This matches TypeScript's generator behavior where track_by_ops are yielded
626-
// right after the RepeaterCreate op is handled (which includes child view recursion)
604+
// Process track_by_ops IMMEDIATELY after the create op, BEFORE child view recursion.
605+
// This matches Angular's ops() generator which yields track_by_ops inline with the
606+
// RepeaterCreate op. The switch statement in naming.ts processes the RepeaterCreate,
607+
// then the generator yields track_by_ops (processed as Variable ops), and only then
608+
// does the iteration continue to child views.
627609
if is_repeater_with_track_by {
628610
let create_ops = match view_xref {
629611
None => &mut job.root.create,
@@ -652,6 +634,22 @@ fn process_create_ops_with_child_recursion<'a>(
652634
}
653635
}
654636
}
637+
638+
// After processing the create op and its track_by_ops, recurse into any child views
639+
// at this index. This is the key to depth-first processing!
640+
if let Some(children) = child_views_by_index.remove(&index) {
641+
for (child_xref, child_base_name) in children {
642+
add_names_to_child_view(
643+
job,
644+
child_xref,
645+
&child_base_name,
646+
allocator,
647+
state,
648+
var_names,
649+
semantic_var_names,
650+
);
651+
}
652+
}
655653
}
656654
}
657655

@@ -867,10 +865,9 @@ fn process_single_create_op_ref<'a>(
867865
}
868866
}
869867
CreateOp::RepeaterCreate(_) => {
870-
// NOTE: track_by_ops are processed AFTER child view recursion
871-
// in process_create_ops_with_child_recursion, matching TypeScript's
872-
// generator behavior where track_by_ops are yielded after the
873-
// RepeaterCreate op is handled (which includes child view recursion)
868+
// NOTE: track_by_ops are processed in process_create_ops_with_child_recursion,
869+
// BEFORE child view recursion. This matches Angular's ops() generator which
870+
// yields track_by_ops inline with the RepeaterCreate op.
874871
}
875872
CreateOp::Variable(var_op) => {
876873
name_create_variable_op(var_op, allocator, state, var_names, semantic_var_names);

0 commit comments

Comments
 (0)