From dd0a02f393c8b9616b74cbbe9ad3e094827b3ddb Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 25 Jun 2026 12:38:40 -0500 Subject: [PATCH 1/5] fix: use exported name for namespaced di token references Aliased imports used as DI tokens emitted a namespace member access keyed by the local binding instead of the module's exported name. For `import { Foo as Bar } from "./m"` used as a constructor dependency, the compiler generated `i1.Bar` even though `m` only exports `Foo`, so the reference resolved to `undefined` at runtime and broke injection (the bundler also flags it as a missing export). Both namespaced-DI-reference paths used the local name: - factory deps and host directives in component/transform.rs (resolve_factory_dep_namespaces, resolve_host_directive_namespaces) - the component constructor-dep path in component/dependency.rs (create_token_expression) Carry the exported name through to both: transform.rs already had `imported_name` on its import info; dependency.rs gains a `token_imported_name` field populated from the import map in component/decorator.rs. A namespace member access now uses the exported name, falling back to the local name when they match. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/component/decorator.rs | 3 ++ .../src/component/dependency.rs | 37 ++++++++++++++++++- .../src/component/transform.rs | 9 ++++- 3 files changed, 46 insertions(+), 3 deletions(-) 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, }, From 5d8d904670816b78de2b5d6c5e3648b90930013f Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 11 Aug 2026 22:40:37 +0800 Subject: [PATCH 2/5] fix(class_metadata): use exported name in ctorParameters for aliased DI Address Codex review on PR #375: setClassMetadata ctorParameters still emitted i1.LocalAlias for `import { Exported as LocalAlias }` while the factory correctly used i1.Exported. Use token_imported_name / import imported_name when building namespaced type expressions in build_param_type_expression. Also merge latest origin/main. --- .../src/class_metadata/builders.rs | 12 +++- .../src/component/transform.rs | 63 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index d0096b91d..495f05335 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/builders.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/builders.rs @@ -775,7 +775,11 @@ fn build_param_type_expression<'a>( type_name.as_ref().is_some_and(|tn| tn.as_str() == token.as_str()); if type_matches_token { - let name = type_name.unwrap_or_else(|| token.clone()); + // Namespace member access must use the module's exported name, + // not the local binding. For `import { Foo as Bar }` with + // `constructor(x: Bar)`, emit `i1.Foo` (not `i1.Bar`). + let local_name = type_name.unwrap_or_else(|| token.clone()); + let name = dep.token_imported_name.clone().unwrap_or_else(|| local_name); let namespace = namespace_registry.get_or_assign(source_module); return Some(OutputExpression::ReadProp(Box::new_in( ReadPropExpr { @@ -810,6 +814,10 @@ fn build_param_type_expression<'a>( return None; } let namespace = namespace_registry.get_or_assign(&import_info.source_module); + // Use the module's exported name for namespaced type refs so + // aliased imports (`import { Foo as Bar }`) emit `i1.Foo`, not + // `i1.Bar` (which is undefined on the namespace object). + let name = import_info.imported_name.clone().unwrap_or_else(|| tn.clone()); return Some(OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( @@ -819,7 +827,7 @@ fn build_param_type_expression<'a>( )), &allocator, ), - name: tn.clone(), + name, optional: false, source_span: None, }, diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 5a3ce23b3..abc7ef7e2 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -6850,6 +6850,69 @@ export class TestComponent { ); } + #[test] + fn test_aliased_import_di_token_uses_exported_name_in_factory_and_ctor_params() { + // Regression for PR #375 / Codex review: aliased named imports used as DI + // tokens must emit the module export name for BOTH the factory inject path + // and setClassMetadata ctorParameters. + // + // import { ExportedName as LocalAlias } from './svc'; + // constructor(x: LocalAlias) {} + // + // must produce `i1.ExportedName` (not `i1.LocalAlias`, which is undefined + // on the namespace object at runtime). + let allocator = Allocator::default(); + let source = r#" +import { Component } from '@angular/core'; +import { ExportedName as LocalAlias } from './svc'; + +@Component({ + selector: 'app-x', + template: '', + standalone: true, +}) +export class X { + constructor(x: LocalAlias) {} +} +"#; + + let mut options = TransformOptions::default(); + options.emit_class_metadata = true; + + let result = + transform_angular_file(&allocator, "x.component.ts", source, Some(&options), None); + + assert!(!result.has_errors(), "Transform should not have errors: {:?}", result.diagnostics); + + // Namespace import for the service module + assert!( + result.code.contains("import * as i1 from './svc'"), + "Should generate namespace import for './svc', but got:\n{}", + result.code + ); + + // Factory inject AND setClassMetadata ctorParameters must use the export name + assert!( + result.code.contains("i1.ExportedName"), + "Factory and/or ctorParameters should use i1.ExportedName, but got:\n{}", + result.code + ); + assert!( + !result.code.contains("i1.LocalAlias"), + "Must not emit i1.LocalAlias (undefined on namespace); got:\n{}", + result.code + ); + + // setClassMetadata ctorParameters type field specifically + let ctor_params_ok = result.code.contains("type: i1.ExportedName") + || result.code.contains("type:i1.ExportedName"); + assert!( + ctor_params_ok, + "setClassMetadata ctorParameters should use type: i1.ExportedName, but got:\n{}", + result.code + ); + } + #[test] fn test_directive_factory_deps_get_correct_namespace_resolution() { // Regression test for bug where resolve_factory_dep_namespaces() was NOT called From 542885d3d4990f17fe29147c398ff71f88a2f405 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 11 Aug 2026 22:48:57 +0800 Subject: [PATCH 3/5] fix(di): drop barrel export name after resolved_imports path rewrite Verified Codex review on PR #375: when resolved_imports rewrites the module path to the file behind a barrel, imported_name still names the barrel export. Emitting i1.PublicService against import * as i1 from './service' is wrong if that file only exports Service. When the path is overridden, clear imported_name so namespace property access falls back to the local binding (restoring pre-alias-fix behavior for path-rewritten imports). Un-rewritten aliased imports still use the export name (Foo as Bar -> i1.Foo). Add regression test for barrel rename + local alias + path rewrite. --- .../src/component/transform.rs | 93 +++++++++++++++++-- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index abc7ef7e2..9d68cf5ef 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -519,19 +519,39 @@ pub fn build_import_map<'a>( decl_is_type_only || spec.import_kind == ImportOrExportKind::Type; // Check if we have a resolved path for this identifier + let path_overridden = + resolved_imports.and_then(|m| m.get(local_name.as_str())).is_some(); let source_module = resolved_imports .and_then(|m| m.get(local_name.as_str())) .map(|resolved| Ident::from(allocator.alloc_str(resolved))) .unwrap_or_else(|| default_source_module.clone().into()); // Capture the original exported name when it differs from the - // local binding (i.e., `import { Foo as Bar }`). This is used - // when building `@defer` dependency resolvers so the dynamic - // import chain references the original export, not the alias. + // local binding (i.e., `import { Foo as Bar }`). Used so + // namespace property access (`i1.X`) and `@defer` resolvers + // reference the module export, not the local alias. + // + // IMPORTANT: when `resolved_imports` rewrites `source_module` to + // the file behind a barrel, the original specifier's export name + // still names the *barrel* export, not the target file's export. + // Example: + // // service.ts: export class Service {} + // // barrel: export { Service as PublicService } from './service' + // import { PublicService as Service } from './barrel'; + // resolved_imports: Service -> ./service + // Emitting `i1.PublicService` against `./service` is wrong (that + // file only has `Service`). Drop `imported_name` so we fall back + // to the local binding — restoring pre-alias-fix behavior for + // path-overridden imports. Callers that need the true target + // export after multi-hop renames must extend `resolved_imports`. let imported_export_name = module_export_name_to_str(&spec.imported); - let imported_name = imported_export_name - .filter(|exported| *exported != local_name.as_str()) - .map(|exported| Ident::from(allocator.alloc_str(exported))); + let imported_name = if path_overridden { + None + } else { + imported_export_name + .filter(|exported| *exported != local_name.as_str()) + .map(|exported| Ident::from(allocator.alloc_str(exported))) + }; import_map.insert( local_name, @@ -6913,6 +6933,67 @@ export class X { ); } + /// Codex claim (PR #375): when `resolved_imports` rewrites the module path to the + /// file behind a barrel, `imported_name` still names the barrel export, not the + /// target file export. + /// + /// // service.ts exports `Service` + /// // barrel: export { Service as PublicService } from './service' + /// import { PublicService as Service } from './barrel'; + /// resolved_imports: Service -> ./service + /// + /// Namespace is `import * as i1 from './service'`, which only has `.Service`. + /// Emitting `i1.PublicService` would be wrong. + #[test] + fn test_resolved_imports_with_barrel_rename_and_local_alias() { + use std::collections::HashMap; + + let allocator = Allocator::default(); + let source = r#" +import { Component } from '@angular/core'; +import { PublicService as Service } from './barrel'; + +@Component({ + selector: 'app-x', + template: '', + standalone: true, +}) +export class X { + constructor(x: Service) {} +} +"#; + + let mut resolved = HashMap::new(); + // Path rewritten to the file behind the barrel. That file exports `Service`, + // not `PublicService`. + resolved.insert("Service".to_string(), "./service".to_string()); + + let mut options = TransformOptions::default(); + options.emit_class_metadata = true; + options.resolved_imports = Some(resolved); + + let result = + transform_angular_file(&allocator, "x.component.ts", source, Some(&options), None); + + assert!(!result.has_errors(), "errors: {:?}", result.diagnostics); + assert!( + result.code.contains("import * as i1 from './service'"), + "namespace import should use resolved path; got:\n{}", + result.code + ); + // Correct property on the resolved module is `Service` (target export / local name). + assert!( + result.code.contains("i1.Service"), + "should use target/local name Service on resolved module; got:\n{}", + result.code + ); + assert!( + !result.code.contains("i1.PublicService"), + "must NOT use barrel export name PublicService after path rewrite; got:\n{}", + result.code + ); + } + #[test] fn test_directive_factory_deps_get_correct_namespace_resolution() { // Regression test for bug where resolve_factory_dep_namespaces() was NOT called From 538242b55a7d4d88075c39a9aa47afc3965fe1c1 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 11 Aug 2026 22:52:33 +0800 Subject: [PATCH 4/5] style: slim DI alias/barrel comments Keep the logic; drop review-thread narrative from comments. --- .../src/class_metadata/builders.rs | 8 +-- .../src/component/dependency.rs | 11 +--- .../src/component/transform.rs | 56 ++++--------------- 3 files changed, 15 insertions(+), 60 deletions(-) diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index 495f05335..120427316 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/builders.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/builders.rs @@ -775,9 +775,7 @@ fn build_param_type_expression<'a>( type_name.as_ref().is_some_and(|tn| tn.as_str() == token.as_str()); if type_matches_token { - // Namespace member access must use the module's exported name, - // not the local binding. For `import { Foo as Bar }` with - // `constructor(x: Bar)`, emit `i1.Foo` (not `i1.Bar`). + // `import { Foo as Bar }` + `constructor(x: Bar)` → `i1.Foo`. let local_name = type_name.unwrap_or_else(|| token.clone()); let name = dep.token_imported_name.clone().unwrap_or_else(|| local_name); let namespace = namespace_registry.get_or_assign(source_module); @@ -814,9 +812,7 @@ fn build_param_type_expression<'a>( return None; } let namespace = namespace_registry.get_or_assign(&import_info.source_module); - // Use the module's exported name for namespaced type refs so - // aliased imports (`import { Foo as Bar }`) emit `i1.Foo`, not - // `i1.Bar` (which is undefined on the namespace object). + // Prefer export name over local alias for namespace property access. let name = import_info.imported_name.clone().unwrap_or_else(|| tn.clone()); return Some(OutputExpression::ReadProp(Box::new_in( ReadPropExpr { diff --git a/crates/oxc_angular_compiler/src/component/dependency.rs b/crates/oxc_angular_compiler/src/component/dependency.rs index 585c26d69..db7bdbde5 100644 --- a/crates/oxc_angular_compiler/src/component/dependency.rs +++ b/crates/oxc_angular_compiler/src/component/dependency.rs @@ -388,9 +388,7 @@ fn create_token_expression<'a>( )), &allocator, ), - // 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). + // Export name when aliased (`Foo as Bar` → `i1.Foo`). name: dep.token_imported_name.clone().unwrap_or_else(|| token_name.clone()), optional: false, source_span: None, @@ -553,12 +551,7 @@ mod tests { #[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. + // Aliased DI token must use export name on the namespace object. let allocator = Allocator::default(); let mut dep = R3DependencyMetadata::new(Ident::from("LocalAlias")); dep.token_source_module = Some(Ident::from("@scope/pkg")); diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 9d68cf5ef..dd880476b 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -526,24 +526,10 @@ pub fn build_import_map<'a>( .map(|resolved| Ident::from(allocator.alloc_str(resolved))) .unwrap_or_else(|| default_source_module.clone().into()); - // Capture the original exported name when it differs from the - // local binding (i.e., `import { Foo as Bar }`). Used so - // namespace property access (`i1.X`) and `@defer` resolvers - // reference the module export, not the local alias. - // - // IMPORTANT: when `resolved_imports` rewrites `source_module` to - // the file behind a barrel, the original specifier's export name - // still names the *barrel* export, not the target file's export. - // Example: - // // service.ts: export class Service {} - // // barrel: export { Service as PublicService } from './service' - // import { PublicService as Service } from './barrel'; - // resolved_imports: Service -> ./service - // Emitting `i1.PublicService` against `./service` is wrong (that - // file only has `Service`). Drop `imported_name` so we fall back - // to the local binding — restoring pre-alias-fix behavior for - // path-overridden imports. Callers that need the true target - // export after multi-hop renames must extend `resolved_imports`. + // `import { Foo as Bar }` → export name `Foo` for `i1.Foo` / @defer. + // After `resolved_imports` rewrites the path to the file behind a + // barrel, that export name is barrel-side only — drop it and use + // the local binding against the resolved module. let imported_export_name = module_export_name_to_str(&spec.imported); let imported_name = if path_overridden { None @@ -671,9 +657,7 @@ fn resolve_factory_dep_namespaces<'a>( )), &allocator, ), - // 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")`. + // Export name when aliased (`Foo as Bar` → `i1.Foo`). name: import_info.imported_name.clone().unwrap_or_else(|| name.clone()), optional: false, source_span: None, @@ -711,8 +695,7 @@ fn resolve_host_directive_namespaces<'a>( )), &allocator, ), - // Use the module's exported name, not the local binding (see - // resolve_factory_dep_namespaces): `i1.X` must use the export name. + // Export name when aliased (`Foo as Bar` → `i1.Foo`). name: import_info.imported_name.clone().unwrap_or_else(|| name.clone()), optional: false, source_span: None, @@ -6872,15 +6855,8 @@ export class TestComponent { #[test] fn test_aliased_import_di_token_uses_exported_name_in_factory_and_ctor_params() { - // Regression for PR #375 / Codex review: aliased named imports used as DI - // tokens must emit the module export name for BOTH the factory inject path - // and setClassMetadata ctorParameters. - // - // import { ExportedName as LocalAlias } from './svc'; - // constructor(x: LocalAlias) {} - // - // must produce `i1.ExportedName` (not `i1.LocalAlias`, which is undefined - // on the namespace object at runtime). + // Aliased DI token: factory inject and ctorParameters both use the export + // name (`i1.ExportedName`), never the local alias. let allocator = Allocator::default(); let source = r#" import { Component } from '@angular/core'; @@ -6933,17 +6909,8 @@ export class X { ); } - /// Codex claim (PR #375): when `resolved_imports` rewrites the module path to the - /// file behind a barrel, `imported_name` still names the barrel export, not the - /// target file export. - /// - /// // service.ts exports `Service` - /// // barrel: export { Service as PublicService } from './service' - /// import { PublicService as Service } from './barrel'; - /// resolved_imports: Service -> ./service - /// - /// Namespace is `import * as i1 from './service'`, which only has `.Service`. - /// Emitting `i1.PublicService` would be wrong. + /// Barrel re-export rename + local alias + path rewrite: property access must + /// use the resolved module's export (local `Service`), not the barrel name. #[test] fn test_resolved_imports_with_barrel_rename_and_local_alias() { use std::collections::HashMap; @@ -6964,8 +6931,7 @@ export class X { "#; let mut resolved = HashMap::new(); - // Path rewritten to the file behind the barrel. That file exports `Service`, - // not `PublicService`. + // Target file exports `Service`, not `PublicService`. resolved.insert("Service".to_string(), "./service".to_string()); let mut options = TransformOptions::default(); From eb3647b38e457f11e8ff84af5194e9075e6ac1d0 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 11 Aug 2026 22:54:23 +0800 Subject: [PATCH 5/5] fix(di): keep export name when resolved_imports only rewrites path Path rewrite does not change the target export. For `import { Foo as Bar }` + resolved_imports Bar -> ./pkg, still emit i1.Foo. Clearing imported_name on every override was too aggressive. --- .../src/component/transform.rs | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index dd880476b..772fed993 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -519,25 +519,19 @@ pub fn build_import_map<'a>( decl_is_type_only || spec.import_kind == ImportOrExportKind::Type; // Check if we have a resolved path for this identifier - let path_overridden = - resolved_imports.and_then(|m| m.get(local_name.as_str())).is_some(); let source_module = resolved_imports .and_then(|m| m.get(local_name.as_str())) .map(|resolved| Ident::from(allocator.alloc_str(resolved))) .unwrap_or_else(|| default_source_module.clone().into()); // `import { Foo as Bar }` → export name `Foo` for `i1.Foo` / @defer. - // After `resolved_imports` rewrites the path to the file behind a - // barrel, that export name is barrel-side only — drop it and use - // the local binding against the resolved module. + // Path rewrite via `resolved_imports` does not change the export + // name on the target (still `Foo`); only an extended API could + // supply a different target export after a barrel rename. let imported_export_name = module_export_name_to_str(&spec.imported); - let imported_name = if path_overridden { - None - } else { - imported_export_name - .filter(|exported| *exported != local_name.as_str()) - .map(|exported| Ident::from(allocator.alloc_str(exported))) - }; + let imported_name = imported_export_name + .filter(|exported| *exported != local_name.as_str()) + .map(|exported| Ident::from(allocator.alloc_str(exported))); import_map.insert( local_name, @@ -6909,16 +6903,16 @@ export class X { ); } - /// Barrel re-export rename + local alias + path rewrite: property access must - /// use the resolved module's export (local `Service`), not the barrel name. + /// Path rewrite keeps the specifier export name: `Foo as Bar` + resolve to + /// `./pkg` still emits `i1.Foo` (target exports `Foo`, not `Bar`). #[test] - fn test_resolved_imports_with_barrel_rename_and_local_alias() { + fn test_resolved_imports_path_only_keeps_export_name() { use std::collections::HashMap; let allocator = Allocator::default(); let source = r#" import { Component } from '@angular/core'; -import { PublicService as Service } from './barrel'; +import { Foo as Bar } from '@pkg'; @Component({ selector: 'app-x', @@ -6926,13 +6920,12 @@ import { PublicService as Service } from './barrel'; standalone: true, }) export class X { - constructor(x: Service) {} + constructor(x: Bar) {} } "#; let mut resolved = HashMap::new(); - // Target file exports `Service`, not `PublicService`. - resolved.insert("Service".to_string(), "./service".to_string()); + resolved.insert("Bar".to_string(), "./pkg".to_string()); let mut options = TransformOptions::default(); options.emit_class_metadata = true; @@ -6943,19 +6936,18 @@ export class X { assert!(!result.has_errors(), "errors: {:?}", result.diagnostics); assert!( - result.code.contains("import * as i1 from './service'"), + result.code.contains("import * as i1 from './pkg'"), "namespace import should use resolved path; got:\n{}", result.code ); - // Correct property on the resolved module is `Service` (target export / local name). assert!( - result.code.contains("i1.Service"), - "should use target/local name Service on resolved module; got:\n{}", + result.code.contains("i1.Foo"), + "should keep export name Foo after path rewrite; got:\n{}", result.code ); assert!( - !result.code.contains("i1.PublicService"), - "must NOT use barrel export name PublicService after path rewrite; got:\n{}", + !result.code.contains("i1.Bar"), + "must not use local alias Bar on namespace; got:\n{}", result.code ); }