Skip to content

Commit 53ac26f

Browse files
Brooooooklynclaude
andauthored
fix: linker emits unique constant names when component has both ng-content and host styles (#64)
The linker's host binding compilation now continues from the template's pool index instead of restarting at 0, preventing duplicate `_c0` declarations when a component has both `<ng-content>` and host style bindings. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 872f726 commit 53ac26f

File tree

2 files changed

+81
-7
lines changed

2 files changed

+81
-7
lines changed

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1989,12 +1989,14 @@ pub fn compile_template_to_js_with_options<'a>(
19891989
}
19901990

19911991
// Stage 6: Compile host bindings if provided via options
1992+
let host_pool_starting_index = job.pool.next_name_index();
19921993
if let Some(ref host_input) = options.host {
19931994
if let Some(host_result) = compile_host_bindings_from_input(
19941995
allocator,
19951996
host_input,
19961997
component_name,
19971998
options.selector.as_deref(),
1999+
host_pool_starting_index,
19982000
) {
19992001
// Add host binding pool declarations (pure functions, etc.)
20002002
for decl in host_result.declarations {
@@ -2556,6 +2558,7 @@ fn compile_host_bindings_from_input<'a>(
25562558
host_input: &HostMetadataInput,
25572559
component_name: &str,
25582560
selector: Option<&str>,
2561+
pool_starting_index: u32,
25592562
) -> Option<HostBindingCompilationResult<'a>> {
25602563
use oxc_allocator::FromIn;
25612564

@@ -2576,10 +2579,9 @@ fn compile_host_bindings_from_input<'a>(
25762579
selector.map(|s| Atom::from_in(s, allocator)).unwrap_or_else(|| Atom::from(""));
25772580

25782581
// Convert to HostBindingInput and compile
2579-
// Use 0 as starting index since this is standalone compilation (not part of a larger file)
25802582
let input =
25812583
convert_host_metadata_to_input(allocator, &host, component_name_atom, component_selector);
2582-
let mut job = ingest_host_binding(allocator, input, 0);
2584+
let mut job = ingest_host_binding(allocator, input, pool_starting_index);
25832585
let result = compile_host_bindings(&mut job);
25842586

25852587
Some(result)
@@ -2605,10 +2607,16 @@ pub fn compile_host_bindings_for_linker(
26052607
host_input: &HostMetadataInput,
26062608
component_name: &str,
26072609
selector: Option<&str>,
2610+
pool_starting_index: u32,
26082611
) -> Option<LinkerHostBindingOutput> {
26092612
let allocator = Allocator::default();
2610-
let result =
2611-
compile_host_bindings_from_input(&allocator, host_input, component_name, selector)?;
2613+
let result = compile_host_bindings_from_input(
2614+
&allocator,
2615+
host_input,
2616+
component_name,
2617+
selector,
2618+
pool_starting_index,
2619+
)?;
26122620

26132621
let emitter = JsEmitter::new();
26142622

@@ -2653,6 +2661,10 @@ pub struct LinkerTemplateOutput {
26532661

26542662
/// The ngContentSelectors array as a JavaScript expression string, if any.
26552663
pub ng_content_selectors_js: Option<String>,
2664+
2665+
/// The next available pool index after template compilation.
2666+
/// Used to continue constant numbering in host binding compilation.
2667+
pub next_pool_index: u32,
26562668
}
26572669

26582670
/// Compile a template for the linker, returning all data needed to build a `defineComponent` call.
@@ -2824,6 +2836,7 @@ pub fn compile_template_for_linker<'a>(
28242836
}
28252837

28262838
let declarations_js = emitter.emit_statements(&all_statements);
2839+
let next_pool_index = job.pool.next_name_index();
28272840

28282841
Ok(LinkerTemplateOutput {
28292842
declarations_js,
@@ -2832,6 +2845,7 @@ pub fn compile_template_for_linker<'a>(
28322845
vars,
28332846
consts_js,
28342847
ng_content_selectors_js,
2848+
next_pool_index,
28352849
})
28362850
}
28372851

crates/oxc_angular_compiler/src/linker/mod.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,9 +1399,12 @@ fn link_component(
13991399
// through the full Angular expression parser for correct output.
14001400
let host_input = extract_host_metadata_input(host_obj);
14011401
let selector = get_string_property(meta, "selector");
1402-
if let Some(host_output) =
1403-
crate::component::compile_host_bindings_for_linker(&host_input, type_name, selector)
1404-
{
1402+
if let Some(host_output) = crate::component::compile_host_bindings_for_linker(
1403+
&host_input,
1404+
type_name,
1405+
selector,
1406+
template_output.next_pool_index,
1407+
) {
14051408
if host_output.host_vars > 0 {
14061409
parts.push(format!("hostVars: {}", host_output.host_vars));
14071410
}
@@ -1976,6 +1979,63 @@ MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
19761979
);
19771980
}
19781981

1982+
/// Regression test for https://github.com/voidzero-dev/oxc-angular-compiler/issues/59
1983+
/// When a component has both <ng-content> (which pools a `_c0` constant for selectors)
1984+
/// and a host style binding (which pools a `_c0` constant for the style factory),
1985+
/// the linker must use unique names (`_c0`, `_c1`) instead of duplicating `_c0`.
1986+
#[test]
1987+
fn test_link_component_ng_content_with_host_style_no_duplicate_constants() {
1988+
let allocator = Allocator::default();
1989+
let code = r#"
1990+
import * as i0 from '@angular/core';
1991+
import { ChangeDetectionStrategy, Component } from '@angular/core';
1992+
1993+
class MyCheckbox {
1994+
static ɵfac = i0.ɵɵngDeclareFactory({
1995+
minVersion: "12.0.0", version: "19.2.8", ngImport: i0,
1996+
type: MyCheckbox, deps: [], target: i0.ɵɵFactoryTarget.Component
1997+
});
1998+
static ɵcmp = i0.ɵɵngDeclareComponent({
1999+
minVersion: "14.0.0", version: "19.2.8", ngImport: i0,
2000+
type: MyCheckbox, isStandalone: true, selector: "my-checkbox",
2001+
host: {
2002+
properties: { "style": "{display: \"contents\"}" }
2003+
},
2004+
template: `<button><ng-content /></button>`,
2005+
isInline: true,
2006+
changeDetection: i0.ChangeDetectionStrategy.OnPush
2007+
});
2008+
}
2009+
2010+
export { MyCheckbox };
2011+
"#;
2012+
let result = link(&allocator, code, "test.mjs");
2013+
assert!(result.linked, "Component should be linked");
2014+
assert!(
2015+
result.code.contains("defineComponent"),
2016+
"Should contain defineComponent, got:\n{}",
2017+
result.code
2018+
);
2019+
// Must not have duplicate _c0 declarations
2020+
let c0_count = result.code.matches("const _c0").count();
2021+
assert!(
2022+
c0_count <= 1,
2023+
"Should not have duplicate 'const _c0' declarations (found {c0_count}), got:\n{}",
2024+
result.code
2025+
);
2026+
// Should have both ngContentSelectors and hostBindings
2027+
assert!(
2028+
result.code.contains("ngContentSelectors"),
2029+
"Should contain ngContentSelectors, got:\n{}",
2030+
result.code
2031+
);
2032+
assert!(
2033+
result.code.contains("hostBindings"),
2034+
"Should contain hostBindings, got:\n{}",
2035+
result.code
2036+
);
2037+
}
2038+
19792039
#[test]
19802040
fn test_link_component_with_template_literal_static_field() {
19812041
let allocator = Allocator::default();

0 commit comments

Comments
 (0)