Skip to content

Commit 5d357ca

Browse files
fix(compiler): correct host directive mapping array order (#152)
1 parent b60a032 commit 5d357ca

File tree

5 files changed

+214
-11
lines changed

5 files changed

+214
-11
lines changed

crates/oxc_angular_compiler/src/component/definition.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,7 +1201,7 @@ fn create_host_directives_arg<'a>(
12011201
quoted: false,
12021202
});
12031203

1204-
// inputs: ['publicName', 'internalName', ...]
1204+
// inputs: ['internalName', 'publicName', ...]
12051205
if !directive.inputs.is_empty() {
12061206
let inputs_array =
12071207
create_host_directive_mappings_array(allocator, &directive.inputs);
@@ -1212,7 +1212,7 @@ fn create_host_directives_arg<'a>(
12121212
});
12131213
}
12141214

1215-
// outputs: ['publicName', 'internalName', ...]
1215+
// outputs: ['internalName', 'publicName', ...]
12161216
if !directive.outputs.is_empty() {
12171217
let outputs_array =
12181218
create_host_directive_mappings_array(allocator, &directive.outputs);

crates/oxc_angular_compiler/src/directive/compiler.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ fn create_host_directives_feature_arg<'a>(
915915

916916
/// Creates a host directive mappings array.
917917
///
918-
/// Format: `['publicName', 'internalName', 'publicName2', 'internalName2']`
918+
/// Format: `['internalName', 'publicName', 'internalName2', 'publicName2']`
919919
///
920920
/// Shared between directive and component compilers, mirroring Angular's
921921
/// `createHostDirectivesMappingArray` in `view/compiler.ts`.
@@ -927,11 +927,11 @@ pub(crate) fn create_host_directive_mappings_array<'a>(
927927

928928
for (public_name, internal_name) in mappings {
929929
entries.push(OutputExpression::Literal(Box::new_in(
930-
LiteralExpr { value: LiteralValue::String(public_name.clone()), source_span: None },
930+
LiteralExpr { value: LiteralValue::String(internal_name.clone()), source_span: None },
931931
allocator,
932932
)));
933933
entries.push(OutputExpression::Literal(Box::new_in(
934-
LiteralExpr { value: LiteralValue::String(internal_name.clone()), source_span: None },
934+
LiteralExpr { value: LiteralValue::String(public_name.clone()), source_span: None },
935935
allocator,
936936
)));
937937
}
@@ -1476,10 +1476,11 @@ mod tests {
14761476
let output = emitter.emit_expression(&result.expression);
14771477
let normalized = output.replace([' ', '\n', '\t'], "");
14781478

1479-
// Must contain flat array format: inputs:["uTooltip","brnTooltipTrigger"]
1479+
// Must contain flat array format: inputs:["brnTooltipTrigger","uTooltip"]
1480+
// (internalName first, then publicName — matching Angular's createHostDirectivesMappingArray)
14801481
assert!(
1481-
normalized.contains(r#"inputs:["uTooltip","brnTooltipTrigger"]"#),
1482-
"Host directive inputs should be flat array [\"publicName\",\"internalName\"], not object. Got:\n{}",
1482+
normalized.contains(r#"inputs:["brnTooltipTrigger","uTooltip"]"#),
1483+
"Host directive inputs should be flat array [\"internalName\",\"publicName\"]. Got:\n{}",
14831484
output
14841485
);
14851486
// Must NOT contain object format: inputs:{uTooltip:"brnTooltipTrigger"}
@@ -1540,10 +1541,11 @@ mod tests {
15401541
let output = emitter.emit_expression(&result.expression);
15411542
let normalized = output.replace([' ', '\n', '\t'], "");
15421543

1543-
// Must contain flat array format: outputs:["clicked","trackClick"]
1544+
// Must contain flat array format: outputs:["trackClick","clicked"]
1545+
// (internalName first, then publicName — matching Angular's createHostDirectivesMappingArray)
15441546
assert!(
1545-
normalized.contains(r#"outputs:["clicked","trackClick"]"#),
1546-
"Host directive outputs should be flat array. Got:\n{}",
1547+
normalized.contains(r#"outputs:["trackClick","clicked"]"#),
1548+
"Host directive outputs should be flat array [\"internalName\",\"publicName\"]. Got:\n{}",
15471549
output
15481550
);
15491551
}

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7833,3 +7833,131 @@ fn test_property_singleton_interpolation_with_sanitizer_angular_v19() {
78337833
assert!(js.contains("ɵɵsanitizeUrl"), "Should include ɵɵsanitizeUrl sanitizer. Got:\n{js}");
78347834
insta::assert_snapshot!("property_singleton_interpolation_with_sanitizer_v19", js);
78357835
}
7836+
7837+
// ============================================================================
7838+
// Host Directive Alias Tests
7839+
// ============================================================================
7840+
7841+
/// Test host directives with simple aliased inputs/outputs.
7842+
///
7843+
/// Mirrors the compliance test `host_directives_with_inputs_outputs.ts`.
7844+
/// The mapping array must use `[internalName, publicName]` ordering.
7845+
#[test]
7846+
fn test_host_directives_with_inputs_outputs() {
7847+
let allocator = Allocator::default();
7848+
let source = r#"
7849+
import { Component, Directive, EventEmitter, Input, Output } from '@angular/core';
7850+
7851+
@Directive({})
7852+
export class HostDir {
7853+
@Input() value = 0;
7854+
@Input() color = '';
7855+
@Output() opened = new EventEmitter();
7856+
@Output() closed = new EventEmitter();
7857+
}
7858+
7859+
@Component({
7860+
selector: 'my-component',
7861+
template: '',
7862+
hostDirectives: [{
7863+
directive: HostDir,
7864+
inputs: ['value', 'color: colorAlias'],
7865+
outputs: ['opened', 'closed: closedAlias'],
7866+
}],
7867+
standalone: false,
7868+
})
7869+
export class MyComponent {
7870+
}
7871+
"#;
7872+
7873+
let result = transform_angular_file(
7874+
&allocator,
7875+
"test.component.ts",
7876+
source,
7877+
&ComponentTransformOptions::default(),
7878+
None,
7879+
);
7880+
7881+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
7882+
7883+
let normalized = result.code.replace([' ', '\n', '\t'], "");
7884+
7885+
// Input mappings: 'value' (no alias) → ["value", "value"], 'color: colorAlias' → ["color", "colorAlias"]
7886+
// The array must be [internalName, publicName, ...] i.e. ["value", "value", "color", "colorAlias"]
7887+
assert!(
7888+
normalized.contains(r#"inputs:["value","value","color","colorAlias"]"#),
7889+
"Input mappings should be [internalName, publicName]. Got:\n{}",
7890+
result.code
7891+
);
7892+
7893+
// Output mappings: 'opened' → ["opened", "opened"], 'closed: closedAlias' → ["closed", "closedAlias"]
7894+
assert!(
7895+
normalized.contains(r#"outputs:["opened","opened","closed","closedAlias"]"#),
7896+
"Output mappings should be [internalName, publicName]. Got:\n{}",
7897+
result.code
7898+
);
7899+
7900+
insta::assert_snapshot!("host_directives_with_inputs_outputs", result.code);
7901+
}
7902+
7903+
/// Test host directives where the directive has `@Input('alias')` and the host re-aliases.
7904+
///
7905+
/// Mirrors the compliance test `host_directives_with_host_aliases.ts`.
7906+
#[test]
7907+
fn test_host_directives_with_host_aliases() {
7908+
let allocator = Allocator::default();
7909+
let source = r#"
7910+
import { Component, Directive, EventEmitter, Input, Output } from '@angular/core';
7911+
7912+
@Directive({})
7913+
export class HostDir {
7914+
@Input('valueAlias') value = 1;
7915+
@Input('colorAlias') color = '';
7916+
@Output('openedAlias') opened = new EventEmitter();
7917+
@Output('closedAlias') closed = new EventEmitter();
7918+
}
7919+
7920+
@Component({
7921+
selector: 'my-component',
7922+
template: '',
7923+
hostDirectives: [{
7924+
directive: HostDir,
7925+
inputs: ['valueAlias', 'colorAlias: customColorAlias'],
7926+
outputs: ['openedAlias', 'closedAlias: customClosedAlias'],
7927+
}],
7928+
standalone: false,
7929+
})
7930+
export class MyComponent {
7931+
}
7932+
"#;
7933+
7934+
let result = transform_angular_file(
7935+
&allocator,
7936+
"test.component.ts",
7937+
source,
7938+
&ComponentTransformOptions::default(),
7939+
None,
7940+
);
7941+
7942+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
7943+
7944+
let normalized = result.code.replace([' ', '\n', '\t'], "");
7945+
7946+
// Input mappings: 'valueAlias' → ["valueAlias", "valueAlias"], 'colorAlias: customColorAlias' → ["colorAlias", "customColorAlias"]
7947+
assert!(
7948+
normalized
7949+
.contains(r#"inputs:["valueAlias","valueAlias","colorAlias","customColorAlias"]"#),
7950+
"Input mappings should be [internalName, publicName]. Got:\n{}",
7951+
result.code
7952+
);
7953+
7954+
// Output mappings: 'openedAlias' → ["openedAlias", "openedAlias"], 'closedAlias: customClosedAlias' → ["closedAlias", "customClosedAlias"]
7955+
assert!(
7956+
normalized
7957+
.contains(r#"outputs:["openedAlias","openedAlias","closedAlias","customClosedAlias"]"#),
7958+
"Output mappings should be [internalName, publicName]. Got:\n{}",
7959+
result.code
7960+
);
7961+
7962+
insta::assert_snapshot!("host_directives_with_host_aliases", result.code);
7963+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
source: crates/oxc_angular_compiler/tests/integration_test.rs
3+
expression: result.code
4+
---
5+
6+
import { Component, Directive, EventEmitter, Input, Output } from '@angular/core';
7+
import * as i0 from '@angular/core';
8+
9+
export class HostDir {
10+
value = 1;
11+
color = '';
12+
opened = new EventEmitter();
13+
closed = new EventEmitter();
14+
15+
static ɵfac = function HostDir_Factory(__ngFactoryType__) {
16+
return new (__ngFactoryType__ || HostDir)();
17+
};
18+
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({type:HostDir,inputs:{value:[0,"valueAlias","value"],
19+
color:[0,"colorAlias","color"]},outputs:{opened:"openedAlias",closed:"closedAlias"}});
20+
}
21+
22+
export class MyComponent {
23+
24+
static ɵfac = function MyComponent_Factory(__ngFactoryType__) {
25+
return new (__ngFactoryType__ || MyComponent)();
26+
};
27+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:MyComponent,selectors:[["my-component"]],
28+
standalone:false,features:[i0.ɵɵHostDirectivesFeature([{directive:HostDir,inputs:["valueAlias",
29+
"valueAlias","colorAlias","customColorAlias"],outputs:["openedAlias","openedAlias",
30+
"closedAlias","customClosedAlias"]}])],decls:0,vars:0,template:function MyComponent_Template(rf,
31+
ctx) {
32+
},dependencies:i0.ɵɵgetComponentDepsFactory(MyComponent),encapsulation:2});
33+
}
34+
(() =>{
35+
(((typeof ngDevMode === "undefined") || ngDevMode) && i0setClassDebugInfo(MyComponent,
36+
{className:"MyComponent",filePath:"test.component.ts",lineNumber:1}));
37+
})();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: crates/oxc_angular_compiler/tests/integration_test.rs
3+
expression: result.code
4+
---
5+
6+
import { Component, Directive, EventEmitter, Input, Output } from '@angular/core';
7+
import * as i0 from '@angular/core';
8+
9+
export class HostDir {
10+
value = 0;
11+
color = '';
12+
opened = new EventEmitter();
13+
closed = new EventEmitter();
14+
15+
static ɵfac = function HostDir_Factory(__ngFactoryType__) {
16+
return new (__ngFactoryType__ || HostDir)();
17+
};
18+
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({type:HostDir,inputs:{value:"value",color:"color"},
19+
outputs:{opened:"opened",closed:"closed"}});
20+
}
21+
22+
export class MyComponent {
23+
24+
static ɵfac = function MyComponent_Factory(__ngFactoryType__) {
25+
return new (__ngFactoryType__ || MyComponent)();
26+
};
27+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({type:MyComponent,selectors:[["my-component"]],
28+
standalone:false,features:[i0.ɵɵHostDirectivesFeature([{directive:HostDir,inputs:["value",
29+
"value","color","colorAlias"],outputs:["opened","opened","closed","closedAlias"]}])],
30+
decls:0,vars:0,template:function MyComponent_Template(rf,ctx) {
31+
},dependencies:i0.ɵɵgetComponentDepsFactory(MyComponent),encapsulation:2});
32+
}
33+
(() =>{
34+
(((typeof ngDevMode === "undefined") || ngDevMode) && i0setClassDebugInfo(MyComponent,
35+
{className:"MyComponent",filePath:"test.component.ts",lineNumber:1}));
36+
})();

0 commit comments

Comments
 (0)