diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index 0ac348a0a..7bd5f83c3 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -989,6 +989,9 @@ fn extract_param_dependency<'a>( } else { let mut d = R3DependencyMetadata::new(token_name.clone()); d.token_source_module = Some(import_info.source_module.clone()); + // Carry the exported name so namespaced refs (`i1.X`) use the export + // name, not the local alias, for aliased imports. + d.token_imported_name = import_info.imported_name.clone(); // Always use namespace imports for DI tokens (has_named_import = false). // Import elision removes @Inject(TOKEN) argument imports since they're // only used in decorator positions that get compiled away. diff --git a/crates/oxc_angular_compiler/src/component/dependency.rs b/crates/oxc_angular_compiler/src/component/dependency.rs index 963ff7c8a..54d885e75 100644 --- a/crates/oxc_angular_compiler/src/component/dependency.rs +++ b/crates/oxc_angular_compiler/src/component/dependency.rs @@ -45,6 +45,12 @@ pub struct R3DependencyMetadata<'a> { /// `DIALOG_DATA` directly instead of `i1.DIALOG_DATA`. pub has_named_import: bool, + /// The module's exported name for this token, when it differs from the local + /// binding (i.e. an aliased import `import { Foo as Bar }` → `Some("Foo")`). + /// A namespace member access (`i1.X`) must use the export name, not the local + /// alias, or it resolves to `undefined` at runtime. + pub token_imported_name: Option>, + /// For `@Attribute()` dependencies, the attribute name. /// `None` for regular dependencies. pub attribute_name: Option>, @@ -78,6 +84,7 @@ impl<'a> R3DependencyMetadata<'a> { token: Some(token), token_source_module: None, has_named_import: false, + token_imported_name: None, attribute_name: None, host: false, optional: false, @@ -93,6 +100,7 @@ impl<'a> R3DependencyMetadata<'a> { token: None, token_source_module: None, has_named_import: false, + token_imported_name: None, attribute_name: None, host: false, optional: false, @@ -111,6 +119,7 @@ impl<'a> R3DependencyMetadata<'a> { token: None, token_source_module: None, has_named_import: false, + token_imported_name: None, attribute_name: None, host: false, optional: false, @@ -150,6 +159,7 @@ impl<'a> R3DependencyMetadata<'a> { token: Some(attribute_name.clone()), token_source_module: None, has_named_import: false, + token_imported_name: None, attribute_name: Some(attribute_name), host: false, optional: false, @@ -378,7 +388,10 @@ fn create_token_expression<'a>( )), allocator, ), - name: token_name.clone(), + // Namespace member access must use the module's exported name, not the + // local binding. For an aliased import `import { Foo as Bar }`, emit + // `i1.Foo` (not `i1.Bar`, which would be undefined at runtime). + name: dep.token_imported_name.clone().unwrap_or_else(|| token_name.clone()), optional: false, source_span: None, }, @@ -538,6 +551,28 @@ mod tests { assert!(!js.contains(",")); // No flags argument } + #[test] + fn test_aliased_import_uses_exported_name() { + // Regression: an aliased import used as a DI token, e.g. + // import { ExportedName as LocalAlias } from "@scope/pkg"; + // constructor(x: LocalAlias) {} + // must emit a namespace member access with the module's EXPORTED name + // (`i1.ExportedName`), not the local alias (`i1.LocalAlias`) which resolves + // to `undefined` at runtime and breaks injection. + let allocator = Allocator::default(); + let mut dep = R3DependencyMetadata::new(Ident::from("LocalAlias")); + dep.token_source_module = Some(Ident::from("@scope/pkg")); + dep.token_imported_name = Some(Ident::from("ExportedName")); + let mut registry = NamespaceRegistry::new(&allocator); + + let result = + compile_inject_dependency(&allocator, &dep, FactoryTarget::Component, 0, &mut registry); + let js = JsEmitter::new().emit_expression(&result); + + assert!(js.contains("ExportedName"), "should use exported name: {js}"); + assert!(!js.contains("LocalAlias"), "must not use local alias: {js}"); + } + #[test] fn test_optional_dependency() { let allocator = Allocator::default(); diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index b236048ec..5c5c164a8 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -651,7 +651,10 @@ fn resolve_factory_dep_namespaces<'a>( )), allocator, ), - name: name.clone(), + // Use the module's exported name, not the local binding: a namespace + // member access (`i1.X`) must reference the export name. For an aliased + // import `import { Foo as Bar }`, `imported_name` is `Some("Foo")`. + name: import_info.imported_name.clone().unwrap_or_else(|| name.clone()), optional: false, source_span: None, }, @@ -688,7 +691,9 @@ fn resolve_host_directive_namespaces<'a>( )), allocator, ), - name: name.clone(), + // Use the module's exported name, not the local binding (see + // resolve_factory_dep_namespaces): `i1.X` must use the export name. + name: import_info.imported_name.clone().unwrap_or_else(|| name.clone()), optional: false, source_span: None, },