Skip to content

Commit fb4b6ad

Browse files
Brooooooklynclaude
andauthored
fix: linker handles old-style directives/components/pipes fields for pre-v14 libraries (#92)
The linker only read the new-style `dependencies` field (v14+), ignoring the old-style `directives`, `components`, and `pipes` fields used by Angular v12–v13 libraries. This caused the dependencies array to be completely missing from linked output for those libraries. - Close #88 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ed0568b commit fb4b6ad

File tree

1 file changed

+147
-27
lines changed
  • crates/oxc_angular_compiler/src/linker

1 file changed

+147
-27
lines changed

crates/oxc_angular_compiler/src/linker/mod.rs

Lines changed: 147 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,29 +1302,6 @@ fn extract_host_metadata_input(
13021302
input
13031303
}
13041304

1305-
/// In the defineComponent output, we just need the type references:
1306-
/// ```javascript
1307-
/// dependencies: [RouterOutlet]
1308-
/// ```
1309-
fn extract_dependency_types(
1310-
arr: &oxc_ast::ast::ArrayExpression<'_>,
1311-
source: &str,
1312-
) -> Option<String> {
1313-
let mut types: Vec<String> = Vec::new();
1314-
for el in &arr.elements {
1315-
let expr = match el {
1316-
ArrayExpressionElement::SpreadElement(_) => continue,
1317-
_ => el.to_expression(),
1318-
};
1319-
if let Expression::ObjectExpression(obj) = expr
1320-
&& let Some(type_src) = get_property_source(obj.as_ref(), "type", source)
1321-
{
1322-
types.push(type_src.to_string());
1323-
}
1324-
}
1325-
if types.is_empty() { None } else { Some(format!("[{}]", types.join(", "))) }
1326-
}
1327-
13281305
/// Build a query function (contentQueries or viewQuery) from query metadata.
13291306
///
13301307
/// Content query metadata format:
@@ -1634,11 +1611,73 @@ fn link_component(
16341611
// 17. template (reference to the compiled function)
16351612
parts.push(format!("template: {}", template_output.template_fn_name));
16361613

1637-
// 18. dependencies (extract type references from dependency objects)
1638-
if let Some(deps_arr) = get_array_property(meta, "dependencies")
1639-
&& let Some(deps_str) = extract_dependency_types(deps_arr, source)
1614+
// 18. dependencies — support both new-style (v14+) and old-style (v12-v13) fields
16401615
{
1641-
parts.push(format!("dependencies: {deps_str}"));
1616+
let capacity = get_array_property(meta, "dependencies").map_or(0, |a| a.elements.len())
1617+
+ get_array_property(meta, "directives").map_or(0, |a| a.elements.len())
1618+
+ get_array_property(meta, "components").map_or(0, |a| a.elements.len())
1619+
+ get_object_property(meta, "pipes").map_or(0, |o| o.properties.len());
1620+
let mut dep_types: Vec<String> = Vec::with_capacity(capacity);
1621+
1622+
// New style: unified `dependencies` array (v14+)
1623+
if let Some(deps_arr) = get_array_property(meta, "dependencies") {
1624+
for el in &deps_arr.elements {
1625+
let expr = match el {
1626+
ArrayExpressionElement::SpreadElement(_) => continue,
1627+
_ => el.to_expression(),
1628+
};
1629+
if let Expression::ObjectExpression(obj) = expr
1630+
&& let Some(type_src) = get_property_source(obj.as_ref(), "type", source)
1631+
{
1632+
dep_types.push(type_src.to_string());
1633+
}
1634+
}
1635+
}
1636+
1637+
// Old style: separate `directives` array (v12-v13)
1638+
if let Some(dirs_arr) = get_array_property(meta, "directives") {
1639+
for el in &dirs_arr.elements {
1640+
let expr = match el {
1641+
ArrayExpressionElement::SpreadElement(_) => continue,
1642+
_ => el.to_expression(),
1643+
};
1644+
if let Expression::ObjectExpression(obj) = expr
1645+
&& let Some(type_src) = get_property_source(obj.as_ref(), "type", source)
1646+
{
1647+
dep_types.push(type_src.to_string());
1648+
}
1649+
}
1650+
}
1651+
1652+
// Old style: separate `components` array (v12-v13)
1653+
if let Some(comps_arr) = get_array_property(meta, "components") {
1654+
for el in &comps_arr.elements {
1655+
let expr = match el {
1656+
ArrayExpressionElement::SpreadElement(_) => continue,
1657+
_ => el.to_expression(),
1658+
};
1659+
if let Expression::ObjectExpression(obj) = expr
1660+
&& let Some(type_src) = get_property_source(obj.as_ref(), "type", source)
1661+
{
1662+
dep_types.push(type_src.to_string());
1663+
}
1664+
}
1665+
}
1666+
1667+
// Old style: `pipes` object { pipeName: PipeType, ... } (v12-v13)
1668+
if let Some(pipes_obj) = get_object_property(meta, "pipes") {
1669+
for prop in &pipes_obj.properties {
1670+
if let ObjectPropertyKind::ObjectProperty(p) = prop {
1671+
let span = p.value.span();
1672+
let type_src = &source[span.start as usize..span.end as usize];
1673+
dep_types.push(type_src.to_string());
1674+
}
1675+
}
1676+
}
1677+
1678+
if !dep_types.is_empty() {
1679+
parts.push(format!("dependencies: [{}]", dep_types.join(", ")));
1680+
}
16421681
}
16431682

16441683
// 19-20. styles + encapsulation (interdependent)
@@ -3536,4 +3575,85 @@ MyService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
35363575
result.code
35373576
);
35383577
}
3578+
3579+
/// Issue #88: Old-style `directives` field (v12-v13) should be extracted into `dependencies`
3580+
#[test]
3581+
fn test_link_component_v12_with_old_style_directives() {
3582+
let allocator = Allocator::default();
3583+
let code = r#"
3584+
import * as i0 from "@angular/core";
3585+
import * as i1 from "@angular/common";
3586+
class OverlayPanel {
3587+
}
3588+
OverlayPanel.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: OverlayPanel, selector: "p-overlayPanel", template: "<div *ngIf=\"render\">Hello</div>", directives: [{ type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
3589+
"#;
3590+
let result = link(&allocator, code, "test.mjs");
3591+
assert!(result.linked, "Should be linked");
3592+
assert!(
3593+
result.code.contains("dependencies: [i1.NgIf, i1.NgClass]"),
3594+
"Should extract directive types into dependencies array, got:\n{}",
3595+
result.code
3596+
);
3597+
}
3598+
3599+
/// Issue #88: Old-style `components` field (v12-v13) should be extracted into `dependencies`
3600+
#[test]
3601+
fn test_link_component_v12_with_old_style_components() {
3602+
let allocator = Allocator::default();
3603+
let code = r#"
3604+
import * as i0 from "@angular/core";
3605+
import * as i1 from "./child";
3606+
class ParentComponent {
3607+
}
3608+
ParentComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: ParentComponent, selector: "app-parent", template: "<my-child></my-child>", components: [{ type: i1.ChildComponent, selector: "my-child" }] });
3609+
"#;
3610+
let result = link(&allocator, code, "test.mjs");
3611+
assert!(result.linked, "Should be linked");
3612+
assert!(
3613+
result.code.contains("dependencies: [i1.ChildComponent]"),
3614+
"Should extract component types into dependencies array, got:\n{}",
3615+
result.code
3616+
);
3617+
}
3618+
3619+
/// Issue #88: Old-style `pipes` object (v12-v13) should be extracted into `dependencies`
3620+
#[test]
3621+
fn test_link_component_v12_with_old_style_pipes() {
3622+
let allocator = Allocator::default();
3623+
let code = r#"
3624+
import * as i0 from "@angular/core";
3625+
import * as i1 from "@angular/common";
3626+
class MyComponent {
3627+
}
3628+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: MyComponent, selector: "my-comp", template: "<div>{{ value | async }}</div>", pipes: { async: i1.AsyncPipe } });
3629+
"#;
3630+
let result = link(&allocator, code, "test.mjs");
3631+
assert!(result.linked, "Should be linked");
3632+
assert!(
3633+
result.code.contains("dependencies: [i1.AsyncPipe]"),
3634+
"Should extract pipe types into dependencies array, got:\n{}",
3635+
result.code
3636+
);
3637+
}
3638+
3639+
/// Issue #88: Mixed old-style fields — directives + components + pipes combined
3640+
#[test]
3641+
fn test_link_component_v12_with_mixed_old_style_deps() {
3642+
let allocator = Allocator::default();
3643+
let code = r#"
3644+
import * as i0 from "@angular/core";
3645+
import * as i1 from "@angular/common";
3646+
import * as i2 from "./child";
3647+
class MyComponent {
3648+
}
3649+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: MyComponent, selector: "my-comp", template: "<div *ngIf=\"x\"><my-child></my-child>{{ val | async }}</div>", directives: [{ type: i1.NgIf, selector: "[ngIf]" }], components: [{ type: i2.ChildComponent, selector: "my-child" }], pipes: { async: i1.AsyncPipe } });
3650+
"#;
3651+
let result = link(&allocator, code, "test.mjs");
3652+
assert!(result.linked, "Should be linked");
3653+
assert!(
3654+
result.code.contains("dependencies: [i1.NgIf, i2.ChildComponent, i1.AsyncPipe]"),
3655+
"Should extract all dependency types into dependencies array, got:\n{}",
3656+
result.code
3657+
);
3658+
}
35393659
}

0 commit comments

Comments
 (0)