Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use exported names in ctor metadata too

When emit_class_metadata is enabled, which is the default, these same component constructor_deps are later passed to build_ctor_params_metadata; that path still uses the local token/type name for namespace property access (class_metadata/builders.rs:778 and :822). For import { Foo as Bar } ... constructor(x: Bar), the factory now emits i1.Foo, but ɵsetClassMetadata still emits ctorParameters: () => [{ type: i1.Bar }], so dev/TestBed metadata and bundlers still see the missing export for the exact aliased DI case this patch is addressing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex address the feedback

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex[agent] address the feedback

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// 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.
Expand Down
37 changes: 36 additions & 1 deletion crates/oxc_angular_compiler/src/component/dependency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ident<'a>>,

/// For `@Attribute()` dependencies, the attribute name.
/// `None` for regular dependencies.
pub attribute_name: Option<Ident<'a>>,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
},
Expand Down
Loading