Skip to content

Commit 8d5b53b

Browse files
Brooooooklynclaude
andauthored
fix: linker converts hostDirectives to ɵɵHostDirectivesFeature in features array (#78)
The linker was emitting `hostDirectives` as a direct property on `defineDirective`/`defineComponent`, but the Angular runtime only processes it via `ɵɵHostDirectivesFeature` in the `features` array. This caused host directives to be silently dropped at runtime. - Fix: #71 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d95c236 commit 8d5b53b

1 file changed

Lines changed: 121 additions & 13 deletions

File tree

  • crates/oxc_angular_compiler/src/linker

crates/oxc_angular_compiler/src/linker/mod.rs

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,9 +1047,6 @@ fn link_directive(
10471047
let standalone = get_bool_property(meta, "isStandalone").unwrap_or(true);
10481048
parts.push(format!("standalone: {standalone}"));
10491049

1050-
if let Some(host_directives) = get_property_source(meta, "hostDirectives", source) {
1051-
parts.push(format!("hostDirectives: {host_directives}"));
1052-
}
10531050
if let Some(features) = build_features(meta, source, ns) {
10541051
parts.push(format!("features: {features}"));
10551052
}
@@ -1283,11 +1280,12 @@ fn build_queries(
12831280
/// Build the features array from component metadata.
12841281
///
12851282
/// Examines boolean flags and providers to build the features array:
1283+
/// - `providers: [...]` → `ns.ɵɵProvidersFeature([...])`
1284+
/// - `hostDirectives: [...]` → `ns.ɵɵHostDirectivesFeature([...])`
12861285
/// - `usesInheritance: true` → `ns.ɵɵInheritDefinitionFeature`
12871286
/// - `usesOnChanges: true` → `ns.ɵɵNgOnChangesFeature`
1288-
/// - `providers: [...]` → `ns.ɵɵProvidersFeature([...])`
1289-
/// Order is important: ProvidersFeature → InheritDefinitionFeature → NgOnChangesFeature
1290-
/// (see definition.rs line 990 and packages/compiler/src/render3/view/compiler.ts:119-161)
1287+
/// Order is important: ProvidersFeature → HostDirectivesFeature → InheritDefinitionFeature → NgOnChangesFeature
1288+
/// (see packages/compiler/src/render3/view/compiler.ts:119-161)
12911289
fn build_features(meta: &ObjectExpression<'_>, source: &str, ns: &str) -> Option<String> {
12921290
let mut features: Vec<String> = Vec::new();
12931291

@@ -1307,12 +1305,17 @@ fn build_features(meta: &ObjectExpression<'_>, source: &str, ns: &str) -> Option
13071305
(None, None) => {}
13081306
}
13091307

1310-
// 2. InheritDefinitionFeature
1308+
// 2. HostDirectivesFeature — must come before InheritDefinitionFeature
1309+
if let Some(host_directives) = get_property_source(meta, "hostDirectives", source) {
1310+
features.push(format!("{ns}.\u{0275}\u{0275}HostDirectivesFeature({host_directives})"));
1311+
}
1312+
1313+
// 3. InheritDefinitionFeature
13111314
if get_bool_property(meta, "usesInheritance") == Some(true) {
13121315
features.push(format!("{ns}.\u{0275}\u{0275}InheritDefinitionFeature"));
13131316
}
13141317

1315-
// 3. NgOnChangesFeature
1318+
// 4. NgOnChangesFeature
13161319
if get_bool_property(meta, "usesOnChanges") == Some(true) {
13171320
features.push(format!("{ns}.\u{0275}\u{0275}NgOnChangesFeature"));
13181321
}
@@ -1435,11 +1438,6 @@ fn link_component(
14351438
let standalone = get_bool_property(meta, "isStandalone").unwrap_or(true);
14361439
parts.push(format!("standalone: {standalone}"));
14371440

1438-
// 11b. hostDirectives (Directive Composition API)
1439-
if let Some(host_directives) = get_property_source(meta, "hostDirectives", source) {
1440-
parts.push(format!("hostDirectives: {host_directives}"));
1441-
}
1442-
14431441
// 12. features
14441442
if let Some(features) = build_features(meta, source, ns) {
14451443
parts.push(format!("features: {features}"));
@@ -2177,4 +2175,114 @@ MyDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "
21772175
result.code
21782176
);
21792177
}
2178+
2179+
/// Issue #71: hostDirectives must be converted to ɵɵHostDirectivesFeature in features array
2180+
/// instead of being emitted as a direct property.
2181+
#[test]
2182+
fn test_link_directive_with_host_directives() {
2183+
let allocator = Allocator::default();
2184+
let code = r#"
2185+
import * as i0 from "@angular/core";
2186+
class BrnContextMenuTrigger {
2187+
}
2188+
BrnContextMenuTrigger.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: BrnContextMenuTrigger, selector: "[brnCtxMenuTriggerFor]", isStandalone: true, hostDirectives: [{ directive: CdkContextMenuTrigger }] });
2189+
"#;
2190+
let result = link(&allocator, code, "test.mjs");
2191+
assert!(result.linked);
2192+
// Must have HostDirectivesFeature in the features array
2193+
assert!(
2194+
result.code.contains("HostDirectivesFeature"),
2195+
"Should have HostDirectivesFeature in features array, got:\n{}",
2196+
result.code
2197+
);
2198+
// Must NOT have hostDirectives as a direct property
2199+
assert!(
2200+
!result.code.contains("hostDirectives:"),
2201+
"Should NOT have hostDirectives as a direct property, got:\n{}",
2202+
result.code
2203+
);
2204+
}
2205+
2206+
/// Issue #71: hostDirectives with input/output mappings on a directive
2207+
#[test]
2208+
fn test_link_directive_with_host_directives_mappings() {
2209+
let allocator = Allocator::default();
2210+
let code = r#"
2211+
import * as i0 from "@angular/core";
2212+
class UnityTooltipTrigger {
2213+
}
2214+
UnityTooltipTrigger.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: UnityTooltipTrigger, selector: "[uTooltip]", isStandalone: true, hostDirectives: [{ directive: BrnTooltipTrigger, inputs: ["brnTooltipTrigger", "uTooltip"], outputs: ["onHide", "tooltipHidden"] }] });
2215+
"#;
2216+
let result = link(&allocator, code, "test.mjs");
2217+
assert!(result.linked);
2218+
assert!(
2219+
result.code.contains("HostDirectivesFeature"),
2220+
"Should have HostDirectivesFeature, got:\n{}",
2221+
result.code
2222+
);
2223+
assert!(
2224+
!result.code.contains("hostDirectives:"),
2225+
"Should NOT have hostDirectives as a direct property, got:\n{}",
2226+
result.code
2227+
);
2228+
}
2229+
2230+
/// Issue #71: hostDirectives on a component must go to HostDirectivesFeature
2231+
#[test]
2232+
fn test_link_component_with_host_directives() {
2233+
let allocator = Allocator::default();
2234+
let code = r#"
2235+
import * as i0 from "@angular/core";
2236+
class BrnMenu {
2237+
}
2238+
BrnMenu.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: BrnMenu, selector: "[brnMenu]", isStandalone: true, hostDirectives: [{ directive: CdkMenu }], template: "<ng-content></ng-content>" });
2239+
"#;
2240+
let result = link(&allocator, code, "test.mjs");
2241+
assert!(result.linked);
2242+
assert!(
2243+
result.code.contains("HostDirectivesFeature"),
2244+
"Should have HostDirectivesFeature in features array, got:\n{}",
2245+
result.code
2246+
);
2247+
assert!(
2248+
!result.code.contains("hostDirectives:"),
2249+
"Should NOT have hostDirectives as a direct property, got:\n{}",
2250+
result.code
2251+
);
2252+
}
2253+
2254+
/// Issue #71: Feature ordering — HostDirectivesFeature must come after ProvidersFeature
2255+
/// and before InheritDefinitionFeature
2256+
#[test]
2257+
fn test_features_order_with_host_directives() {
2258+
let allocator = Allocator::default();
2259+
let code = r#"
2260+
import * as i0 from "@angular/core";
2261+
class MyComp {
2262+
}
2263+
MyComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: MyComp, selector: "my-comp", providers: [SomeProvider], hostDirectives: [{ directive: SomeDirective }], usesInheritance: true, usesOnChanges: true, template: "<div></div>" });
2264+
"#;
2265+
let result = link(&allocator, code, "test.mjs");
2266+
assert!(result.linked);
2267+
let code = &result.code;
2268+
let providers_pos = code.find("ProvidersFeature").expect("should have ProvidersFeature");
2269+
let host_dir_pos =
2270+
code.find("HostDirectivesFeature").expect("should have HostDirectivesFeature");
2271+
let inherit_pos =
2272+
code.find("InheritDefinitionFeature").expect("should have InheritDefinitionFeature");
2273+
let on_changes_pos =
2274+
code.find("NgOnChangesFeature").expect("should have NgOnChangesFeature");
2275+
assert!(
2276+
providers_pos < host_dir_pos,
2277+
"ProvidersFeature must come before HostDirectivesFeature"
2278+
);
2279+
assert!(
2280+
host_dir_pos < inherit_pos,
2281+
"HostDirectivesFeature must come before InheritDefinitionFeature"
2282+
);
2283+
assert!(
2284+
inherit_pos < on_changes_pos,
2285+
"InheritDefinitionFeature must come before NgOnChangesFeature"
2286+
);
2287+
}
21802288
}

0 commit comments

Comments
 (0)