From 62d47efbaafe8c3d7d3d3890e7c19bc4890ad6d5 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 7 Oct 2026 21:40:15 +0800 Subject: [PATCH 1/8] fix(compiler): dedupe generated i0 namespace imports against all file identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user binding named `i0` (const, namespace import, anything in the file's identifier set) collided with the injected `import * as i0 from '@angular/core'`, producing a duplicate declaration. Upstream's AOT/dts ImportManager (`presetImportManagerForceNamespaceImports`) never reuses an existing namespace import: it mints `i0` and renames to `i0_1`, `i0_2`, ... until the name is free in `SourceFile.identifiers` — every identifier in the original file, decorator positions included. The dts transform runs a second ImportManager over the same original identifier set, so `.d.ts` members use the same alias the JS emit picked. - `NamespaceRegistry` now dedupes every generated alias against `file_identifiers` (all identifier nodes in the original program), never reuses an existing `@angular/core` import, and keeps `next_index` consumed on collisions like ImportManager's `nextUniqueIndex++`. - All `i0`/`ɵfac`/`ɵcmp` emit sites — components, directives, pipes, injectables, NgModules, injectors, services, partial declarations, HMR initializer, setClassMetadata/setClassDebugInfo — reference the registry's alias (`core_namespace` threaded through, ~30 signatures). - `DtsDeclaration` gains `namespace_imports` (alias → module specifier, `@angular/core` included) so consumers can emit the imports the members reference; `input_transform_types` prints `@angular/core` transform types through the same alias (`i0_1.Signal` on collision). - Jit-forced classes preserve decorator-position identifiers (member, `declare` member, param decorators) that `__decorate`/`propDecorators` re-emit, alongside the existing ctorParameters preservation. - Vite plugin `utils/dts.ts` rewritten on oxc-parser (zero regexes): class bodies located via AST, member namespace heads rewritten via TSQualifiedName spans, members appended at end of class body like upstream's `[...members, ...newMembers]`, imports emitted after the last import's line (skipping multiline trailing comments), `.d.mts`/ `.d.cts` covered, canonical alias per module identity (relative specifiers resolved against the source module), and structural per-member idempotency so re-runs can't duplicate. Fixes #509 --- .../src/class_debug_info/compiler.rs | 16 +- .../src/class_metadata/compiler.rs | 42 +- .../src/component/definition.rs | 138 +++-- .../src/component/dependency.rs | 35 +- .../src/component/namespace_registry.rs | 150 +++++- .../src/component/transform.rs | 298 +++++++++-- .../src/directive/compiler.rs | 97 +++- .../src/directive/definition.rs | 61 ++- .../src/directive/dts_type.rs | 12 +- .../src/directive/evaluator.rs | 9 +- .../src/directive/query.rs | 85 +++- crates/oxc_angular_compiler/src/dts.rs | 158 ++++-- .../src/factory/compiler.rs | 66 ++- .../src/hmr/initializer.rs | 23 +- .../src/injectable/compiler.rs | 59 ++- .../src/injectable/definition.rs | 30 +- .../src/injector/compiler.rs | 19 +- .../src/ng_module/compiler.rs | 33 +- .../src/ng_module/definition.rs | 58 ++- .../src/partial/class_metadata.rs | 31 +- .../src/partial/component.rs | 43 +- .../src/partial/directive.rs | 42 +- .../src/partial/factory.rs | 18 +- .../src/partial/injectable.rs | 33 +- .../src/partial/injector.rs | 16 +- .../oxc_angular_compiler/src/partial/mod.rs | 3 +- .../src/partial/ng_module.rs | 19 +- .../oxc_angular_compiler/src/partial/pipe.rs | 16 +- .../oxc_angular_compiler/src/pipe/compiler.rs | 19 +- .../src/pipe/definition.rs | 28 +- .../src/pipeline/compilation.rs | 12 + .../phases/has_const_expression_collection.rs | 9 +- .../pipeline/phases/i18n_const_collection.rs | 13 +- .../pipeline/phases/reify/ir_expression.rs | 330 +++++++++--- .../src/pipeline/phases/reify/mod.rs | 435 ++++++++++++---- .../phases/reify/statements/bindings.rs | 68 ++- .../phases/reify/statements/control_flow.rs | 37 +- .../pipeline/phases/reify/statements/defer.rs | 11 +- .../phases/reify/statements/elements.rs | 77 ++- .../pipeline/phases/reify/statements/i18n.rs | 27 +- .../pipeline/phases/reify/statements/misc.rs | 73 ++- .../src/pipeline/phases/reify/utils.rs | 14 +- .../src/service/compiler.rs | 16 +- .../src/service/definition.rs | 13 +- .../ctor_param_decorator_identity_test.rs | 6 +- .../tests/decorator_metadata_ngtsc_test.rs | 19 +- .../tests/i0_collision_test.rs | 229 +++++++++ .../partial_class_metadata_async_test.rs | 14 +- .../tests/partial_directive_test.rs | 22 +- .../tests/partial_factory_test.rs | 20 +- .../tests/partial_injectable_test.rs | 26 +- .../tests/partial_ng_module_test.rs | 16 +- .../tests/partial_pipe_test.rs | 12 +- napi/angular-compiler/index.d.ts | 17 +- napi/angular-compiler/package.json | 3 +- napi/angular-compiler/src/lib.rs | 34 +- napi/angular-compiler/test/dts.test.ts | 265 ++++++++++ napi/angular-compiler/vite-plugin/index.ts | 35 +- .../angular-compiler/vite-plugin/utils/dts.ts | 480 +++++++++++++++--- pnpm-lock.yaml | 3 + 60 files changed, 3158 insertions(+), 835 deletions(-) create mode 100644 crates/oxc_angular_compiler/tests/i0_collision_test.rs diff --git a/crates/oxc_angular_compiler/src/class_debug_info/compiler.rs b/crates/oxc_angular_compiler/src/class_debug_info/compiler.rs index 0aefa3660..9750eabea 100644 --- a/crates/oxc_angular_compiler/src/class_debug_info/compiler.rs +++ b/crates/oxc_angular_compiler/src/class_debug_info/compiler.rs @@ -33,9 +33,10 @@ use crate::r3::Identifiers; /// Ported from Angular's `compileClassDebugInfo` function. pub fn compile_class_debug_info<'a>( allocator: &'a Allocator, + core_namespace: &'a str, debug_info: &R3ClassDebugInfo<'a>, ) -> OutputExpression<'a> { - let fn_call = internal_compile_class_debug_info(allocator, debug_info); + let fn_call = internal_compile_class_debug_info(allocator, core_namespace, debug_info); let guarded = dev_only_guarded_expression(allocator, fn_call); let stmt = expr_stmt(allocator, guarded); create_arrow_iife(allocator, stmt) @@ -44,9 +45,10 @@ pub fn compile_class_debug_info<'a>( /// Compiles the internal `setClassDebugInfo` call without wrappers. fn internal_compile_class_debug_info<'a>( allocator: &'a Allocator, + core_namespace: &'a str, debug_info: &R3ClassDebugInfo<'a>, ) -> OutputExpression<'a> { - let import = import_expr(allocator, Identifiers::SET_CLASS_DEBUG_INFO); + let import = import_expr(allocator, core_namespace, Identifiers::SET_CLASS_DEBUG_INFO); // Build the debug info object // Always include className @@ -161,13 +163,17 @@ fn dev_only_guarded_expression<'a>( )) } -/// Creates an import expression: i0.identifier -fn import_expr<'a>(allocator: &'a Allocator, identifier: &'static str) -> OutputExpression<'a> { +/// Creates an import expression: ns.identifier +fn import_expr<'a>( + allocator: &'a Allocator, + core_namespace: &'a str, + identifier: &'static str, +) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, diff --git a/crates/oxc_angular_compiler/src/class_metadata/compiler.rs b/crates/oxc_angular_compiler/src/class_metadata/compiler.rs index 193a88f7b..b2f2cac53 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/compiler.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/compiler.rs @@ -31,9 +31,10 @@ use crate::r3::Identifiers; /// Ported from Angular's `compileClassMetadata` function. pub fn compile_class_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3ClassMetadata<'a>, ) -> OutputExpression<'a> { - let fn_call = internal_compile_class_metadata(allocator, metadata); + let fn_call = internal_compile_class_metadata(allocator, core_namespace, metadata); let guarded = dev_only_guarded_expression(allocator, fn_call); let stmt = expr_stmt(allocator, guarded); let arrow = create_arrow_iife(allocator, stmt); @@ -48,13 +49,14 @@ pub fn compile_class_metadata<'a>( /// Ported from Angular's `compileComponentClassMetadata` function. pub fn compile_component_class_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3ClassMetadata<'a>, dependencies: Option<&[R3DeferPerComponentDependency<'a>]>, ) -> OutputExpression<'a> { match dependencies { None | Some([]) => { // No deferred dependencies - use regular setClassMetadata - compile_class_metadata(allocator, metadata) + compile_class_metadata(allocator, core_namespace, metadata) } Some(deps) => { // Has deferred dependencies - use setClassMetadataAsync @@ -63,7 +65,13 @@ pub fn compile_component_class_metadata<'a>( params.push(FnParam { name: dep.param_name.clone() }); } let resolver = compile_component_metadata_async_resolver(allocator, deps); - internal_compile_set_class_metadata_async(allocator, metadata, params, resolver) + internal_compile_set_class_metadata_async( + allocator, + core_namespace, + metadata, + params, + resolver, + ) } } } @@ -75,6 +83,7 @@ pub fn compile_component_class_metadata<'a>( /// Ported from Angular's `compileOpaqueAsyncClassMetadata` function. pub fn compile_opaque_async_class_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3ClassMetadata<'a>, defer_resolver: OutputExpression<'a>, deferred_dependency_names: &[Ident<'a>], @@ -83,7 +92,13 @@ pub fn compile_opaque_async_class_metadata<'a>( for name in deferred_dependency_names { params.push(FnParam { name: name.clone() }); } - internal_compile_set_class_metadata_async(allocator, metadata, params, defer_resolver) + internal_compile_set_class_metadata_async( + allocator, + core_namespace, + metadata, + params, + defer_resolver, + ) } /// Compiles the dependency resolver function for `setClassMetadataAsync`. @@ -197,9 +212,10 @@ pub fn compile_component_metadata_async_resolver<'a>( /// Compiles the internal `setClassMetadata` call without wrappers. fn internal_compile_class_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3ClassMetadata<'a>, ) -> OutputExpression<'a> { - let import = import_expr(allocator, Identifiers::SET_CLASS_METADATA); + let import = import_expr(allocator, core_namespace, Identifiers::SET_CLASS_METADATA); let mut args = Vec::new_in(&allocator); args.push(metadata.r#type.clone_in(allocator)); @@ -234,12 +250,14 @@ fn internal_compile_class_metadata<'a>( /// Compiles `setClassMetadataAsync` with wrapper params and resolver. fn internal_compile_set_class_metadata_async<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3ClassMetadata<'a>, wrapper_params: Vec<'a, FnParam<'a>>, dependency_resolver_fn: OutputExpression<'a>, ) -> OutputExpression<'a> { // Create the inner setClassMetadata call - let set_class_metadata_call = internal_compile_class_metadata(allocator, metadata); + let set_class_metadata_call = + internal_compile_class_metadata(allocator, core_namespace, metadata); // Create wrapper: (deps...) => { setClassMetadata(...); } let mut wrapper_stmts = Vec::new_in(&allocator); @@ -255,7 +273,7 @@ fn internal_compile_set_class_metadata_async<'a>( )); // Create: setClassMetadataAsync(type, resolver, wrapper) - let import = import_expr(allocator, Identifiers::SET_CLASS_METADATA_ASYNC); + let import = import_expr(allocator, core_namespace, Identifiers::SET_CLASS_METADATA_ASYNC); let mut args = Vec::new_in(&allocator); args.push(metadata.r#type.clone_in(allocator)); @@ -343,13 +361,17 @@ fn guarded_expression<'a>( )) } -/// Creates an import expression: i0.identifier -fn import_expr<'a>(allocator: &'a Allocator, identifier: &'static str) -> OutputExpression<'a> { +/// Creates an import expression: ns.identifier +fn import_expr<'a>( + allocator: &'a Allocator, + core_namespace: &'a str, + identifier: &'static str, +) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, diff --git a/crates/oxc_angular_compiler/src/component/definition.rs b/crates/oxc_angular_compiler/src/component/definition.rs index 8ea598a32..ea4f317a5 100644 --- a/crates/oxc_angular_compiler/src/component/definition.rs +++ b/crates/oxc_angular_compiler/src/component/definition.rs @@ -79,8 +79,10 @@ pub fn generate_component_definitions<'a>( // so factory dependencies get registered first, followed by component definition dependencies. // This ensures namespace indices (i0, i1, i2, ...) are assigned in the same order. let fac_definition = generate_fac_definition(allocator, metadata, namespace_registry); + let core_namespace = namespace_registry.angular_core_ns().as_str(); let cmp_definition = generate_cmp_definition( &allocator, + core_namespace, metadata, options, job, @@ -112,6 +114,7 @@ pub fn generate_component_definitions<'a>( /// ``` fn generate_cmp_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ComponentMetadata<'a>, options: &TransformOptions, job: &mut ComponentCompilationJob<'a>, @@ -266,7 +269,9 @@ fn generate_cmp_definition<'a>( // 13. features: [...] - component features like providers, lifecycle hooks, inheritance // See: packages/compiler/src/render3/view/compiler.ts:119-161 - if let Some(features) = generate_features_array(allocator, metadata, namespace_registry) { + if let Some(features) = + generate_features_array(allocator, core_namespace, metadata, namespace_registry) + { entries.push(LiteralMapEntry::new(Ident::from("features"), features, false)); } @@ -324,7 +329,7 @@ fn generate_cmp_definition<'a>( if !job.consts.is_empty() { let mut const_entries: OxcVec<'a, OutputExpression<'a>> = OxcVec::new_in(&allocator); for const_value in &job.consts { - const_entries.push(const_value_to_expression(allocator, const_value)); + const_entries.push(const_value_to_expression(allocator, core_namespace, const_value)); } let consts_value = if !job.consts_initializers.is_empty() { @@ -381,7 +386,7 @@ fn generate_cmp_definition<'a>( // 20. dependencies: [...] - template dependencies (directives and pipes) // Per Angular compiler.ts lines 272-289 if let Some(dependencies) = - generate_dependencies_expression(allocator, metadata, namespace_registry) + generate_dependencies_expression(allocator, core_namespace, metadata, namespace_registry) { entries.push(LiteralMapEntry::new(Ident::from("dependencies"), dependencies, false)); } @@ -524,7 +529,7 @@ fn generate_cmp_definition<'a>( )); // Wrap in ɵɵdefineComponent call - create_define_component_call(allocator, config) + create_define_component_call(allocator, core_namespace, config) } /// Generate the ɵfac factory function. @@ -569,7 +574,7 @@ fn generate_fac_definition<'a>( match &metadata.constructor_deps { None => { // No constructor - use inherited factory IIFE pattern - generate_inherited_factory(allocator, metadata) + generate_inherited_factory(allocator, namespace_registry.angular_core_ns(), metadata) } Some(deps) => { // Constructor exists - generate normal factory @@ -633,7 +638,10 @@ fn generate_constructor_factory<'a>( if deps.iter().any(|d| d.type_only_invalid) { statements.push(OutputStatement::Expression(Box::new_in( crate::output::ast::ExpressionStatement { - expr: create_invalid_factory_call(allocator), + expr: create_invalid_factory_call( + allocator, + namespace_registry.angular_core_ns().as_str(), + ), source_span: None, }, &allocator, @@ -693,6 +701,7 @@ fn generate_constructor_factory<'a>( /// See: packages/compiler/src/render3/r3_factory.ts:160-193 fn generate_inherited_factory<'a>( allocator: &'a Allocator, + core_namespace: Ident<'a>, metadata: &ComponentMetadata<'a>, ) -> OutputExpression<'a> { use crate::output::ast::{ @@ -716,7 +725,7 @@ fn generate_inherited_factory<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: core_namespace, source_span: None }, &allocator, )), &allocator, @@ -883,17 +892,18 @@ fn generate_inherited_factory<'a>( )) } -/// Create an i0.ɵɵdefineComponent(config) call expression. +/// Create an ns.ɵɵdefineComponent(config) call expression. fn create_define_component_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, config: OutputExpression<'a>, ) -> OutputExpression<'a> { - // Access: i0.ɵɵdefineComponent + // Access: ns.ɵɵdefineComponent let define_component = OutputExpression::ReadProp(Box::new_in( crate::output::ast::ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -973,6 +983,7 @@ fn parse_selector_to_array<'a>( /// See: packages/compiler/src/render3/view/compiler.ts:119-161 fn generate_features_array<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ComponentMetadata<'a>, namespace_registry: &mut NamespaceRegistry<'a>, ) -> Option> { @@ -981,7 +992,7 @@ fn generate_features_array<'a>( // 1. ProvidersFeature - when providers or viewProviders are defined // Format: ɵɵProvidersFeature([providers], [viewProviders]?) if metadata.providers.is_some() || metadata.view_providers.is_some() { - let providers_feature = generate_providers_feature(allocator, metadata); + let providers_feature = generate_providers_feature(allocator, core_namespace, metadata); features.push(providers_feature); } @@ -989,27 +1000,40 @@ fn generate_features_array<'a>( // Must come before InheritDefinitionFeature for correct execution order // Format: ɵɵHostDirectivesFeature([directives]) if !metadata.host_directives.is_empty() { - let host_directives_feature = - generate_host_directives_feature(allocator, metadata, namespace_registry); + let host_directives_feature = generate_host_directives_feature( + allocator, + core_namespace, + metadata, + namespace_registry, + ); features.push(host_directives_feature); } // 3. InheritDefinitionFeature - when the component extends another class // Format: ɵɵInheritDefinitionFeature (direct reference, no call) if metadata.uses_inheritance { - features.push(create_angular_fn_ref(allocator, Identifiers::INHERIT_DEFINITION_FEATURE)); + features.push(create_angular_fn_ref( + allocator, + core_namespace, + Identifiers::INHERIT_DEFINITION_FEATURE, + )); } // 4. NgOnChangesFeature - when ngOnChanges lifecycle hook is implemented // Format: ɵɵNgOnChangesFeature (direct reference, no call) if metadata.lifecycle.uses_on_changes { - features.push(create_angular_fn_ref(allocator, Identifiers::NG_ON_CHANGES_FEATURE)); + features.push(create_angular_fn_ref( + allocator, + core_namespace, + Identifiers::NG_ON_CHANGES_FEATURE, + )); } // 5. ExternalStylesFeature - when external stylesheets need to be loaded // Format: ɵɵExternalStylesFeature(['style1.css', 'style2.css']) if !metadata.external_styles.is_empty() { - let external_styles_feature = generate_external_styles_feature(allocator, metadata); + let external_styles_feature = + generate_external_styles_feature(allocator, core_namespace, metadata); features.push(external_styles_feature); } @@ -1029,9 +1053,10 @@ fn generate_features_array<'a>( /// See: packages/compiler/src/render3/view/compiler.ts:119-135 fn generate_providers_feature<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ComponentMetadata<'a>, ) -> OutputExpression<'a> { - let fn_expr = create_angular_fn_ref(allocator, Identifiers::PROVIDERS_FEATURE); + let fn_expr = create_angular_fn_ref(allocator, core_namespace, Identifiers::PROVIDERS_FEATURE); // Build args: [providers, viewProviders?] let has_view_providers = metadata.view_providers.is_some(); @@ -1078,10 +1103,12 @@ fn generate_providers_feature<'a>( /// See: packages/compiler/src/render3/view/compiler.ts:138-141, 683-723 fn generate_host_directives_feature<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ComponentMetadata<'a>, namespace_registry: &mut NamespaceRegistry<'a>, ) -> OutputExpression<'a> { - let fn_expr = create_angular_fn_ref(allocator, Identifiers::HOST_DIRECTIVES_FEATURE); + let fn_expr = + create_angular_fn_ref(allocator, core_namespace, Identifiers::HOST_DIRECTIVES_FEATURE); // Create the host directives argument let host_directives_arg = @@ -1229,9 +1256,11 @@ fn create_host_directives_arg<'a>( /// See: packages/compiler/src/render3/view/compiler.ts:150-155 fn generate_external_styles_feature<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ComponentMetadata<'a>, ) -> OutputExpression<'a> { - let fn_expr = create_angular_fn_ref(allocator, Identifiers::EXTERNAL_STYLES_FEATURE); + let fn_expr = + create_angular_fn_ref(allocator, core_namespace, Identifiers::EXTERNAL_STYLES_FEATURE); // Create array of external style paths let mut style_entries: OxcVec<'a, OutputExpression<'a>> = @@ -1267,13 +1296,14 @@ fn generate_external_styles_feature<'a>( /// Create an `i0.functionName` reference expression. fn create_angular_fn_ref<'a>( allocator: &'a Allocator, + core_namespace: &'a str, fn_name: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -1305,12 +1335,13 @@ fn create_angular_fn_ref<'a>( /// See: packages/compiler/src/render3/view/compiler.ts:272-289 fn generate_dependencies_expression<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ComponentMetadata<'a>, namespace_registry: &mut NamespaceRegistry<'a>, ) -> Option> { // RuntimeResolved mode uses getComponentDepsFactory if metadata.declaration_list_emit_mode == DeclarationListEmitMode::RuntimeResolved { - return Some(generate_runtime_resolved_dependencies(allocator, metadata)); + return Some(generate_runtime_resolved_dependencies(allocator, core_namespace, metadata)); } // No dependencies to emit @@ -1322,7 +1353,12 @@ fn generate_dependencies_expression<'a>( let deps_array = create_dependencies_array(allocator, metadata, namespace_registry); // Compile based on emit mode - Some(compile_declaration_list(allocator, deps_array, metadata.declaration_list_emit_mode)) + Some(compile_declaration_list( + allocator, + core_namespace, + deps_array, + metadata.declaration_list_emit_mode, + )) } /// Generate runtime-resolved dependencies expression. @@ -1333,9 +1369,11 @@ fn generate_dependencies_expression<'a>( /// See: packages/compiler/src/render3/view/compiler.ts:283-288 fn generate_runtime_resolved_dependencies<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ComponentMetadata<'a>, ) -> OutputExpression<'a> { - let fn_expr = create_angular_fn_ref(allocator, Identifiers::GET_COMPONENT_DEPS_FACTORY); + let fn_expr = + create_angular_fn_ref(allocator, core_namespace, Identifiers::GET_COMPONENT_DEPS_FACTORY); let capacity = if metadata.raw_imports.is_some() { 2 } else { 1 }; let mut args: OxcVec<'a, OutputExpression<'a>> = OxcVec::with_capacity_in(capacity, &allocator); @@ -1416,6 +1454,7 @@ fn create_dependencies_array<'a>( /// See: packages/compiler/src/render3/view/compiler.ts:378-396 fn compile_declaration_list<'a>( allocator: &'a Allocator, + core_namespace: &'a str, list: OutputExpression<'a>, mode: DeclarationListEmitMode, ) -> OutputExpression<'a> { @@ -1430,7 +1469,7 @@ fn compile_declaration_list<'a>( } DeclarationListEmitMode::ClosureResolved => { // ClosureResolved: function() { return [MyDir].map(ng.resolveForwardRef); } - let resolve_fn = create_angular_fn_ref(allocator, "resolveForwardRef"); + let resolve_fn = create_angular_fn_ref(allocator, core_namespace, "resolveForwardRef"); // list.map(ng.resolveForwardRef) let map_call = OutputExpression::InvokeFunction(Box::new_in( @@ -1501,6 +1540,7 @@ fn wrap_in_arrow_function<'a>( /// This is used to serialize the consts array entries for the component definition. pub fn const_value_to_expression<'a>( allocator: &'a Allocator, + core_namespace: &'a str, value: &ConstValue<'a>, ) -> OutputExpression<'a> { match value { @@ -1523,7 +1563,7 @@ pub fn const_value_to_expression<'a>( ConstValue::Array(arr) => { let mut entries: OxcVec<'a, OutputExpression<'a>> = OxcVec::new_in(&allocator); for item in arr.iter() { - entries.push(const_value_to_expression(allocator, item)); + entries.push(const_value_to_expression(allocator, core_namespace, item)); } OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries, source_span: None }, @@ -1536,7 +1576,7 @@ pub fn const_value_to_expression<'a>( crate::output::ast::ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -1664,7 +1704,7 @@ mod tests { let metadata = create_test_metadata(&allocator); let mut namespace_registry = NamespaceRegistry::new(&allocator); - let result = generate_features_array(&allocator, &metadata, &mut namespace_registry); + let result = generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry); assert!(result.is_none(), "Should return None when no features are needed"); } @@ -1696,7 +1736,7 @@ mod tests { Some(create_test_providers_array(&allocator, &["ServiceA", "ServiceB"])); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1715,7 +1755,7 @@ mod tests { metadata.view_providers = Some(create_test_providers_array(&allocator, &["ViewService"])); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1734,7 +1774,7 @@ mod tests { metadata.view_providers = Some(create_test_providers_array(&allocator, &["ViewService"])); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1752,7 +1792,7 @@ mod tests { metadata.uses_inheritance = true; let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1770,7 +1810,7 @@ mod tests { metadata.lifecycle = LifecycleMetadata { uses_on_changes: true }; let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1789,7 +1829,7 @@ mod tests { metadata.external_styles.push(Ident::from("./theme.css")); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1809,7 +1849,7 @@ mod tests { metadata.host_directives.push(directive); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1830,7 +1870,7 @@ mod tests { metadata.host_directives.push(directive); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1854,7 +1894,7 @@ mod tests { metadata.host_directives.push(directive); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1877,7 +1917,7 @@ mod tests { metadata.host_directives.push(directive); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1910,7 +1950,7 @@ mod tests { metadata.host_directives.push(imported_directive); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1940,7 +1980,7 @@ mod tests { metadata.lifecycle = LifecycleMetadata { uses_on_changes: true }; let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); @@ -1962,7 +2002,7 @@ mod tests { let mut namespace_registry = NamespaceRegistry::new(&allocator); let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry); + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry); assert!(result.is_none(), "Should return None when no dependencies"); } @@ -1991,7 +2031,7 @@ mod tests { metadata.declaration_list_emit_mode = DeclarationListEmitMode::Direct; let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry) + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry) .unwrap(); let emitter = JsEmitter::new(); @@ -2020,7 +2060,7 @@ mod tests { metadata.declaration_list_emit_mode = DeclarationListEmitMode::Closure; let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry) + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry) .unwrap(); let emitter = JsEmitter::new(); @@ -2049,7 +2089,7 @@ mod tests { metadata.declaration_list_emit_mode = DeclarationListEmitMode::ClosureResolved; let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry) + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry) .unwrap(); let emitter = JsEmitter::new(); @@ -2071,7 +2111,7 @@ mod tests { metadata.declaration_list_emit_mode = DeclarationListEmitMode::RuntimeResolved; let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry) + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry) .unwrap(); let emitter = JsEmitter::new(); @@ -2095,7 +2135,7 @@ mod tests { ))); let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry) + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry) .unwrap(); let emitter = JsEmitter::new(); @@ -2137,7 +2177,7 @@ mod tests { ))); let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry) + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry) .unwrap(); let emitter = JsEmitter::new(); @@ -2166,7 +2206,7 @@ mod tests { metadata.declaration_list_emit_mode = DeclarationListEmitMode::Direct; let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry) + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry) .unwrap(); let emitter = JsEmitter::new(); @@ -2207,7 +2247,7 @@ mod tests { metadata.declaration_list_emit_mode = DeclarationListEmitMode::Direct; let result = - generate_dependencies_expression(&allocator, &metadata, &mut namespace_registry) + generate_dependencies_expression(&allocator, "i0", &metadata, &mut namespace_registry) .unwrap(); let emitter = JsEmitter::new(); @@ -2234,7 +2274,7 @@ mod tests { metadata.host_directives.push(directive); let result = - generate_features_array(&allocator, &metadata, &mut namespace_registry).unwrap(); + generate_features_array(&allocator, "i0", &metadata, &mut namespace_registry).unwrap(); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&result); diff --git a/crates/oxc_angular_compiler/src/component/dependency.rs b/crates/oxc_angular_compiler/src/component/dependency.rs index db7bdbde5..66486d411 100644 --- a/crates/oxc_angular_compiler/src/component/dependency.rs +++ b/crates/oxc_angular_compiler/src/component/dependency.rs @@ -304,14 +304,16 @@ fn compile_inject_dependency<'a>( index: usize, namespace_registry: &mut NamespaceRegistry<'a>, ) -> OutputExpression<'a> { + let core_namespace = namespace_registry.angular_core_ns(); + // Case 1: Invalid dependency - token is None let Some(ref token_name) = dep.token else { - return create_invalid_factory_dep_call(allocator, index); + return create_invalid_factory_dep_call(allocator, core_namespace, index); }; // Case 2: @Attribute() dependency if let Some(ref attr_name) = dep.attribute_name { - return create_inject_attribute_call(allocator, attr_name.clone()); + return create_inject_attribute_call(allocator, core_namespace, attr_name.clone()); } // Case 3: Regular token injection @@ -343,9 +345,15 @@ fn compile_inject_dependency<'a>( // Only include flags if they are non-default OR if optional is true // (Angular special-cases optional to always include the flags param) if flags.is_default() && !dep.optional { - create_inject_call_with_expr(allocator, inject_fn_name, token_expr, None) + create_inject_call_with_expr(allocator, core_namespace, inject_fn_name, token_expr, None) } else { - create_inject_call_with_expr(allocator, inject_fn_name, token_expr, Some(flags.value())) + create_inject_call_with_expr( + allocator, + core_namespace, + inject_fn_name, + token_expr, + Some(flags.value()), + ) } } @@ -404,12 +412,14 @@ fn create_token_expression<'a>( } } -/// Create an `i0.ɵɵinvalidFactoryDep(index)` call. +/// Create an `ns.ɵɵinvalidFactoryDep(index)` call. fn create_invalid_factory_dep_call<'a>( allocator: &'a Allocator, + core_namespace: Ident<'a>, index: usize, ) -> OutputExpression<'a> { - let fn_expr = create_angular_fn_ref(allocator, Identifiers::INVALID_FACTORY_DEP); + let fn_expr = + create_angular_fn_ref(allocator, core_namespace, Identifiers::INVALID_FACTORY_DEP); let mut args = OxcVec::with_capacity_in(1, &allocator); args.push(OutputExpression::Literal(Box::new_in( @@ -429,12 +439,13 @@ fn create_invalid_factory_dep_call<'a>( )) } -/// Create an `i0.ɵɵinjectAttribute(attrName)` call. +/// Create an `ns.ɵɵinjectAttribute(attrName)` call. fn create_inject_attribute_call<'a>( allocator: &'a Allocator, + core_namespace: Ident<'a>, attr_name: Ident<'a>, ) -> OutputExpression<'a> { - let fn_expr = create_angular_fn_ref(allocator, Identifiers::INJECT_ATTRIBUTE); + let fn_expr = create_angular_fn_ref(allocator, core_namespace, Identifiers::INJECT_ATTRIBUTE); let mut args = OxcVec::with_capacity_in(1, &allocator); // Attribute name is passed as a string literal @@ -461,11 +472,12 @@ fn create_inject_attribute_call<'a>( /// The token expression can be either a bare variable or a namespaced reference. fn create_inject_call_with_expr<'a>( allocator: &'a Allocator, + core_namespace: Ident<'a>, fn_name: &'static str, token_expr: OutputExpression<'a>, flags: Option, ) -> OutputExpression<'a> { - let fn_expr = create_angular_fn_ref(allocator, fn_name); + let fn_expr = create_angular_fn_ref(allocator, core_namespace, fn_name); let capacity = if flags.is_some() { 2 } else { 1 }; let mut args = OxcVec::with_capacity_in(capacity, &allocator); @@ -492,16 +504,17 @@ fn create_inject_call_with_expr<'a>( )) } -/// Create an `i0.functionName` reference expression. +/// Create an `ns.functionName` reference expression. fn create_angular_fn_ref<'a>( allocator: &'a Allocator, + core_namespace: Ident<'a>, fn_name: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: core_namespace, source_span: None }, &allocator, )), &allocator, diff --git a/crates/oxc_angular_compiler/src/component/namespace_registry.rs b/crates/oxc_angular_compiler/src/component/namespace_registry.rs index b09d94ae0..a356da08d 100644 --- a/crates/oxc_angular_compiler/src/component/namespace_registry.rs +++ b/crates/oxc_angular_compiler/src/component/namespace_registry.rs @@ -1,15 +1,28 @@ use oxc_allocator::{Allocator, FromIn}; use oxc_str::Ident; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; /// Registry for assigning namespace aliases to imported modules. /// /// Angular uses namespace aliases like i0, i1, i2... for imported modules: /// - i0 is always @angular/core /// - i1, i2, i3... are assigned to other modules in order of first reference +/// +/// Like ngtsc's `ImportManager` (`import_manager.ts:229`), an alias colliding +/// with any identifier the file already declares is uniquified with a `_N` +/// suffix (`i0` → `i0_1`, ...), and an existing non-type-only +/// `import * as ns from "..."` for the module is reused instead of emitting a +/// second import (`reuse_source_file_imports.ts`). pub struct NamespaceRegistry<'a> { /// Map of module_path -> namespace alias modules: FxHashMap, Ident<'a>>, + /// Module paths whose alias names an import that already exists in the + /// file — `generate_import_statements` must not re-emit them. + reused_modules: FxHashSet>, + /// Every identifier the source file already declares or references. Any + /// newly assigned alias is uniquified against this set (and against + /// aliases the registry itself assigned earlier). + used_names: FxHashSet, /// Counter for next alias index (starts at 1, since i0 is reserved for @angular/core) next_index: usize, /// The allocator for creating atoms @@ -21,26 +34,77 @@ impl<'a> NamespaceRegistry<'a> { /// /// The registry is initialized with `@angular/core` pre-registered as `i0`. pub fn new(allocator: &'a Allocator) -> Self { + Self::with_file_scope(allocator, None, None) + } + + /// Creates a namespace registry that is aware of the file's existing names. + /// + /// `reused_core_alias` is the local name of an existing + /// `import * as ns from "@angular/core"` in the file; when provided, the + /// registry references `@angular/core` through `ns` and does not emit its + /// own import. Otherwise the `@angular/core` alias is `i0`, uniquified as + /// `i0_1`, `i0_2`, ... while `used_names` contains it. + pub fn with_file_scope( + allocator: &'a Allocator, + reused_core_alias: Option>, + used_names: Option>, + ) -> Self { let mut registry = Self { modules: FxHashMap::default(), + reused_modules: FxHashSet::default(), + used_names: used_names.unwrap_or_default(), next_index: 1, // Start at 1 (i0 is reserved) allocator, }; - // Pre-register @angular/core as i0 - registry.modules.insert(Ident::from("@angular/core"), Ident::from("i0")); + // Pre-register @angular/core + let core_module = Ident::from("@angular/core"); + let core_alias = match reused_core_alias { + Some(alias) => { + registry.reused_modules.insert(core_module); + alias + } + None => registry.unique_name("i0"), + }; + registry.modules.insert(core_module, core_alias); registry } + /// Returns `name` unchanged when the file does not already use it, else + /// `name_1`, `name_2`, ... — the same rule as ngtsc's + /// `generateUniqueIdentifier` (`check_unique_identifier_name.ts`). + fn unique_name(&mut self, name: &str) -> Ident<'a> { + if !self.used_names.contains(name) { + self.used_names.insert(name.to_string()); + return Ident::from_in(name, self.allocator); + } + let mut counter = 1; + loop { + let candidate = format!("{name}_{counter}"); + counter += 1; + if !self.used_names.contains(&candidate) { + self.used_names.insert(candidate.clone()); + return Ident::from_in(candidate, self.allocator); + } + } + } + + /// The alias generated `@angular/core` references use (usually `i0`). + pub fn angular_core_ns(&self) -> Ident<'a> { + // @angular/core is always pre-registered by the constructors. + self.modules[&Ident::from("@angular/core")] + } + /// Get the namespace alias for a module, assigning one if not yet assigned. pub fn get_or_assign(&mut self, module_path: &Ident<'a>) -> Ident<'a> { if let Some(alias) = self.modules.get(module_path) { - return alias.clone(); + return *alias; } - // Assign new alias - let alias = Ident::from_in(format!("i{}", self.next_index).as_str(), &self.allocator); + // Assign new alias. `next_index` is consumed even when the name needs + // a uniquifying suffix, matching ImportManager's `nextUniqueIndex++`. + let alias = self.unique_name(&format!("i{}", self.next_index)); self.next_index += 1; - self.modules.insert(module_path.clone(), alias.clone()); + self.modules.insert(*module_path, alias); alias } @@ -65,11 +129,18 @@ impl<'a> NamespaceRegistry<'a> { /// import * as i1 from "@bitwarden/common/auth/abstractions/auth.service"; /// import * as i2 from "@angular/router"; /// ``` + /// + /// Modules whose import the file already declares (registered through + /// [`NamespaceRegistry::with_file_scope`]) are skipped — the existing + /// import is reused. pub fn generate_import_statements(&self) -> String { let modules = self.get_all_modules(); let mut result = String::new(); for (module_path, alias) in modules { + if self.reused_modules.contains(module_path) { + continue; + } result.push_str("import * as "); result.push_str(alias.as_str()); result.push_str(" from '"); @@ -87,16 +158,21 @@ impl<'a> NamespaceRegistry<'a> { /// the same alias (which should be true if they were registered in the same order /// from the @angular/core starting point). pub fn merge_from(&mut self, other: &Self) { + for name in &other.used_names { + self.used_names.insert(name.clone()); + } + for module in &other.reused_modules { + self.reused_modules.insert(*module); + } for (module_path, alias) in &other.modules { if !self.modules.contains_key(module_path) { - self.modules.insert(module_path.clone(), alias.clone()); + self.modules.insert(*module_path, *alias); // Update next_index if the merged alias index is higher - if let Some(idx_str) = alias.strip_prefix('i') { - if let Ok(idx) = idx_str.parse::() { - if idx >= self.next_index { - self.next_index = idx + 1; - } - } + if let Some(idx_str) = alias.strip_prefix('i') + && let Ok(idx) = idx_str.parse::() + && idx >= self.next_index + { + self.next_index = idx + 1; } } } @@ -251,4 +327,50 @@ import * as i2 from '@bitwarden/common/auth/abstractions/auth.service'; let http_alias = registry1.get_or_assign(&Ident::from("@angular/common/http")); assert_eq!(http_alias.as_str(), "i3"); } + + #[test] + fn test_core_alias_uniquified_when_i0_taken() { + let allocator = Allocator::default(); + let mut used = FxHashSet::default(); + used.insert("i0".to_string()); + let registry = NamespaceRegistry::with_file_scope(&allocator, None, Some(used)); + + assert_eq!(registry.angular_core_ns().as_str(), "i0_1"); + assert_eq!( + registry.generate_import_statements(), + "import * as i0_1 from '@angular/core';\n" + ); + } + + #[test] + fn test_core_alias_reused_import_emits_no_import() { + let allocator = Allocator::default(); + let mut registry = + NamespaceRegistry::with_file_scope(&allocator, Some(Ident::from("core")), None); + + assert_eq!(registry.angular_core_ns().as_str(), "core"); + assert_eq!(registry.generate_import_statements(), ""); + + // Other modules still get imports; the reused one stays skipped. + registry.get_or_assign(&Ident::from("@angular/router")); + assert_eq!( + registry.generate_import_statements(), + "import * as i1 from '@angular/router';\n" + ); + } + + #[test] + fn test_other_alias_uniquified_against_file_identifiers() { + let allocator = Allocator::default(); + let mut used = FxHashSet::default(); + used.insert("i1".to_string()); + let mut registry = NamespaceRegistry::with_file_scope(&allocator, None, Some(used)); + + // i1 is taken by the file, so the first non-core module gets i1_1; + // the index is still consumed (upstream nextUniqueIndex++). + let router = registry.get_or_assign(&Ident::from("@angular/router")); + assert_eq!(router.as_str(), "i1_1"); + let forms = registry.get_or_assign(&Ident::from("@angular/forms")); + assert_eq!(forms.as_str(), "i2"); + } } diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index a3364af5a..5c29ca62b 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::path::Path; -use oxc_allocator::{Allocator, Vec as OxcVec}; +use oxc_allocator::{Allocator, FromIn, Vec as OxcVec}; use oxc_ast::ast::{ Argument, Declaration, ExportDefaultDeclarationKind, Expression, ImportDeclarationSpecifier, ImportOrExportKind, ModuleExportName, ObjectPropertyKind, Program, PropertyKey, Statement, @@ -837,11 +837,16 @@ fn build_set_class_metadata_decls<'a>( Some(string_consts), ), }; + let core_namespace = namespace_registry.angular_core_ns().as_str(); let metadata_expr = match options.compilation_mode { - crate::CompilationMode::Full => compile_class_metadata(allocator, &class_metadata), - crate::CompilationMode::Partial => { - crate::partial::compile_declare_class_metadata(allocator, &class_metadata) + crate::CompilationMode::Full => { + compile_class_metadata(allocator, core_namespace, &class_metadata) } + crate::CompilationMode::Partial => crate::partial::compile_declare_class_metadata( + allocator, + core_namespace, + &class_metadata, + ), }; let emitter = JsEmitter::new(); format!("{};", emitter.emit_expression(&metadata_expr)) @@ -1462,6 +1467,60 @@ impl<'a> oxc_ast_visit::Visit<'a> for JitCtorParamIdentifiers<'a> { } } +/// Identifier references inside a class's decorators — the class's own, +/// its members', and its constructor parameters'. Downleveled metadata +/// (`ɵsetClassMetadata`, `ctorParameters`, `propDecorators`, `__decorate`) +/// re-emits those references as values, so the imports stay live even +/// though the analyzer only saw them in removable positions — the same +/// imports TypeScript's elision keeps on ngtsc's rewritten AST. +#[derive(Default)] +struct DecoratorIdentifiers<'a>(rustc_hash::FxHashSet<&'a str>); + +impl<'a> oxc_ast_visit::Visit<'a> for DecoratorIdentifiers<'a> { + fn visit_identifier_reference(&mut self, it: &oxc_ast::ast::IdentifierReference<'a>) { + self.0.insert(it.name.as_str()); + } +} + +/// Identifier references inside every decorator on `class` and its +/// members — the pieces `emit_class_metadata` or `__decorate` re-emit. +fn collect_decorator_identifiers<'a>( + class: &'a oxc_ast::ast::Class<'a>, +) -> rustc_hash::FxHashSet<&'a str> { + use oxc_ast_visit::Visit; + let mut identifiers = DecoratorIdentifiers::default(); + for decorator in &class.decorators { + identifiers.visit_decorator(decorator); + } + for element in &class.body.body { + match element { + oxc_ast::ast::ClassElement::MethodDefinition(method) => { + for decorator in &method.decorators { + identifiers.visit_decorator(decorator); + } + for param in &method.value.params.items { + for decorator in ¶m.decorators { + identifiers.visit_decorator(decorator); + } + } + } + oxc_ast::ast::ClassElement::PropertyDefinition(property) => { + for decorator in &property.decorators { + identifiers.visit_decorator(decorator); + } + } + oxc_ast::ast::ClassElement::AccessorProperty(property) => { + for decorator in &property.decorators { + identifiers.visit_decorator(decorator); + } + } + oxc_ast::ast::ClassElement::StaticBlock(_) + | oxc_ast::ast::ClassElement::TSIndexSignature(_) => {} + } + } + identifiers.0 +} + /// Every identifier a file has, like TypeScript's `SourceFile.identifiers`. #[derive(Default)] struct FileIdentifiers(rustc_hash::FxHashSet); @@ -1481,13 +1540,21 @@ impl<'a> oxc_ast_visit::Visit<'a> for FileIdentifiers { } } -/// The namespace JIT synthesis references `@angular/core` through, and whether -/// the file already imports it, like ngc's `ImportManager`: the last -/// `import * as x from '@angular/core'` that isn't type-only is reused; -/// otherwise a new [`JIT_ANGULAR_CORE_NS`] import, renamed `i0_1`, `i0_2`, ... -/// while the file uses that identifier anywhere (an `input as i0` import, a -/// `const i0`, ...), so the added import can't redeclare it. -fn jit_angular_core_namespace(program: &oxc_ast::ast::Program<'_>) -> (String, bool) { +/// Every identifier the file declares or references (TypeScript's +/// `SourceFile.identifiers`), used to keep generated names from colliding. +fn file_identifiers(program: &oxc_ast::ast::Program<'_>) -> rustc_hash::FxHashSet { + let mut identifiers = FileIdentifiers::default(); + oxc_ast_visit::Visit::visit_program(&mut identifiers, program); + identifiers.0 +} + +/// The local name of the last non-type-only `import * as ns from +/// '@angular/core'` in the file, like the namespace reuse in ngc's +/// `reuse_source_file_imports.ts`. +fn reused_angular_core_namespace<'a>( + program: &'a oxc_ast::ast::Program<'a>, + allocator: &'a Allocator, +) -> Option> { for stmt in program.body.iter().rev() { let Statement::ImportDeclaration(import) = stmt else { continue }; if import.source.value != "@angular/core" || import.import_kind.is_type() { @@ -1498,19 +1565,34 @@ fn jit_angular_core_namespace(program: &oxc_ast::ast::Program<'_>) -> (String, b _ => None, }); if let Some(namespace) = namespace { - return (namespace.to_string(), true); + return Some(Ident::from_in(namespace.as_str(), &allocator)); } } + None +} - let mut identifiers = FileIdentifiers::default(); - oxc_ast_visit::Visit::visit_program(&mut identifiers, program); +/// The namespace JIT synthesis references `@angular/core` through, and whether +/// the file already imports it, like ngc's `ImportManager`: the last +/// `import * as x from '@angular/core'` that isn't type-only is reused; +/// otherwise a new [`JIT_ANGULAR_CORE_NS`] import, renamed `i0_1`, `i0_2`, ... +/// while the file uses that identifier anywhere (an `input as i0` import, a +/// `const i0`, ...), so the added import can't redeclare it. +fn jit_angular_core_namespace<'a>( + program: &'a oxc_ast::ast::Program<'a>, + allocator: &'a Allocator, +) -> (Ident<'a>, bool) { + if let Some(namespace) = reused_angular_core_namespace(program, allocator) { + return (namespace, true); + } + + let identifiers = file_identifiers(program); let mut name = JIT_ANGULAR_CORE_NS.to_string(); let mut counter = 1; - while identifiers.0.contains(&name) { + while identifiers.contains(&name) { name = format!("{JIT_ANGULAR_CORE_NS}_{counter}"); counter += 1; } - (name, false) + (Ident::from_in(name, &allocator), false) } /// Inspect a property initializer; if it matches a recognized signal initializer API, @@ -2646,7 +2728,8 @@ fn transform_angular_file_jit( build_import_map(allocator, &parser_ret.program.body, options.resolved_imports.as_ref()); // How synthesized `propDecorators` reference `@angular/core` (`i0.Input`). - let (core_namespace, core_namespace_imported) = jit_angular_core_namespace(&parser_ret.program); + let (core_namespace, core_namespace_imported) = + jit_angular_core_namespace(&parser_ret.program, allocator); // 3. Walk AST to find Angular-decorated classes let mut jit_classes: std::vec::Vec = std::vec::Vec::new(); @@ -2723,7 +2806,7 @@ fn transform_angular_file_jit( &mut resource_counter, &mut resource_imports, &string_consts, - &core_namespace, + core_namespace.as_str(), )); result.component_count += @@ -2756,7 +2839,7 @@ fn transform_angular_file_jit( additional_imports.push_str("import { __decorate } from \"tslib\";\n"); } if !core_namespace_imported - && jit_classes_need_angular_core_namespace(&jit_classes, &core_namespace) + && jit_classes_need_angular_core_namespace(&jit_classes, core_namespace.as_str()) { additional_imports .push_str(&format!("import * as {core_namespace} from \"@angular/core\";\n")); @@ -2915,9 +2998,12 @@ pub fn transform_angular_file( let mut jit_edits: std::vec::Vec = std::vec::Vec::new(); let mut jit_resource_counter: u32 = 0; let mut jit_resource_imports: std::vec::Vec<(String, String)> = std::vec::Vec::new(); - // Lazily computed on the first jit-forced class; the name synthesized - // propDecorators use to reference `@angular/core` (`i0.Input`). - let mut jit_core_namespace: Option<(String, bool)> = None; + // Computed below alongside the file-level namespace registry; the name + // synthesized propDecorators use to reference `@angular/core` + // (`i0.Input`). Defaults to the same name the registry picks for + // `@angular/core`, so jit-forced classes and AOT definitions agree on + // the namespace when a file mixes both. + let mut jit_core_namespace: Option<(Ident, bool)>; // Lazily collected once a nameless jit-forced class needs a name // (top-level bindings + synthesized names). let mut taken_names: Option> = None; @@ -2930,8 +3016,24 @@ pub fn transform_angular_file( // source positions look elidable; accumulated here for the elision pass. let mut import_elision_mut = import_elision; - // File-level namespace registry to collect all module imports - let mut file_namespace_registry = NamespaceRegistry::new(allocator); + // File-level namespace registry to collect all module imports. + // + // Like ngtsc's AOT `ImportManager` + // (`presetImportManagerForceNamespaceImports`): existing + // `import * as ns from "@angular/core"` is never reused — a fresh alias + // is minted and renamed (`i0` → `i0_1`, ...) until it no longer + // collides with any identifier in the file — otherwise the emitted + // `import * as i0` would redeclare a user binding of the same name. + let file_used_names = file_identifiers(&parser_ret.program); + let mut file_namespace_registry = + NamespaceRegistry::with_file_scope(allocator, None, Some(file_used_names)); + jit_core_namespace = Some((file_namespace_registry.angular_core_ns(), false)); + // `.d.ts` members use the same alias: upstream's second ImportManager + // (`IvyDeclarationDtsTransform`) dedupes `i0` against the ORIGINAL + // file's `SourceFile.identifiers` too (declaration.ts:96-98,160; the + // dts transform receives `original.getSourceFile()`, not the emitted + // file), so the alias it picks is identical to the registry's. + let dts_core_namespace = file_namespace_registry.angular_core_ns(); // Shared constant pool index across all components in this file. // This ensures constant names (_c0, _c1, etc.) don't conflict when @@ -3091,9 +3193,10 @@ pub fn transform_angular_file( find_jit_forced_decorator(class, &string_consts) { let core_namespace = jit_core_namespace - .get_or_insert_with(|| jit_angular_core_namespace(&parser_ret.program)) - .0 - .clone(); + .get_or_insert_with(|| { + jit_angular_core_namespace(&parser_ret.program, allocator) + }) + .0; let (is_exported, is_default_export) = match stmt { Statement::ExportDeclaration(_) => (true, false), Statement::ExportDefaultDeclaration(_) => (false, true), @@ -3124,7 +3227,7 @@ pub fn transform_angular_file( &mut jit_resource_counter, &mut jit_resource_imports, &string_consts, - &core_namespace, + core_namespace.as_str(), ); // A co-located @Injectable still compiles: upstream's @@ -3151,6 +3254,7 @@ pub fn transform_angular_file( } if let Some(inj_def) = generate_injectable_definition_from_decorator( &allocator, + file_namespace_registry.angular_core_ns().as_str(), &injectable_metadata, options.compilation_mode, ) { @@ -3164,6 +3268,7 @@ pub fn transform_angular_file( let type_argument_count = class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32); result.dts_declarations.push(dts::generate_injectable_dts( + dts_core_namespace.as_str(), &injectable_metadata, type_argument_count, )); @@ -3194,6 +3299,15 @@ pub fn transform_angular_file( ); jit_classes.push(info); + // Identifiers used only in decorator positions (member, + // `declare` member, and param decorators) survive in the + // emitted __decorate / propDecorators calls, so their imports + // must be kept — the same result TypeScript's elision + // produces on ngtsc's rewritten AST. + for name in collect_decorator_identifiers(class) { + import_elision_mut.preserve(name); + } + // Identifiers used only in constructor parameters (types, // param decorators) survive in the emitted ctorParameters / // __param, so their imports must be kept — the same result @@ -3383,6 +3497,7 @@ pub fn transform_angular_file( } if let Some(inj_def) = generate_injectable_definition_from_decorator( &allocator, + file_namespace_registry.angular_core_ns().as_str(), injectable_metadata, options.compilation_mode, ) { @@ -3510,6 +3625,8 @@ pub fn transform_angular_file( } else { oxc_allocator::Vec::new_in(&allocator) }; + let core_namespace = + file_namespace_registry.angular_core_ns().as_str(); let metadata_expr = match options.compilation_mode { crate::CompilationMode::Partial => { // Partial mode mirrors upstream's @@ -3519,16 +3636,22 @@ pub fn transform_angular_file( // or more → async ɵɵngDeclareClassMetadataAsync. crate::partial::compile_component_declare_class_metadata( &allocator, + core_namespace, &class_metadata, deferred_deps.as_slice(), ) } crate::CompilationMode::Full => { if deferred_deps.is_empty() { - compile_class_metadata(allocator, &class_metadata) + compile_class_metadata( + allocator, + core_namespace, + &class_metadata, + ) } else { compile_component_class_metadata( &allocator, + core_namespace, &class_metadata, Some(deferred_deps.as_slice()), ) @@ -3577,8 +3700,10 @@ pub fn transform_angular_file( class, &string_consts, source, + dts_core_namespace.as_str(), ); result.dts_declarations.push(dts::generate_component_dts( + dts_core_namespace.as_str(), &metadata, type_argument_count, &content_query_names, @@ -3667,6 +3792,7 @@ pub fn transform_angular_file( // Pass shared_pool_index to ensure unique constant names across the file let definitions = generate_directive_definitions( &allocator, + file_namespace_registry.angular_core_ns().as_str(), &directive_metadata, shared_pool_index, options.compilation_mode, @@ -3704,6 +3830,7 @@ pub fn transform_angular_file( } if let Some(inj_def) = generate_injectable_definition_from_decorator( &allocator, + file_namespace_registry.angular_core_ns().as_str(), injectable_metadata, options.compilation_mode, ) { @@ -3719,9 +3846,14 @@ pub fn transform_angular_file( let type_argument_count = class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32); directive_metadata.type_argument_count = type_argument_count; - let accept_types = - crate::directive::input_transform_types(class, &string_consts, source); + let accept_types = crate::directive::input_transform_types( + class, + &string_consts, + source, + dts_core_namespace.as_str(), + ); result.dts_declarations.push(dts::generate_directive_dts( + dts_core_namespace.as_str(), &directive_metadata, has_injectable, &accept_types, @@ -3787,6 +3919,7 @@ pub fn transform_angular_file( // Compile pipe and generate both ɵfac and ɵpipe definitions as external property assignments if let Some(definition) = generate_full_pipe_definition_from_decorator( &allocator, + file_namespace_registry.angular_core_ns().as_str(), &pipe_metadata, options.compilation_mode, ) { @@ -3818,6 +3951,7 @@ pub fn transform_angular_file( } if let Some(inj_def) = generate_injectable_definition_from_decorator( &allocator, + file_namespace_registry.angular_core_ns().as_str(), injectable_metadata, options.compilation_mode, ) { @@ -3833,6 +3967,7 @@ pub fn transform_angular_file( let type_argument_count = class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32); result.dts_declarations.push(dts::generate_pipe_dts( + dts_core_namespace.as_str(), &pipe_metadata, type_argument_count, has_injectable, @@ -3899,6 +4034,7 @@ pub fn transform_angular_file( // Compile NgModule and generate all definitions as external property assignments if let Some(definition) = generate_full_ng_module_definition( &allocator, + file_namespace_registry.angular_core_ns().as_str(), &ng_module_metadata, options.compilation_mode, ) { @@ -3932,6 +4068,7 @@ pub fn transform_angular_file( } if let Some(inj_def) = generate_injectable_definition_from_decorator( &allocator, + file_namespace_registry.angular_core_ns().as_str(), injectable_metadata, options.compilation_mode, ) { @@ -3956,6 +4093,7 @@ pub fn transform_angular_file( let type_argument_count = class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32); result.dts_declarations.push(dts::generate_ng_module_dts( + dts_core_namespace.as_str(), &ng_module_metadata, type_argument_count, has_injectable, @@ -4045,6 +4183,7 @@ pub fn transform_angular_file( class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32); let definition = generate_service_definition_from_decorator( &allocator, + file_namespace_registry.angular_core_ns().as_str(), &service_metadata, type_argument_count, ); @@ -4059,6 +4198,7 @@ pub fn transform_angular_file( ); result.dts_declarations.push(dts::generate_service_dts( + dts_core_namespace.as_str(), &service_metadata, type_argument_count, )); @@ -4122,6 +4262,7 @@ pub fn transform_angular_file( // Compile injectable and generate definitions if let Some(definition) = generate_injectable_definition_from_decorator( &allocator, + file_namespace_registry.angular_core_ns().as_str(), &injectable_metadata, options.compilation_mode, ) { @@ -4139,6 +4280,7 @@ pub fn transform_angular_file( let type_argument_count = class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32); result.dts_declarations.push(dts::generate_injectable_dts( + dts_core_namespace.as_str(), &injectable_metadata, type_argument_count, )); @@ -4200,17 +4342,12 @@ pub fn transform_angular_file( .push_str(&format!("import {{ {} }} from \"tslib\";\n", tslib_names.join(", "))); } let (core_namespace, core_namespace_imported) = jit_core_namespace - .clone() - .unwrap_or_else(|| jit_angular_core_namespace(&parser_ret.program)); + .unwrap_or_else(|| jit_angular_core_namespace(&parser_ret.program, allocator)); jit_needs_core_ns = !core_namespace_imported - && jit_classes_need_angular_core_namespace(&jit_classes, &core_namespace); - // When synthesized references need `@angular/core` under a name other - // than `i0`, it must be imported here — `i0` itself is covered by the - // namespace registry below. - if jit_needs_core_ns && core_namespace != "i0" { - additional_imports - .push_str(&format!("import * as {core_namespace} from \"@angular/core\";\n")); - } + && jit_classes_need_angular_core_namespace(&jit_classes, core_namespace.as_str()); + // The `@angular/core` import itself is emitted by the namespace + // registry below under whatever alias the file allows (`i0` or a + // uniquified `i0_1`, ...), for every alias shape. for (import_name, specifier) in &jit_resource_imports { additional_imports .push_str(&format!("import {} from \"{}\";\n", import_name, specifier)); @@ -4219,11 +4356,9 @@ pub fn transform_angular_file( // The registry's `import * as i0` (and any `i1…` aliases) is only emitted // when something references it: compiled definitions, or ɵfac/ɵprov on a - // jit-forced class, or synthesized `i0.*` propDecorators. - let emit_ns_imports = !class_definitions.is_empty() - || jit_emitted_i0_statics - || (jit_needs_core_ns - && jit_core_namespace.map(|(name, _)| name == "i0").unwrap_or_default()); + // jit-forced class, or synthesized `{ns}.*` propDecorators. + let emit_ns_imports = + !class_definitions.is_empty() || jit_emitted_i0_statics || jit_needs_core_ns; if emit_ns_imports { additional_imports.push_str(&file_namespace_registry.generate_import_statements()); } @@ -4349,6 +4484,27 @@ pub fn transform_angular_file( )); } + // Members can reference namespace aliases the registry assigned to + // other modules (e.g. `typeof i1.SomeDirective` for host directives). + // Record each referenced alias's module so the consumer emits the + // matching `import * as from ""` in the `.d.ts` — + // what upstream's `IvyDeclarationDtsTransform` `ImportManager` adds + // when a declaration references an external type. + if !result.dts_declarations.is_empty() { + let module_by_alias: HashMap = file_namespace_registry + .get_all_modules() + .into_iter() + .map(|(module, alias)| (alias.as_str().to_string(), module.as_str().to_string())) + .collect(); + for declaration in &mut result.dts_declarations { + for (alias, module) in &module_by_alias { + if declaration.members.contains(&format!("{alias}.")) { + declaration.namespace_imports.insert(alias.clone(), module.clone()); + } + } + } + } + // Apply all edits in one pass if options.sourcemap { let (code, map) = apply_edits_with_sourcemap(source, edits, path); @@ -4425,6 +4581,7 @@ struct FullCompilationResult { /// time. See `crate::partial::component` for the shape. fn compile_component_partial<'a>( allocator: &'a Allocator, + core_namespace: &'a str, template: &'a str, metadata: &ComponentMetadata<'a>, view_queries: &[R3QueryMetadata<'a>], @@ -4437,10 +4594,17 @@ fn compile_component_partial<'a>( content_queries, view_queries, }; - let cmp_expr = - crate::partial::compile_declare_component_from_metadata(allocator, metadata, &inputs); - let fac_expr = - crate::partial::component::compile_declare_factory_for_component(allocator, metadata); + let cmp_expr = crate::partial::compile_declare_component_from_metadata( + allocator, + core_namespace, + metadata, + &inputs, + ); + let fac_expr = crate::partial::component::compile_declare_factory_for_component( + allocator, + core_namespace, + metadata, + ); // Detect `@defer` block presence by a cheap string scan — partial // mode skips the template pipeline, but the caller's class-metadata @@ -4489,6 +4653,7 @@ fn compile_component_full<'a>( if matches!(options.compilation_mode, crate::CompilationMode::Partial) { return Ok(compile_component_partial( allocator, + namespace_registry.angular_core_ns().as_str(), template, metadata, &view_queries, @@ -4672,6 +4837,7 @@ fn compile_component_full<'a>( r3_result.nodes, ingest_options, ); + job.core_namespace = namespace_registry.angular_core_ns(); // BEFORE template compilation: Pool attrs const if first selector has attributes. // This matches TypeScript Angular which adds attrs to the pool BEFORE template ingestion. @@ -4694,6 +4860,7 @@ fn compile_component_full<'a>( let fn_name = Some(metadata.class_name.as_str()); Some(create_content_queries_function( &allocator, + job.core_namespace.as_str(), content_queries.as_slice(), fn_name, Some(&mut job.pool), @@ -4707,6 +4874,7 @@ fn compile_component_full<'a>( let fn_name = Some(metadata.class_name.as_str()); Some(create_view_queries_function( &allocator, + job.core_namespace.as_str(), view_queries.as_slice(), fn_name, Some(&mut job.pool), @@ -4724,6 +4892,7 @@ fn compile_component_full<'a>( let template_pool_index = job.pool.next_name_index(); let host_binding_output = compile_component_host_bindings( &allocator, + job.core_namespace.as_str(), metadata, template_pool_index, options.angular_version, @@ -4809,8 +4978,11 @@ fn compile_component_full<'a>( Ident::from_in(file_path, &allocator), ); - // Add the @angular/core namespace dependency (i0) - hmr_meta.add_namespace_dependency(Ident::from("@angular/core"), Ident::from("i0")); + // Add the @angular/core namespace dependency under whatever alias the + // file's registry assigned (`i0`, a uniquified `i0_1`, or a reused + // `import * as ns`). + let core_ns = namespace_registry.get_or_assign(&Ident::from("@angular/core")); + hmr_meta.add_namespace_dependency(Ident::from("@angular/core"), core_ns); // Generate the HMR initializer expression let hmr_expr = compile_hmr_initializer(allocator, &hmr_meta); @@ -4851,7 +5023,8 @@ fn compile_component_full<'a>( .with_line_number(class_line_number); // Compile to IIFE-wrapped expression - let debug_info_expr = compile_class_debug_info(allocator, &debug_info); + let debug_info_expr = + compile_class_debug_info(allocator, job.core_namespace.as_str(), &debug_info); Some(emitter.emit_expression(&debug_info_expr)) } else { @@ -5183,6 +5356,7 @@ pub fn compile_template_to_js_with_options<'a>( if let Some(ref host_input) = options.host { if let Some(host_result) = compile_host_bindings_from_input( &allocator, + "i0", host_input, component_name, options.selector.as_deref(), @@ -5355,7 +5529,11 @@ pub fn compile_template_for_hmr<'a>( let mut const_entries: OxcVec<'a, OutputExpression<'a>> = OxcVec::new_in(&allocator); for const_value in &job.consts { - const_entries.push(const_value_to_expression(allocator, const_value)); + const_entries.push(const_value_to_expression( + allocator, + job.core_namespace.as_str(), + const_value, + )); } let consts_expr = if !job.consts_initializers.is_empty() { @@ -5453,6 +5631,7 @@ struct HostBindingCompilationOutput<'a> { /// Returns None if the component has no host bindings. fn compile_component_host_bindings<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ComponentMetadata<'a>, pool_starting_index: u32, angular_version: Option, @@ -5487,6 +5666,7 @@ fn compile_component_host_bindings<'a>( angular_version, legacy_optional_chaining, ); + job.core_namespace = Ident::from(core_namespace); let result = compile_host_bindings(&mut job); // Get the next pool index after host binding compilation @@ -5805,6 +5985,7 @@ fn pool_selector_attrs<'a>( /// via TransformOptions for isolated template compilation. fn compile_host_bindings_from_input<'a>( allocator: &'a Allocator, + core_namespace: &'a str, host_input: &HostMetadataInput, component_name: &str, selector: Option<&str>, @@ -5843,6 +6024,7 @@ fn compile_host_bindings_from_input<'a>( angular_version, legacy_optional_chaining, ); + job.core_namespace = Ident::from(core_namespace); let result = compile_host_bindings(&mut job); Some(result) @@ -5874,6 +6056,7 @@ pub fn compile_host_bindings_for_linker( let allocator = Allocator::default(); let result = compile_host_bindings_from_input( &allocator, + allocator.alloc_str(core_namespace), host_input, component_name, selector, @@ -6012,6 +6195,7 @@ pub fn compile_template_for_linker<'a>( r3_result.nodes, ingest_options, ); + job.core_namespace = Ident::from_in(core_namespace, &allocator); let compiled = compile_template(&mut job); @@ -6028,7 +6212,11 @@ pub fn compile_template_for_linker<'a>( let consts_js = if !job.consts.is_empty() { let mut const_entries: OxcVec<'a, OutputExpression<'a>> = OxcVec::new_in(&allocator); for const_value in &job.consts { - const_entries.push(const_value_to_expression(allocator, const_value)); + const_entries.push(const_value_to_expression( + allocator, + allocator.alloc_str(core_namespace), + const_value, + )); } let consts_expr = if !job.consts_initializers.is_empty() { diff --git a/crates/oxc_angular_compiler/src/directive/compiler.rs b/crates/oxc_angular_compiler/src/directive/compiler.rs index 1e43842b0..3e102173b 100644 --- a/crates/oxc_angular_compiler/src/directive/compiler.rs +++ b/crates/oxc_angular_compiler/src/directive/compiler.rs @@ -59,30 +59,43 @@ pub struct DirectiveCompileResult<'a> { /// where the previous directive's pool left off. pub fn compile_directive<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3DirectiveMetadata<'a>, pool_starting_index: u32, angular_version: Option, ) -> DirectiveCompileResult<'a> { - compile_directive_from_metadata(allocator, metadata, pool_starting_index, angular_version) + compile_directive_from_metadata( + allocator, + core_namespace, + metadata, + pool_starting_index, + angular_version, + ) } /// Internal implementation of directive compilation. pub fn compile_directive_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3DirectiveMetadata<'a>, pool_starting_index: u32, angular_version: Option, ) -> DirectiveCompileResult<'a> { // Build the base directive fields, passing pool_starting_index for host bindings - let (definition_map, next_pool_index, host_declarations) = - build_base_directive_fields(allocator, metadata, pool_starting_index, angular_version); + let (definition_map, next_pool_index, host_declarations) = build_base_directive_fields( + allocator, + core_namespace, + metadata, + pool_starting_index, + angular_version, + ); // Add features let mut definition_map = definition_map; - add_features(allocator, metadata, &mut definition_map); + add_features(allocator, core_namespace, metadata, &mut definition_map); // Create the expression: ɵɵdefineDirective(definitionMap) - let expression = create_define_directive_call(allocator, definition_map); + let expression = create_define_directive_call(allocator, core_namespace, definition_map); // Convert host binding declarations to statements let mut statements = Vec::new_in(&allocator); @@ -102,6 +115,7 @@ pub fn compile_directive_from_metadata<'a>( /// contains any pooled constants (pure functions) from host binding compilation. fn build_base_directive_fields<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3DirectiveMetadata<'a>, pool_starting_index: u32, angular_version: Option, @@ -130,6 +144,7 @@ fn build_base_directive_fields<'a>( // are not pooled. For components, pool is passed from component compilation. let content_queries_fn = super::query::create_content_queries_function( &allocator, + core_namespace, &metadata.queries, Some(metadata.name.as_str()), None, @@ -148,6 +163,7 @@ fn build_base_directive_fields<'a>( // are not pooled. For components, pool is passed from component compilation. let view_queries_fn = super::query::create_view_queries_function( &allocator, + core_namespace, &metadata.view_queries, Some(metadata.name.as_str()), None, @@ -165,6 +181,7 @@ fn build_base_directive_fields<'a>( if metadata.host.has_bindings() { if let Some((result, new_pool_index)) = compile_directive_host_bindings( allocator, + core_namespace, metadata, pool_starting_index, angular_version, @@ -272,6 +289,7 @@ fn build_base_directive_fields<'a>( /// Corresponds to `addFeatures()` in Angular's compiler. fn add_features<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3DirectiveMetadata<'a>, definition_map: &mut Vec<'a, LiteralMapEntry<'a>>, ) { @@ -281,26 +299,47 @@ fn add_features<'a>( if let Some(providers) = &metadata.providers { let mut args = Vec::new_in(&allocator); args.push(providers.clone_in(allocator)); - features.push(create_feature_call(allocator, Identifiers::PROVIDERS_FEATURE, args)); + features.push(create_feature_call( + allocator, + core_namespace, + Identifiers::PROVIDERS_FEATURE, + args, + )); } // HostDirectivesFeature (before InheritDefinitionFeature) if !metadata.host_directives.is_empty() { - let host_directives_arg = - create_host_directives_feature_arg(allocator, &metadata.host_directives); + let host_directives_arg = create_host_directives_feature_arg( + allocator, + core_namespace, + &metadata.host_directives, + ); let mut args = Vec::new_in(&allocator); args.push(host_directives_arg); - features.push(create_feature_call(allocator, Identifiers::HOST_DIRECTIVES_FEATURE, args)); + features.push(create_feature_call( + allocator, + core_namespace, + Identifiers::HOST_DIRECTIVES_FEATURE, + args, + )); } // InheritDefinitionFeature if metadata.uses_inheritance { - features.push(create_feature_ref(allocator, Identifiers::INHERIT_DEFINITION_FEATURE)); + features.push(create_feature_ref( + allocator, + core_namespace, + Identifiers::INHERIT_DEFINITION_FEATURE, + )); } // NgOnChangesFeature if metadata.uses_on_changes { - features.push(create_feature_ref(allocator, Identifiers::NG_ON_CHANGES_FEATURE)); + features.push(create_feature_ref( + allocator, + core_namespace, + Identifiers::NG_ON_CHANGES_FEATURE, + )); } if !features.is_empty() { @@ -318,14 +357,15 @@ fn add_features<'a>( /// Creates the `ɵɵdefineDirective({...})` call expression. fn create_define_directive_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, definition_map: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { - // Create i0.ɵɵdefineDirective + // Create ns.ɵɵdefineDirective let define_directive_fn = OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -563,6 +603,7 @@ pub fn create_outputs_literal<'a>( /// next available constant pool index after host binding compilation. fn compile_directive_host_bindings<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3DirectiveMetadata<'a>, pool_starting_index: u32, angular_version: Option, @@ -591,6 +632,7 @@ fn compile_directive_host_bindings<'a>( angular_version, None, ); + job.core_namespace = Ident::from(core_namespace); let result = compile_host_bindings(&mut job); // Get the next pool index after host binding compilation @@ -806,9 +848,10 @@ fn parse_event_target(event_name: &str) -> (&str, Option<&str>) { } } -/// Creates a feature call expression: i0.FeatureName(args) +/// Creates a feature call expression: ns.FeatureName(args) fn create_feature_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, feature_name: &'static str, args: Vec<'a, OutputExpression<'a>>, ) -> OutputExpression<'a> { @@ -816,7 +859,7 @@ fn create_feature_call<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -840,16 +883,17 @@ fn create_feature_call<'a>( )) } -/// Creates a feature reference expression: i0.FeatureName +/// Creates a feature reference expression: ns.FeatureName fn create_feature_ref<'a>( allocator: &'a Allocator, + core_namespace: &'a str, feature_name: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -865,6 +909,7 @@ fn create_feature_ref<'a>( /// Creates the host directives feature argument. fn create_host_directives_feature_arg<'a>( allocator: &'a Allocator, + core_namespace: &'a str, host_directives: &[R3HostDirectiveMetadata<'a>], ) -> OutputExpression<'a> { let mut items = Vec::new_in(&allocator); @@ -903,7 +948,7 @@ fn create_host_directives_feature_arg<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -1018,7 +1063,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let result = compile_directive(&allocator, &metadata, 0, None); + let result = compile_directive(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -1062,7 +1107,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let result = compile_directive(&allocator, &metadata, 0, None); + let result = compile_directive(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -1315,7 +1360,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let result = compile_directive(&allocator, &metadata, 0, None); + let result = compile_directive(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -1357,7 +1402,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let result = compile_directive(&allocator, &metadata, 0, None); + let result = compile_directive(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -1397,7 +1442,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let result = compile_directive(&allocator, &metadata, 0, None); + let result = compile_directive(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -1452,7 +1497,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let result = compile_directive(&allocator, &metadata, 0, None); + let result = compile_directive(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -1513,7 +1558,7 @@ mod tests { host_directives, }; - let result = compile_directive(&allocator, &metadata, 0, None); + let result = compile_directive(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); let normalized = output.replace([' ', '\n', '\t'], ""); @@ -1578,7 +1623,7 @@ mod tests { host_directives, }; - let result = compile_directive(&allocator, &metadata, 0, None); + let result = compile_directive(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); let normalized = output.replace([' ', '\n', '\t'], ""); diff --git a/crates/oxc_angular_compiler/src/directive/definition.rs b/crates/oxc_angular_compiler/src/directive/definition.rs index df8bdca65..e5330b57e 100644 --- a/crates/oxc_angular_compiler/src/directive/definition.rs +++ b/crates/oxc_angular_compiler/src/directive/definition.rs @@ -74,6 +74,7 @@ pub struct DirectiveDefinitions<'a> { /// ``` pub fn generate_directive_definitions<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3DirectiveMetadata<'a>, pool_starting_index: u32, compilation_mode: CompilationMode, @@ -86,9 +87,14 @@ pub fn generate_directive_definitions<'a>( // This ensures namespace indices (i0, i1, i2, ...) are assigned in the same order. match compilation_mode { CompilationMode::Full => { - let fac_definition = generate_fac_definition(allocator, metadata); - let (dir_definition, next_pool_index) = - generate_dir_definition(allocator, metadata, pool_starting_index, angular_version); + let fac_definition = generate_fac_definition(allocator, core_namespace, metadata); + let (dir_definition, next_pool_index) = generate_dir_definition( + allocator, + core_namespace, + metadata, + pool_starting_index, + angular_version, + ); DirectiveDefinitions { dir_definition, fac_definition, next_pool_index } } CompilationMode::Partial => { @@ -97,8 +103,10 @@ pub fn generate_directive_definitions<'a>( // constants are emitted here. `angular_version` is irrelevant // because partial mode emits the verbatim selector and lets // the linker pick up the consumer's runtime version. - let fac_definition = compile_declare_factory_for_directive(allocator, metadata); - let dir_definition = compile_declare_directive_from_metadata(allocator, metadata); + let fac_definition = + compile_declare_factory_for_directive(allocator, core_namespace, metadata); + let dir_definition = + compile_declare_directive_from_metadata(allocator, core_namespace, metadata); DirectiveDefinitions { dir_definition, fac_definition, @@ -125,11 +133,18 @@ pub fn generate_directive_definitions<'a>( /// Returns a tuple of (expression, next_pool_index). fn generate_dir_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3DirectiveMetadata<'a>, pool_starting_index: u32, angular_version: Option, ) -> (OutputExpression<'a>, u32) { - let result = compile_directive(allocator, metadata, pool_starting_index, angular_version); + let result = compile_directive( + allocator, + core_namespace, + metadata, + pool_starting_index, + angular_version, + ); (result.expression, result.next_pool_index) } @@ -154,6 +169,7 @@ fn generate_dir_definition<'a>( /// Ported from: `packages/compiler/src/render3/r3_factory.ts` fn generate_fac_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3DirectiveMetadata<'a>, ) -> OutputExpression<'a> { // Factory function name: DirectiveName_Factory @@ -207,7 +223,7 @@ fn generate_fac_definition<'a>( }); // Compile the factory function - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = compile_factory_function(allocator, core_namespace, &factory_meta, factory_name); result.expression } @@ -252,8 +268,14 @@ mod tests { let allocator = Allocator::default(); let metadata = create_test_metadata(&allocator); - let definitions = - generate_directive_definitions(&allocator, &metadata, 0, CompilationMode::Full, None); + let definitions = generate_directive_definitions( + &allocator, + "i0", + &metadata, + 0, + CompilationMode::Full, + None, + ); let emitter = JsEmitter::new(); @@ -276,7 +298,7 @@ mod tests { let allocator = Allocator::default(); let metadata = create_test_metadata(&allocator); - let fac = generate_fac_definition(&allocator, &metadata); + let fac = generate_fac_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&fac); @@ -320,7 +342,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let fac = generate_fac_definition(&allocator, &metadata); + let fac = generate_fac_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&fac); @@ -359,7 +381,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let fac = generate_fac_definition(&allocator, &metadata); + let fac = generate_fac_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&fac); @@ -408,7 +430,7 @@ mod tests { host_directives: Vec::new_in(&&allocator), }; - let fac = generate_fac_definition(&allocator, &metadata); + let fac = generate_fac_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&fac); @@ -423,7 +445,7 @@ mod tests { let allocator = Allocator::default(); let metadata = create_test_metadata(&allocator); - let (dir, _next_pool_index) = generate_dir_definition(&allocator, &metadata, 0, None); + let (dir, _next_pool_index) = generate_dir_definition(&allocator, "i0", &metadata, 0, None); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&dir); @@ -473,8 +495,14 @@ mod tests { }; // Compile first directive - let definitions1 = - generate_directive_definitions(&allocator, &metadata1, 0, CompilationMode::Full, None); + let definitions1 = generate_directive_definitions( + &allocator, + "i0", + &metadata1, + 0, + CompilationMode::Full, + None, + ); let next_index = definitions1.next_pool_index; // The next_pool_index should be 0 when no constants are pooled @@ -513,6 +541,7 @@ mod tests { // Compile second directive starting from where first left off let definitions2 = generate_directive_definitions( &allocator, + "i0", &metadata2, next_index, CompilationMode::Full, diff --git a/crates/oxc_angular_compiler/src/directive/dts_type.rs b/crates/oxc_angular_compiler/src/directive/dts_type.rs index 0c016bb06..f26231069 100644 --- a/crates/oxc_angular_compiler/src/directive/dts_type.rs +++ b/crates/oxc_angular_compiler/src/directive/dts_type.rs @@ -25,6 +25,10 @@ use crate::output::emitter::format_number_like_js; pub(crate) struct TypePrinter<'s, 'a> { pub scope: &'s FileScope<'a>, pub source: &'a str, + /// The alias `@angular/core` names are printed through — the namespace + /// the file's `ImportManager` picked (`i0`, or `i0_1`, … when a user + /// binding collided), like ngtsc's `TypeEmitter` qualifier. + pub core_ns: &'a str, /// Set when the type references a module other than `@angular/core`. /// ngtsc would add an `import * as iN` for it; oxc emits `unknown` instead, /// since aliases numbered per source file can't be merged into bundled @@ -212,7 +216,7 @@ impl<'a> TypePrinter<'_, 'a> { let mut parts = std::vec::Vec::new(); entity_parts(name, &mut parts)?; Some(match self.resolve_aliases(parts)? { - Resolved::Core(rest) => format!("i0{rest}"), + Resolved::Core(rest) => format!("{}{rest}", self.core_ns), Resolved::OtherModule(parts) => { self.other_module = true; parts.join(".") @@ -283,8 +287,8 @@ impl<'a> TypePrinter<'_, 'a> { fn value_name(&mut self, head: &str, rest: &str) -> String { match self.scope.import(head) { Some(import) if import.module == "@angular/core" => match import.imported { - Some(imported) => format!("i0.{imported}{rest}"), - None => format!("i0{rest}"), + Some(imported) => format!("{}.{imported}{rest}", self.core_ns), + None => format!("{}{rest}", self.core_ns), }, Some(_) => { self.other_module = true; @@ -465,7 +469,7 @@ impl<'a> TypePrinter<'_, 'a> { fn computed_name(&mut self, parts: std::vec::Vec<&str>) -> Option { let written = parts.join("."); Some(match self.resolve_aliases(parts)? { - Resolved::Core(rest) => format!("i0{rest}"), + Resolved::Core(rest) => format!("{}{rest}", self.core_ns), Resolved::OtherModule(_) => { self.other_module = true; written diff --git a/crates/oxc_angular_compiler/src/directive/evaluator.rs b/crates/oxc_angular_compiler/src/directive/evaluator.rs index 0727cd814..e718e1b78 100644 --- a/crates/oxc_angular_compiler/src/directive/evaluator.rs +++ b/crates/oxc_angular_compiler/src/directive/evaluator.rs @@ -3504,6 +3504,7 @@ pub fn input_transform_types<'a>( class: &'a Class<'a>, consts: &super::StringConsts<'a>, source: &'a str, + core_namespace: &'a str, ) -> HashMap { let evaluator = Evaluator::new(consts); let scope = consts.scope(); @@ -3565,8 +3566,12 @@ pub fn input_transform_types<'a>( }; let ty = match def.first_param_type().ok()? { Some(ty) => { - let mut printer = - super::dts_type::TypePrinter { scope, source, other_module: false }; + let mut printer = super::dts_type::TypePrinter { + scope, + source, + core_ns: core_namespace, + other_module: false, + }; let ty = printer.print(ty); if printer.other_module { return None; diff --git a/crates/oxc_angular_compiler/src/directive/query.rs b/crates/oxc_angular_compiler/src/directive/query.rs index 023df5ce9..559400445 100644 --- a/crates/oxc_angular_compiler/src/directive/query.rs +++ b/crates/oxc_angular_compiler/src/directive/query.rs @@ -101,13 +101,17 @@ mod render_flags { // Helper Functions // ============================================================================ -/// Create i0.identifier reference. -fn import_expr<'a>(allocator: &'a Allocator, identifier: &'static str) -> OutputExpression<'a> { +/// Create ns.identifier reference. +fn import_expr<'a>( + allocator: &'a Allocator, + core_namespace: &'a str, + identifier: &'static str, +) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -354,6 +358,7 @@ enum MaybeAdvanceStatement<'a> { /// ɵɵqueryAdvance(count) call. fn collapse_advance_statements<'a>( allocator: &'a Allocator, + core_namespace: &'a str, statements: Vec<'a, MaybeAdvanceStatement<'a>>, ) -> Vec<'a, OutputStatement<'a>> { let mut result = Vec::new_in(&allocator); @@ -367,7 +372,11 @@ fn collapse_advance_statements<'a>( if *count > 1 { args.push(literal_number(allocator, *count)); } - let call = call_fn(allocator, import_expr(allocator, Identifiers::QUERY_ADVANCE), args); + let call = call_fn( + allocator, + import_expr(allocator, core_namespace, Identifiers::QUERY_ADVANCE), + args, + ); result.push(expr_stmt(allocator, call)); *count = 0; } @@ -443,6 +452,7 @@ impl TempAllocator { /// ``` pub fn create_view_queries_function<'a>( allocator: &'a Allocator, + core_namespace: &'a str, view_queries: &[R3QueryMetadata<'a>], name: Option<&str>, pool: Option<&mut ConstantPool<'a>>, @@ -497,7 +507,8 @@ pub fn create_view_queries_function<'a>( if !chain_emit { // Pre-v21.0.4: one statement per query, no chaining. - let call = call_fn(allocator, import_expr(allocator, identifier), params); + let call = + call_fn(allocator, import_expr(allocator, core_namespace, identifier), params); create_statements.push(expr_stmt(allocator, call)); } else { // Flush the pending chain if this query's signal-ness differs @@ -510,7 +521,7 @@ pub fn create_view_queries_function<'a>( Some(prev) => prev, None => { current_chain_is_signal = query.is_signal; - import_expr(allocator, identifier) + import_expr(allocator, core_namespace, identifier) } }; current_chain = Some(call_fn(allocator, callee, params)); @@ -528,7 +539,7 @@ pub fn create_view_queries_function<'a>( // _t = ɵɵloadQuery() let load_query = call_fn( &allocator, - import_expr(allocator, Identifiers::LOAD_QUERY), + import_expr(allocator, core_namespace, Identifiers::LOAD_QUERY), Vec::new_in(&allocator), ); let temp_set = OutputExpression::BinaryOperator(Box::new_in( @@ -546,7 +557,7 @@ pub fn create_view_queries_function<'a>( refresh_args.push(temp_set); let refresh = call_fn( &allocator, - import_expr(allocator, Identifiers::QUERY_REFRESH), + import_expr(allocator, core_namespace, Identifiers::QUERY_REFRESH), refresh_args, ); @@ -612,7 +623,7 @@ pub fn create_view_queries_function<'a>( } // Collapse advances and add update statements - for stmt in collapse_advance_statements(allocator, update_statements) { + for stmt in collapse_advance_statements(allocator, core_namespace, update_statements) { final_update_statements.push(stmt); } @@ -666,6 +677,7 @@ pub fn create_view_queries_function<'a>( /// ``` pub fn create_content_queries_function<'a>( allocator: &'a Allocator, + core_namespace: &'a str, queries: &[R3QueryMetadata<'a>], name: Option<&str>, pool: Option<&mut ConstantPool<'a>>, @@ -720,7 +732,8 @@ pub fn create_content_queries_function<'a>( }; if !chain_emit { - let call = call_fn(allocator, import_expr(allocator, identifier), params); + let call = + call_fn(allocator, import_expr(allocator, core_namespace, identifier), params); create_statements.push(expr_stmt(allocator, call)); } else { if current_chain.is_some() && current_chain_is_signal != query.is_signal { @@ -730,7 +743,7 @@ pub fn create_content_queries_function<'a>( Some(prev) => prev, None => { current_chain_is_signal = query.is_signal; - import_expr(allocator, identifier) + import_expr(allocator, core_namespace, identifier) } }; current_chain = Some(call_fn(allocator, callee, params)); @@ -744,7 +757,7 @@ pub fn create_content_queries_function<'a>( let load_query = call_fn( &allocator, - import_expr(allocator, Identifiers::LOAD_QUERY), + import_expr(allocator, core_namespace, Identifiers::LOAD_QUERY), Vec::new_in(&allocator), ); let temp_set = OutputExpression::BinaryOperator(Box::new_in( @@ -761,7 +774,7 @@ pub fn create_content_queries_function<'a>( refresh_args.push(temp_set); let refresh = call_fn( &allocator, - import_expr(allocator, Identifiers::QUERY_REFRESH), + import_expr(allocator, core_namespace, Identifiers::QUERY_REFRESH), refresh_args, ); @@ -823,7 +836,7 @@ pub fn create_content_queries_function<'a>( ))); } - for stmt in collapse_advance_statements(allocator, update_statements) { + for stmt in collapse_advance_statements(allocator, core_namespace, update_statements) { final_update_statements.push(stmt); } @@ -880,8 +893,14 @@ mod tests { let allocator = Allocator::default(); let queries: &[R3QueryMetadata<'_>] = &[]; - let result = - create_view_queries_function(&allocator, queries, Some("TestComponent"), None, None); + let result = create_view_queries_function( + &allocator, + "i0", + queries, + Some("TestComponent"), + None, + None, + ); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result); @@ -896,8 +915,14 @@ mod tests { let allocator = Allocator::default(); let queries: &[R3QueryMetadata<'_>] = &[]; - let result = - create_content_queries_function(&allocator, queries, Some("TestDirective"), None, None); + let result = create_content_queries_function( + &allocator, + "i0", + queries, + Some("TestDirective"), + None, + None, + ); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result); @@ -930,8 +955,14 @@ mod tests { }; let queries = [query]; - let result = - create_view_queries_function(&allocator, &queries, Some("TestComponent"), None, None); + let result = create_view_queries_function( + &allocator, + "i0", + &queries, + Some("TestComponent"), + None, + None, + ); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result); @@ -975,6 +1006,7 @@ mod tests { let queries = [query]; let result = create_content_queries_function( &allocator, + "i0", &queries, Some("TestDirective"), None, @@ -1044,6 +1076,7 @@ mod tests { // separate statements instead — see `supports_chained_queries`. let result = create_view_queries_function( &allocator, + "i0", &queries, Some("TestComponent"), None, @@ -1114,6 +1147,7 @@ mod tests { let queries = [query1, query2]; let result = create_view_queries_function( &allocator, + "i0", &queries, Some("TestComponent"), None, @@ -1179,6 +1213,7 @@ mod tests { let queries = [query1, query2]; let result = create_content_queries_function( &allocator, + "i0", &queries, Some("TestDirective"), None, @@ -1228,8 +1263,14 @@ mod tests { }; let queries = [query]; - let result = - create_view_queries_function(&allocator, &queries, Some("TestComponent"), None, None); + let result = create_view_queries_function( + &allocator, + "i0", + &queries, + Some("TestComponent"), + None, + None, + ); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result); diff --git a/crates/oxc_angular_compiler/src/dts.rs b/crates/oxc_angular_compiler/src/dts.rs index fb8d238cd..4f50cd203 100644 --- a/crates/oxc_angular_compiler/src/dts.rs +++ b/crates/oxc_angular_compiler/src/dts.rs @@ -4,11 +4,15 @@ //! to `.d.ts` files for Angular library builds. These declarations enable //! Angular's template type-checking system to work with pre-compiled libraries. //! -//! The generated declarations use `i0` as the namespace alias for `@angular/core`, -//! matching Angular's convention. Consumers must ensure their `.d.ts` files include: -//! ```typescript -//! import * as i0 from "@angular/core"; -//! ``` +//! The generated declarations reference `@angular/core` through +//! `core_namespace` — `i0` uniquified (`i0_1`, `i0_2`, …) against every +//! identifier in the ORIGINAL `.ts` file. That mirrors upstream's +//! `IvyDeclarationDtsTransform`, whose `ImportManager` +//! (`presetImportManagerForceNamespaceImports`) dedupes against the +//! original `SourceFile.identifiers` and never reuses an existing +//! namespace import — so the alias always matches the JS emit's. +//! Consumers must ensure their `.d.ts` files include a matching +//! `import * as from "@angular/core";`. //! //! Reference: Angular's `IvyDeclarationDtsTransform` in //! `packages/compiler-cli/src/ngtsc/transform/src/declaration.ts` @@ -39,6 +43,16 @@ pub struct DtsDeclaration { /// static ɵcmp: i0.ɵɵComponentDeclaration; /// ``` pub members: String, + /// Module specifier for every namespace alias the members reference + /// (alias → specifier: `i0` → `"@angular/core"` for `i0.ɵɵCmp` and + /// `i0.Signal`, `i1` → `"./dep"` for `typeof i1.SomeDirective` + /// host-directive references). Consumers must emit + /// `import * as from ""` for each — or rewrite the + /// heads to their own canonical aliases — matching what upstream's + /// `IvyDeclarationDtsTransform` `ImportManager` produces. + /// Populated by the transform, which knows each alias's module; + /// empty when the members reference no namespace at all. + pub namespace_imports: HashMap, } // ============================================================================= @@ -48,9 +62,10 @@ pub struct DtsDeclaration { /// Generate `.d.ts` declarations for a `@Component` class. /// /// Produces: -/// - `static ɵfac: i0.ɵɵFactoryDeclaration;` -/// - `static ɵcmp: i0.ɵɵComponentDeclaration;` +/// - `static ɵfac: {core_namespace}.ɵɵFactoryDeclaration;` +/// - `static ɵcmp: {core_namespace}.ɵɵComponentDeclaration;` pub fn generate_component_dts( + core_namespace: &str, metadata: &ComponentMetadata, type_argument_count: u32, content_query_names: &[String], @@ -65,8 +80,9 @@ pub fn generate_component_dts( let ctor_deps_type = generate_ctor_deps_type_from_component_deps( metadata.constructor_deps.as_ref().map(|v| v.as_slice() as &[R3DependencyMetadata]), ); - let fac = - format!("static ɵfac: i0.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;"); + let fac = format!( + "static ɵfac: {core_namespace}.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;" + ); // ɵcmp declaration let selector = match &metadata.selector { @@ -146,20 +162,28 @@ pub fn generate_component_dts( type_params.push("true".to_string()); } - let cmp = format!("static ɵcmp: i0.ɵɵComponentDeclaration<{}>;", type_params.join(", ")); + let cmp = format!( + "static ɵcmp: {core_namespace}.ɵɵComponentDeclaration<{}>;", + type_params.join(", ") + ); let mut members = format!("{fac}\n{cmp}"); // Add ɵprov if @Injectable is also present if has_injectable { - members - .push_str(&format!("\nstatic ɵprov: i0.ɵɵInjectableDeclaration<{type_with_params}>;")); + members.push_str(&format!( + "\nstatic ɵprov: {core_namespace}.ɵɵInjectableDeclaration<{type_with_params}>;" + )); } // Add ngAcceptInputType_* fields for non-signal inputs with transform functions generate_input_transform_fields(&metadata.inputs, accept_types, &mut members); - DtsDeclaration { class_name: class_name.to_string(), members } + DtsDeclaration { + class_name: class_name.to_string(), + members, + namespace_imports: HashMap::new(), + } } // ============================================================================= @@ -169,9 +193,10 @@ pub fn generate_component_dts( /// Generate `.d.ts` declarations for a `@Directive` class. /// /// Produces: -/// - `static ɵfac: i0.ɵɵFactoryDeclaration;` -/// - `static ɵdir: i0.ɵɵDirectiveDeclaration;` +/// - `static ɵfac: {core_namespace}.ɵɵFactoryDeclaration;` +/// - `static ɵdir: {core_namespace}.ɵɵDirectiveDeclaration;` pub fn generate_directive_dts( + core_namespace: &str, metadata: &R3DirectiveMetadata, has_injectable: bool, accept_types: &HashMap, @@ -182,8 +207,9 @@ pub fn generate_directive_dts( // ɵfac declaration let ctor_deps_type = generate_ctor_deps_type_from_factory_deps(metadata.deps.as_ref().map(|v| v.as_slice())); - let fac = - format!("static ɵfac: i0.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;"); + let fac = format!( + "static ɵfac: {core_namespace}.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;" + ); // ɵdir declaration let selector = match &metadata.selector { @@ -252,19 +278,27 @@ pub fn generate_directive_dts( type_params.push("true".to_string()); } - let dir = format!("static ɵdir: i0.ɵɵDirectiveDeclaration<{}>;", type_params.join(", ")); + let dir = format!( + "static ɵdir: {core_namespace}.ɵɵDirectiveDeclaration<{}>;", + type_params.join(", ") + ); let mut members = format!("{fac}\n{dir}"); if has_injectable { - members - .push_str(&format!("\nstatic ɵprov: i0.ɵɵInjectableDeclaration<{type_with_params}>;")); + members.push_str(&format!( + "\nstatic ɵprov: {core_namespace}.ɵɵInjectableDeclaration<{type_with_params}>;" + )); } // Add ngAcceptInputType_* fields for non-signal inputs with transform functions generate_input_transform_fields(&metadata.inputs, accept_types, &mut members); - DtsDeclaration { class_name: class_name.to_string(), members } + DtsDeclaration { + class_name: class_name.to_string(), + members, + namespace_imports: HashMap::new(), + } } // ============================================================================= @@ -274,9 +308,10 @@ pub fn generate_directive_dts( /// Generate `.d.ts` declarations for a `@Pipe` class. /// /// Produces: -/// - `static ɵfac: i0.ɵɵFactoryDeclaration;` -/// - `static ɵpipe: i0.ɵɵPipeDeclaration;` +/// - `static ɵfac: {core_namespace}.ɵɵFactoryDeclaration;` +/// - `static ɵpipe: {core_namespace}.ɵɵPipeDeclaration;` pub fn generate_pipe_dts( + core_namespace: &str, metadata: &PipeMetadata, type_argument_count: u32, has_injectable: bool, @@ -287,8 +322,9 @@ pub fn generate_pipe_dts( // ɵfac declaration let ctor_deps_type = generate_ctor_deps_type_from_factory_deps(metadata.deps.as_ref().map(|v| v.as_slice())); - let fac = - format!("static ɵfac: i0.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;"); + let fac = format!( + "static ɵfac: {core_namespace}.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;" + ); // ɵpipe declaration let pipe_name = match &metadata.pipe_name { @@ -299,17 +335,22 @@ pub fn generate_pipe_dts( let is_standalone = if metadata.standalone { "true" } else { "false" }; let pipe = format!( - "static ɵpipe: i0.ɵɵPipeDeclaration<{type_with_params}, {pipe_name}, {is_standalone}>;" + "static ɵpipe: {core_namespace}.ɵɵPipeDeclaration<{type_with_params}, {pipe_name}, {is_standalone}>;" ); let mut members = format!("{fac}\n{pipe}"); if has_injectable { - members - .push_str(&format!("\nstatic ɵprov: i0.ɵɵInjectableDeclaration<{type_with_params}>;")); + members.push_str(&format!( + "\nstatic ɵprov: {core_namespace}.ɵɵInjectableDeclaration<{type_with_params}>;" + )); } - DtsDeclaration { class_name: class_name.to_string(), members } + DtsDeclaration { + class_name: class_name.to_string(), + members, + namespace_imports: HashMap::new(), + } } // ============================================================================= @@ -319,10 +360,11 @@ pub fn generate_pipe_dts( /// Generate `.d.ts` declarations for a `@NgModule` class. /// /// Produces: -/// - `static ɵfac: i0.ɵɵFactoryDeclaration;` -/// - `static ɵmod: i0.ɵɵNgModuleDeclaration;` -/// - `static ɵinj: i0.ɵɵInjectorDeclaration;` +/// - `static ɵfac: {core_namespace}.ɵɵFactoryDeclaration;` +/// - `static ɵmod: {core_namespace}.ɵɵNgModuleDeclaration;` +/// - `static ɵinj: {core_namespace}.ɵɵInjectorDeclaration;` pub fn generate_ng_module_dts( + core_namespace: &str, metadata: &NgModuleMetadata, type_argument_count: u32, has_injectable: bool, @@ -333,8 +375,9 @@ pub fn generate_ng_module_dts( // ɵfac declaration let ctor_deps_type = generate_ctor_deps_type_from_factory_deps(metadata.deps.as_ref().map(|v| v.as_slice())); - let fac = - format!("static ɵfac: i0.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;"); + let fac = format!( + "static ɵfac: {core_namespace}.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;" + ); // ɵmod declaration - uses typeof references for declarations/imports/exports let declarations_type = if metadata.declarations.is_empty() { @@ -380,20 +423,25 @@ pub fn generate_ng_module_dts( }; let mod_decl = format!( - "static ɵmod: i0.ɵɵNgModuleDeclaration<{type_with_params}, {declarations_type}, {imports_type}, {exports_type}>;" + "static ɵmod: {core_namespace}.ɵɵNgModuleDeclaration<{type_with_params}, {declarations_type}, {imports_type}, {exports_type}>;" ); // ɵinj declaration - let inj = format!("static ɵinj: i0.ɵɵInjectorDeclaration<{type_with_params}>;"); + let inj = format!("static ɵinj: {core_namespace}.ɵɵInjectorDeclaration<{type_with_params}>;"); let mut members = format!("{fac}\n{mod_decl}\n{inj}"); if has_injectable { - members - .push_str(&format!("\nstatic ɵprov: i0.ɵɵInjectableDeclaration<{type_with_params}>;")); + members.push_str(&format!( + "\nstatic ɵprov: {core_namespace}.ɵɵInjectableDeclaration<{type_with_params}>;" + )); } - DtsDeclaration { class_name: class_name.to_string(), members } + DtsDeclaration { + class_name: class_name.to_string(), + members, + namespace_imports: HashMap::new(), + } } // ============================================================================= @@ -403,9 +451,10 @@ pub fn generate_ng_module_dts( /// Generate `.d.ts` declarations for a standalone `@Injectable` class. /// /// Produces: -/// - `static ɵfac: i0.ɵɵFactoryDeclaration;` -/// - `static ɵprov: i0.ɵɵInjectableDeclaration;` +/// - `static ɵfac: {core_namespace}.ɵɵFactoryDeclaration;` +/// - `static ɵprov: {core_namespace}.ɵɵInjectableDeclaration;` pub fn generate_injectable_dts( + core_namespace: &str, metadata: &InjectableMetadata, type_argument_count: u32, ) -> DtsDeclaration { @@ -415,15 +464,21 @@ pub fn generate_injectable_dts( // ɵfac declaration let ctor_deps_type = generate_ctor_deps_type_from_factory_deps(metadata.deps.as_ref().map(|v| v.as_slice())); - let fac = - format!("static ɵfac: i0.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;"); + let fac = format!( + "static ɵfac: {core_namespace}.ɵɵFactoryDeclaration<{type_with_params}, {ctor_deps_type}>;" + ); // ɵprov declaration - let prov = format!("static ɵprov: i0.ɵɵInjectableDeclaration<{type_with_params}>;"); + let prov = + format!("static ɵprov: {core_namespace}.ɵɵInjectableDeclaration<{type_with_params}>;"); let members = format!("{fac}\n{prov}"); - DtsDeclaration { class_name: class_name.to_string(), members } + DtsDeclaration { + class_name: class_name.to_string(), + members, + namespace_imports: HashMap::new(), + } } // ============================================================================= @@ -437,18 +492,25 @@ pub fn generate_injectable_dts( /// `createInjectableType`). The ctor deps tuple is always `never` because /// `@Service` ɵfac is generated with empty deps. pub fn generate_service_dts( + core_namespace: &str, metadata: &ServiceMetadata, type_argument_count: u32, ) -> DtsDeclaration { let class_name = metadata.class_name.as_str(); let type_with_params = type_with_parameters(class_name, type_argument_count); - let fac = format!("static ɵfac: i0.ɵɵFactoryDeclaration<{type_with_params}, never>;"); - let prov = format!("static ɵprov: i0.ɵɵInjectableDeclaration<{type_with_params}>;"); + let fac = + format!("static ɵfac: {core_namespace}.ɵɵFactoryDeclaration<{type_with_params}, never>;"); + let prov = + format!("static ɵprov: {core_namespace}.ɵɵInjectableDeclaration<{type_with_params}>;"); let members = format!("{fac}\n{prov}"); - DtsDeclaration { class_name: class_name.to_string(), members } + DtsDeclaration { + class_name: class_name.to_string(), + members, + namespace_imports: HashMap::new(), + } } // ============================================================================= diff --git a/crates/oxc_angular_compiler/src/factory/compiler.rs b/crates/oxc_angular_compiler/src/factory/compiler.rs index 7191a77b2..33215725d 100644 --- a/crates/oxc_angular_compiler/src/factory/compiler.rs +++ b/crates/oxc_angular_compiler/src/factory/compiler.rs @@ -100,6 +100,7 @@ pub struct FactoryCompileResult<'a> { /// See: `packages/compiler/src/render3/r3_factory.ts:106-200` pub fn compile_factory_function<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3FactoryMetadata<'a>, factory_name: &'a str, ) -> FactoryCompileResult<'a> { @@ -147,7 +148,8 @@ pub fn compile_factory_function<'a>( } R3FactoryDeps::Valid(deps) => { // new (type)(ɵɵinject(Dep1), ɵɵinject(Dep2), ...) - let inject_args = inject_dependencies(allocator, deps.as_slice(), base.target); + let inject_args = + inject_dependencies(allocator, core_namespace, deps.as_slice(), base.target); Some(OutputExpression::Instantiate(Box::new_in( InstantiateExpr { class_expr: Box::new_in(type_for_ctor.clone_in(allocator), &allocator), @@ -167,7 +169,7 @@ pub fn compile_factory_function<'a>( // Check if we need inherited factory pattern for non-delegated case with no deps if !meta.is_delegated() && !meta.is_expression() && matches!(&base.deps, R3FactoryDeps::None) { - return compile_inherited_factory(allocator, base, factory_name); + return compile_inherited_factory(allocator, core_namespace, base, factory_name); } let mut body: Vec<'a, OutputStatement<'a>> = Vec::new_in(&allocator); @@ -179,8 +181,12 @@ pub fn compile_factory_function<'a>( R3FactoryMetadata::Delegated(delegated_meta) => { // This type is created with a delegated factory. If a type parameter is not specified, // call the factory instead. - let delegate_args = - inject_dependencies(allocator, &delegated_meta.delegate_deps, base.target); + let delegate_args = inject_dependencies( + allocator, + core_namespace, + &delegated_meta.delegate_deps, + base.target, + ); // Either call `new delegate(...)` or `delegate(...)` depending on delegate_type let factory_expr = match delegated_meta.delegate_type { @@ -218,6 +224,7 @@ pub fn compile_factory_function<'a>( ret_expr = Some(make_conditional_factory( &allocator, + core_namespace, &mut body, &factory_type_param, ctor_expr, @@ -228,6 +235,7 @@ pub fn compile_factory_function<'a>( // useValue or useExisting case ret_expr = Some(make_conditional_factory( &allocator, + core_namespace, &mut body, &factory_type_param, ctor_expr, @@ -245,7 +253,7 @@ pub fn compile_factory_function<'a>( match ret_expr { None => { // The expression cannot be formed so render an `ɵɵinvalidFactory()` call. - let invalid_factory_call = create_invalid_factory_call(allocator); + let invalid_factory_call = create_invalid_factory_call(allocator, core_namespace); body.push(OutputStatement::Expression(Box::new_in( ExpressionStatement { expr: invalid_factory_call, source_span: None }, &allocator, @@ -294,6 +302,7 @@ pub fn compile_factory_function<'a>( /// See: `packages/compiler/src/render3/r3_factory.ts:134-143` fn make_conditional_factory<'a>( allocator: &'a Allocator, + core_namespace: &'a str, body: &mut Vec<'a, OutputStatement<'a>>, factory_type_param: &Ident<'a>, ctor_expr: Option>, @@ -345,7 +354,7 @@ fn make_conditional_factory<'a>( } None => { // invalidFactory() - let invalid_factory_call = create_invalid_factory_call(allocator); + let invalid_factory_call = create_invalid_factory_call(allocator, core_namespace); OutputStatement::Expression(Box::new_in( ExpressionStatement { expr: invalid_factory_call, source_span: None }, &allocator, @@ -415,6 +424,7 @@ fn make_conditional_factory<'a>( /// See: packages/compiler/src/render3/r3_factory.ts:160-193 fn compile_inherited_factory<'a>( allocator: &'a Allocator, + core_namespace: &'a str, base: &crate::factory::metadata::R3ConstructorFactoryMetadata<'a>, factory_name: &'a str, ) -> FactoryCompileResult<'a> { @@ -429,7 +439,7 @@ fn compile_inherited_factory<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -592,12 +602,13 @@ fn compile_inherited_factory<'a>( /// Injects dependencies by creating inject calls. fn inject_dependencies<'a>( allocator: &'a Allocator, + core_namespace: &'a str, deps: &[R3DependencyMetadata<'a>], target: FactoryTarget, ) -> Vec<'a, OutputExpression<'a>> { let mut result = Vec::new_in(&allocator); for (index, dep) in deps.iter().enumerate() { - result.push(compile_inject_dependency(allocator, dep, target, index)); + result.push(compile_inject_dependency(allocator, core_namespace, dep, target, index)); } result } @@ -605,6 +616,7 @@ fn inject_dependencies<'a>( /// Compiles a single dependency injection call. fn compile_inject_dependency<'a>( allocator: &'a Allocator, + core_namespace: &'a str, dep: &R3DependencyMetadata<'a>, target: FactoryTarget, index: usize, @@ -612,7 +624,7 @@ fn compile_inject_dependency<'a>( match (&dep.token, &dep.attribute_name_type) { (None, _) => { // Invalid dependency - call invalidFactoryDep(index) - create_invalid_factory_dep_call(allocator, index) + create_invalid_factory_dep_call(allocator, core_namespace, index) } (Some(token), None) => { // Regular inject call @@ -644,13 +656,13 @@ fn compile_inject_dependency<'a>( ))); } - create_import_call(allocator, inject_fn, args) + create_import_call(allocator, core_namespace, inject_fn, args) } (Some(token), Some(_attr_type)) => { // Attribute injection let mut args = Vec::new_in(&allocator); args.push(token.clone_in(allocator)); - create_import_call(allocator, Identifiers::INJECT_ATTRIBUTE, args) + create_import_call(allocator, core_namespace, Identifiers::INJECT_ATTRIBUTE, args) } } } @@ -667,9 +679,10 @@ fn get_inject_fn(target: FactoryTarget) -> &'static str { } } -/// Creates an import expression call: i0.ɵɵinject(args...) +/// Creates an import expression call: ns.ɵɵinject(args...) fn create_import_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, name: &'static str, args: Vec<'a, OutputExpression<'a>>, ) -> OutputExpression<'a> { @@ -677,7 +690,7 @@ fn create_import_call<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -701,14 +714,23 @@ fn create_import_call<'a>( )) } -/// Creates `i0.ɵɵinvalidFactory()` call. -pub fn create_invalid_factory_call<'a>(allocator: &'a Allocator) -> OutputExpression<'a> { - create_import_call(allocator, Identifiers::INVALID_FACTORY, Vec::new_in(&allocator)) +/// Creates `ns.ɵɵinvalidFactory()` call. +pub fn create_invalid_factory_call<'a>( + allocator: &'a Allocator, + core_namespace: &'a str, +) -> OutputExpression<'a> { + create_import_call( + allocator, + core_namespace, + Identifiers::INVALID_FACTORY, + Vec::new_in(&allocator), + ) } -/// Creates i0.ɵɵinvalidFactoryDep(index) call. +/// Creates ns.ɵɵinvalidFactoryDep(index) call. fn create_invalid_factory_dep_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, index: usize, ) -> OutputExpression<'a> { let mut args = Vec::new_in(&allocator); @@ -716,7 +738,7 @@ fn create_invalid_factory_dep_call<'a>( LiteralExpr { value: LiteralValue::Number(index as f64), source_span: None }, &allocator, ))); - create_import_call(allocator, Identifiers::INVALID_FACTORY_DEP, args) + create_import_call(allocator, core_namespace, Identifiers::INVALID_FACTORY_DEP, args) } #[cfg(test)] @@ -742,7 +764,7 @@ mod tests { target: FactoryTarget::Pipe, }); - let result = compile_factory_function(&allocator, &meta, "TestClass_Factory"); + let result = compile_factory_function(&allocator, "i0", &meta, "TestClass_Factory"); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -775,7 +797,7 @@ mod tests { target: FactoryTarget::Pipe, }); - let result = compile_factory_function(&allocator, &meta, "MyPipe_Factory"); + let result = compile_factory_function(&allocator, "i0", &meta, "MyPipe_Factory"); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -800,7 +822,7 @@ mod tests { target: FactoryTarget::Injectable, }); - let result = compile_factory_function(&allocator, &meta, "BrokenClass_Factory"); + let result = compile_factory_function(&allocator, "i0", &meta, "BrokenClass_Factory"); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -825,7 +847,7 @@ mod tests { target: FactoryTarget::Component, }); - let result = compile_factory_function(&allocator, &meta, "ChildClass_Factory"); + let result = compile_factory_function(&allocator, "i0", &meta, "ChildClass_Factory"); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); diff --git a/crates/oxc_angular_compiler/src/hmr/initializer.rs b/crates/oxc_angular_compiler/src/hmr/initializer.rs index 6cf993c75..a33436f52 100644 --- a/crates/oxc_angular_compiler/src/hmr/initializer.rs +++ b/crates/oxc_angular_compiler/src/hmr/initializer.rs @@ -58,10 +58,20 @@ pub fn compile_hmr_initializer<'a>( // m.default let default_read = read_prop(allocator, read_var(allocator, module_name), "default"); + // The alias generated `@angular/core` references resolve through: the + // `assigned_name` the caller registered for `@angular/core` (usually + // `i0`, but the file may force a uniquified or reused name). + let core_ns = meta + .namespace_dependencies + .iter() + .find(|dep| dep.module_name.as_str() == "@angular/core") + .map(|dep| dep.assigned_name) + .unwrap_or_else(|| Ident::from("i0")); + // i0.ɵɵreplaceMetadata(Comp, m.default, [...namespaces], [...locals], import.meta, id) let replace_call = invoke_fn( &allocator, - read_prop(allocator, read_var(allocator, "i0"), Identifiers::REPLACE_METADATA), + read_prop(allocator, read_var_ident(allocator, core_ns), Identifiers::REPLACE_METADATA), vec![ meta.component_type.clone_in(allocator), default_read.clone_in(allocator), @@ -88,7 +98,11 @@ pub fn compile_hmr_initializer<'a>( // i0.ɵɵgetReplaceMetadataURL(id, timestamp, import.meta.url) let url = invoke_fn( &allocator, - read_prop(allocator, read_var(allocator, "i0"), Identifiers::GET_REPLACE_METADATA_URL), + read_prop( + allocator, + read_var_ident(allocator, core_ns), + Identifiers::GET_REPLACE_METADATA_URL, + ), vec![ read_var(allocator, id_name), read_var(allocator, timestamp_name), @@ -394,6 +408,11 @@ fn read_var<'a>(allocator: &'a Allocator, name: &str) -> OutputExpression<'a> { )) } +/// Create a read variable expression from an existing [`Ident`]. +fn read_var_ident<'a>(allocator: &'a Allocator, name: Ident<'a>) -> OutputExpression<'a> { + OutputExpression::ReadVar(Box::new_in(ReadVarExpr { name, source_span: None }, &allocator)) +} + /// Create a property read expression. fn read_prop<'a>( allocator: &'a Allocator, diff --git a/crates/oxc_angular_compiler/src/injectable/compiler.rs b/crates/oxc_angular_compiler/src/injectable/compiler.rs index 7d7a70968..1edd863b1 100644 --- a/crates/oxc_angular_compiler/src/injectable/compiler.rs +++ b/crates/oxc_angular_compiler/src/injectable/compiler.rs @@ -41,24 +41,26 @@ pub struct InjectableCompileResult<'a> { /// This is the main entry point for injectable compilation. pub fn compile_injectable<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3InjectableMetadata<'a>, ) -> InjectableCompileResult<'a> { - compile_injectable_from_metadata(allocator, metadata) + compile_injectable_from_metadata(allocator, core_namespace, metadata) } /// Internal implementation of injectable compilation. pub fn compile_injectable_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3InjectableMetadata<'a>, ) -> InjectableCompileResult<'a> { // Build the factory expression based on provider type - let factory_expr = build_factory_expression(allocator, metadata); + let factory_expr = build_factory_expression(allocator, core_namespace, metadata); // Build the definition map let definition_map = build_definition_map(allocator, metadata, factory_expr); // Create the expression: ɵɵdefineInjectable(definitionMap) - let expression = create_define_injectable_call(allocator, definition_map); + let expression = create_define_injectable_call(allocator, core_namespace, definition_map); InjectableCompileResult { expression, statements: Vec::new_in(&allocator) } } @@ -74,6 +76,7 @@ pub fn compile_injectable_from_metadata<'a>( /// See: `packages/compiler/src/injectable_compiler_2.ts:53-121` fn build_factory_expression<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3InjectableMetadata<'a>, ) -> OutputExpression<'a> { // Create base factory metadata (used for delegated/expression cases) @@ -107,12 +110,17 @@ fn build_factory_expression<'a>( }); let factory_name = format!("{}_Factory", metadata.name); let factory_name = allocator.alloc_str(&factory_name); - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = compile_factory_function( + allocator, + core_namespace, + &factory_meta, + factory_name, + ); result.expression } _ if *is_forward_ref => { // Forward reference: wrap in arrow function - create_forward_ref_factory(allocator, class_expr) + create_forward_ref_factory(allocator, core_namespace, class_expr) } _ => { // useClass without deps: delegate to the alternative class's factory @@ -135,7 +143,12 @@ fn build_factory_expression<'a>( }); let factory_name = format!("{}_Factory", metadata.name); let factory_name = allocator.alloc_str(&factory_name); - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = compile_factory_function( + allocator, + core_namespace, + &factory_meta, + factory_name, + ); result.expression } _ => { @@ -154,21 +167,24 @@ fn build_factory_expression<'a>( }); let factory_name = format!("{}_Factory", metadata.name); let factory_name = allocator.alloc_str(&factory_name); - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = + compile_factory_function(allocator, core_namespace, &factory_meta, factory_name); result.expression } InjectableProvider::UseExisting { existing, is_forward_ref } => { // useExisting: use expression factory with inject() call // See: injectable_compiler_2.ts:107-112 - let inject_expr = create_inject_call(allocator, existing, *is_forward_ref); + let inject_expr = + create_inject_call(allocator, core_namespace, existing, *is_forward_ref); let factory_meta = R3FactoryMetadata::Expression(R3ExpressionFactoryMetadata { base: base_meta, expression: inject_expr, }); let factory_name = format!("{}_Factory", metadata.name); let factory_name = allocator.alloc_str(&factory_name); - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = + compile_factory_function(allocator, core_namespace, &factory_meta, factory_name); result.expression } } @@ -197,6 +213,7 @@ fn clone_deps_vec<'a>( /// Creates an inject() call expression for useExisting. fn create_inject_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, existing: &OutputExpression<'a>, is_forward_ref: bool, ) -> OutputExpression<'a> { @@ -206,7 +223,7 @@ fn create_inject_call<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -240,7 +257,7 @@ fn create_inject_call<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -286,6 +303,7 @@ fn create_factory_delegation<'a>( /// Creates a forward reference factory: `(t) => resolveForwardRef(Type).ɵfac(t)` fn create_forward_ref_factory<'a>( allocator: &'a Allocator, + core_namespace: &'a str, type_expr: &OutputExpression<'a>, ) -> OutputExpression<'a> { let param_name = Ident::from("t"); @@ -297,7 +315,7 @@ fn create_forward_ref_factory<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -473,14 +491,15 @@ fn build_definition_map<'a>( /// Creates the `ɵɵdefineInjectable({...})` call expression. fn create_define_injectable_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, definition_map: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { - // Create i0.ɵɵdefineInjectable + // Create ns.ɵɵdefineInjectable let define_injectable_fn = OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -535,7 +554,7 @@ mod tests { .build() .unwrap(); - let result = compile_injectable(&allocator, &metadata); + let result = compile_injectable(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -568,7 +587,7 @@ mod tests { .build() .unwrap(); - let result = compile_injectable(&allocator, &metadata); + let result = compile_injectable(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -597,7 +616,7 @@ mod tests { .build() .unwrap(); - let result = compile_injectable(&allocator, &metadata); + let result = compile_injectable(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -619,7 +638,7 @@ mod tests { .build() .unwrap(); - let result = compile_injectable(&allocator, &metadata); + let result = compile_injectable(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -676,7 +695,7 @@ mod tests { .build() .unwrap(); - let result = compile_injectable(&allocator, &metadata); + let result = compile_injectable(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -734,7 +753,7 @@ mod tests { .build() .unwrap(); - let result = compile_injectable(&allocator, &metadata); + let result = compile_injectable(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); diff --git a/crates/oxc_angular_compiler/src/injectable/definition.rs b/crates/oxc_angular_compiler/src/injectable/definition.rs index 4fb42876d..f12f86c3f 100644 --- a/crates/oxc_angular_compiler/src/injectable/definition.rs +++ b/crates/oxc_angular_compiler/src/injectable/definition.rs @@ -96,6 +96,7 @@ pub struct InjectableDefinition<'a> { /// ``` pub fn generate_injectable_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3InjectableMetadata<'a>, compilation_mode: CompilationMode, ) -> InjectableDefinition<'a> { @@ -106,13 +107,15 @@ pub fn generate_injectable_definition<'a>( // This ensures namespace indices (i0, i1, i2, ...) are assigned in the same order. match compilation_mode { CompilationMode::Full => { - let fac_definition = generate_fac_definition(allocator, metadata); - let prov_result = compile_injectable(allocator, metadata); + let fac_definition = generate_fac_definition(allocator, core_namespace, metadata); + let prov_result = compile_injectable(allocator, core_namespace, metadata); InjectableDefinition { prov_definition: prov_result.expression, fac_definition } } CompilationMode::Partial => { - let fac_definition = compile_declare_factory_for_injectable(allocator, metadata); - let prov_definition = compile_declare_injectable_from_metadata(allocator, metadata); + let fac_definition = + compile_declare_factory_for_injectable(allocator, core_namespace, metadata); + let prov_definition = + compile_declare_injectable_from_metadata(allocator, core_namespace, metadata); InjectableDefinition { prov_definition, fac_definition } } } @@ -145,6 +148,7 @@ pub fn generate_injectable_definition<'a>( /// ``` fn generate_fac_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3InjectableMetadata<'a>, ) -> OutputExpression<'a> { // Factory function name: ServiceName_Factory @@ -186,7 +190,7 @@ fn generate_fac_definition<'a>( }); // Compile the factory function - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = compile_factory_function(allocator, core_namespace, &factory_meta, factory_name); result.expression } @@ -206,11 +210,12 @@ fn generate_fac_definition<'a>( /// `None` if the metadata couldn't be converted to R3 format. pub fn generate_injectable_definition_from_decorator<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &InjectableMetadata<'a>, compilation_mode: CompilationMode, ) -> Option> { let r3_metadata = metadata.to_r3_metadata(allocator)?; - Some(generate_injectable_definition(allocator, &r3_metadata, compilation_mode)) + Some(generate_injectable_definition(allocator, core_namespace, &r3_metadata, compilation_mode)) } #[cfg(test)] @@ -238,7 +243,7 @@ mod tests { .unwrap(); let definition = - generate_injectable_definition(&allocator, &metadata, CompilationMode::Full); + generate_injectable_definition(&allocator, "i0", &metadata, CompilationMode::Full); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.prov_definition); @@ -270,7 +275,7 @@ mod tests { .unwrap(); let definition = - generate_injectable_definition(&allocator, &metadata, CompilationMode::Full); + generate_injectable_definition(&allocator, "i0", &metadata, CompilationMode::Full); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.prov_definition); @@ -297,7 +302,7 @@ mod tests { .unwrap(); let definition = - generate_injectable_definition(&allocator, &metadata, CompilationMode::Full); + generate_injectable_definition(&allocator, "i0", &metadata, CompilationMode::Full); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.prov_definition); @@ -321,7 +326,7 @@ mod tests { .unwrap(); let definition = - generate_injectable_definition(&allocator, &metadata, CompilationMode::Full); + generate_injectable_definition(&allocator, "i0", &metadata, CompilationMode::Full); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.prov_definition); @@ -348,7 +353,7 @@ mod tests { .unwrap(); let definition = - generate_injectable_definition(&allocator, &metadata, CompilationMode::Full); + generate_injectable_definition(&allocator, "i0", &metadata, CompilationMode::Full); // The prov_definition should be an InvokeFunction with pure=true match &definition.prov_definition { @@ -399,6 +404,7 @@ mod tests { // Generate the full definition and check the output let definition = generate_injectable_definition_from_decorator( &allocator, + "i0", &metadata, CompilationMode::Full, ); @@ -457,6 +463,7 @@ mod tests { // Generate definition let definition = generate_injectable_definition_from_decorator( &allocator, + "i0", &metadata, CompilationMode::Full, ); @@ -503,6 +510,7 @@ mod tests { // Generate definition let definition = generate_injectable_definition_from_decorator( &allocator, + "i0", &metadata, CompilationMode::Full, ); diff --git a/crates/oxc_angular_compiler/src/injector/compiler.rs b/crates/oxc_angular_compiler/src/injector/compiler.rs index de7f709b2..62314d60d 100644 --- a/crates/oxc_angular_compiler/src/injector/compiler.rs +++ b/crates/oxc_angular_compiler/src/injector/compiler.rs @@ -35,21 +35,23 @@ pub struct InjectorCompileResult<'a> { /// This is the main entry point for injector compilation. pub fn compile_injector<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3InjectorMetadata<'a>, ) -> InjectorCompileResult<'a> { - compile_injector_from_metadata(allocator, metadata) + compile_injector_from_metadata(allocator, core_namespace, metadata) } /// Internal implementation of injector compilation. pub fn compile_injector_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3InjectorMetadata<'a>, ) -> InjectorCompileResult<'a> { // Build the definition map let definition_map = build_definition_map(allocator, metadata); // Create the expression: ɵɵdefineInjector(definitionMap) - let expression = create_define_injector_call(allocator, definition_map); + let expression = create_define_injector_call(allocator, core_namespace, definition_map); InjectorCompileResult { expression, statements: Vec::new_in(&allocator) } } @@ -95,14 +97,15 @@ fn build_definition_map<'a>( /// Creates the `ɵɵdefineInjector({...})` call expression. fn create_define_injector_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, definition_map: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { - // Create i0.ɵɵdefineInjector + // Create ns.ɵɵdefineInjector let define_injector_fn = OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -156,7 +159,7 @@ mod tests { .build() .unwrap(); - let result = compile_injector(&allocator, &metadata); + let result = compile_injector(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -184,7 +187,7 @@ mod tests { .build() .unwrap(); - let result = compile_injector(&allocator, &metadata); + let result = compile_injector(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -218,7 +221,7 @@ mod tests { .build() .unwrap(); - let result = compile_injector(&allocator, &metadata); + let result = compile_injector(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -242,7 +245,7 @@ mod tests { .build() .unwrap(); - let result = compile_injector(&allocator, &metadata); + let result = compile_injector(&allocator, "i0", &metadata); // Statements should always be empty for injectors assert!(result.statements.is_empty()); diff --git a/crates/oxc_angular_compiler/src/ng_module/compiler.rs b/crates/oxc_angular_compiler/src/ng_module/compiler.rs index eda484972..674c55fb4 100644 --- a/crates/oxc_angular_compiler/src/ng_module/compiler.rs +++ b/crates/oxc_angular_compiler/src/ng_module/compiler.rs @@ -39,14 +39,16 @@ pub struct NgModuleCompileResult<'a> { /// This is the main entry point for NgModule compilation. pub fn compile_ng_module<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3NgModuleMetadata<'a>, ) -> NgModuleCompileResult<'a> { - compile_ng_module_from_metadata(allocator, metadata) + compile_ng_module_from_metadata(allocator, core_namespace, metadata) } /// Internal implementation of NgModule compilation. pub fn compile_ng_module_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3NgModuleMetadata<'a>, ) -> NgModuleCompileResult<'a> { let mut statements = Vec::new_in(&allocator); @@ -55,18 +57,20 @@ pub fn compile_ng_module_from_metadata<'a>( let definition_map = build_definition_map(allocator, metadata); // Create the expression: ɵɵdefineNgModule(definitionMap) - let expression = create_define_ng_module_call(allocator, definition_map); + let expression = create_define_ng_module_call(allocator, core_namespace, definition_map); // Add scope side effect if needed if metadata.should_set_scope_side_effect() { - if let Some(scope_stmt) = create_set_scope_side_effect(allocator, metadata) { + if let Some(scope_stmt) = create_set_scope_side_effect(allocator, core_namespace, metadata) + { statements.push(scope_stmt); } } // Add module ID registration if needed if let Some(id) = &metadata.id { - let register_stmt = create_register_ng_module_type(allocator, &metadata.r#type.value, id); + let register_stmt = + create_register_ng_module_type(allocator, core_namespace, &metadata.r#type.value, id); statements.push(register_stmt); } @@ -184,14 +188,15 @@ fn create_reference_array<'a>( /// Creates the `ɵɵdefineNgModule({...})` call expression. fn create_define_ng_module_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, definition_map: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { - // Create i0.ɵɵdefineNgModule + // Create ns.ɵɵdefineNgModule let define_ng_module_fn = OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -240,6 +245,7 @@ fn create_define_ng_module_call<'a>( /// ``` fn create_set_scope_side_effect<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3NgModuleMetadata<'a>, ) -> Option> { // Only create if there's something to set @@ -281,7 +287,7 @@ fn create_set_scope_side_effect<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -392,6 +398,7 @@ fn create_set_scope_side_effect<'a>( /// Creates the ɵɵregisterNgModuleType call for module ID registration. fn create_register_ng_module_type<'a>( allocator: &'a Allocator, + core_namespace: &'a str, module_type: &OutputExpression<'a>, id: &OutputExpression<'a>, ) -> OutputStatement<'a> { @@ -399,7 +406,7 @@ fn create_register_ng_module_type<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -451,7 +458,7 @@ mod tests { .build() .unwrap(); - let result = compile_ng_module(&allocator, &metadata); + let result = compile_ng_module(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -479,7 +486,7 @@ mod tests { .build() .unwrap(); - let result = compile_ng_module(&allocator, &metadata); + let result = compile_ng_module(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -512,7 +519,7 @@ mod tests { .build() .unwrap(); - let result = compile_ng_module(&allocator, &metadata); + let result = compile_ng_module(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -541,7 +548,7 @@ mod tests { .build() .unwrap(); - let result = compile_ng_module(&allocator, &metadata); + let result = compile_ng_module(&allocator, "i0", &metadata); // Should have side effect statement assert_eq!(result.statements.len(), 1); @@ -567,7 +574,7 @@ mod tests { .build() .unwrap(); - let result = compile_ng_module(&allocator, &metadata); + let result = compile_ng_module(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); diff --git a/crates/oxc_angular_compiler/src/ng_module/definition.rs b/crates/oxc_angular_compiler/src/ng_module/definition.rs index 0eb4371ae..bc17a793e 100644 --- a/crates/oxc_angular_compiler/src/ng_module/definition.rs +++ b/crates/oxc_angular_compiler/src/ng_module/definition.rs @@ -110,9 +110,10 @@ pub struct FullNgModuleDefinition<'a> { /// ``` pub fn generate_ng_module_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3NgModuleMetadata<'a>, ) -> NgModuleDefinition<'a> { - let result = compile_ng_module(allocator, metadata); + let result = compile_ng_module(allocator, core_namespace, metadata); NgModuleDefinition { mod_definition: result.expression, statements: result.statements } } @@ -132,10 +133,11 @@ pub fn generate_ng_module_definition<'a>( /// `None` if the metadata couldn't be converted to R3 format. pub fn generate_ng_module_definition_from_decorator<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &NgModuleMetadata<'a>, ) -> Option> { let r3_metadata = metadata.to_r3_metadata(allocator)?; - Some(generate_ng_module_definition(allocator, &r3_metadata)) + Some(generate_ng_module_definition(allocator, core_namespace, &r3_metadata)) } /// Generate the full JavaScript output for an NgModule definition. @@ -187,6 +189,7 @@ pub fn emit_ng_module_definition(class_name: &str, definition: &NgModuleDefiniti /// `None` if the metadata couldn't be converted to R3 format. pub fn generate_full_ng_module_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &NgModuleMetadata<'a>, compilation_mode: CompilationMode, ) -> Option> { @@ -200,9 +203,9 @@ pub fn generate_full_ng_module_definition<'a>( match compilation_mode { CompilationMode::Full => { - let fac_definition = generate_ng_module_fac(allocator, metadata); - let mod_result = compile_ng_module(allocator, &r3_metadata); - let inj_definition = generate_ng_module_inj(allocator, metadata); + let fac_definition = generate_ng_module_fac(allocator, core_namespace, metadata); + let mod_result = compile_ng_module(allocator, core_namespace, &r3_metadata); + let inj_definition = generate_ng_module_inj(allocator, core_namespace, metadata); Some(FullNgModuleDefinition { mod_definition: mod_result.expression, fac_definition, @@ -217,15 +220,18 @@ pub fn generate_full_ng_module_definition<'a>( // `compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts:971`. let fac_definition = compile_declare_factory_for_ng_module( allocator, + core_namespace, &r3_metadata, metadata.deps.as_ref(), ); - let mod_definition = compile_declare_ng_module_from_metadata(allocator, &r3_metadata); + let mod_definition = + compile_declare_ng_module_from_metadata(allocator, core_namespace, &r3_metadata); // Build the injector metadata using the same conversion the // full path uses (`generate_ng_module_inj`'s builder), then // hand it to the partial injector emitter. let inj_metadata = build_injector_metadata(allocator, metadata); - let inj_definition = compile_declare_injector_from_metadata(allocator, &inj_metadata); + let inj_definition = + compile_declare_injector_from_metadata(allocator, core_namespace, &inj_metadata); Some(FullNgModuleDefinition { mod_definition, fac_definition, @@ -239,6 +245,7 @@ pub fn generate_full_ng_module_definition<'a>( /// Generate ɵfac factory function for an NgModule. fn generate_ng_module_fac<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &NgModuleMetadata<'a>, ) -> OutputExpression<'a> { let factory_name = allocator.alloc_str(&format!("{}_Factory", metadata.class_name)); @@ -281,17 +288,18 @@ fn generate_ng_module_fac<'a>( target: FactoryTarget::NgModule, }); - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = compile_factory_function(allocator, core_namespace, &factory_meta, factory_name); result.expression } /// Generate ɵinj injector definition for an NgModule (full mode). fn generate_ng_module_inj<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &NgModuleMetadata<'a>, ) -> OutputExpression<'a> { let inj_metadata = build_injector_metadata(allocator, metadata); - let result = compile_injector(allocator, &inj_metadata); + let result = compile_injector(allocator, core_namespace, &inj_metadata); result.expression } @@ -385,7 +393,7 @@ mod tests { .build() .expect("Failed to build metadata"); - let definition = generate_ng_module_definition(&allocator, &metadata); + let definition = generate_ng_module_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.mod_definition); @@ -418,7 +426,7 @@ mod tests { .build() .expect("Failed to build metadata"); - let definition = generate_ng_module_definition(&allocator, &metadata); + let definition = generate_ng_module_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.mod_definition); @@ -452,7 +460,7 @@ mod tests { .build() .expect("Failed to build metadata"); - let definition = generate_ng_module_definition(&allocator, &metadata); + let definition = generate_ng_module_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.mod_definition); @@ -482,7 +490,7 @@ mod tests { .build() .expect("Failed to build metadata"); - let definition = generate_ng_module_definition(&allocator, &metadata); + let definition = generate_ng_module_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.mod_definition); @@ -511,7 +519,7 @@ mod tests { .build() .expect("Failed to build metadata"); - let definition = generate_ng_module_definition(&allocator, &metadata); + let definition = generate_ng_module_definition(&allocator, "i0", &metadata); // Should have side-effect statements assert!(!definition.statements.is_empty(), "Should have side-effect statements"); @@ -531,7 +539,7 @@ mod tests { .build() .expect("Failed to build metadata"); - let definition = generate_ng_module_definition(&allocator, &metadata); + let definition = generate_ng_module_definition(&allocator, "i0", &metadata); let js = emit_ng_module_definition("TestModule", &definition); // Should have proper assignment format @@ -576,7 +584,7 @@ mod tests { let metadata = extract_ng_module_metadata(&allocator, class, Some(code)); let metadata = metadata.expect("Should extract NgModule metadata"); - let definition = generate_ng_module_definition_from_decorator(&allocator, &metadata); + let definition = generate_ng_module_definition_from_decorator(&allocator, "i0", &metadata); let definition = definition.expect("Should generate definition"); let js = emit_ng_module_definition("AppModule", &definition); @@ -626,7 +634,7 @@ mod tests { // Generate the full definition and check the output let definition = - generate_full_ng_module_definition(&allocator, &metadata, CompilationMode::Full); + generate_full_ng_module_definition(&allocator, "i0", &metadata, CompilationMode::Full); let definition = definition.expect("Should generate definition"); let js = emit_full_ng_module_definition("CoreModule", &definition); @@ -694,7 +702,7 @@ mod tests { assert!(deps[0].token.is_some(), "@Inject(TOKEN) should give the dep a token"); let definition = - generate_full_ng_module_definition(&allocator, &metadata, CompilationMode::Full) + generate_full_ng_module_definition(&allocator, "i0", &metadata, CompilationMode::Full) .expect("Should generate definition"); let js = emit_full_ng_module_definition("M", &definition); assert!( @@ -724,7 +732,7 @@ mod tests { let metadata = module_metadata_from_code(&allocator, code); let definition = - generate_full_ng_module_definition(&allocator, &metadata, CompilationMode::Full) + generate_full_ng_module_definition(&allocator, "i0", &metadata, CompilationMode::Full) .expect("Should generate definition"); let js = emit_full_ng_module_definition("M", &definition); assert!(js.contains("ɵɵinject(Other"), "Should inject Other, got: {js}"); @@ -744,9 +752,13 @@ mod tests { "#; let metadata = module_metadata_from_code(&allocator, code); - let definition = - generate_full_ng_module_definition(&allocator, &metadata, CompilationMode::Partial) - .expect("Should generate definition"); + let definition = generate_full_ng_module_definition( + &allocator, + "i0", + &metadata, + CompilationMode::Partial, + ) + .expect("Should generate definition"); let js = emit_full_ng_module_definition("M", &definition); assert!( js.contains("token:TOKEN") || js.contains("token: TOKEN"), @@ -773,7 +785,7 @@ mod tests { .build() .expect("Failed to build metadata"); - let definition = generate_ng_module_definition(&allocator, &metadata); + let definition = generate_ng_module_definition(&allocator, "i0", &metadata); // The mod_definition should be an InvokeFunction with pure=true match &definition.mod_definition { diff --git a/crates/oxc_angular_compiler/src/partial/class_metadata.rs b/crates/oxc_angular_compiler/src/partial/class_metadata.rs index 0ebdb57fa..2ba5e0f85 100644 --- a/crates/oxc_angular_compiler/src/partial/class_metadata.rs +++ b/crates/oxc_angular_compiler/src/partial/class_metadata.rs @@ -45,13 +45,18 @@ use crate::r3::Identifiers; /// Emits the `i0.ɵɵngDeclareClassMetadata({...})` call. pub fn compile_declare_class_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3ClassMetadata<'a>, ) -> OutputExpression<'a> { let mut entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); entries.push(string_entry(allocator, "minVersion", MIN_VERSION_CLASS_METADATA)); entries.push(string_entry(allocator, "version", PLACEHOLDER_VERSION)); - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); entries.push(LiteralMapEntry::new(Ident::from("type"), meta.r#type.clone_in(allocator), false)); entries.push(LiteralMapEntry::new( Ident::from("decorators"), @@ -83,7 +88,7 @@ pub fn compile_declare_class_metadata<'a>( OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { fn_expr: Box::new_in( - namespaced_prop(allocator, "i0", Identifiers::DECLARE_CLASS_METADATA), + namespaced_prop(allocator, core_namespace, Identifiers::DECLARE_CLASS_METADATA), &allocator, ), args, @@ -128,6 +133,7 @@ pub fn compile_declare_class_metadata<'a>( /// support. pub fn compile_declare_class_metadata_async<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3ClassMetadata<'a>, dependencies: &[R3DeferPerComponentDependency<'a>], ) -> OutputExpression<'a> { @@ -182,7 +188,11 @@ pub fn compile_declare_class_metadata_async<'a>( let mut entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); entries.push(string_entry(allocator, "minVersion", MIN_VERSION_CLASS_METADATA_ASYNC)); entries.push(string_entry(allocator, "version", PLACEHOLDER_VERSION)); - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); entries.push(LiteralMapEntry::new(Ident::from("type"), meta.r#type.clone_in(allocator), false)); entries.push(LiteralMapEntry::new( Ident::from("resolveDeferredDeps"), @@ -201,7 +211,11 @@ pub fn compile_declare_class_metadata_async<'a>( OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { fn_expr: Box::new_in( - namespaced_prop(allocator, "i0", Identifiers::DECLARE_CLASS_METADATA_ASYNC), + namespaced_prop( + allocator, + core_namespace, + Identifiers::DECLARE_CLASS_METADATA_ASYNC, + ), &allocator, ), args, @@ -218,19 +232,20 @@ pub fn compile_declare_class_metadata_async<'a>( /// no deferred deps → sync form; one or more → async form. pub fn compile_component_declare_class_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3ClassMetadata<'a>, dependencies: &[R3DeferPerComponentDependency<'a>], ) -> OutputExpression<'a> { if dependencies.is_empty() { - compile_declare_class_metadata(allocator, meta) + compile_declare_class_metadata(allocator, core_namespace, meta) } else { - compile_declare_class_metadata_async(allocator, meta, dependencies) + compile_declare_class_metadata_async(allocator, core_namespace, meta, dependencies) } } // ---- helpers --------------------------------------------------------------- -fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpression<'a> { +fn read_var<'a>(allocator: &'a Allocator, name: &'a str) -> OutputExpression<'a> { OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: Ident::from(name), source_span: None }, &allocator, @@ -239,7 +254,7 @@ fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpressio fn namespaced_prop<'a>( allocator: &'a Allocator, - receiver: &'static str, + receiver: &'a str, prop: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/partial/component.rs b/crates/oxc_angular_compiler/src/partial/component.rs index c36bef332..ee9ba8497 100644 --- a/crates/oxc_angular_compiler/src/partial/component.rs +++ b/crates/oxc_angular_compiler/src/partial/component.rs @@ -74,6 +74,7 @@ pub struct PartialComponentInputs<'a, 'm> { /// Emits the `ɵɵngDeclareComponent` call for a component's `ɵcmp` static. pub fn compile_declare_component_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &ComponentMetadata<'a>, inputs: &PartialComponentInputs<'a, '_>, ) -> OutputExpression<'a> { @@ -153,14 +154,18 @@ pub fn compile_declare_component_from_metadata<'a>( if !inputs.content_queries.is_empty() { entries.push(LiteralMapEntry::new( Ident::from("queries"), - super::directive::compile_queries_array(allocator, inputs.content_queries), + super::directive::compile_queries_array( + allocator, + core_namespace, + inputs.content_queries, + ), false, )); } if !inputs.view_queries.is_empty() { entries.push(LiteralMapEntry::new( Ident::from("viewQueries"), - super::directive::compile_queries_array(allocator, inputs.view_queries), + super::directive::compile_queries_array(allocator, core_namespace, inputs.view_queries), false, )); } @@ -198,7 +203,7 @@ pub fn compile_declare_component_from_metadata<'a>( if !meta.host_directives.is_empty() { entries.push(LiteralMapEntry::new( Ident::from("hostDirectives"), - create_host_directives_array(allocator, &meta.host_directives), + create_host_directives_array(allocator, core_namespace, &meta.host_directives), false, )); } @@ -207,7 +212,11 @@ pub fn compile_declare_component_from_metadata<'a>( // specific fields come AFTER ngImport — that's how upstream emits // them (createComponentDefinitionMap calls createDirectiveDefinitionMap // first, then appends). - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); // ---- Component-specific fields ---- @@ -269,6 +278,7 @@ pub fn compile_declare_component_from_metadata<'a>( Ident::from("dependencies"), create_dependencies_array_from_slice( &allocator, + core_namespace, declarations_to_emit, meta.declaration_list_emit_mode, ), @@ -308,7 +318,7 @@ pub fn compile_declare_component_from_metadata<'a>( }; entries.push(LiteralMapEntry::new( Ident::from("changeDetection"), - namespaced_enum_member(allocator, "ChangeDetectionStrategy", variant), + namespaced_enum_member(allocator, core_namespace, "ChangeDetectionStrategy", variant), false, )); } @@ -322,7 +332,7 @@ pub fn compile_declare_component_from_metadata<'a>( }; entries.push(LiteralMapEntry::new( Ident::from("encapsulation"), - namespaced_enum_member(allocator, "ViewEncapsulation", variant), + namespaced_enum_member(allocator, core_namespace, "ViewEncapsulation", variant), false, )); } @@ -349,12 +359,13 @@ pub fn compile_declare_component_from_metadata<'a>( // to populate per-block resolvers — out of scope for the partial // emitter slice. - invoke_declare(allocator, Identifiers::DECLARE_COMPONENT, entries) + invoke_declare(allocator, core_namespace, Identifiers::DECLARE_COMPONENT, entries) } /// Builds the partial ɵfac factory paired with a Component. pub fn compile_declare_factory_for_component<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &ComponentMetadata<'a>, ) -> OutputExpression<'a> { let type_expr = OutputExpression::ReadVar(Box::new_in( @@ -370,7 +381,7 @@ pub fn compile_declare_factory_for_component<'a>( deps: clone_factory_deps(allocator, &meta.constructor_deps, meta.uses_inheritance), target: FactoryTarget::Component, }); - compile_declare_factory_function(allocator, &factory_meta) + compile_declare_factory_function(allocator, core_namespace, &factory_meta) } // ---- min version --------------------------------------------------------- @@ -611,6 +622,7 @@ fn ident_pairs_to_string_map<'a>( fn create_host_directives_array<'a>( allocator: &'a Allocator, + core_namespace: &'a str, host_directives: &Vec<'a, HostDirectiveMetadata<'a>>, ) -> OutputExpression<'a> { let mut entries: Vec<'a, OutputExpression<'a>> = @@ -622,7 +634,7 @@ fn create_host_directives_array<'a>( &allocator, )); let directive_expr = if hd.is_forward_reference { - wrap_forward_ref(allocator, directive_expr) + wrap_forward_ref(allocator, core_namespace, directive_expr) } else { directive_expr }; @@ -698,6 +710,7 @@ fn lower_imports_to_declarations<'a>( fn create_dependencies_array_from_slice<'a>( allocator: &'a Allocator, + core_namespace: &'a str, deps: &[TemplateDependency<'a>], emit_mode: DeclarationListEmitMode, ) -> OutputExpression<'a> { @@ -731,7 +744,7 @@ fn create_dependencies_array_from_slice<'a>( &allocator, )); if wrap_in_forward_ref || dep.is_forward_reference { - type_expr = wrap_forward_ref(allocator, type_expr); + type_expr = wrap_forward_ref(allocator, core_namespace, type_expr); } dep_map.push(LiteralMapEntry::new(Ident::from("type"), type_expr, false)); @@ -861,6 +874,7 @@ fn is_unsafe_object_key(key: &str) -> bool { fn invoke_declare<'a>( allocator: &'a Allocator, + core_namespace: &'a str, name: &'static str, entries: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { @@ -872,7 +886,7 @@ fn invoke_declare<'a>( args.push(map_expr); OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { - fn_expr: Box::new_in(namespaced_prop(allocator, "i0", name), &allocator), + fn_expr: Box::new_in(namespaced_prop(allocator, core_namespace, name), &allocator), args, pure: false, optional: false, @@ -882,7 +896,7 @@ fn invoke_declare<'a>( )) } -fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpression<'a> { +fn read_var<'a>(allocator: &'a Allocator, name: &'a str) -> OutputExpression<'a> { OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: Ident::from(name), source_span: None }, &allocator, @@ -891,7 +905,7 @@ fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpressio fn namespaced_prop<'a>( allocator: &'a Allocator, - receiver: &'static str, + receiver: &'a str, prop: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( @@ -909,10 +923,11 @@ fn namespaced_prop<'a>( /// `i0.ChangeDetectionStrategy.OnPush`. fn namespaced_enum_member<'a>( allocator: &'a Allocator, + core_namespace: &'a str, enum_name: &'static str, variant: &'static str, ) -> OutputExpression<'a> { - let enum_ref = namespaced_prop(allocator, "i0", enum_name); + let enum_ref = namespaced_prop(allocator, core_namespace, enum_name); OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in(enum_ref, &allocator), diff --git a/crates/oxc_angular_compiler/src/partial/directive.rs b/crates/oxc_angular_compiler/src/partial/directive.rs index 9cca2f7c0..39ad7839e 100644 --- a/crates/oxc_angular_compiler/src/partial/directive.rs +++ b/crates/oxc_angular_compiler/src/partial/directive.rs @@ -58,10 +58,11 @@ use crate::r3::Identifiers; /// Emits the `ɵɵngDeclareDirective` call for a directive's `ɵdir` static. pub fn compile_declare_directive_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3DirectiveMetadata<'a>, ) -> OutputExpression<'a> { - let entries = create_directive_definition_map(allocator, meta); - invoke_declare(allocator, Identifiers::DECLARE_DIRECTIVE, entries) + let entries = create_directive_definition_map(allocator, core_namespace, meta); + invoke_declare(allocator, core_namespace, Identifiers::DECLARE_DIRECTIVE, entries) } /// Builds the directive partial-declaration definition map. @@ -72,6 +73,7 @@ pub fn compile_declare_directive_from_metadata<'a>( /// identifier to `ɵɵngDeclareComponent`. pub(crate) fn create_directive_definition_map<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3DirectiveMetadata<'a>, ) -> Vec<'a, LiteralMapEntry<'a>> { let mut entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); @@ -139,14 +141,14 @@ pub(crate) fn create_directive_definition_map<'a>( if !meta.queries.is_empty() { entries.push(LiteralMapEntry::new( Ident::from("queries"), - compile_queries_array(allocator, &meta.queries), + compile_queries_array(allocator, core_namespace, &meta.queries), false, )); } if !meta.view_queries.is_empty() { entries.push(LiteralMapEntry::new( Ident::from("viewQueries"), - compile_queries_array(allocator, &meta.view_queries), + compile_queries_array(allocator, core_namespace, &meta.view_queries), false, )); } @@ -185,13 +187,17 @@ pub(crate) fn create_directive_definition_map<'a>( if !meta.host_directives.is_empty() { entries.push(LiteralMapEntry::new( Ident::from("hostDirectives"), - create_host_directives_array(allocator, &meta.host_directives), + create_host_directives_array(allocator, core_namespace, &meta.host_directives), false, )); } // ngImport is emitted LAST per upstream convention (directive.ts:114). - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); entries } @@ -199,6 +205,7 @@ pub(crate) fn create_directive_definition_map<'a>( /// Builds the partial ɵfac factory paired with a Directive. pub fn compile_declare_factory_for_directive<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3DirectiveMetadata<'a>, ) -> OutputExpression<'a> { let factory_meta = R3FactoryMetadata::Constructor(R3ConstructorFactoryMetadata { @@ -209,7 +216,7 @@ pub fn compile_declare_factory_for_directive<'a>( deps: clone_factory_deps(allocator, &meta.deps, meta.uses_inheritance), target: FactoryTarget::Directive, }); - compile_declare_factory_function(allocator, &factory_meta) + compile_declare_factory_function(allocator, core_namespace, &factory_meta) } /// Public min-version calculator. Exposed so the component emitter (next @@ -441,12 +448,13 @@ fn ident_pairs_to_string_map<'a>( /// same `compileQuery`). pub(crate) fn compile_queries_array<'a>( allocator: &'a Allocator, + core_namespace: &'a str, queries: &[R3QueryMetadata<'a>], ) -> OutputExpression<'a> { let mut entries: Vec<'a, OutputExpression<'a>> = Vec::with_capacity_in(queries.len(), &allocator); for q in queries { - entries.push(compile_query(allocator, q)); + entries.push(compile_query(allocator, core_namespace, q)); } OutputExpression::LiteralArray(Box::new_in( LiteralArrayExpr { entries, source_span: None }, @@ -454,7 +462,11 @@ pub(crate) fn compile_queries_array<'a>( )) } -fn compile_query<'a>(allocator: &'a Allocator, q: &R3QueryMetadata<'a>) -> OutputExpression<'a> { +fn compile_query<'a>( + allocator: &'a Allocator, + core_namespace: &'a str, + q: &R3QueryMetadata<'a>, +) -> OutputExpression<'a> { let mut entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); entries.push(LiteralMapEntry::new( @@ -472,7 +484,7 @@ fn compile_query<'a>(allocator: &'a Allocator, q: &R3QueryMetadata<'a>) -> Outpu let predicate_expr = match &q.predicate { QueryPredicate::Type(expr) => { let expr = expr.clone_in(allocator); - if q.is_forward_ref { wrap_forward_ref(allocator, expr) } else { expr } + if q.is_forward_ref { wrap_forward_ref(allocator, core_namespace, expr) } else { expr } } QueryPredicate::Selectors(selectors) => { let mut elements: Vec<'a, OutputExpression<'a>> = @@ -526,6 +538,7 @@ fn compile_query<'a>(allocator: &'a Allocator, q: &R3QueryMetadata<'a>) -> Outpu fn create_host_directives_array<'a>( allocator: &'a Allocator, + core_namespace: &'a str, host_directives: &Vec<'a, R3HostDirectiveMetadata<'a>>, ) -> OutputExpression<'a> { let mut entries: Vec<'a, OutputExpression<'a>> = @@ -533,7 +546,7 @@ fn create_host_directives_array<'a>( for hd in host_directives { let mut hd_entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); let directive_expr = if hd.is_forward_reference { - wrap_forward_ref(allocator, hd.directive.clone_in(allocator)) + wrap_forward_ref(allocator, core_namespace, hd.directive.clone_in(allocator)) } else { hd.directive.clone_in(allocator) }; @@ -629,6 +642,7 @@ fn is_unsafe_object_key(key: &str) -> bool { fn invoke_declare<'a>( allocator: &'a Allocator, + core_namespace: &'a str, name: &'static str, entries: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { @@ -640,7 +654,7 @@ fn invoke_declare<'a>( args.push(map_expr); OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { - fn_expr: Box::new_in(namespaced_prop(allocator, "i0", name), &allocator), + fn_expr: Box::new_in(namespaced_prop(allocator, core_namespace, name), &allocator), args, pure: false, optional: false, @@ -650,7 +664,7 @@ fn invoke_declare<'a>( )) } -fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpression<'a> { +fn read_var<'a>(allocator: &'a Allocator, name: &'a str) -> OutputExpression<'a> { OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: Ident::from(name), source_span: None }, &allocator, @@ -659,7 +673,7 @@ fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpressio fn namespaced_prop<'a>( allocator: &'a Allocator, - receiver: &'static str, + receiver: &'a str, prop: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/partial/factory.rs b/crates/oxc_angular_compiler/src/partial/factory.rs index c7a54db39..7adb24630 100644 --- a/crates/oxc_angular_compiler/src/partial/factory.rs +++ b/crates/oxc_angular_compiler/src/partial/factory.rs @@ -30,6 +30,7 @@ use crate::r3::Identifiers; /// See: upstream `packages/compiler/src/render3/partial/factory.ts:27`. pub fn compile_declare_factory_function<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3FactoryMetadata<'a>, ) -> OutputExpression<'a> { let base = meta.base(); @@ -38,7 +39,11 @@ pub fn compile_declare_factory_function<'a>( entries.push(string_entry(allocator, "minVersion", MIN_VERSION_FACTORY)); entries.push(string_entry(allocator, "version", PLACEHOLDER_VERSION)); - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); entries.push(LiteralMapEntry::new( Ident::from("type"), base.type_expr.clone_in(allocator), @@ -51,7 +56,7 @@ pub fn compile_declare_factory_function<'a>( )); entries.push(LiteralMapEntry::new( Ident::from("target"), - factory_target_expr(allocator, base.target), + factory_target_expr(allocator, core_namespace, base.target), false, )); @@ -66,7 +71,7 @@ pub fn compile_declare_factory_function<'a>( OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { fn_expr: Box::new_in( - namespaced_prop(allocator, "i0", Identifiers::DECLARE_FACTORY), + namespaced_prop(allocator, core_namespace, Identifiers::DECLARE_FACTORY), &allocator, ), args, @@ -168,6 +173,7 @@ fn compile_dependency<'a>( /// Builds the `target` field: `i0.ɵɵFactoryTarget.`. fn factory_target_expr<'a>( allocator: &'a Allocator, + core_namespace: &'a str, target: FactoryTarget, ) -> OutputExpression<'a> { let variant = match target { @@ -180,7 +186,7 @@ fn factory_target_expr<'a>( let factory_target_ref = OutputExpression::ReadProp(Box::new_in( ReadPropExpr { - receiver: Box::new_in(read_var(allocator, "i0"), &allocator), + receiver: Box::new_in(read_var(allocator, core_namespace), &allocator), name: Ident::from(Identifiers::FACTORY_TARGET), optional: false, source_span: None, @@ -201,7 +207,7 @@ fn factory_target_expr<'a>( // ---- small helpers -------------------------------------------------------- -fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpression<'a> { +fn read_var<'a>(allocator: &'a Allocator, name: &'a str) -> OutputExpression<'a> { OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: Ident::from(name), source_span: None }, &allocator, @@ -210,7 +216,7 @@ fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpressio fn namespaced_prop<'a>( allocator: &'a Allocator, - receiver: &'static str, + receiver: &'a str, prop: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/partial/injectable.rs b/crates/oxc_angular_compiler/src/partial/injectable.rs index 9d81e2328..e976a3083 100644 --- a/crates/oxc_angular_compiler/src/partial/injectable.rs +++ b/crates/oxc_angular_compiler/src/partial/injectable.rs @@ -46,13 +46,18 @@ use crate::r3::Identifiers; /// See: upstream `packages/compiler/src/render3/partial/injectable.ts:29`. pub fn compile_declare_injectable_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3InjectableMetadata<'a>, ) -> OutputExpression<'a> { let mut entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); entries.push(string_entry(allocator, "minVersion", MIN_VERSION_INJECTABLE)); entries.push(string_entry(allocator, "version", PLACEHOLDER_VERSION)); - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); entries.push(LiteralMapEntry::new(Ident::from("type"), meta.r#type.clone_in(allocator), false)); // providedIn — only emit when set. ProvidedIn::None is the "omit" case. @@ -65,14 +70,24 @@ pub fn compile_declare_injectable_from_metadata<'a>( InjectableProvider::UseClass { class_expr, is_forward_ref, .. } => { entries.push(LiteralMapEntry::new( Ident::from("useClass"), - maybe_forward_ref(allocator, class_expr.clone_in(allocator), *is_forward_ref), + maybe_forward_ref( + allocator, + core_namespace, + class_expr.clone_in(allocator), + *is_forward_ref, + ), false, )); } InjectableProvider::UseExisting { existing, is_forward_ref } => { entries.push(LiteralMapEntry::new( Ident::from("useExisting"), - maybe_forward_ref(allocator, existing.clone_in(allocator), *is_forward_ref), + maybe_forward_ref( + allocator, + core_namespace, + existing.clone_in(allocator), + *is_forward_ref, + ), false, )); } @@ -118,7 +133,7 @@ pub fn compile_declare_injectable_from_metadata<'a>( OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { fn_expr: Box::new_in( - namespaced_prop(allocator, "i0", Identifiers::DECLARE_INJECTABLE), + namespaced_prop(allocator, core_namespace, Identifiers::DECLARE_INJECTABLE), &allocator, ), args, @@ -137,6 +152,7 @@ pub fn compile_declare_injectable_from_metadata<'a>( /// the constructor deps) and delegates to `compile_declare_factory_function`. pub fn compile_declare_factory_for_injectable<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3InjectableMetadata<'a>, ) -> OutputExpression<'a> { let factory_meta = R3FactoryMetadata::Constructor(R3ConstructorFactoryMetadata { @@ -148,7 +164,7 @@ pub fn compile_declare_factory_for_injectable<'a>( target: FactoryTarget::Injectable, }); - compile_declare_factory_function(allocator, &factory_meta) + compile_declare_factory_function(allocator, core_namespace, &factory_meta) } // ---- helpers --------------------------------------------------------------- @@ -203,10 +219,11 @@ fn clone_constructor_deps<'a>( fn maybe_forward_ref<'a>( allocator: &'a Allocator, + core_namespace: &'a str, expr: OutputExpression<'a>, is_forward_ref: bool, ) -> OutputExpression<'a> { - if is_forward_ref { wrap_forward_ref(allocator, expr) } else { expr } + if is_forward_ref { wrap_forward_ref(allocator, core_namespace, expr) } else { expr } } fn provided_in_expr<'a>( @@ -276,7 +293,7 @@ fn compile_one_dependency<'a>( )) } -fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpression<'a> { +fn read_var<'a>(allocator: &'a Allocator, name: &'a str) -> OutputExpression<'a> { OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: Ident::from(name), source_span: None }, &allocator, @@ -285,7 +302,7 @@ fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpressio fn namespaced_prop<'a>( allocator: &'a Allocator, - receiver: &'static str, + receiver: &'a str, prop: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/partial/injector.rs b/crates/oxc_angular_compiler/src/partial/injector.rs index 6687c2038..858ae1707 100644 --- a/crates/oxc_angular_compiler/src/partial/injector.rs +++ b/crates/oxc_angular_compiler/src/partial/injector.rs @@ -29,13 +29,18 @@ use crate::r3::Identifiers; /// Emits the `ɵɵngDeclareInjector` call. pub fn compile_declare_injector_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3InjectorMetadata<'a>, ) -> OutputExpression<'a> { let mut entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); entries.push(string_entry(allocator, "minVersion", MIN_VERSION_INJECTOR)); entries.push(string_entry(allocator, "version", PLACEHOLDER_VERSION)); - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); entries.push(LiteralMapEntry::new(Ident::from("type"), meta.r#type.clone_in(allocator), false)); // providers: upstream emits this slot only when defined. Omitting it @@ -74,13 +79,14 @@ pub fn compile_declare_injector_from_metadata<'a>( )); } - invoke_declare(allocator, Identifiers::DECLARE_INJECTOR, entries) + invoke_declare(allocator, core_namespace, Identifiers::DECLARE_INJECTOR, entries) } // ---- helpers --------------------------------------------------------------- fn invoke_declare<'a>( allocator: &'a Allocator, + core_namespace: &'a str, name: &'static str, entries: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { @@ -92,7 +98,7 @@ fn invoke_declare<'a>( args.push(map_expr); OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { - fn_expr: Box::new_in(namespaced_prop(allocator, "i0", name), &allocator), + fn_expr: Box::new_in(namespaced_prop(allocator, core_namespace, name), &allocator), args, pure: false, optional: false, @@ -102,7 +108,7 @@ fn invoke_declare<'a>( )) } -fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpression<'a> { +fn read_var<'a>(allocator: &'a Allocator, name: &'a str) -> OutputExpression<'a> { OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: Ident::from(name), source_span: None }, &allocator, @@ -111,7 +117,7 @@ fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpressio fn namespaced_prop<'a>( allocator: &'a Allocator, - receiver: &'static str, + receiver: &'a str, prop: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/partial/mod.rs b/crates/oxc_angular_compiler/src/partial/mod.rs index f92f1fb7d..62bd5e68d 100644 --- a/crates/oxc_angular_compiler/src/partial/mod.rs +++ b/crates/oxc_angular_compiler/src/partial/mod.rs @@ -74,6 +74,7 @@ use crate::r3::Identifiers; /// exact shape and unwraps it before forwarding to the full-mode emitter. pub(crate) fn wrap_forward_ref<'a>( allocator: &'a Allocator, + core_namespace: &'a str, expr: OutputExpression<'a>, ) -> OutputExpression<'a> { let mut body: Vec<'a, OutputStatement<'a>> = Vec::new_in(&allocator); @@ -93,7 +94,7 @@ pub(crate) fn wrap_forward_ref<'a>( )); let i0 = OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )); let forward_ref_fn = OutputExpression::ReadProp(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/partial/ng_module.rs b/crates/oxc_angular_compiler/src/partial/ng_module.rs index e006f0189..4fdba1a37 100644 --- a/crates/oxc_angular_compiler/src/partial/ng_module.rs +++ b/crates/oxc_angular_compiler/src/partial/ng_module.rs @@ -44,13 +44,18 @@ use crate::r3::Identifiers; /// Emits the `ɵɵngDeclareNgModule` call. pub fn compile_declare_ng_module_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3NgModuleMetadata<'a>, ) -> OutputExpression<'a> { let mut entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); entries.push(string_entry(allocator, "minVersion", MIN_VERSION_NG_MODULE)); entries.push(string_entry(allocator, "version", PLACEHOLDER_VERSION)); - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); entries.push(LiteralMapEntry::new( Ident::from("type"), meta.r#type.value.clone_in(allocator), @@ -75,7 +80,7 @@ pub fn compile_declare_ng_module_from_metadata<'a>( entries.push(LiteralMapEntry::new(Ident::from("id"), id.clone_in(allocator), false)); } - invoke_declare(allocator, Identifiers::DECLARE_NG_MODULE, entries) + invoke_declare(allocator, core_namespace, Identifiers::DECLARE_NG_MODULE, entries) } /// Builds the partial ɵfac factory paired with this NgModule. @@ -90,6 +95,7 @@ pub fn compile_declare_ng_module_from_metadata<'a>( /// becomes `[]` here — a suboptimal-but-correct factory. pub fn compile_declare_factory_for_ng_module<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3NgModuleMetadata<'a>, deps: Option<&Vec<'a, crate::factory::R3DependencyMetadata<'a>>>, ) -> OutputExpression<'a> { @@ -123,7 +129,7 @@ pub fn compile_declare_factory_for_ng_module<'a>( deps: factory_deps, target: FactoryTarget::NgModule, }); - compile_declare_factory_function(allocator, &factory_meta) + compile_declare_factory_function(allocator, core_namespace, &factory_meta) } // ---- ref list helpers ------------------------------------------------------ @@ -183,6 +189,7 @@ fn refs_to_array<'a>( fn invoke_declare<'a>( allocator: &'a Allocator, + core_namespace: &'a str, name: &'static str, entries: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { @@ -194,7 +201,7 @@ fn invoke_declare<'a>( args.push(map_expr); OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { - fn_expr: Box::new_in(namespaced_prop(allocator, "i0", name), &allocator), + fn_expr: Box::new_in(namespaced_prop(allocator, core_namespace, name), &allocator), args, pure: false, optional: false, @@ -204,7 +211,7 @@ fn invoke_declare<'a>( )) } -fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpression<'a> { +fn read_var<'a>(allocator: &'a Allocator, name: &'a str) -> OutputExpression<'a> { OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: Ident::from(name), source_span: None }, &allocator, @@ -213,7 +220,7 @@ fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpressio fn namespaced_prop<'a>( allocator: &'a Allocator, - receiver: &'static str, + receiver: &'a str, prop: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/partial/pipe.rs b/crates/oxc_angular_compiler/src/partial/pipe.rs index 525468706..1856642de 100644 --- a/crates/oxc_angular_compiler/src/partial/pipe.rs +++ b/crates/oxc_angular_compiler/src/partial/pipe.rs @@ -40,13 +40,18 @@ use crate::r3::Identifiers; /// See: upstream `packages/compiler/src/render3/partial/pipe.ts:28`. pub fn compile_declare_pipe_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3PipeMetadata<'a>, ) -> OutputExpression<'a> { let mut entries: Vec<'a, LiteralMapEntry<'a>> = Vec::new_in(&allocator); entries.push(string_entry(allocator, "minVersion", MIN_VERSION_PIPE)); entries.push(string_entry(allocator, "version", PLACEHOLDER_VERSION)); - entries.push(LiteralMapEntry::new(Ident::from("ngImport"), read_var(allocator, "i0"), false)); + entries.push(LiteralMapEntry::new( + Ident::from("ngImport"), + read_var(allocator, core_namespace), + false, + )); entries.push(LiteralMapEntry::new(Ident::from("type"), meta.r#type.clone_in(allocator), false)); // isStandalone: upstream only emits when defined. OXC's metadata is a @@ -93,7 +98,7 @@ pub fn compile_declare_pipe_from_metadata<'a>( OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { fn_expr: Box::new_in( - namespaced_prop(allocator, "i0", Identifiers::DECLARE_PIPE), + namespaced_prop(allocator, core_namespace, Identifiers::DECLARE_PIPE), &allocator, ), args, @@ -112,6 +117,7 @@ pub fn compile_declare_pipe_from_metadata<'a>( /// `target = Pipe` and delegates to `compile_declare_factory_function`. pub fn compile_declare_factory_for_pipe<'a>( allocator: &'a Allocator, + core_namespace: &'a str, meta: &R3PipeMetadata<'a>, ) -> OutputExpression<'a> { let factory_meta = R3FactoryMetadata::Constructor(R3ConstructorFactoryMetadata { @@ -123,7 +129,7 @@ pub fn compile_declare_factory_for_pipe<'a>( target: FactoryTarget::Pipe, }); - compile_declare_factory_function(allocator, &factory_meta) + compile_declare_factory_function(allocator, core_namespace, &factory_meta) } // ---- helpers --------------------------------------------------------------- @@ -170,7 +176,7 @@ fn clone_constructor_deps<'a>( } } -fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpression<'a> { +fn read_var<'a>(allocator: &'a Allocator, name: &'a str) -> OutputExpression<'a> { OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: Ident::from(name), source_span: None }, &allocator, @@ -179,7 +185,7 @@ fn read_var<'a>(allocator: &'a Allocator, name: &'static str) -> OutputExpressio fn namespaced_prop<'a>( allocator: &'a Allocator, - receiver: &'static str, + receiver: &'a str, prop: &'static str, ) -> OutputExpression<'a> { OutputExpression::ReadProp(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/pipe/compiler.rs b/crates/oxc_angular_compiler/src/pipe/compiler.rs index a40d200d7..d41909000 100644 --- a/crates/oxc_angular_compiler/src/pipe/compiler.rs +++ b/crates/oxc_angular_compiler/src/pipe/compiler.rs @@ -36,21 +36,23 @@ pub struct PipeCompileResult<'a> { /// ``` pub fn compile_pipe<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3PipeMetadata<'a>, ) -> PipeCompileResult<'a> { - compile_pipe_from_metadata(allocator, metadata) + compile_pipe_from_metadata(allocator, core_namespace, metadata) } /// Internal implementation of pipe compilation. pub fn compile_pipe_from_metadata<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3PipeMetadata<'a>, ) -> PipeCompileResult<'a> { // Build the definition map let definition_map = build_definition_map(allocator, metadata); // Create the expression: ɵɵdefinePipe(definitionMap) - let expression = create_define_pipe_call(allocator, definition_map); + let expression = create_define_pipe_call(allocator, core_namespace, definition_map); PipeCompileResult { expression, statements: Vec::new_in(&allocator) } } @@ -108,14 +110,15 @@ fn build_definition_map<'a>( /// Creates the `ɵɵdefinePipe({...})` call expression. fn create_define_pipe_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, definition_map: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { - // Create i0.ɵɵdefinePipe + // Create ns.ɵɵdefinePipe let define_pipe_fn = OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -173,7 +176,7 @@ mod tests { is_standalone: false, }; - let result = compile_pipe(&allocator, &metadata); + let result = compile_pipe(&allocator, "i0", &metadata); // Emit to string to verify output let emitter = JsEmitter::new(); @@ -203,7 +206,7 @@ mod tests { is_standalone: false, }; - let result = compile_pipe(&allocator, &metadata); + let result = compile_pipe(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -231,7 +234,7 @@ mod tests { is_standalone: true, }; - let result = compile_pipe(&allocator, &metadata); + let result = compile_pipe(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); @@ -259,7 +262,7 @@ mod tests { is_standalone: false, }; - let result = compile_pipe(&allocator, &metadata); + let result = compile_pipe(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result.expression); diff --git a/crates/oxc_angular_compiler/src/pipe/definition.rs b/crates/oxc_angular_compiler/src/pipe/definition.rs index 7d748837c..9603af84d 100644 --- a/crates/oxc_angular_compiler/src/pipe/definition.rs +++ b/crates/oxc_angular_compiler/src/pipe/definition.rs @@ -97,9 +97,10 @@ pub struct FullPipeDefinition<'a> { /// ``` pub fn generate_pipe_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3PipeMetadata<'a>, ) -> PipeDefinition<'a> { - let result = compile_pipe(allocator, metadata); + let result = compile_pipe(allocator, core_namespace, metadata); PipeDefinition { pipe_definition: result.expression } } @@ -119,10 +120,11 @@ pub fn generate_pipe_definition<'a>( /// `None` if the metadata couldn't be converted to R3 format. pub fn generate_pipe_definition_from_decorator<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &PipeMetadata<'a>, ) -> Option> { let r3_metadata = metadata.to_r3_metadata(allocator)?; - Some(generate_pipe_definition(allocator, &r3_metadata)) + Some(generate_pipe_definition(allocator, core_namespace, &r3_metadata)) } /// Generate both ɵpipe and ɵfac definitions from pipe decorator metadata. @@ -142,6 +144,7 @@ pub fn generate_pipe_definition_from_decorator<'a>( /// `None` if the metadata couldn't be converted to R3 format. pub fn generate_full_pipe_definition_from_decorator<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &PipeMetadata<'a>, compilation_mode: CompilationMode, ) -> Option> { @@ -153,13 +156,15 @@ pub fn generate_full_pipe_definition_from_decorator<'a>( // This ensures namespace indices (i0, i1, i2, ...) are assigned in the same order. match compilation_mode { CompilationMode::Full => { - let fac_definition = generate_pipe_fac(allocator, metadata); - let pipe_result = compile_pipe(allocator, &r3_metadata); + let fac_definition = generate_pipe_fac(allocator, core_namespace, metadata); + let pipe_result = compile_pipe(allocator, core_namespace, &r3_metadata); Some(FullPipeDefinition { pipe_definition: pipe_result.expression, fac_definition }) } CompilationMode::Partial => { - let fac_definition = compile_declare_factory_for_pipe(allocator, &r3_metadata); - let pipe_definition = compile_declare_pipe_from_metadata(allocator, &r3_metadata); + let fac_definition = + compile_declare_factory_for_pipe(allocator, core_namespace, &r3_metadata); + let pipe_definition = + compile_declare_pipe_from_metadata(allocator, core_namespace, &r3_metadata); Some(FullPipeDefinition { pipe_definition, fac_definition }) } } @@ -168,6 +173,7 @@ pub fn generate_full_pipe_definition_from_decorator<'a>( /// Generate ɵfac factory function for a pipe. fn generate_pipe_fac<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &PipeMetadata<'a>, ) -> OutputExpression<'a> { let factory_name = allocator.alloc_str(&format!("{}_Factory", metadata.class_name)); @@ -210,7 +216,7 @@ fn generate_pipe_fac<'a>( target: FactoryTarget::Pipe, }); - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = compile_factory_function(allocator, core_namespace, &factory_meta, factory_name); result.expression } @@ -237,7 +243,7 @@ mod tests { .is_standalone(true) .build(); - let definition = generate_pipe_definition(&allocator, &metadata); + let definition = generate_pipe_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.pipe_definition); @@ -270,7 +276,7 @@ mod tests { .is_standalone(true) .build(); - let definition = generate_pipe_definition(&allocator, &metadata); + let definition = generate_pipe_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.pipe_definition); @@ -294,7 +300,7 @@ mod tests { .is_standalone(false) .build(); - let definition = generate_pipe_definition(&allocator, &metadata); + let definition = generate_pipe_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let js = emitter.emit_expression(&definition.pipe_definition); @@ -320,7 +326,7 @@ mod tests { .is_standalone(true) .build(); - let definition = generate_pipe_definition(&allocator, &metadata); + let definition = generate_pipe_definition(&allocator, "i0", &metadata); // The pipe_definition should be an InvokeFunction with pure=true match &definition.pipe_definition { diff --git a/crates/oxc_angular_compiler/src/pipeline/compilation.rs b/crates/oxc_angular_compiler/src/pipeline/compilation.rs index 29787255e..7952f7df5 100644 --- a/crates/oxc_angular_compiler/src/pipeline/compilation.rs +++ b/crates/oxc_angular_compiler/src/pipeline/compilation.rs @@ -221,6 +221,13 @@ pub struct ComponentCompilationJob<'a> { /// is derived from `angular_version` (legacy for < v22, modern for >= v22, /// legacy when the version is unknown). See `legacy_optional_chaining()`. pub legacy_optional_chaining: Option, + /// Namespace identifier used for `@angular/core` runtime imports + /// (`i0.ɵɵdefineComponent`, `i0.forwardRef`, ...). + /// + /// Defaults to `i0`; the file-level `NamespaceRegistry` assigns a + /// collision-free alias (`i0_1`, ...) when the source file already binds + /// `i0` or another namespace import to `@angular/core`. + pub core_namespace: Ident<'a>, /// Diagnostics collected during compilation. pub diagnostics: std::vec::Vec, } @@ -272,6 +279,7 @@ impl<'a> ComponentCompilationJob<'a> { content_selectors: None, angular_version: None, legacy_optional_chaining: None, + core_namespace: Ident::from("i0"), diagnostics: std::vec::Vec::new(), } } @@ -672,6 +680,9 @@ pub struct HostBindingCompilationJob<'a> { /// See [`ComponentCompilationJob::legacy_optional_chaining`] for the resolution /// rules; `None` derives the default from `angular_version`. pub legacy_optional_chaining: Option, + /// Namespace identifier used for `@angular/core` runtime imports. + /// See [`ComponentCompilationJob::core_namespace`]. + pub core_namespace: Ident<'a>, } impl<'a> HostBindingCompilationJob<'a> { @@ -719,6 +730,7 @@ impl<'a> HostBindingCompilationJob<'a> { diagnostics: std::vec::Vec::new(), angular_version: None, legacy_optional_chaining: None, + core_namespace: Ident::from("i0"), } } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/has_const_expression_collection.rs b/crates/oxc_angular_compiler/src/pipeline/phases/has_const_expression_collection.rs index 227028ac6..805143e8c 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/has_const_expression_collection.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/has_const_expression_collection.rs @@ -78,8 +78,13 @@ pub fn collect_const_expressions(job: &mut ComponentCompilationJob<'_>) { for (inner_expr, _index) in collected { // Convert IrExpression to OutputExpression - let output_expr = - convert_ir_expression(allocator, &inner_expr, &job.expressions, root_xref); + let output_expr = convert_ir_expression( + allocator, + job.core_namespace.as_str(), + &inner_expr, + &job.expressions, + root_xref, + ); // Add to const pool - indices should match what we pre-allocated job.add_const(ConstValue::Expression(output_expr)); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/i18n_const_collection.rs b/crates/oxc_angular_compiler/src/pipeline/phases/i18n_const_collection.rs index 47c0d75eb..426941f5b 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/i18n_const_collection.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/i18n_const_collection.rs @@ -191,6 +191,7 @@ pub fn collect_i18n_consts(job: &mut ComponentCompilationJob<'_>) { // Collect messages recursively and generate statements let (main_var_name, statements) = collect_message( &allocator, + job.core_namespace.as_str(), &messages, ¶ms_by_context, &postprocessing_params_by_context, @@ -465,6 +466,7 @@ struct I18nExpressionInfo { /// Collects a message and its sub-messages, returning the main variable name and statements. fn collect_message<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, messages: &FxHashMap, params_by_context: &FxHashMap>, postprocessing_params_by_context: &FxHashMap>, @@ -483,6 +485,7 @@ fn collect_message<'a>( if let Some(sub_msg) = messages.get(&sub_msg_xref) { let (sub_var_name, sub_statements) = collect_message( &allocator, + core_namespace, messages, params_by_context, postprocessing_params_by_context, @@ -594,7 +597,8 @@ fn collect_message<'a>( &allocator, )) }; - let postprocess = wrap_with_postprocess(allocator, read_var(), &postprocessing_params); + let postprocess = + wrap_with_postprocess(allocator, core_namespace, read_var(), &postprocessing_params); let assignment = OutputExpression::BinaryOperator(oxc_allocator::Box::new_in( crate::output::ast::BinaryOperatorExpr { operator: crate::output::ast::BinaryOperator::Assign, @@ -877,6 +881,7 @@ fn find_param_value( /// Wrap an i18n expression with i18nPostprocess for ICU message handling. fn wrap_with_postprocess<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, expr: OutputExpression<'a>, postprocessing_params: &[(String, I18nParamExpr)], ) -> OutputExpression<'a> { @@ -887,7 +892,7 @@ fn wrap_with_postprocess<'a>( crate::output::ast::ReadPropExpr { receiver: oxc_allocator::Box::new_in( OutputExpression::ReadVar(oxc_allocator::Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -962,7 +967,7 @@ mod tests { )); // Call wrap_with_postprocess with no extra params - let result = wrap_with_postprocess(&allocator, input_expr, &[]); + let result = wrap_with_postprocess(&allocator, "i0", input_expr, &[]); // Emit the result to a string and verify let emitter = JsEmitter::new(); @@ -992,7 +997,7 @@ mod tests { I18nParamExpr::Vars(vec!["i18n_1".to_string(), "i18n_2".to_string()]), )]; - let result = wrap_with_postprocess(&allocator, input_expr, ¶ms); + let result = wrap_with_postprocess(&allocator, "i0", input_expr, ¶ms); let emitter = JsEmitter::new(); let output = emitter.emit_expression(&result); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/ir_expression.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/ir_expression.rs index bc062c597..8b0c603ce 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/ir_expression.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/ir_expression.rs @@ -24,6 +24,7 @@ use super::utils::{create_instruction_call_expr, create_value_interpolate_expr}; /// to output expressions that will be emitted as JavaScript. pub fn convert_ir_expression<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, expr: &IrExpression<'a>, expressions: &ExpressionStore<'a>, root_xref: XrefId, @@ -118,7 +119,7 @@ pub fn convert_ir_expression<'a>( &allocator, ))); } - create_instruction_call_expr(allocator, Identifiers::NEXT_CONTEXT, args) + create_instruction_call_expr(allocator, core_namespace, Identifiers::NEXT_CONTEXT, args) } IrExpression::Reference(ref_expr) => { @@ -135,7 +136,7 @@ pub fn convert_ir_expression<'a>( &allocator, ))); } - create_instruction_call_expr(allocator, Identifiers::REFERENCE, args) + create_instruction_call_expr(allocator, core_namespace, Identifiers::REFERENCE, args) } IrExpression::RestoreView(rv) => { @@ -145,7 +146,13 @@ pub fn convert_ir_expression<'a>( match &rv.view { crate::ir::expression::RestoreViewTarget::Dynamic(inner_expr) => { // The view was resolved to a variable reference - args.push(convert_ir_expression(allocator, inner_expr, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + inner_expr, + expressions, + root_xref, + )); } crate::ir::expression::RestoreViewTarget::Static(_) => { // Fallback: use _r if not resolved (shouldn't happen in correct flow) @@ -155,13 +162,14 @@ pub fn convert_ir_expression<'a>( ))); } } - create_instruction_call_expr(allocator, Identifiers::RESTORE_VIEW, args) + create_instruction_call_expr(allocator, core_namespace, Identifiers::RESTORE_VIEW, args) } IrExpression::GetCurrentView(_) => { // i0.ɵɵgetCurrentView() create_instruction_call_expr( &allocator, + core_namespace, Identifiers::GET_CURRENT_VIEW, OxcVec::new_in(&allocator), ) @@ -170,8 +178,14 @@ pub fn convert_ir_expression<'a>( IrExpression::ResetView(rv) => { // i0.ɵɵresetView(expr) let mut args = OxcVec::new_in(&allocator); - args.push(convert_ir_expression(allocator, &rv.expr, expressions, root_xref)); - create_instruction_call_expr(allocator, Identifiers::RESET_VIEW, args) + args.push(convert_ir_expression( + allocator, + core_namespace, + &rv.expr, + expressions, + root_xref, + )); + create_instruction_call_expr(allocator, core_namespace, Identifiers::RESET_VIEW, args) } IrExpression::PipeBinding(pipe) => { @@ -191,10 +205,16 @@ pub fn convert_ir_expression<'a>( ))); // Add pipe arguments for arg in pipe.args.iter() { - args.push(convert_ir_expression(allocator, arg, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + arg, + expressions, + root_xref, + )); } let instruction = get_pipe_bind_instruction(pipe.args.len()); - create_instruction_call_expr(allocator, instruction, args) + create_instruction_call_expr(allocator, core_namespace, instruction, args) } IrExpression::PipeBindingVariadic(pipe) => { @@ -215,8 +235,14 @@ pub fn convert_ir_expression<'a>( &allocator, ))); // Add the pure function expression containing all arguments - args.push(convert_ir_expression(allocator, &pipe.args, expressions, root_xref)); - create_instruction_call_expr(allocator, Identifiers::PIPE_BIND_V, args) + args.push(convert_ir_expression( + allocator, + core_namespace, + &pipe.args, + expressions, + root_xref, + )); + create_instruction_call_expr(allocator, core_namespace, Identifiers::PIPE_BIND_V, args) } IrExpression::PureFunction(pf) => { @@ -243,7 +269,13 @@ pub fn convert_ir_expression<'a>( // placeholder to avoid panicking (will cause runtime error instead). match &pf.fn_ref { Some(fn_ref) => { - args.push(convert_ir_expression(allocator, fn_ref, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + fn_ref, + expressions, + root_xref, + )); } None => { // fn_ref should have been set by pure_function_extraction phase @@ -262,6 +294,7 @@ pub fn convert_ir_expression<'a>( for arg in pf.args.iter() { array_entries.push(convert_ir_expression( &allocator, + core_namespace, arg, expressions, root_xref, @@ -274,11 +307,17 @@ pub fn convert_ir_expression<'a>( } else { // Constant calling pattern: pureFunction1/2/etc(offset, fn, arg1, arg2, ...) for arg in pf.args.iter() { - args.push(convert_ir_expression(allocator, arg, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + arg, + expressions, + root_xref, + )); } } let instruction = get_pure_function_instruction(pf.args.len()); - create_instruction_call_expr(allocator, instruction, args) + create_instruction_call_expr(allocator, core_namespace, instruction, args) } IrExpression::SlotLiteral(slot_lit) => { @@ -296,8 +335,14 @@ pub fn convert_ir_expression<'a>( IrExpression::StoreLet(store) => { // i0.ɵɵstoreLet(value) let mut args = OxcVec::new_in(&allocator); - args.push(convert_ir_expression(allocator, &store.value, expressions, root_xref)); - create_instruction_call_expr(allocator, Identifiers::STORE_LET, args) + args.push(convert_ir_expression( + allocator, + core_namespace, + &store.value, + expressions, + root_xref, + )); + create_instruction_call_expr(allocator, core_namespace, Identifiers::STORE_LET, args) } IrExpression::ContextLetReference(ctx_let) => { @@ -309,7 +354,12 @@ pub fn convert_ir_expression<'a>( &allocator, ))); } - create_instruction_call_expr(allocator, Identifiers::READ_CONTEXT_LET, args) + create_instruction_call_expr( + allocator, + core_namespace, + Identifiers::READ_CONTEXT_LET, + args, + ) } IrExpression::TrackContext(_) => { @@ -332,7 +382,13 @@ pub fn convert_ir_expression<'a>( IrExpression::AssignTemporary(assign) => { // Assign to a temporary variable: _tmp = expr let var_name = assign.name.clone().unwrap_or_else(|| Ident::from("_tmp")); - let value = convert_ir_expression(allocator, &assign.expr, expressions, root_xref); + let value = convert_ir_expression( + allocator, + core_namespace, + &assign.expr, + expressions, + root_xref, + ); OutputExpression::BinaryOperator(Box::new_in( BinaryOperatorExpr { operator: BinaryOperator::Assign, @@ -355,8 +411,10 @@ pub fn convert_ir_expression<'a>( // Angular uses the `== null` pattern for consistency with optional chaining. // The expression is wrapped in parentheses to ensure correct operator precedence // when used in a larger expression context. - let guard = convert_ir_expression(allocator, &st.guard, expressions, root_xref); - let true_case = convert_ir_expression(allocator, &st.expr, expressions, root_xref); + let guard = + convert_ir_expression(allocator, core_namespace, &st.guard, expressions, root_xref); + let true_case = + convert_ir_expression(allocator, core_namespace, &st.expr, expressions, root_xref); // Build: guard == null ? null : expr let null_check = OutputExpression::BinaryOperator(Box::new_in( BinaryOperatorExpr { @@ -413,12 +471,13 @@ pub fn convert_ir_expression<'a>( if expr_count == 1 && interp.strings.iter().all(|s| s.is_empty()) { args.push(convert_ir_expression( &allocator, + core_namespace, &interp.expressions[0], expressions, root_xref, )); // Pass 0 as expr_count to get ɵɵinterpolate (not ɵɵinterpolate1) - create_value_interpolate_expr(allocator, args, 0) + create_value_interpolate_expr(allocator, core_namespace, args, 0) } else { // Build args: [s0, v0, s1, v1, s2, ...] (strings and expressions interleaved) for (i, ir_expr) in interp.expressions.iter().enumerate() { @@ -431,7 +490,13 @@ pub fn convert_ir_expression<'a>( &allocator, ))); } - args.push(convert_ir_expression(allocator, ir_expr, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + ir_expr, + expressions, + root_xref, + )); } // Add trailing string if present (and not empty) // TypeScript drops trailing empty strings - the runtime handles it. @@ -448,14 +513,14 @@ pub fn convert_ir_expression<'a>( } } } - create_value_interpolate_expr(allocator, args, expr_count) + create_value_interpolate_expr(allocator, core_namespace, args, expr_count) } } IrExpression::TwoWayBindingSet(tbs) => { // Two-way binding: generate `i0.ɵɵtwoWayBindingSet(target, value) || (target = value)` // For ReadVariable targets, just generate `i0.ɵɵtwoWayBindingSet(target, value)` - convert_two_way_binding_set(allocator, tbs, expressions, root_xref) + convert_two_way_binding_set(allocator, core_namespace, tbs, expressions, root_xref) } IrExpression::ConstReference(cr) => { @@ -469,8 +534,20 @@ pub fn convert_ir_expression<'a>( IrExpression::Binary(binary) => { // Convert binary expression for @for loop computed variables // and for expressions containing nested pipes like `a ?? (b | pipe)` - let lhs = convert_ir_expression(allocator, &binary.lhs, expressions, root_xref); - let rhs = convert_ir_expression(allocator, &binary.rhs, expressions, root_xref); + let lhs = convert_ir_expression( + allocator, + core_namespace, + &binary.lhs, + expressions, + root_xref, + ); + let rhs = convert_ir_expression( + allocator, + core_namespace, + &binary.rhs, + expressions, + root_xref, + ); let operator = convert_ir_binary_operator(binary.operator); OutputExpression::BinaryOperator(Box::new_in( BinaryOperatorExpr { @@ -487,8 +564,13 @@ pub fn convert_ir_expression<'a>( // Property read where the receiver was resolved during name resolution // Convert: ResolvedPropertyRead { receiver: ReadVariable(item), name: "title" } // To: item_i4.title - let receiver = - convert_ir_expression(allocator, &resolved.receiver, expressions, root_xref); + let receiver = convert_ir_expression( + allocator, + core_namespace, + &resolved.receiver, + expressions, + root_xref, + ); OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in(receiver, &allocator), @@ -504,8 +586,20 @@ pub fn convert_ir_expression<'a>( // Binary expression where sub-expressions were resolved during name resolution // Convert: ResolvedBinary { operator: Assign, left: ResolvedPropertyRead(...), right: Ast($event) } // To: todo_i7.done = $event - let left = convert_ir_expression(allocator, &resolved.left, expressions, root_xref); - let right = convert_ir_expression(allocator, &resolved.right, expressions, root_xref); + let left = convert_ir_expression( + allocator, + core_namespace, + &resolved.left, + expressions, + root_xref, + ); + let right = convert_ir_expression( + allocator, + core_namespace, + &resolved.right, + expressions, + root_xref, + ); // Map Angular binary operator to output binary operator use crate::ast::expression::BinaryOperator as AngularOp; @@ -560,11 +654,22 @@ pub fn convert_ir_expression<'a>( // Function call where receiver and/or arguments were resolved during name resolution // Convert: ResolvedCall { receiver: Ast(ctx.removeTodo), args: [ReadVariable(todo_i8)] } // To: ctx.removeTodo(todo_i8) - let receiver = - convert_ir_expression(allocator, &resolved.receiver, expressions, root_xref); + let receiver = convert_ir_expression( + allocator, + core_namespace, + &resolved.receiver, + expressions, + root_xref, + ); let mut args = OxcVec::new_in(&allocator); for arg in resolved.args.iter() { - args.push(convert_ir_expression(allocator, arg, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + arg, + expressions, + root_xref, + )); } OutputExpression::InvokeFunction(Box::new_in( InvokeFunctionExpr { @@ -582,9 +687,20 @@ pub fn convert_ir_expression<'a>( // Keyed read where the receiver was resolved during name resolution // Convert: ResolvedKeyedRead { receiver: ReadVariable(item), key: Ast(0) } // To: item_i4[0] - let receiver = - convert_ir_expression(allocator, &resolved.receiver, expressions, root_xref); - let index = convert_ir_expression(allocator, &resolved.key, expressions, root_xref); + let receiver = convert_ir_expression( + allocator, + core_namespace, + &resolved.receiver, + expressions, + root_xref, + ); + let index = convert_ir_expression( + allocator, + core_namespace, + &resolved.key, + expressions, + root_xref, + ); OutputExpression::ReadKey(Box::new_in( ReadKeyExpr { receiver: Box::new_in(receiver, &allocator), @@ -602,8 +718,13 @@ pub fn convert_ir_expression<'a>( // This should be handled by the expand_safe_reads phase before reification, // but we handle it here as a fallback to avoid errors. // Output: receiver?.name (using conditional access pattern) - let receiver = - convert_ir_expression(allocator, &resolved.receiver, expressions, root_xref); + let receiver = convert_ir_expression( + allocator, + core_namespace, + &resolved.receiver, + expressions, + root_xref, + ); // For now, output as a regular property read - the expand_safe_reads phase // should have already transformed this into a safe ternary pattern OutputExpression::ReadProp(Box::new_in( @@ -629,7 +750,13 @@ pub fn convert_ir_expression<'a>( // Note: This is a simplified fallback - the expand_safe_reads phase // should have already transformed this with proper temp handling let span = safe.source_span; - let receiver = convert_ir_expression(allocator, &safe.receiver, expressions, root_xref); + let receiver = convert_ir_expression( + allocator, + core_namespace, + &safe.receiver, + expressions, + root_xref, + ); let receiver_clone = receiver.clone_in(allocator); let null_check = OutputExpression::BinaryOperator(Box::new_in( BinaryOperatorExpr { @@ -681,9 +808,21 @@ pub fn convert_ir_expression<'a>( IrExpression::SafeKeyedRead(safe) => { // Convert to: (receiver == null ? null : receiver[key]) let span = safe.source_span; - let receiver = convert_ir_expression(allocator, &safe.receiver, expressions, root_xref); + let receiver = convert_ir_expression( + allocator, + core_namespace, + &safe.receiver, + expressions, + root_xref, + ); let receiver_clone = receiver.clone_in(allocator); - let index = convert_ir_expression(allocator, &safe.index, expressions, root_xref); + let index = convert_ir_expression( + allocator, + core_namespace, + &safe.index, + expressions, + root_xref, + ); let null_check = OutputExpression::BinaryOperator(Box::new_in( BinaryOperatorExpr { operator: BinaryOperator::Equals, @@ -734,11 +873,23 @@ pub fn convert_ir_expression<'a>( IrExpression::SafeInvokeFunction(safe) => { // Convert to: (receiver == null ? null : receiver()) let span = safe.source_span; - let receiver = convert_ir_expression(allocator, &safe.receiver, expressions, root_xref); + let receiver = convert_ir_expression( + allocator, + core_namespace, + &safe.receiver, + expressions, + root_xref, + ); let receiver_clone = receiver.clone_in(allocator); let mut args = OxcVec::new_in(&allocator); for arg in safe.args.iter() { - args.push(convert_ir_expression(allocator, arg, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + arg, + expressions, + root_xref, + )); } let null_check = OutputExpression::BinaryOperator(Box::new_in( BinaryOperatorExpr { @@ -789,12 +940,27 @@ pub fn convert_ir_expression<'a>( // Ternary expression - convert to conditional output expression IrExpression::Ternary(ternary) => { - let condition = - convert_ir_expression(allocator, &ternary.condition, expressions, root_xref); - let true_case = - convert_ir_expression(allocator, &ternary.true_expr, expressions, root_xref); - let false_case = - convert_ir_expression(allocator, &ternary.false_expr, expressions, root_xref); + let condition = convert_ir_expression( + allocator, + core_namespace, + &ternary.condition, + expressions, + root_xref, + ); + let true_case = convert_ir_expression( + allocator, + core_namespace, + &ternary.true_expr, + expressions, + root_xref, + ); + let false_case = convert_ir_expression( + allocator, + core_namespace, + &ternary.false_expr, + expressions, + root_xref, + ); OutputExpression::Conditional(Box::new_in( ConditionalExpr { condition: Box::new_in(condition, &allocator), @@ -809,7 +975,8 @@ pub fn convert_ir_expression<'a>( IrExpression::LiteralArray(arr) => { let mut entries = OxcVec::with_capacity_in(arr.elements.len(), &allocator); for (i, elem) in arr.elements.iter().enumerate() { - let converted = convert_ir_expression(allocator, elem, expressions, root_xref); + let converted = + convert_ir_expression(allocator, core_namespace, elem, expressions, root_xref); if arr.spreads.get(i).copied().unwrap_or(false) { entries.push(OutputExpression::SpreadElement(Box::new_in( SpreadElementExpr { @@ -835,7 +1002,7 @@ pub fn convert_ir_expression<'a>( let quoted = map.quoted.get(i).copied().unwrap_or(false); let is_spread = map.spreads.get(i).copied().unwrap_or(false); let converted_value = - convert_ir_expression(allocator, value, expressions, root_xref); + convert_ir_expression(allocator, core_namespace, value, expressions, root_xref); entries.push(LiteralMapEntry { key, value: converted_value, quoted, is_spread }); } OutputExpression::LiteralMap(Box::new_in( @@ -847,7 +1014,8 @@ pub fn convert_ir_expression<'a>( IrExpression::DerivedLiteralArray(arr) => { let mut entries = OxcVec::with_capacity_in(arr.entries.len(), &allocator); for (i, entry) in arr.entries.iter().enumerate() { - let converted = convert_ir_expression(allocator, entry, expressions, root_xref); + let converted = + convert_ir_expression(allocator, core_namespace, entry, expressions, root_xref); if arr.spreads.get(i).copied().unwrap_or(false) { entries.push(OutputExpression::SpreadElement(Box::new_in( SpreadElementExpr { @@ -873,7 +1041,7 @@ pub fn convert_ir_expression<'a>( let quoted = map.quoted.get(i).copied().unwrap_or(false); let is_spread = map.spreads.get(i).copied().unwrap_or(false); let converted_value = - convert_ir_expression(allocator, value, expressions, root_xref); + convert_ir_expression(allocator, core_namespace, value, expressions, root_xref); entries.push(LiteralMapEntry { key, value: converted_value, quoted, is_spread }); } OutputExpression::LiteralMap(Box::new_in( @@ -884,7 +1052,8 @@ pub fn convert_ir_expression<'a>( // Logical NOT expression (!expr) IrExpression::Not(not) => { - let expr = convert_ir_expression(allocator, ¬.expr, expressions, root_xref); + let expr = + convert_ir_expression(allocator, core_namespace, ¬.expr, expressions, root_xref); OutputExpression::Not(Box::new_in( crate::output::ast::NotExpr { condition: Box::new_in(expr, &allocator), @@ -896,7 +1065,13 @@ pub fn convert_ir_expression<'a>( // Unary expression (+expr or -expr) IrExpression::Unary(unary) => { - let expr = convert_ir_expression(allocator, &unary.expr, expressions, root_xref); + let expr = convert_ir_expression( + allocator, + core_namespace, + &unary.expr, + expressions, + root_xref, + ); let operator = match unary.operator { crate::ir::expression::IrUnaryOperator::Plus => { crate::output::ast::UnaryOperator::Plus @@ -918,7 +1093,13 @@ pub fn convert_ir_expression<'a>( // Typeof expression (typeof expr) IrExpression::Typeof(typeof_expr) => { - let expr = convert_ir_expression(allocator, &typeof_expr.expr, expressions, root_xref); + let expr = convert_ir_expression( + allocator, + core_namespace, + &typeof_expr.expr, + expressions, + root_xref, + ); OutputExpression::Typeof(Box::new_in( crate::output::ast::TypeofExpr { expr: Box::new_in(expr, &allocator), @@ -930,7 +1111,13 @@ pub fn convert_ir_expression<'a>( // Void expression (void expr) IrExpression::Void(void_expr) => { - let expr = convert_ir_expression(allocator, &void_expr.expr, expressions, root_xref); + let expr = convert_ir_expression( + allocator, + core_namespace, + &void_expr.expr, + expressions, + root_xref, + ); OutputExpression::Void(Box::new_in( crate::output::ast::VoidExpr { expr: Box::new_in(expr, &allocator), @@ -957,6 +1144,7 @@ pub fn convert_ir_expression<'a>( for expr in rtl.expressions.iter() { output_expressions.push(convert_ir_expression( &allocator, + core_namespace, expr, expressions, root_xref, @@ -974,7 +1162,13 @@ pub fn convert_ir_expression<'a>( } IrExpression::Parenthesized(paren) => { - let inner = convert_ir_expression(allocator, &paren.expr, expressions, root_xref); + let inner = convert_ir_expression( + allocator, + core_namespace, + &paren.expr, + expressions, + root_xref, + ); OutputExpression::Parenthesized(Box::new_in( ParenthesizedExpr { expr: Box::new_in(inner, &allocator), @@ -1001,12 +1195,15 @@ pub fn convert_ir_expression<'a>( /// `i0.ɵɵtwoWayBindingSet(target, value)` fn convert_two_way_binding_set<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, tbs: &TwoWayBindingSetExpr<'a>, expressions: &ExpressionStore<'a>, root_xref: XrefId, ) -> OutputExpression<'a> { - let target = convert_ir_expression(allocator, &tbs.target, expressions, root_xref); - let value = convert_ir_expression(allocator, &tbs.value, expressions, root_xref); + let target = + convert_ir_expression(allocator, core_namespace, &tbs.target, expressions, root_xref); + let value = + convert_ir_expression(allocator, core_namespace, &tbs.value, expressions, root_xref); // Determine if target is a settable property/keyed expression or a variable let is_property_or_keyed = is_settable_property_target(&tbs.target); @@ -1026,8 +1223,12 @@ fn convert_two_way_binding_set<'a>( let mut args = OxcVec::new_in(&allocator); args.push(target); args.push(value); - let instruction_call = - create_instruction_call_expr(allocator, Identifiers::TWO_WAY_BINDING_SET, args); + let instruction_call = create_instruction_call_expr( + allocator, + core_namespace, + Identifiers::TWO_WAY_BINDING_SET, + args, + ); // Create the OR expression: instruction_call || (target = value) OutputExpression::BinaryOperator(Box::new_in( @@ -1044,7 +1245,12 @@ fn convert_two_way_binding_set<'a>( let mut args = OxcVec::new_in(&allocator); args.push(target); args.push(value); - create_instruction_call_expr(allocator, Identifiers::TWO_WAY_BINDING_SET, args) + create_instruction_call_expr( + allocator, + core_namespace, + Identifiers::TWO_WAY_BINDING_SET, + args, + ) } } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs index c0de5db04..b7df7c844 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs @@ -45,6 +45,7 @@ use crate::output::ast::ExpressionStatement; /// to a proper OutputStatement with all IR expressions converted. fn convert_statement_ir_nodes<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, stmt: &OutputStatement<'a>, expressions: &ExpressionStore<'a>, root_xref: XrefId, @@ -52,16 +53,26 @@ fn convert_statement_ir_nodes<'a>( ) -> OutputStatement<'a> { match stmt { OutputStatement::Return(ret) => { - let converted_expr = - convert_output_expr_ir_nodes(allocator, &ret.value, expressions, root_xref); + let converted_expr = convert_output_expr_ir_nodes( + allocator, + core_namespace, + &ret.value, + expressions, + root_xref, + ); OutputStatement::Return(Box::new_in( ReturnStatement { value: converted_expr, source_span: ret.source_span }, &allocator, )) } OutputStatement::Expression(expr_stmt) => { - let converted_expr = - convert_output_expr_ir_nodes(allocator, &expr_stmt.expr, expressions, root_xref); + let converted_expr = convert_output_expr_ir_nodes( + allocator, + core_namespace, + &expr_stmt.expr, + expressions, + root_xref, + ); OutputStatement::Expression(Box::new_in( ExpressionStatement { expr: converted_expr, source_span: expr_stmt.source_span }, &allocator, @@ -85,6 +96,7 @@ fn convert_statement_ir_nodes<'a>( /// Converts an OutputExpression that may be a WrappedIrNode to a proper OutputExpression. fn convert_output_expr_ir_nodes<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, expr: &OutputExpression<'a>, expressions: &ExpressionStore<'a>, root_xref: XrefId, @@ -92,7 +104,7 @@ fn convert_output_expr_ir_nodes<'a>( match expr { OutputExpression::WrappedIrNode(wrapped) => { // Convert the wrapped IR expression to a proper output expression - convert_ir_expression(allocator, &wrapped.node, expressions, root_xref) + convert_ir_expression(allocator, core_namespace, &wrapped.node, expressions, root_xref) } // All other expressions are already proper output expressions // Clone them to get owned values @@ -117,6 +129,8 @@ struct ReifyContext<'a> { supports_value_interpolation: bool, /// Whether to use `ɵɵdomProperty` (Angular 20+) or `ɵɵhostProperty` (Angular 19-). supports_dom_property: bool, + /// Namespace identifier for `@angular/core` runtime imports (`i0.ɵɵ*`). + core_namespace: &'a str, } /// Reifies IR expressions to Output AST. @@ -156,6 +170,7 @@ pub fn reify(job: &mut ComponentCompilationJob<'_>) { supports_conditional_create, supports_value_interpolation, supports_dom_property, + core_namespace: job.core_namespace.as_str(), }; // Collect xrefs of embedded views (excluding root) before splitting borrows @@ -212,11 +227,21 @@ fn reify_view_to_stmts<'a>( ) -> (std::vec::Vec>, std::vec::Vec>) { let mut create_stmts = std::vec::Vec::new(); let mut update_stmts = std::vec::Vec::new(); + let core_namespace = ctx.core_namespace; // Reify create operations // Use iter_mut() so we can take ownership of expressions that can't be cloned for op in view.create.iter_mut() { - let stmt = reify_create_op(allocator, op, expressions, pool, root_xref, ctx, diagnostics); + let stmt = reify_create_op( + allocator, + core_namespace, + op, + expressions, + pool, + root_xref, + ctx, + diagnostics, + ); if let Some(s) = stmt { create_stmts.push(s); } @@ -226,6 +251,7 @@ fn reify_view_to_stmts<'a>( for op in view.update.iter() { let stmt = reify_update_op( &allocator, + core_namespace, op, expressions, root_xref, @@ -245,6 +271,7 @@ fn reify_view_to_stmts<'a>( /// Reify a single create operation to a statement. fn reify_create_op<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, op: &mut CreateOp<'a>, expressions: &ExpressionStore<'a>, pool: &mut ConstantPool<'a>, @@ -261,6 +288,7 @@ fn reify_create_op<'a>( if is_dom_only { Some(create_dom_element_start_stmt( &allocator, + core_namespace, &elem.tag, slot, elem.attributes, @@ -269,6 +297,7 @@ fn reify_create_op<'a>( } else { Some(create_element_start_stmt( &allocator, + core_namespace, &elem.tag, slot, elem.attributes, @@ -282,6 +311,7 @@ fn reify_create_op<'a>( if is_dom_only { Some(create_dom_element_stmt( &allocator, + core_namespace, &elem.tag, slot, elem.attributes, @@ -290,6 +320,7 @@ fn reify_create_op<'a>( } else { Some(create_element_stmt( &allocator, + core_namespace, &elem.tag, slot, elem.attributes, @@ -299,9 +330,9 @@ fn reify_create_op<'a>( } CreateOp::ElementEnd(_) => { if is_dom_only { - Some(create_dom_element_end_stmt(allocator)) + Some(create_dom_element_end_stmt(allocator, core_namespace)) } else { - Some(create_element_end_stmt(allocator)) + Some(create_element_end_stmt(allocator, core_namespace)) } } CreateOp::Text(text) => { @@ -311,7 +342,7 @@ fn reify_create_op<'a>( } else { Some(text.initial_value.as_str()) }; - Some(create_text_stmt(allocator, slot, initial_value)) + Some(create_text_stmt(allocator, core_namespace, slot, initial_value)) } CreateOp::Template(tmpl) => { // Look up the function name for this template's embedded view @@ -331,6 +362,7 @@ fn reify_create_op<'a>( if use_dom_template { Some(create_dom_template_stmt( &allocator, + core_namespace, slot, fn_name, decls, @@ -342,6 +374,7 @@ fn reify_create_op<'a>( } else { Some(create_template_stmt( &allocator, + core_namespace, slot, fn_name, decls, @@ -363,6 +396,7 @@ fn reify_create_op<'a>( // Convert WrappedIrNode expressions in the statement to output expressions let converted_stmt = convert_statement_ir_nodes( &allocator, + core_namespace, &stmt_op.statement, expressions, root_xref, @@ -371,6 +405,7 @@ fn reify_create_op<'a>( handler_stmts.push(converted_stmt); } else if let Some(stmt) = reify_update_op( &allocator, + core_namespace, handler_op, expressions, root_xref, @@ -386,8 +421,13 @@ fn reify_create_op<'a>( // Always add handler_expression as a return statement at the end // This is the actual event handler logic if let Some(handler_expr) = &listener.handler_expression { - let output_expr = - convert_ir_expression(allocator, handler_expr, expressions, root_xref); + let output_expr = convert_ir_expression( + allocator, + core_namespace, + handler_expr, + expressions, + root_xref, + ); // Use return statement so the handler returns the result handler_stmts.push(OutputStatement::Return(Box::new_in( ReturnStatement { value: output_expr, source_span: None }, @@ -408,6 +448,7 @@ fn reify_create_op<'a>( if use_dom_listener { Some(create_dom_listener_stmt_with_handler( &allocator, + core_namespace, &listener.name, handler_stmts, event_target, @@ -417,6 +458,7 @@ fn reify_create_op<'a>( } else { Some(create_listener_stmt_with_handler( &allocator, + core_namespace, &listener.name, handler_stmts, event_target, @@ -446,6 +488,7 @@ fn reify_create_op<'a>( Some(create_projection_stmt( &allocator, + core_namespace, slot, projection_slot_index, attributes, @@ -459,21 +502,33 @@ fn reify_create_op<'a>( let attributes = container.attributes; let local_refs_index = container.local_refs_index; if is_dom_only { - Some(create_dom_container_stmt(allocator, slot, attributes, local_refs_index)) + Some(create_dom_container_stmt( + allocator, + core_namespace, + slot, + attributes, + local_refs_index, + )) } else { - Some(create_container_stmt(allocator, slot, attributes, local_refs_index)) + Some(create_container_stmt( + allocator, + core_namespace, + slot, + attributes, + local_refs_index, + )) } } CreateOp::ContainerEnd(_) => { if is_dom_only { - Some(create_dom_container_end_stmt(allocator)) + Some(create_dom_container_end_stmt(allocator, core_namespace)) } else { - Some(create_container_end_stmt(allocator)) + Some(create_container_end_stmt(allocator, core_namespace)) } } CreateOp::DeclareLet(decl) => { let slot = decl.slot.map(|s| s.0).unwrap_or(0); - Some(create_declare_let_stmt(allocator, slot)) + Some(create_declare_let_stmt(allocator, core_namespace, slot)) } CreateOp::Conditional(cond) => { // Look up the function name for this branch's view @@ -483,6 +538,7 @@ fn reify_create_op<'a>( // Angular 20+: Emit ɵɵconditionalCreate for the first branch in @if/@switch Some(create_conditional_create_stmt( &allocator, + core_namespace, slot, fn_name, cond.decls, @@ -495,6 +551,7 @@ fn reify_create_op<'a>( // Angular 19: Emit ɵɵtemplate instead (conditionalCreate doesn't exist) Some(create_template_stmt( &allocator, + core_namespace, slot, fn_name, cond.decls, @@ -518,6 +575,7 @@ fn reify_create_op<'a>( // Ported from Angular's reifyTrackBy() in reify.ts let track_fn_expr = reify_track_by( &allocator, + core_namespace, pool, expressions, root_xref, @@ -530,6 +588,7 @@ fn reify_create_op<'a>( Some(create_repeater_create_stmt_with_track_expr( &allocator, + core_namespace, slot, fn_name, repeater.decls, @@ -548,7 +607,7 @@ fn reify_create_op<'a>( CreateOp::Pipe(pipe) => { // Emit pipe instruction let slot = pipe.slot.map(|s| s.0).unwrap_or(0); - Some(create_pipe_stmt(allocator, slot, &pipe.name)) + Some(create_pipe_stmt(allocator, core_namespace, slot, &pipe.name)) } CreateOp::Defer(defer) => { // Emit defer instruction for @defer @@ -572,6 +631,7 @@ fn reify_create_op<'a>( }); Some(create_defer_stmt( &allocator, + core_namespace, slot, defer.main_slot.map(|s| s.0), defer.resolver_fn.take(), @@ -585,12 +645,12 @@ fn reify_create_op<'a>( } CreateOp::DeferOn(defer_on) => { // Emit deferOn instruction based on trigger kind - let options = defer_on - .options - .as_ref() - .map(|expr| convert_ir_expression(allocator, expr, expressions, root_xref)); + let options = defer_on.options.as_ref().map(|expr| { + convert_ir_expression(allocator, core_namespace, expr, expressions, root_xref) + }); Some(create_defer_on_stmt( &allocator, + core_namespace, defer_on.trigger, defer_on.target_slot.map(|s| s.0), defer_on.target_slot_view_steps, @@ -604,6 +664,7 @@ fn reify_create_op<'a>( let slot = i18n.slot.map(|s| s.0).unwrap_or(0); Some(create_i18n_start_stmt( &allocator, + core_namespace, slot, i18n.message_index, i18n.sub_template_index, @@ -612,27 +673,37 @@ fn reify_create_op<'a>( CreateOp::I18n(i18n) => { // Emit i18n instruction let slot = i18n.slot.map(|s| s.0).unwrap_or(0); - Some(create_i18n_stmt(allocator, slot, i18n.message_index, i18n.sub_template_index)) + Some(create_i18n_stmt( + allocator, + core_namespace, + slot, + i18n.message_index, + i18n.sub_template_index, + )) } CreateOp::I18nEnd(_) => { // Emit i18nEnd instruction - Some(create_i18n_end_stmt(allocator)) + Some(create_i18n_end_stmt(allocator, core_namespace)) } CreateOp::Namespace(ns) => { // Emit namespace change instruction - Some(create_namespace_stmt(allocator, ns.active)) + Some(create_namespace_stmt(allocator, core_namespace, ns.active)) } CreateOp::ProjectionDef(proj_def) => { // Emit projectionDef instruction with R3 format def expression - Some(create_projection_def_stmt_from_expr(allocator, proj_def.def.as_ref())) + Some(create_projection_def_stmt_from_expr( + allocator, + core_namespace, + proj_def.def.as_ref(), + )) } CreateOp::DisableBindings(_) => { // Emit disableBindings instruction - Some(create_disable_bindings_stmt(allocator)) + Some(create_disable_bindings_stmt(allocator, core_namespace)) } CreateOp::EnableBindings(_) => { // Emit enableBindings instruction - Some(create_enable_bindings_stmt(allocator)) + Some(create_enable_bindings_stmt(allocator, core_namespace)) } CreateOp::TwoWayListener(listener) => { // Emit twoWayListener instruction @@ -644,6 +715,7 @@ fn reify_create_op<'a>( // Convert WrappedIrNode expressions in the statement to output expressions let converted_stmt = convert_statement_ir_nodes( &allocator, + core_namespace, &stmt_op.statement, expressions, root_xref, @@ -652,6 +724,7 @@ fn reify_create_op<'a>( handler_stmts.push(converted_stmt); } else if let Some(stmt) = reify_update_op( &allocator, + core_namespace, handler_op, expressions, root_xref, @@ -665,6 +738,7 @@ fn reify_create_op<'a>( } Some(create_two_way_listener_stmt( &allocator, + core_namespace, &listener.name, handler_stmts, listener.handler_fn_name.as_ref(), @@ -676,6 +750,7 @@ fn reify_create_op<'a>( for handler_op in listener.handler_ops.iter() { if let Some(stmt) = reify_update_op( &allocator, + core_namespace, handler_op, expressions, root_xref, @@ -689,6 +764,7 @@ fn reify_create_op<'a>( } Some(create_animation_listener_stmt( &allocator, + core_namespace, &listener.name, listener.phase, handler_stmts, @@ -698,8 +774,14 @@ fn reify_create_op<'a>( } CreateOp::AnimationString(anim) => { // Emit ɵɵanimateEnter or ɵɵanimateLeave instruction for animation string bindings - let expr = convert_ir_expression(allocator, &anim.expression, expressions, root_xref); - Some(create_animation_string_stmt(allocator, anim.animation_kind, expr)) + let expr = convert_ir_expression( + allocator, + core_namespace, + &anim.expression, + expressions, + root_xref, + ); + Some(create_animation_string_stmt(allocator, core_namespace, anim.animation_kind, expr)) } CreateOp::Animation(anim) => { // Emit ɵɵanimateEnter or ɵɵanimateLeave instruction for animation bindings (Value kind) @@ -708,6 +790,7 @@ fn reify_create_op<'a>( for handler_op in anim.handler_ops.iter() { if let Some(stmt) = reify_update_op( &allocator, + core_namespace, handler_op, expressions, root_xref, @@ -721,6 +804,7 @@ fn reify_create_op<'a>( } Some(create_animation_op_stmt( &allocator, + core_namespace, anim.animation_kind, handler_stmts, anim.handler_fn_name.as_ref(), @@ -729,7 +813,13 @@ fn reify_create_op<'a>( CreateOp::Variable(var) => { // Emit variable declaration with initializer // All Variable ops use `const` (StmtModifier::Final), matching Angular's reify.ts - let value = convert_ir_expression(allocator, &var.initializer, expressions, root_xref); + let value = convert_ir_expression( + allocator, + core_namespace, + &var.initializer, + expressions, + root_xref, + ); Some(create_variable_decl_stmt_with_value(allocator, &var.name, value)) } CreateOp::ContainerStart(container) => { @@ -738,9 +828,21 @@ fn reify_create_op<'a>( let attributes = container.attributes; let local_refs_index = container.local_refs_index; if is_dom_only { - Some(create_dom_container_start_stmt(allocator, slot, attributes, local_refs_index)) + Some(create_dom_container_start_stmt( + allocator, + core_namespace, + slot, + attributes, + local_refs_index, + )) } else { - Some(create_container_start_stmt(allocator, slot, attributes, local_refs_index)) + Some(create_container_start_stmt( + allocator, + core_namespace, + slot, + attributes, + local_refs_index, + )) } } CreateOp::I18nAttributes(i18n_attrs) => { @@ -751,7 +853,7 @@ fn reify_create_op<'a>( I18nSlotHandle::Single(slot_id) => slot_id.0, I18nSlotHandle::Range(start, _) => start.0, }; - Some(create_i18n_attributes_stmt(allocator, slot, config_index)) + Some(create_i18n_attributes_stmt(allocator, core_namespace, slot, config_index)) } else { None } @@ -764,6 +866,7 @@ fn reify_create_op<'a>( // Angular 20+: Emit ɵɵconditionalBranchCreate for branches after the first Some(create_conditional_branch_create_stmt( &allocator, + core_namespace, slot, fn_name, branch.decls, @@ -776,6 +879,7 @@ fn reify_create_op<'a>( // Angular 19: Emit ɵɵtemplate instead (conditionalBranchCreate doesn't exist) Some(create_template_stmt( &allocator, + core_namespace, slot, fn_name, branch.decls, @@ -788,7 +892,7 @@ fn reify_create_op<'a>( } CreateOp::ControlCreate(_) => { // Emit ɵɵcontrolCreate instruction for control binding initialization - Some(create_control_create_stmt(allocator)) + Some(create_control_create_stmt(allocator, core_namespace)) } // Non-emitting operations CreateOp::ListEnd(_) @@ -806,6 +910,7 @@ fn reify_create_op<'a>( /// Reify a single update operation to a statement. fn reify_update_op<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, op: &UpdateOp<'a>, expressions: &ExpressionStore<'a>, root_xref: XrefId, @@ -829,6 +934,7 @@ fn reify_update_op<'a>( let has_extra_args = prop.sanitizer.is_some(); let (interp_args, expr_count) = reify_interpolation( &allocator, + core_namespace, &prop.expression, expressions, root_xref, @@ -836,6 +942,7 @@ fn reify_update_op<'a>( ); Some(create_property_interpolate_stmt( &allocator, + core_namespace, &prop.name, interp_args, expr_count, @@ -843,12 +950,18 @@ fn reify_update_op<'a>( )) } else { // Angular 20+: Use ɵɵproperty("name", ɵɵinterpolate1(...)) or ɵɵproperty("name", expr) - let expr = - convert_ir_expression(allocator, &prop.expression, expressions, root_xref); + let expr = convert_ir_expression( + allocator, + core_namespace, + &prop.expression, + expressions, + root_xref, + ); if is_dom_only && !is_animation { if supports_dom_property { Some(create_dom_property_stmt( &allocator, + core_namespace, &prop.name, expr, prop.sanitizer.as_ref(), @@ -856,16 +969,18 @@ fn reify_update_op<'a>( } else { Some(create_host_property_stmt( &allocator, + core_namespace, &prop.name, expr, prop.sanitizer.as_ref(), )) } } else if is_aria_attribute(prop.name.as_str()) { - Some(create_aria_property_stmt(allocator, &prop.name, expr)) + Some(create_aria_property_stmt(allocator, core_namespace, &prop.name, expr)) } else { Some(create_property_stmt_with_expr( &allocator, + core_namespace, &prop.name, expr, prop.sanitizer.as_ref(), @@ -877,17 +992,28 @@ fn reify_update_op<'a>( // Handle multiple interpolations like "{{a}} and {{b}}" let (args, expr_count) = reify_interpolation( &allocator, + core_namespace, &interp.interpolation, expressions, root_xref, false, ); - Some(create_text_interpolate_stmt_with_args(allocator, args, expr_count)) + Some(create_text_interpolate_stmt_with_args( + allocator, + core_namespace, + args, + expr_count, + )) } UpdateOp::Binding(binding) => { - let expr = - convert_ir_expression(allocator, &binding.expression, expressions, root_xref); - Some(create_binding_stmt_with_expr(allocator, &binding.name, expr)) + let expr = convert_ir_expression( + allocator, + core_namespace, + &binding.expression, + expressions, + root_xref, + ); + Some(create_binding_stmt_with_expr(allocator, core_namespace, &binding.name, expr)) } UpdateOp::StyleProp(style) => { // Strip "style." prefix if present @@ -899,6 +1025,7 @@ fn reify_update_op<'a>( let has_extra_args = style.unit.is_some(); let (interp_args, expr_count) = reify_interpolation( &allocator, + core_namespace, &style.expression, expressions, root_xref, @@ -906,6 +1033,7 @@ fn reify_update_op<'a>( ); Some(create_style_prop_interpolate_stmt( &allocator, + core_namespace, &name, interp_args, expr_count, @@ -913,16 +1041,33 @@ fn reify_update_op<'a>( )) } else { // Angular 20+: Use ɵɵstyleProp("name", ɵɵinterpolate1(...), [unit]) - let expr = - convert_ir_expression(allocator, &style.expression, expressions, root_xref); - Some(create_style_prop_stmt_with_expr(allocator, &name, expr, style.unit.as_ref())) + let expr = convert_ir_expression( + allocator, + core_namespace, + &style.expression, + expressions, + root_xref, + ); + Some(create_style_prop_stmt_with_expr( + allocator, + core_namespace, + &name, + expr, + style.unit.as_ref(), + )) } } UpdateOp::ClassProp(class) => { - let expr = convert_ir_expression(allocator, &class.expression, expressions, root_xref); + let expr = convert_ir_expression( + allocator, + core_namespace, + &class.expression, + expressions, + root_xref, + ); // Strip "class." prefix if present let name = strip_prefix(&class.name, "class."); - Some(create_class_prop_stmt_with_expr(allocator, &name, expr)) + Some(create_class_prop_stmt_with_expr(allocator, core_namespace, &name, expr)) } UpdateOp::Attribute(attr) => { // Strip "attr." prefix if present @@ -934,6 +1079,7 @@ fn reify_update_op<'a>( let has_extra_args = attr.sanitizer.is_some() || attr.namespace.is_some(); let (interp_args, expr_count) = reify_interpolation( &allocator, + core_namespace, &attr.expression, expressions, root_xref, @@ -941,6 +1087,7 @@ fn reify_update_op<'a>( ); Some(create_attribute_interpolate_stmt( &allocator, + core_namespace, &name, interp_args, expr_count, @@ -949,10 +1096,16 @@ fn reify_update_op<'a>( )) } else { // Angular 20+: Use ɵɵattribute("name", ɵɵinterpolate1(...)) - let expr = - convert_ir_expression(allocator, &attr.expression, expressions, root_xref); + let expr = convert_ir_expression( + allocator, + core_namespace, + &attr.expression, + expressions, + root_xref, + ); Some(create_attribute_stmt_with_expr( &allocator, + core_namespace, &name, expr, attr.sanitizer.as_ref(), @@ -960,7 +1113,7 @@ fn reify_update_op<'a>( )) } } - UpdateOp::Advance(adv) => Some(create_advance_stmt(allocator, adv.delta)), + UpdateOp::Advance(adv) => Some(create_advance_stmt(allocator, core_namespace, adv.delta)), UpdateOp::StoreLet(store) => { // StoreLet as an update op should have been converted to a StoreLet expression // during the store_let_optimization phase. If it reaches reify, it's a compiler bug. @@ -972,29 +1125,46 @@ fn reify_update_op<'a>( None } UpdateOp::TwoWayProperty(twp) => { - let expr = convert_ir_expression(allocator, &twp.expression, expressions, root_xref); - Some(create_two_way_property_stmt(allocator, &twp.name, expr, twp.sanitizer.as_ref())) + let expr = convert_ir_expression( + allocator, + core_namespace, + &twp.expression, + expressions, + root_xref, + ); + Some(create_two_way_property_stmt( + allocator, + core_namespace, + &twp.name, + expr, + twp.sanitizer.as_ref(), + )) } UpdateOp::Repeater(rep) => { - let expr = convert_ir_expression(allocator, &rep.collection, expressions, root_xref); - Some(create_repeater_stmt(allocator, expr)) + let expr = convert_ir_expression( + allocator, + core_namespace, + &rep.collection, + expressions, + root_xref, + ); + Some(create_repeater_stmt(allocator, core_namespace, expr)) } UpdateOp::Conditional(cond) => { // Use processed expression (built by conditionals phase). // Angular asserts that processed is always set by this point // (throws "Conditional test was not set." in reify.ts:698). let expr = if let Some(ref processed) = cond.processed { - convert_ir_expression(allocator, processed, expressions, root_xref) + convert_ir_expression(allocator, core_namespace, processed, expressions, root_xref) } else { diagnostics .push(OxcDiagnostic::error("AssertionError: Conditional test was not set.")); return None; }; - let context_value = cond - .context_value - .as_ref() - .map(|cv| convert_ir_expression(allocator, cv, expressions, root_xref)); - Some(create_conditional_update_stmt(allocator, expr, context_value)) + let context_value = cond.context_value.as_ref().map(|cv| { + convert_ir_expression(allocator, core_namespace, cv, expressions, root_xref) + }); + Some(create_conditional_update_stmt(allocator, core_namespace, expr, context_value)) } UpdateOp::StyleMap(style) => { let is_interpolation = matches!(*style.expression, IrExpression::Interpolation(_)); @@ -1003,17 +1173,28 @@ fn reify_update_op<'a>( // Angular 19: Use ɵɵstyleMapInterpolate*(s0, v0, s1, ...) let (interp_args, expr_count) = reify_interpolation( &allocator, + core_namespace, &style.expression, expressions, root_xref, false, ); - Some(create_style_map_interpolate_stmt(allocator, interp_args, expr_count)) + Some(create_style_map_interpolate_stmt( + allocator, + core_namespace, + interp_args, + expr_count, + )) } else { // Angular 20+: Use ɵɵstyleMap(ɵɵinterpolate1(...)) - let expr = - convert_ir_expression(allocator, &style.expression, expressions, root_xref); - Some(create_style_map_stmt(allocator, expr)) + let expr = convert_ir_expression( + allocator, + core_namespace, + &style.expression, + expressions, + root_xref, + ); + Some(create_style_map_stmt(allocator, core_namespace, expr)) } } UpdateOp::ClassMap(class) => { @@ -1023,33 +1204,57 @@ fn reify_update_op<'a>( // Angular 19: Use ɵɵclassMapInterpolate*(s0, v0, s1, ...) let (interp_args, expr_count) = reify_interpolation( &allocator, + core_namespace, &class.expression, expressions, root_xref, false, ); - Some(create_class_map_interpolate_stmt(allocator, interp_args, expr_count)) + Some(create_class_map_interpolate_stmt( + allocator, + core_namespace, + interp_args, + expr_count, + )) } else { // Angular 20+: Use ɵɵclassMap(ɵɵinterpolate1(...)) - let expr = - convert_ir_expression(allocator, &class.expression, expressions, root_xref); - Some(create_class_map_stmt(allocator, expr)) + let expr = convert_ir_expression( + allocator, + core_namespace, + &class.expression, + expressions, + root_xref, + ); + Some(create_class_map_stmt(allocator, core_namespace, expr)) } } UpdateOp::DomProperty(prop) => { - let expr = convert_ir_expression(allocator, &prop.expression, expressions, root_xref); + let expr = convert_ir_expression( + allocator, + core_namespace, + &prop.expression, + expressions, + root_xref, + ); // Animation bindings use syntheticHostProperty instead of domProperty // Matches Angular's reify.ts lines 662-675 let is_animation = matches!(prop.binding_kind, BindingKind::LegacyAnimation | BindingKind::Animation); if is_animation { - Some(create_animation_stmt(allocator, &prop.name, expr)) + Some(create_animation_stmt(allocator, core_namespace, &prop.name, expr)) } else if supports_dom_property { - Some(create_dom_property_stmt(allocator, &prop.name, expr, prop.sanitizer.as_ref())) + Some(create_dom_property_stmt( + allocator, + core_namespace, + &prop.name, + expr, + prop.sanitizer.as_ref(), + )) } else { // Angular 19: Use ɵɵhostProperty instead of ɵɵdomProperty Some(create_host_property_stmt( &allocator, + core_namespace, &prop.name, expr, prop.sanitizer.as_ref(), @@ -1057,8 +1262,14 @@ fn reify_update_op<'a>( } } UpdateOp::I18nExpression(i18n) => { - let expr = convert_ir_expression(allocator, &i18n.expression, expressions, root_xref); - Some(create_i18n_exp_stmt(allocator, expr)) + let expr = convert_ir_expression( + allocator, + core_namespace, + &i18n.expression, + expressions, + root_xref, + ); + Some(create_i18n_exp_stmt(allocator, core_namespace, expr)) } UpdateOp::I18nApply(i18n) => { // Use the slot from handle, matching Angular's reify.ts line 648: @@ -1067,24 +1278,41 @@ fn reify_update_op<'a>( crate::ir::ops::I18nSlotHandle::Single(slot_id) => slot_id.0, crate::ir::ops::I18nSlotHandle::Range(start, _) => start.0, }; - Some(create_i18n_apply_stmt(allocator, slot)) + Some(create_i18n_apply_stmt(allocator, core_namespace, slot)) } UpdateOp::AnimationBinding(anim) => { - let expr = convert_ir_expression(allocator, &anim.expression, expressions, root_xref); - Some(create_animation_binding_stmt(allocator, &anim.name, expr)) + let expr = convert_ir_expression( + allocator, + core_namespace, + &anim.expression, + expressions, + root_xref, + ); + Some(create_animation_binding_stmt(allocator, core_namespace, &anim.name, expr)) } - UpdateOp::Control(_) => Some(create_control_stmt(allocator)), + UpdateOp::Control(_) => Some(create_control_stmt(allocator, core_namespace)), UpdateOp::Variable(var) => { // Emit variable declaration with initializer for update phase // All Variable ops use `const` (StmtModifier::Final), matching Angular's reify.ts - let value = convert_ir_expression(allocator, &var.initializer, expressions, root_xref); + let value = convert_ir_expression( + allocator, + core_namespace, + &var.initializer, + expressions, + root_xref, + ); Some(create_variable_decl_stmt_with_value(allocator, &var.name, value)) } UpdateOp::DeferWhen(defer_when) => { // Emit deferWhen runtime instruction with condition - let expr = - convert_ir_expression(allocator, &defer_when.condition, expressions, root_xref); - Some(create_defer_when_stmt(allocator, defer_when.modifier, expr)) + let expr = convert_ir_expression( + allocator, + core_namespace, + &defer_when.condition, + expressions, + root_xref, + ); + Some(create_defer_when_stmt(allocator, core_namespace, defer_when.modifier, expr)) } // Non-emitting or already handled operations UpdateOp::ListEnd(_) => None, @@ -1093,6 +1321,7 @@ fn reify_update_op<'a>( // These may contain WrappedIrNode expressions that need to be converted. UpdateOp::Statement(stmt_op) => Some(convert_statement_ir_nodes( &allocator, + core_namespace, &stmt_op.statement, expressions, root_xref, @@ -1109,6 +1338,7 @@ fn reify_update_op<'a>( /// empty strings are dropped (the Angular runtime defaults them to ""). fn reify_interpolation<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, interpolation: &IrExpression<'a>, expressions: &ExpressionStore<'a>, root_xref: XrefId, @@ -1124,6 +1354,7 @@ fn reify_interpolation<'a>( if expr_count == 1 && ir_interp.strings.iter().all(|s| s.is_empty()) { args.push(convert_ir_expression( &allocator, + core_namespace, &ir_interp.expressions[0], expressions, root_xref, @@ -1139,7 +1370,13 @@ fn reify_interpolation<'a>( &allocator, ))); } - args.push(convert_ir_expression(allocator, expr, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + expr, + expressions, + root_xref, + )); } if ir_interp.strings.len() > ir_interp.expressions.len() { if let Some(trailing) = ir_interp.strings.last() { @@ -1214,7 +1451,13 @@ fn reify_interpolation<'a>( _ => { // Other expression types - convert directly let mut args = OxcVec::new_in(&allocator); - args.push(convert_ir_expression(allocator, interpolation, expressions, root_xref)); + args.push(convert_ir_expression( + allocator, + core_namespace, + interpolation, + expressions, + root_xref, + )); (args, 1) } } @@ -1230,12 +1473,14 @@ pub fn reify_host(job: &mut HostBindingCompilationJob<'_>) { let root_xref = job.root.xref; let supports_value_interpolation = job.supports_value_interpolation(); let supports_dom_property = job.supports_dom_property(); + let core_namespace = job.core_namespace.as_str(); let mut diagnostics = Vec::new(); // Reify create operations (listeners) for op in job.root.create.iter() { let stmt = reify_host_create_op( &allocator, + core_namespace, op, &job.expressions, root_xref, @@ -1253,6 +1498,7 @@ pub fn reify_host(job: &mut HostBindingCompilationJob<'_>) { for op in job.root.update.iter() { let stmt = reify_update_op( &allocator, + core_namespace, op, &job.expressions, root_xref, @@ -1272,6 +1518,7 @@ pub fn reify_host(job: &mut HostBindingCompilationJob<'_>) { /// Reify a single create operation for host bindings. fn reify_host_create_op<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, op: &CreateOp<'a>, expressions: &ExpressionStore<'a>, root_xref: XrefId, @@ -1289,6 +1536,7 @@ fn reify_host_create_op<'a>( // Convert WrappedIrNode expressions in the statement to output expressions let converted_stmt = convert_statement_ir_nodes( &allocator, + core_namespace, &stmt_op.statement, expressions, root_xref, @@ -1297,6 +1545,7 @@ fn reify_host_create_op<'a>( handler_stmts.push(converted_stmt); } else if let Some(stmt) = reify_update_op( &allocator, + core_namespace, handler_op, expressions, root_xref, @@ -1312,8 +1561,13 @@ fn reify_host_create_op<'a>( // Add handler_expression as a return statement at the end // This is the actual event handler logic if let Some(handler_expr) = &listener.handler_expression { - let output_expr = - convert_ir_expression(allocator, handler_expr, expressions, root_xref); + let output_expr = convert_ir_expression( + allocator, + core_namespace, + handler_expr, + expressions, + root_xref, + ); // Use return statement so the handler returns the result handler_stmts.push(OutputStatement::Return(Box::new_in( ReturnStatement { value: output_expr, source_span: None }, @@ -1335,6 +1589,7 @@ fn reify_host_create_op<'a>( if is_synthetic_host { Some(create_synthetic_host_listener_stmt( &allocator, + core_namespace, &listener.name, handler_stmts, listener.handler_fn_name.as_ref(), @@ -1351,6 +1606,7 @@ fn reify_host_create_op<'a>( let use_capture = listener.host_listener && listener.is_animation_listener; Some(create_listener_stmt_with_handler( &allocator, + core_namespace, &listener.name, handler_stmts, event_target, @@ -1367,6 +1623,7 @@ fn reify_host_create_op<'a>( // Host bindings use Full mode (not DomOnly) if let Some(stmt) = reify_update_op( &allocator, + core_namespace, handler_op, expressions, root_xref, @@ -1380,6 +1637,7 @@ fn reify_host_create_op<'a>( } Some(create_animation_listener_stmt( &allocator, + core_namespace, &listener.name, listener.phase, handler_stmts, @@ -1410,6 +1668,7 @@ fn reify_host_create_op<'a>( #[allow(clippy::too_many_arguments)] fn reify_track_by<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, pool: &mut ConstantPool<'a>, expressions: &ExpressionStore<'a>, root_xref: XrefId, @@ -1427,7 +1686,7 @@ fn reify_track_by<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -1463,7 +1722,7 @@ fn reify_track_by<'a>( receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( ReadVarExpr { - name: Ident::from("i0"), + name: Ident::from(core_namespace), source_span: None, }, &allocator, @@ -1541,6 +1800,7 @@ fn reify_track_by<'a>( // so version flags don't matter here. if let Some(stmt) = reify_update_op( &allocator, + core_namespace, track_op, expressions, root_xref, @@ -1599,7 +1859,8 @@ fn reify_track_by<'a>( // fn = op.usesComponentInstance // ? o.fn(params, [new o.ReturnStatement(op.track)]) // : o.arrowFn(params, op.track); - let track_body = convert_ir_expression(allocator, track, expressions, root_xref); + let track_body = + convert_ir_expression(allocator, core_namespace, track, expressions, root_xref); if uses_component_instance { let mut stmts = OxcVec::with_capacity_in(1, &allocator); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/bindings.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/bindings.rs index 49854e565..33312a90e 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/bindings.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/bindings.rs @@ -46,6 +46,7 @@ fn remap_dom_property<'a>(name: &Ident<'a>) -> Ident<'a> { /// This creates an expression like `i0.ɵɵsanitizeHtml`. fn create_sanitizer_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, sanitizer: &Ident<'a>, ) -> OutputExpression<'a> { // Create: i0.ɵɵsanitize* expression @@ -53,7 +54,7 @@ fn create_sanitizer_expr<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -69,6 +70,7 @@ fn create_sanitizer_expr<'a>( /// Creates an ɵɵproperty() call statement with expression value. pub fn create_property_stmt_with_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, sanitizer: Option<&Ident<'a>>, @@ -80,9 +82,9 @@ pub fn create_property_stmt_with_expr<'a>( ))); args.push(value); if let Some(san) = sanitizer { - args.push(create_sanitizer_expr(allocator, san)); + args.push(create_sanitizer_expr(allocator, core_namespace, san)); } - create_instruction_call_stmt(allocator, Identifiers::PROPERTY, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::PROPERTY, args) } /// Creates an ɵɵariaProperty() call statement for ARIA property binding. @@ -91,6 +93,7 @@ pub fn create_property_stmt_with_expr<'a>( /// that sets the ARIA attribute rather than a DOM property. pub fn create_aria_property_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, ) -> OutputStatement<'a> { @@ -100,12 +103,13 @@ pub fn create_aria_property_stmt<'a>( &allocator, ))); args.push(value); - create_instruction_call_stmt(allocator, Identifiers::ARIA_PROPERTY, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::ARIA_PROPERTY, args) } /// Creates a generic binding statement with expression value. pub fn create_binding_stmt_with_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, ) -> OutputStatement<'a> { @@ -116,12 +120,13 @@ pub fn create_binding_stmt_with_expr<'a>( ))); args.push(value); // This should be specialized by binding_specialization phase - create_instruction_call_stmt(allocator, Identifiers::PROPERTY, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::PROPERTY, args) } /// Creates an ɵɵstyleProp() call statement with expression. pub fn create_style_prop_stmt_with_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, unit: Option<&Ident<'a>>, @@ -139,12 +144,13 @@ pub fn create_style_prop_stmt_with_expr<'a>( &allocator, ))); } - create_instruction_call_stmt(allocator, Identifiers::STYLE_PROP, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::STYLE_PROP, args) } /// Creates an ɵɵclassProp() call statement with expression. pub fn create_class_prop_stmt_with_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, ) -> OutputStatement<'a> { @@ -154,7 +160,7 @@ pub fn create_class_prop_stmt_with_expr<'a>( &allocator, ))); args.push(value); - create_instruction_call_stmt(allocator, Identifiers::CLASS_PROP, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::CLASS_PROP, args) } /// Creates an ɵɵattribute() call statement with expression. @@ -163,6 +169,7 @@ pub fn create_class_prop_stmt_with_expr<'a>( /// If sanitizer is None but namespace is Some, emits null for sanitizer. pub fn create_attribute_stmt_with_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, sanitizer: Option<&Ident<'a>>, @@ -177,7 +184,7 @@ pub fn create_attribute_stmt_with_expr<'a>( // Add sanitizer if present, or null if namespace is present if sanitizer.is_some() || namespace.is_some() { if let Some(san) = sanitizer { - args.push(create_sanitizer_expr(allocator, san)); + args.push(create_sanitizer_expr(allocator, core_namespace, san)); } else { args.push(OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Null, source_span: None }, @@ -192,12 +199,13 @@ pub fn create_attribute_stmt_with_expr<'a>( &allocator, ))); } - create_instruction_call_stmt(allocator, Identifiers::ATTRIBUTE, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::ATTRIBUTE, args) } /// Creates an ɵɵtwoWayProperty() call statement. pub fn create_two_way_property_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, sanitizer: Option<&Ident<'a>>, @@ -209,9 +217,9 @@ pub fn create_two_way_property_stmt<'a>( ))); args.push(value); if let Some(san) = sanitizer { - args.push(create_sanitizer_expr(allocator, san)); + args.push(create_sanitizer_expr(allocator, core_namespace, san)); } - create_instruction_call_stmt(allocator, Identifiers::TWO_WAY_PROPERTY, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::TWO_WAY_PROPERTY, args) } /// Creates an ɵɵdomProperty() call statement for DOM property binding. @@ -221,6 +229,7 @@ pub fn create_two_way_property_stmt<'a>( /// The property name is remapped if necessary (e.g., `for` -> `htmlFor`). pub fn create_dom_property_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, sanitizer: Option<&Ident<'a>>, @@ -233,34 +242,37 @@ pub fn create_dom_property_stmt<'a>( ))); args.push(value); if let Some(san) = sanitizer { - args.push(create_sanitizer_expr(allocator, san)); + args.push(create_sanitizer_expr(allocator, core_namespace, san)); } - create_instruction_call_stmt(allocator, Identifiers::DOM_PROPERTY, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::DOM_PROPERTY, args) } /// Creates an ɵɵstyleMap() call statement. pub fn create_style_map_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, value: OutputExpression<'a>, ) -> OutputStatement<'a> { let mut args = OxcVec::new_in(&allocator); args.push(value); - create_instruction_call_stmt(allocator, Identifiers::STYLE_MAP, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::STYLE_MAP, args) } /// Creates an ɵɵclassMap() call statement. pub fn create_class_map_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, value: OutputExpression<'a>, ) -> OutputStatement<'a> { let mut args = OxcVec::new_in(&allocator); args.push(value); - create_instruction_call_stmt(allocator, Identifiers::CLASS_MAP, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::CLASS_MAP, args) } /// Creates an ɵɵtextInterpolate() call statement with arguments. pub fn create_text_interpolate_stmt_with_args<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, args: OxcVec<'a, OutputExpression<'a>>, expr_count: usize, ) -> OutputStatement<'a> { @@ -272,7 +284,7 @@ pub fn create_text_interpolate_stmt_with_args<'a>( } else { get_text_interpolate_instruction(expr_count) }; - create_instruction_call_stmt(allocator, instruction, args) + create_instruction_call_stmt(allocator, core_namespace, instruction, args) } /// Creates an ɵɵpropertyInterpolate*() call statement (Angular 19). @@ -284,6 +296,7 @@ pub fn create_text_interpolate_stmt_with_args<'a>( /// Arguments: name, [s0, v0, s1, v1, ..., sN], [sanitizer] pub fn create_property_interpolate_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, interp_args: OxcVec<'a, OutputExpression<'a>>, expr_count: usize, @@ -306,7 +319,7 @@ pub fn create_property_interpolate_stmt<'a>( } // Optional sanitizer if let Some(san) = sanitizer { - args.push(create_sanitizer_expr(allocator, san)); + args.push(create_sanitizer_expr(allocator, core_namespace, san)); } let instruction = if expr_count == 1 && interp_args_len == 1 { // Simple case: just name + value (no surrounding strings) @@ -315,7 +328,7 @@ pub fn create_property_interpolate_stmt<'a>( } else { get_property_interpolate_instruction(expr_count) }; - create_instruction_call_stmt(allocator, instruction, args) + create_instruction_call_stmt(allocator, core_namespace, instruction, args) } /// Creates an ɵɵattributeInterpolate*() call statement (Angular 19). @@ -327,6 +340,7 @@ pub fn create_property_interpolate_stmt<'a>( /// Arguments: name, [s0, v0, s1, v1, ..., sN], [sanitizer], [namespace] pub fn create_attribute_interpolate_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, interp_args: OxcVec<'a, OutputExpression<'a>>, expr_count: usize, @@ -348,7 +362,7 @@ pub fn create_attribute_interpolate_stmt<'a>( // Optional sanitizer, or null if namespace is present if sanitizer.is_some() || namespace.is_some() { if let Some(san) = sanitizer { - args.push(create_sanitizer_expr(allocator, san)); + args.push(create_sanitizer_expr(allocator, core_namespace, san)); } else { args.push(OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Null, source_span: None }, @@ -368,7 +382,7 @@ pub fn create_attribute_interpolate_stmt<'a>( } else { get_attribute_interpolate_instruction(expr_count) }; - create_instruction_call_stmt(allocator, instruction, args) + create_instruction_call_stmt(allocator, core_namespace, instruction, args) } /// Creates an ɵɵhostProperty() call statement (Angular 19). @@ -376,6 +390,7 @@ pub fn create_attribute_interpolate_stmt<'a>( /// For Angular 19, host/DomOnly property bindings use `ɵɵhostProperty` instead of `ɵɵdomProperty`. pub fn create_host_property_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, sanitizer: Option<&Ident<'a>>, @@ -388,9 +403,9 @@ pub fn create_host_property_stmt<'a>( ))); args.push(value); if let Some(san) = sanitizer { - args.push(create_sanitizer_expr(allocator, san)); + args.push(create_sanitizer_expr(allocator, core_namespace, san)); } - create_instruction_call_stmt(allocator, Identifiers::HOST_PROPERTY, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::HOST_PROPERTY, args) } /// Creates an ɵɵstylePropInterpolate*() call statement (Angular 19). @@ -402,6 +417,7 @@ pub fn create_host_property_stmt<'a>( /// Signature: `ɵɵstylePropInterpolateN(prop, s0, v0, ..., [unit])` pub fn create_style_prop_interpolate_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, interp_args: OxcVec<'a, OutputExpression<'a>>, expr_count: usize, @@ -425,7 +441,7 @@ pub fn create_style_prop_interpolate_stmt<'a>( ))); } let instruction = get_style_prop_interpolate_instruction(expr_count); - create_instruction_call_stmt(allocator, instruction, args) + create_instruction_call_stmt(allocator, core_namespace, instruction, args) } /// Creates an ɵɵstyleMapInterpolate*() call statement (Angular 19). @@ -437,11 +453,12 @@ pub fn create_style_prop_interpolate_stmt<'a>( /// Signature: `ɵɵstyleMapInterpolateN(s0, v0, ...)` pub fn create_style_map_interpolate_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, interp_args: OxcVec<'a, OutputExpression<'a>>, expr_count: usize, ) -> OutputStatement<'a> { let instruction = get_style_map_interpolate_instruction(expr_count); - create_instruction_call_stmt(allocator, instruction, interp_args) + create_instruction_call_stmt(allocator, core_namespace, instruction, interp_args) } /// Creates an ɵɵclassMapInterpolate*() call statement (Angular 19). @@ -453,9 +470,10 @@ pub fn create_style_map_interpolate_stmt<'a>( /// Signature: `ɵɵclassMapInterpolateN(s0, v0, ...)` pub fn create_class_map_interpolate_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, interp_args: OxcVec<'a, OutputExpression<'a>>, expr_count: usize, ) -> OutputStatement<'a> { let instruction = get_class_map_interpolate_instruction(expr_count); - create_instruction_call_stmt(allocator, instruction, interp_args) + create_instruction_call_stmt(allocator, core_namespace, instruction, interp_args) } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/control_flow.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/control_flow.rs index 700643b85..b31fcec47 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/control_flow.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/control_flow.rs @@ -26,6 +26,7 @@ use super::super::utils::create_instruction_call_stmt; /// Args are trimmed from the end if they are null values. pub fn create_conditional_create_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, fn_name: Option>, decls: Option, @@ -109,7 +110,7 @@ pub fn create_conditional_create_stmt<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -131,7 +132,7 @@ pub fn create_conditional_create_stmt<'a>( } } - create_instruction_call_stmt(allocator, Identifiers::CONDITIONAL_CREATE, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::CONDITIONAL_CREATE, args) } /// Creates an ɵɵconditional() update call statement. @@ -141,6 +142,7 @@ pub fn create_conditional_create_stmt<'a>( /// - context_value: Optional expression for alias capture (e.g., `@if (condition as alias)`) pub fn create_conditional_update_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, test: OutputExpression<'a>, context_value: Option>, ) -> OutputStatement<'a> { @@ -149,7 +151,7 @@ pub fn create_conditional_update_stmt<'a>( if let Some(ctx) = context_value { args.push(ctx); } - create_instruction_call_stmt(allocator, Identifiers::CONDITIONAL, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::CONDITIONAL, args) } /// Creates an ɵɵconditionalBranchCreate() call statement for branches after the first in @if/@switch. @@ -167,6 +169,7 @@ pub fn create_conditional_update_stmt<'a>( /// Args are trimmed from the end if they are null values. pub fn create_conditional_branch_create_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, fn_name: Option>, decls: Option, @@ -250,7 +253,7 @@ pub fn create_conditional_branch_create_stmt<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -272,7 +275,12 @@ pub fn create_conditional_branch_create_stmt<'a>( } } - create_instruction_call_stmt(allocator, Identifiers::CONDITIONAL_BRANCH_CREATE, args) + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::CONDITIONAL_BRANCH_CREATE, + args, + ) } /// Creates an ɵɵcontrolCreate() call statement for control binding initialization. @@ -284,14 +292,16 @@ pub fn create_conditional_branch_create_stmt<'a>( /// The controlCreate instruction takes no arguments. pub fn create_control_create_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, ) -> OutputStatement<'a> { let args = OxcVec::new_in(&allocator); - create_instruction_call_stmt(allocator, Identifiers::CONTROL_CREATE, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::CONTROL_CREATE, args) } /// Creates an ɵɵadvance() call statement. pub fn create_advance_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, delta: u32, ) -> OutputStatement<'a> { let mut args = OxcVec::new_in(&allocator); @@ -301,17 +311,18 @@ pub fn create_advance_stmt<'a>( &allocator, ))); } - create_instruction_call_stmt(allocator, Identifiers::ADVANCE, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::ADVANCE, args) } /// Creates an ɵɵrepeater() call statement for @for update. pub fn create_repeater_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, collection: OutputExpression<'a>, ) -> OutputStatement<'a> { let mut args = OxcVec::new_in(&allocator); args.push(collection); - create_instruction_call_stmt(allocator, Identifiers::REPEATER, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::REPEATER, args) } /// Creates an ɵɵrepeaterCreate() call statement for @for with an OutputExpression for the track function. @@ -323,6 +334,7 @@ pub fn create_repeater_stmt<'a>( #[allow(clippy::too_many_arguments)] pub fn create_repeater_create_stmt_with_track_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, fn_name: Option>, body_decl_count: Option, @@ -469,7 +481,7 @@ pub fn create_repeater_create_stmt_with_track_expr<'a>( } } - create_instruction_call_stmt(allocator, Identifiers::REPEATER_CREATE, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::REPEATER_CREATE, args) } /// Creates a variable declaration statement with a value. @@ -494,6 +506,7 @@ pub fn create_variable_decl_stmt_with_value<'a>( /// Creates an ɵɵdeclareLet() call statement. pub fn create_declare_let_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, ) -> OutputStatement<'a> { let mut args = OxcVec::new_in(&allocator); @@ -501,7 +514,7 @@ pub fn create_declare_let_stmt<'a>( LiteralExpr { value: LiteralValue::Number(slot as f64), source_span: None }, &allocator, ))); - create_instruction_call_stmt(allocator, Identifiers::DECLARE_LET, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::DECLARE_LET, args) } // Note: create_store_let_stmt has been removed. @@ -523,6 +536,7 @@ mod tests { // and i0.ɵɵtemplateRefExtractor, matching Angular's instruction.ts conditionalCreate(). let stmt = create_conditional_create_stmt( &allocator, + "i0", 0, Some(Ident::from("TestComponent_Conditional_0_Template")), Some(1), @@ -552,6 +566,7 @@ mod tests { // Without local refs, templateRefExtractor should not appear let stmt = create_conditional_create_stmt( &allocator, + "i0", 0, Some(Ident::from("TestComponent_Conditional_0_Template")), Some(1), @@ -574,6 +589,7 @@ mod tests { let stmt = create_conditional_branch_create_stmt( &allocator, + "i0", 1, Some(Ident::from("TestComponent_Conditional_1_Template")), Some(1), @@ -601,6 +617,7 @@ mod tests { let stmt = create_conditional_branch_create_stmt( &allocator, + "i0", 1, Some(Ident::from("TestComponent_Conditional_1_Template")), Some(1), diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/defer.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/defer.rs index 7ce4c9013..6bc8c616f 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/defer.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/defer.rs @@ -26,6 +26,7 @@ use super::super::utils::create_instruction_call_stmt; /// - flags: Defer block flags (e.g., HasHydrateTriggers = 1) pub fn create_defer_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, main_slot: Option, resolver_fn: Option>, @@ -141,7 +142,7 @@ pub fn create_defer_stmt<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -182,7 +183,7 @@ pub fn create_defer_stmt<'a>( } } - create_instruction_call_stmt(allocator, Identifiers::DEFER, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::DEFER, args) } /// Creates an ɵɵdeferOn*() call statement based on trigger kind. @@ -196,6 +197,7 @@ pub fn create_defer_stmt<'a>( /// For hydrate modifier, Viewport/Interaction/Hover triggers don't support targets. pub fn create_defer_on_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, trigger: DeferTriggerKind, target_slot: Option, target_slot_view_steps: Option, @@ -347,7 +349,7 @@ pub fn create_defer_on_stmt<'a>( (DeferTriggerKind::Never, DeferOpModifierKind::Hydrate) => Identifiers::DEFER_HYDRATE_NEVER, }; - create_instruction_call_stmt(allocator, instruction, args) + create_instruction_call_stmt(allocator, core_namespace, instruction, args) } /// Creates an ɵɵdeferWhen() call statement. @@ -357,6 +359,7 @@ pub fn create_defer_on_stmt<'a>( /// it uses ɵɵdeferPrefetchWhen and ɵɵdeferHydrateWhen respectively. pub fn create_defer_when_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, modifier: DeferOpModifierKind, condition: OutputExpression<'a>, ) -> OutputStatement<'a> { @@ -369,5 +372,5 @@ pub fn create_defer_when_stmt<'a>( DeferOpModifierKind::None => Identifiers::DEFER_WHEN, }; - create_instruction_call_stmt(allocator, instruction, args) + create_instruction_call_stmt(allocator, core_namespace, instruction, args) } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/elements.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/elements.rs index e1e0b23de..7d6f4270c 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/elements.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/elements.rs @@ -64,30 +64,40 @@ pub fn create_element_args<'a>( /// Creates an ɵɵelementStart() call statement. pub fn create_element_start_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, tag: &Ident<'a>, slot: u32, attributes: Option, local_refs_index: Option, ) -> OutputStatement<'a> { let args = create_element_args(allocator, tag, slot, attributes, local_refs_index); - create_instruction_call_stmt(allocator, Identifiers::ELEMENT_START, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::ELEMENT_START, args) } /// Creates an ɵɵelement() call statement. pub fn create_element_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, tag: &Ident<'a>, slot: u32, attributes: Option, local_refs_index: Option, ) -> OutputStatement<'a> { let args = create_element_args(allocator, tag, slot, attributes, local_refs_index); - create_instruction_call_stmt(allocator, Identifiers::ELEMENT, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::ELEMENT, args) } /// Creates an ɵɵelementEnd() call statement. -pub fn create_element_end_stmt<'a>(allocator: &'a oxc_allocator::Allocator) -> OutputStatement<'a> { - create_instruction_call_stmt(allocator, Identifiers::ELEMENT_END, OxcVec::new_in(&allocator)) +pub fn create_element_end_stmt<'a>( + allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, +) -> OutputStatement<'a> { + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::ELEMENT_END, + OxcVec::new_in(&allocator), + ) } // ============================================================================= @@ -100,13 +110,14 @@ pub fn create_element_end_stmt<'a>(allocator: &'a oxc_allocator::Allocator) -> O /// This is an optimized version that skips directive matching at runtime. pub fn create_dom_element_start_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, tag: &Ident<'a>, slot: u32, attributes: Option, local_refs_index: Option, ) -> OutputStatement<'a> { let args = create_element_args(allocator, tag, slot, attributes, local_refs_index); - create_instruction_call_stmt(allocator, Identifiers::DOM_ELEMENT_START, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::DOM_ELEMENT_START, args) } /// Creates an ɵɵdomElement() call statement. @@ -115,13 +126,14 @@ pub fn create_dom_element_start_stmt<'a>( /// This is an optimized version that skips directive matching at runtime. pub fn create_dom_element_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, tag: &Ident<'a>, slot: u32, attributes: Option, local_refs_index: Option, ) -> OutputStatement<'a> { let args = create_element_args(allocator, tag, slot, attributes, local_refs_index); - create_instruction_call_stmt(allocator, Identifiers::DOM_ELEMENT, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::DOM_ELEMENT, args) } /// Creates an ɵɵdomElementEnd() call statement. @@ -129,9 +141,11 @@ pub fn create_dom_element_stmt<'a>( /// Used in DomOnly mode when the component has no directive dependencies. pub fn create_dom_element_end_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, ) -> OutputStatement<'a> { create_instruction_call_stmt( allocator, + core_namespace, Identifiers::DOM_ELEMENT_END, OxcVec::new_in(&allocator), ) @@ -144,6 +158,7 @@ pub fn create_dom_element_end_stmt<'a>( /// - initial_value: Optional initial text content (for static text) pub fn create_text_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, initial_value: Option<&'a str>, ) -> OutputStatement<'a> { @@ -163,7 +178,7 @@ pub fn create_text_stmt<'a>( } } - create_instruction_call_stmt(allocator, Identifiers::TEXT, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::TEXT, args) } /// Creates an ɵɵtemplate() call statement. @@ -178,6 +193,7 @@ pub fn create_text_stmt<'a>( /// - localRefs: Optional const array index for local refs (if present, also adds templateRefExtractor) pub fn create_template_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, fn_name: Option>, decls: Option, @@ -188,6 +204,7 @@ pub fn create_template_stmt<'a>( ) -> OutputStatement<'a> { let args = create_template_args( &allocator, + core_namespace, slot, fn_name, decls, @@ -196,7 +213,7 @@ pub fn create_template_stmt<'a>( attributes, local_refs_index, ); - create_instruction_call_stmt(allocator, Identifiers::TEMPLATE_CREATE, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::TEMPLATE_CREATE, args) } /// Creates arguments for template instructions (shared between template and domTemplate). @@ -205,6 +222,7 @@ pub fn create_template_stmt<'a>( /// Trailing null arguments are stripped. fn create_template_args<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, fn_name: Option>, decls: Option, @@ -287,7 +305,7 @@ fn create_template_args<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -329,6 +347,7 @@ fn create_template_args<'a>( /// - localRefs: Optional const array index for local refs (if present, also adds templateRefExtractor) pub fn create_dom_template_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, fn_name: Option>, decls: Option, @@ -339,6 +358,7 @@ pub fn create_dom_template_stmt<'a>( ) -> OutputStatement<'a> { let args = create_template_args( &allocator, + core_namespace, slot, fn_name, decls, @@ -347,7 +367,7 @@ pub fn create_dom_template_stmt<'a>( attributes, local_refs_index, ); - create_instruction_call_stmt(allocator, Identifiers::DOM_TEMPLATE, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::DOM_TEMPLATE, args) } /// Creates arguments for container instructions. @@ -398,20 +418,23 @@ fn create_container_args<'a>( /// Creates an ɵɵcontainer() call statement. pub fn create_container_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, attributes: Option, local_refs_index: Option, ) -> OutputStatement<'a> { let args = create_container_args(allocator, slot, attributes, local_refs_index); - create_instruction_call_stmt(allocator, Identifiers::ELEMENT_CONTAINER, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::ELEMENT_CONTAINER, args) } /// Creates an ɵɵcontainerEnd() call statement. pub fn create_container_end_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, ) -> OutputStatement<'a> { create_instruction_call_stmt( &allocator, + core_namespace, Identifiers::ELEMENT_CONTAINER_END, OxcVec::new_in(&allocator), ) @@ -420,12 +443,18 @@ pub fn create_container_end_stmt<'a>( /// Creates an ɵɵelementContainerStart() call statement. pub fn create_container_start_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, attributes: Option, local_refs_index: Option, ) -> OutputStatement<'a> { let args = create_container_args(allocator, slot, attributes, local_refs_index); - create_instruction_call_stmt(allocator, Identifiers::ELEMENT_CONTAINER_START, args) + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::ELEMENT_CONTAINER_START, + args, + ) } // ============================================================================= @@ -437,12 +466,18 @@ pub fn create_container_start_stmt<'a>( /// Used in DomOnly mode for ng-container elements. pub fn create_dom_container_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, attributes: Option, local_refs_index: Option, ) -> OutputStatement<'a> { let args = create_container_args(allocator, slot, attributes, local_refs_index); - create_instruction_call_stmt(allocator, Identifiers::DOM_ELEMENT_CONTAINER, args) + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::DOM_ELEMENT_CONTAINER, + args, + ) } /// Creates an ɵɵdomElementContainerStart() call statement. @@ -450,12 +485,18 @@ pub fn create_dom_container_stmt<'a>( /// Used in DomOnly mode for ng-container elements. pub fn create_dom_container_start_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, attributes: Option, local_refs_index: Option, ) -> OutputStatement<'a> { let args = create_container_args(allocator, slot, attributes, local_refs_index); - create_instruction_call_stmt(allocator, Identifiers::DOM_ELEMENT_CONTAINER_START, args) + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::DOM_ELEMENT_CONTAINER_START, + args, + ) } /// Creates an ɵɵdomElementContainerEnd() call statement. @@ -463,9 +504,11 @@ pub fn create_dom_container_start_stmt<'a>( /// Used in DomOnly mode for ng-container elements. pub fn create_dom_container_end_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, ) -> OutputStatement<'a> { create_instruction_call_stmt( &allocator, + core_namespace, Identifiers::DOM_ELEMENT_CONTAINER_END, OxcVec::new_in(&allocator), ) @@ -482,6 +525,7 @@ pub fn create_dom_container_end_stmt<'a>( /// * `fallback_vars` - Optional fallback view variable count pub fn create_projection_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, projection_slot_index: u32, attributes: Option>, @@ -549,12 +593,13 @@ pub fn create_projection_stmt<'a>( } } - create_instruction_call_stmt(allocator, Identifiers::PROJECTION, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::PROJECTION, args) } /// Creates a namespace change statement. pub fn create_namespace_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, namespace: crate::ir::enums::Namespace, ) -> OutputStatement<'a> { let instruction = match namespace { @@ -562,5 +607,5 @@ pub fn create_namespace_stmt<'a>( crate::ir::enums::Namespace::Svg => Identifiers::NAMESPACE_SVG, crate::ir::enums::Namespace::Math => Identifiers::NAMESPACE_MATH_ML, }; - create_instruction_call_stmt(allocator, instruction, OxcVec::new_in(&allocator)) + create_instruction_call_stmt(allocator, core_namespace, instruction, OxcVec::new_in(&allocator)) } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/i18n.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/i18n.rs index 90af94585..3e2c2c15d 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/i18n.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/i18n.rs @@ -17,6 +17,7 @@ use super::super::utils::create_instruction_call_stmt; /// - `subTemplateIndex`: Optional index for nested templates within the i18n block pub fn create_i18n_start_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, message_index: Option, sub_template_index: Option, @@ -45,7 +46,7 @@ pub fn create_i18n_start_stmt<'a>( } } - create_instruction_call_stmt(allocator, Identifiers::I18N_START, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::I18N_START, args) } /// Creates an ɵɵi18n() call statement for self-closing i18n on elements. @@ -54,6 +55,7 @@ pub fn create_i18n_start_stmt<'a>( /// `ɵɵi18n(index: number, messageIndex: number, subTemplateIndex?: number): void` pub fn create_i18n_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, message_index: Option, sub_template_index: Option, @@ -82,27 +84,37 @@ pub fn create_i18n_stmt<'a>( } } - create_instruction_call_stmt(allocator, Identifiers::I18N, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::I18N, args) } /// Creates an ɵɵi18nEnd() call statement. -pub fn create_i18n_end_stmt<'a>(allocator: &'a oxc_allocator::Allocator) -> OutputStatement<'a> { - create_instruction_call_stmt(allocator, Identifiers::I18N_END, OxcVec::new_in(&allocator)) +pub fn create_i18n_end_stmt<'a>( + allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, +) -> OutputStatement<'a> { + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::I18N_END, + OxcVec::new_in(&allocator), + ) } /// Creates an ɵɵi18nExp() call statement. pub fn create_i18n_exp_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, value: OutputExpression<'a>, ) -> OutputStatement<'a> { let mut args = OxcVec::new_in(&allocator); args.push(value); - create_instruction_call_stmt(allocator, Identifiers::I18N_EXP, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::I18N_EXP, args) } /// Creates an ɵɵi18nApply() call statement. pub fn create_i18n_apply_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, ) -> OutputStatement<'a> { let mut args = OxcVec::new_in(&allocator); @@ -110,7 +122,7 @@ pub fn create_i18n_apply_stmt<'a>( LiteralExpr { value: LiteralValue::Number(slot as f64), source_span: None }, &allocator, ))); - create_instruction_call_stmt(allocator, Identifiers::I18N_APPLY, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::I18N_APPLY, args) } /// Creates an ɵɵi18nAttributes() call statement. @@ -122,6 +134,7 @@ pub fn create_i18n_apply_stmt<'a>( /// - `attrsIndex`: Index into the consts array for the attribute config array pub fn create_i18n_attributes_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, attrs_config_index: u32, ) -> OutputStatement<'a> { @@ -139,5 +152,5 @@ pub fn create_i18n_attributes_stmt<'a>( &allocator, ))); - create_instruction_call_stmt(allocator, Identifiers::I18N_ATTRIBUTES, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::I18N_ATTRIBUTES, args) } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/misc.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/misc.rs index 5c013dbbe..6011d4ff5 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/misc.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/misc.rs @@ -55,6 +55,7 @@ impl GlobalEventTarget { /// The `consumes_dollar_event` parameter controls whether the `$event` parameter is included. pub fn create_listener_stmt_with_handler<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, handler_stmts: OxcVec<'a, OutputStatement<'a>>, event_target: Option, @@ -94,7 +95,7 @@ pub fn create_listener_stmt_with_handler<'a>( receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( crate::output::ast::ReadVarExpr { - name: Ident::from("i0"), + name: Ident::from(core_namespace), source_span: None, }, &allocator, @@ -123,7 +124,7 @@ pub fn create_listener_stmt_with_handler<'a>( ))); } - create_instruction_call_stmt(allocator, Identifiers::LISTENER, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::LISTENER, args) } /// Creates an ɵɵdomListener() call statement with a handler function. @@ -135,6 +136,7 @@ pub fn create_listener_stmt_with_handler<'a>( /// The `consumes_dollar_event` parameter controls whether the `$event` parameter is included. pub fn create_dom_listener_stmt_with_handler<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, handler_stmts: OxcVec<'a, OutputStatement<'a>>, event_target: Option, @@ -173,7 +175,7 @@ pub fn create_dom_listener_stmt_with_handler<'a>( receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( crate::output::ast::ReadVarExpr { - name: Ident::from("i0"), + name: Ident::from(core_namespace), source_span: None, }, &allocator, @@ -188,7 +190,7 @@ pub fn create_dom_listener_stmt_with_handler<'a>( ))); } - create_instruction_call_stmt(allocator, Identifiers::DOM_LISTENER, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::DOM_LISTENER, args) } /// Creates an ɵɵtwoWayListener() call statement with a handler function. @@ -196,6 +198,7 @@ pub fn create_dom_listener_stmt_with_handler<'a>( /// The `handler_fn_name` parameter sets the name of the handler function expression. pub fn create_two_way_listener_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, handler_stmts: OxcVec<'a, OutputStatement<'a>>, handler_fn_name: Option<&Ident<'a>>, @@ -223,7 +226,7 @@ pub fn create_two_way_listener_stmt<'a>( )); args.push(handler_fn); - create_instruction_call_stmt(allocator, Identifiers::TWO_WAY_LISTENER, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::TWO_WAY_LISTENER, args) } /// Creates an ɵɵsyntheticHostListener() call for a LegacyAnimation host listener. @@ -233,6 +236,7 @@ pub fn create_two_way_listener_stmt<'a>( /// Matches TypeScript: `syntheticHost = op.hostListener && op.isLegacyAnimationListener`. pub fn create_synthetic_host_listener_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, handler_stmts: OxcVec<'a, OutputStatement<'a>>, handler_fn_name: Option<&Ident<'a>>, @@ -257,7 +261,12 @@ pub fn create_synthetic_host_listener_stmt<'a>( &allocator, )); args.push(handler_fn); - create_instruction_call_stmt(allocator, Identifiers::SYNTHETIC_HOST_LISTENER, args) + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::SYNTHETIC_HOST_LISTENER, + args, + ) } /// Creates an ɵɵsyntheticHostListener() call statement for animation listeners. @@ -270,6 +279,7 @@ pub fn create_synthetic_host_listener_stmt<'a>( /// The `consumes_dollar_event` parameter controls whether the `$event` parameter is included. pub fn create_animation_listener_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, phase: crate::ir::enums::AnimationKind, handler_stmts: OxcVec<'a, OutputStatement<'a>>, @@ -307,7 +317,12 @@ pub fn create_animation_listener_stmt<'a>( )); args.push(handler_fn); - create_instruction_call_stmt(allocator, Identifiers::SYNTHETIC_HOST_LISTENER, args) + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::SYNTHETIC_HOST_LISTENER, + args, + ) } /// Creates an ɵɵanimateEnter() or ɵɵanimateLeave() call statement for animation string bindings. @@ -316,6 +331,7 @@ pub fn create_animation_listener_stmt<'a>( /// Example: `ɵɵanimateEnter("slide")` for `
` pub fn create_animation_string_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, animation_kind: AnimationKind, value: OutputExpression<'a>, ) -> OutputStatement<'a> { @@ -325,7 +341,7 @@ pub fn create_animation_string_stmt<'a>( AnimationKind::Enter => Identifiers::ANIMATION_ENTER, AnimationKind::Leave => Identifiers::ANIMATION_LEAVE, }; - create_instruction_call_stmt(allocator, instruction, args) + create_instruction_call_stmt(allocator, core_namespace, instruction, args) } /// Creates an ɵɵsyntheticHostProperty() call statement for DomProperty animation bindings. @@ -334,6 +350,7 @@ pub fn create_animation_string_stmt<'a>( /// It emits `syntheticHostProperty(name, value)`. pub fn create_animation_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, ) -> OutputStatement<'a> { @@ -347,7 +364,12 @@ pub fn create_animation_stmt<'a>( ))); args.push(value); - create_instruction_call_stmt(allocator, Identifiers::SYNTHETIC_HOST_PROPERTY, args) + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::SYNTHETIC_HOST_PROPERTY, + args, + ) } /// Creates an ɵɵanimateEnter() or ɵɵanimateLeave() call statement for animation CreateOp. @@ -366,6 +388,7 @@ pub fn create_animation_stmt<'a>( /// - `Identifiers.animationLeave` for AnimationKind::LEAVE pub fn create_animation_op_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, animation_kind: AnimationKind, handler_stmts: OxcVec<'a, OutputStatement<'a>>, handler_fn_name: Option<&Ident<'a>>, @@ -391,12 +414,13 @@ pub fn create_animation_op_stmt<'a>( AnimationKind::Enter => Identifiers::ANIMATION_ENTER, AnimationKind::Leave => Identifiers::ANIMATION_LEAVE, }; - create_instruction_call_stmt(allocator, instruction, args) + create_instruction_call_stmt(allocator, core_namespace, instruction, args) } /// Creates an animation binding call statement. pub fn create_animation_binding_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, name: &Ident<'a>, value: OutputExpression<'a>, ) -> OutputStatement<'a> { @@ -406,7 +430,12 @@ pub fn create_animation_binding_stmt<'a>( &allocator, ))); args.push(value); - create_instruction_call_stmt(allocator, Identifiers::SYNTHETIC_HOST_PROPERTY, args) + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::SYNTHETIC_HOST_PROPERTY, + args, + ) } /// Creates a control binding call statement (ɵɵcontrol). @@ -414,8 +443,16 @@ pub fn create_animation_binding_stmt<'a>( /// Angular's control update instruction takes no arguments. The `[formField]` /// value is written through the regular property instruction, and `ɵɵcontrol()` /// performs the form-control synchronization work separately. -pub fn create_control_stmt<'a>(allocator: &'a oxc_allocator::Allocator) -> OutputStatement<'a> { - create_instruction_call_stmt(allocator, Identifiers::CONTROL, OxcVec::new_in(&allocator)) +pub fn create_control_stmt<'a>( + allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, +) -> OutputStatement<'a> { + create_instruction_call_stmt( + allocator, + core_namespace, + Identifiers::CONTROL, + OxcVec::new_in(&allocator), + ) } /// Creates an ɵɵprojectionDef() call statement from a pre-built R3 def expression. @@ -424,6 +461,7 @@ pub fn create_control_stmt<'a>(allocator: &'a oxc_allocator::Allocator) -> Outpu /// If `def` is Some, it's passed as the selector array argument. pub fn create_projection_def_stmt_from_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, def: Option<&OutputExpression<'a>>, ) -> OutputStatement<'a> { let mut args = OxcVec::new_in(&allocator); @@ -432,15 +470,17 @@ pub fn create_projection_def_stmt_from_expr<'a>( args.push(def_expr.clone_in(allocator)); } - create_instruction_call_stmt(allocator, Identifiers::PROJECTION_DEF, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::PROJECTION_DEF, args) } /// Creates an ɵɵdisableBindings() call statement. pub fn create_disable_bindings_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, ) -> OutputStatement<'a> { create_instruction_call_stmt( &allocator, + core_namespace, Identifiers::DISABLE_BINDINGS, OxcVec::new_in(&allocator), ) @@ -449,9 +489,11 @@ pub fn create_disable_bindings_stmt<'a>( /// Creates an ɵɵenableBindings() call statement. pub fn create_enable_bindings_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, ) -> OutputStatement<'a> { create_instruction_call_stmt( allocator, + core_namespace, Identifiers::ENABLE_BINDINGS, OxcVec::new_in(&allocator), ) @@ -460,6 +502,7 @@ pub fn create_enable_bindings_stmt<'a>( /// Creates an ɵɵpipe() call statement. pub fn create_pipe_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, slot: u32, name: &Ident<'a>, ) -> OutputStatement<'a> { @@ -472,5 +515,5 @@ pub fn create_pipe_stmt<'a>( LiteralExpr { value: LiteralValue::String(name.clone()), source_span: None }, &allocator, ))); - create_instruction_call_stmt(allocator, Identifiers::PIPE, args) + create_instruction_call_stmt(allocator, core_namespace, Identifiers::PIPE, args) } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/utils.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/utils.rs index a573cf818..c2a62d4ed 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/utils.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/utils.rs @@ -21,6 +21,7 @@ pub fn strip_prefix<'a>(name: &Ident<'a>, prefix: &str) -> Ident<'a> { /// Creates an instruction call statement. pub fn create_instruction_call_stmt<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, instruction: &'a str, args: OxcVec<'a, OutputExpression<'a>>, ) -> OutputStatement<'a> { @@ -29,7 +30,7 @@ pub fn create_instruction_call_stmt<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -61,6 +62,7 @@ pub fn create_instruction_call_stmt<'a>( /// Creates an instruction call expression (not statement). pub fn create_instruction_call_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, instruction: &'a str, args: OxcVec<'a, OutputExpression<'a>>, ) -> OutputExpression<'a> { @@ -68,7 +70,7 @@ pub fn create_instruction_call_expr<'a>( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -101,9 +103,15 @@ pub fn create_instruction_call_expr<'a>( /// passed to ɵɵproperty, ɵɵattribute, etc. pub fn create_value_interpolate_expr<'a>( allocator: &'a oxc_allocator::Allocator, + core_namespace: &'a str, args: OxcVec<'a, OutputExpression<'a>>, expr_count: usize, ) -> OutputExpression<'a> { use crate::r3::identifiers::get_interpolate_instruction; - create_instruction_call_expr(allocator, get_interpolate_instruction(expr_count), args) + create_instruction_call_expr( + allocator, + core_namespace, + get_interpolate_instruction(expr_count), + args, + ) } diff --git a/crates/oxc_angular_compiler/src/service/compiler.rs b/crates/oxc_angular_compiler/src/service/compiler.rs index 5dde42d73..0cec906b6 100644 --- a/crates/oxc_angular_compiler/src/service/compiler.rs +++ b/crates/oxc_angular_compiler/src/service/compiler.rs @@ -34,11 +34,12 @@ pub struct ServiceCompileResult<'a> { /// Compiles a service from its metadata into an `ɵɵdefineService(...)` call. pub fn compile_service<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3ServiceMetadata<'a>, ) -> ServiceCompileResult<'a> { let factory_expr = build_factory_expression(allocator, metadata); let definition_map = build_definition_map(allocator, metadata, factory_expr); - let expression = create_define_service_call(allocator, definition_map); + let expression = create_define_service_call(allocator, core_namespace, definition_map); ServiceCompileResult { expression } } @@ -135,16 +136,17 @@ fn build_definition_map<'a>( entries } -/// `/*@__PURE__*/ i0.ɵɵdefineService({...})` +/// `/*@__PURE__*/ ns.ɵɵdefineService({...})` fn create_define_service_call<'a>( allocator: &'a Allocator, + core_namespace: &'a str, definition_map: Vec<'a, LiteralMapEntry<'a>>, ) -> OutputExpression<'a> { let define_service_fn = OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: Ident::from("i0"), source_span: None }, + ReadVarExpr { name: Ident::from(core_namespace), source_span: None }, &allocator, )), &allocator, @@ -199,7 +201,7 @@ mod tests { factory: None, }; - let result = compile_service(&allocator, &metadata); + let result = compile_service(&allocator, "i0", &metadata); let js = JsEmitter::new().emit_expression(&result.expression); assert!(js.contains("ɵɵdefineService"), "should emit defineService. Got: {js}"); @@ -222,7 +224,7 @@ mod tests { factory: None, }; - let result = compile_service(&allocator, &metadata); + let result = compile_service(&allocator, "i0", &metadata); let js = JsEmitter::new().emit_expression(&result.expression); assert!(js.contains("autoProvided:false"), "should emit autoProvided: false. Got: {js}"); @@ -239,7 +241,7 @@ mod tests { factory: None, }; - let result = compile_service(&allocator, &metadata); + let result = compile_service(&allocator, "i0", &metadata); let js = JsEmitter::new().emit_expression(&result.expression); assert!( @@ -263,7 +265,7 @@ mod tests { factory: Some(factory_expr), }; - let result = compile_service(&allocator, &metadata); + let result = compile_service(&allocator, "i0", &metadata); let js = JsEmitter::new().emit_expression(&result.expression); assert!(js.contains("makeService()"), "should call user factory inside arrow. Got: {js}"); diff --git a/crates/oxc_angular_compiler/src/service/definition.rs b/crates/oxc_angular_compiler/src/service/definition.rs index e9e0e3020..1273358d6 100644 --- a/crates/oxc_angular_compiler/src/service/definition.rs +++ b/crates/oxc_angular_compiler/src/service/definition.rs @@ -30,12 +30,13 @@ pub struct ServiceDefinition<'a> { /// Generate `ɵfac` and `ɵprov` definitions from R3 metadata. pub fn generate_service_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3ServiceMetadata<'a>, ) -> ServiceDefinition<'a> { // Generate ɵfac BEFORE ɵprov so namespace-index assignment order matches // upstream's [fac, prov, ...] ordering. - let fac_definition = generate_fac_definition(allocator, metadata); - let prov_result = compile_service(allocator, metadata); + let fac_definition = generate_fac_definition(allocator, core_namespace, metadata); + let prov_result = compile_service(allocator, core_namespace, metadata); ServiceDefinition { prov_definition: prov_result.expression, fac_definition } } @@ -43,11 +44,12 @@ pub fn generate_service_definition<'a>( /// Convenience: extract `R3ServiceMetadata` from `ServiceMetadata` and emit. pub fn generate_service_definition_from_decorator<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &ServiceMetadata<'a>, type_argument_count: u32, ) -> ServiceDefinition<'a> { let r3_metadata = metadata.to_r3_metadata(allocator, type_argument_count); - generate_service_definition(allocator, &r3_metadata) + generate_service_definition(allocator, core_namespace, &r3_metadata) } /// Emit the ɵfac factory function. `@Service` factories never inject ctor @@ -55,6 +57,7 @@ pub fn generate_service_definition_from_decorator<'a>( /// runtime expects `inject()` calls inside the constructor body. fn generate_fac_definition<'a>( allocator: &'a Allocator, + core_namespace: &'a str, metadata: &R3ServiceMetadata<'a>, ) -> OutputExpression<'a> { let factory_name = allocator.alloc_str(&format!("{}_Factory", metadata.name)); @@ -68,7 +71,7 @@ fn generate_fac_definition<'a>( target: FactoryTarget::Service, }); - let result = compile_factory_function(allocator, &factory_meta, factory_name); + let result = compile_factory_function(allocator, core_namespace, &factory_meta, factory_name); result.expression } @@ -95,7 +98,7 @@ mod tests { factory: None, }; - let def = generate_service_definition(&allocator, &metadata); + let def = generate_service_definition(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let fac_js = emitter.emit_expression(&def.fac_definition); let prov_js = emitter.emit_expression(&def.prov_definition); diff --git a/crates/oxc_angular_compiler/tests/ctor_param_decorator_identity_test.rs b/crates/oxc_angular_compiler/tests/ctor_param_decorator_identity_test.rs index a1a06420f..83daee860 100644 --- a/crates/oxc_angular_compiler/tests/ctor_param_decorator_identity_test.rs +++ b/crates/oxc_angular_compiler/tests/ctor_param_decorator_identity_test.rs @@ -75,11 +75,13 @@ fn bracketed(text: &str, open: usize) -> &str { /// The arguments `A`'s factory constructs it with. oxc reads an `@Inject` /// token imported from another file through its namespace import (`i1.TOKEN`), -/// where ngtsc names the import (`TOKEN`); both are the same value. +/// where ngtsc names the import (`TOKEN`); both are the same value. oxc also +/// reuses a file's existing `import * as ng from '@angular/core'` instead of +/// emitting a fresh `i0` namespace; both are the same binding. fn factory(js: &str) -> Option { const NEW: &str = "(__ngFactoryType__ || A)"; let at = js.find(&format!("{NEW}("))? + NEW.len(); - Some(normalize(bracketed(js, at)).replace("i1.TOKEN", "TOKEN")) + Some(normalize(bracketed(js, at)).replace("i1.TOKEN", "TOKEN").replace("ng.ɵɵ", "i0.ɵɵ")) } /// The `ctorParameters` array of `setClassMetadata(A, ...)`. diff --git a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs index bebc43527..e66f24b2f 100644 --- a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs +++ b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs @@ -272,7 +272,24 @@ fn decorator_metadata_matches_ngtsc() { if !expected.is_empty() { continue; } - let oxc = oxc_classes(&result.code, &result.dts_declarations); + // oxc reuses a file's existing `import * as X from '@angular/core'` + // instead of emitting a fresh `i0` namespace; both are the same + // binding, so compare with the alias renamed back to `i0`. + let mut code = result.code.clone(); + for (at, _) in source.match_indices("import * as ") { + let rest = &source[at + "import * as ".len()..]; + let end = + rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$').unwrap_or(0); + let alias = &rest[..end]; + let tail = rest[end..].trim_start(); + if !alias.is_empty() + && (tail.starts_with("from '@angular/core'") + || tail.starts_with("from \"@angular/core\"")) + { + code = code.replace(&format!("{alias}.ɵɵ"), "i0.ɵɵ"); + } + } + let oxc = oxc_classes(&code, &result.dts_declarations); for (class, members) in fixture["classes"].as_object().unwrap() { for (key, value) in members.as_object().unwrap() { // A `static ngAcceptInputType_*` the class declares itself comes diff --git a/crates/oxc_angular_compiler/tests/i0_collision_test.rs b/crates/oxc_angular_compiler/tests/i0_collision_test.rs new file mode 100644 index 000000000..fba57dcb4 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/i0_collision_test.rs @@ -0,0 +1,229 @@ +//! A user binding named `i0` collides with the injected +//! `import * as i0 from "@angular/core"` (issue #509). ngtsc's AOT/dts +//! `ImportManager` (`presetImportManagerForceNamespaceImports`) never reuses +//! an existing namespace import: it mints `i0` and renames it `i0_1`, +//! `i0_2`, ... until it no longer collides with any identifier in the +//! original file (`SourceFile.identifiers`, +//! `check_unique_identifier_name.ts`). + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{CompilationMode, TransformOptions, transform_angular_file}; + +fn compile(source: &str) -> String { + let allocator = Allocator::default(); + let options = + TransformOptions { compilation_mode: CompilationMode::Full, ..Default::default() }; + let result = transform_angular_file(&allocator, "test.ts", source, Some(&options), None); + assert!(!result.has_errors(), "should not have errors, got: {:?}", result.diagnostics); + result.code +} + +const COMPONENT: &str = "\ +import { Component } from '@angular/core'; +@Component({ selector: 'c', template: '

hi

', standalone: true }) +export class C {} +"; + +#[test] +fn user_const_i0_gets_uniquified_namespace() { + let out = compile(&format!("const i0 = 1;\n{COMPONENT}")); + + assert!(out.contains("import * as i0_1 from '@angular/core';"), "{out}"); + assert!(out.contains("i0_1.\u{0275}\u{0275}defineComponent"), "{out}"); + assert!(!out.contains("import * as i0 from"), "{out}"); + // The user binding stays untouched. + assert!(out.contains("const i0 = 1"), "{out}"); +} + +#[test] +fn user_namespace_import_i0_gets_uniquified_namespace() { + let out = compile(&format!("import * as i0 from './other';\n{COMPONENT}")); + + assert!(out.contains("import * as i0_1 from '@angular/core';"), "{out}"); + assert!(out.contains("i0_1.\u{0275}\u{0275}defineComponent"), "{out}"); +} + +#[test] +fn existing_core_namespace_import_is_not_reused() { + // Upstream's AOT ImportManager never reuses an existing namespace import + // (`disableOriginalSourceFileReuse`), so `import * as i0` is added next + // to the user's `import * as ng`, and generated code goes through `i0`. + // The `setClassMetadata` block keeps referencing `ng.Component`, so the + // original import survives elision. + let out = compile( + "import * as ng from '@angular/core';\n\ + @ng.Component({ selector: 'c', template: '

hi

', standalone: true })\n\ + export class C {}\n", + ); + + assert!(out.contains("import * as ng from '@angular/core';"), "{out}"); + assert!(out.contains("import * as i0 from '@angular/core';"), "{out}"); + assert!(out.contains("i0.\u{0275}\u{0275}defineComponent"), "{out}"); + assert!(out.contains("type:ng.Component"), "{out}"); +} + +#[test] +fn unused_core_namespace_import_is_elided() { + // `ng` is never referenced, so import elision drops it — upstream + // produces the same, since its transforms run before TypeScript's own + // import elision. + let out = compile( + "import * as ng from '@angular/core';\n\ + import { Component } from '@angular/core';\n\ + @Component({ selector: 'c', template: '', standalone: true })\n\ + export class C {}\n", + ); + + assert!(!out.contains("import * as ng"), "{out}"); + assert!(out.contains("import * as i0 from '@angular/core';"), "{out}"); + assert!(out.contains("i0.\u{0275}\u{0275}defineComponent"), "{out}"); +} + +#[test] +fn i0_and_i0_1_taken_gives_i0_2() { + let out = compile(&format!("const i0 = 1;\nconst i0_1 = 2;\n{COMPONENT}")); + + assert!(out.contains("import * as i0_2 from '@angular/core';"), "{out}"); + assert!(out.contains("i0_2.\u{0275}\u{0275}defineComponent"), "{out}"); +} + +#[test] +fn user_i1_does_not_collide_with_other_module_aliases() { + let out = compile( + "import { Component } from '@angular/core';\n\ + import { Router } from '@angular/router';\n\ + const i1 = 0;\n\ + @Component({ selector: 'c', template: '', standalone: true })\n\ + export class C { constructor(r: Router) {} }\n", + ); + + // `i1` is taken, so `@angular/router` gets `i1_1` (the index is still + // consumed, matching upstream `nextUniqueIndex++`). + assert!(out.contains("import * as i1_1 from '@angular/router';"), "{out}"); + assert!(out.contains("i1_1.Router"), "{out}"); +} + +fn dts_members(source: &str) -> String { + let allocator = Allocator::default(); + let options = + TransformOptions { compilation_mode: CompilationMode::Full, ..Default::default() }; + let result = transform_angular_file(&allocator, "test.ts", source, Some(&options), None); + assert!(!result.has_errors(), "should not have errors, got: {:?}", result.diagnostics); + result.dts_declarations.iter().map(|d| d.members.as_str()).collect() +} + +#[test] +fn dts_members_use_the_dts_file_alias() { + // `.d.ts` emit mints the same alias the JS emit does: upstream's + // `IvyDeclarationDtsTransform` runs a second ImportManager over the + // ORIGINAL file's `SourceFile.identifiers` (declaration.ts), so `i0` + // stays `i0` — never the user's `ng`. + let members = dts_members( + "import * as ng from '@angular/core';\n\ + @ng.Component({ selector: 'c', template: '', standalone: true })\n\ + export class C {}\n", + ); + + assert!(members.contains("i0.\u{0275}\u{0275}ComponentDeclaration"), "{members}"); + assert!(!members.contains("ng."), "{members}"); +} + +#[test] +fn dts_members_do_not_reuse_a_live_namespace_import() { + // Even when `ng` survives declaration emit, upstream never reuses it — + // the dts ImportManager still mints its own `i0`. + let members = dts_members( + "import * as ng from '@angular/core';\n\ + export const x: typeof ng.version = ng.version;\n\ + @ng.Component({ selector: 'c', template: '', standalone: true })\n\ + export class C {}\n", + ); + + assert!(members.contains("i0.\u{0275}\u{0275}ComponentDeclaration"), "{members}"); + assert!(!members.contains("ng.\u{0275}"), "{members}"); +} + +#[test] +fn dts_members_dedupe_against_exported_identifiers() { + // An exported `i0` survives declaration emit, so the `.d.ts` namespace + // is uniquified to `i0_1` independently of the JS emit's choice. + let members = dts_members(&format!("export const i0 = 1;\n{COMPONENT}")); + + assert!(members.contains("i0_1.\u{0275}\u{0275}ComponentDeclaration"), "{members}"); +} + +#[test] +fn dts_members_dedupe_against_all_identifiers_regardless_of_exports() { + // `import * as i0` puts `i0` in the original file's identifier set even + // where it is only named in a type position — the members use `i0_1`, + // and a dropped `interface U` makes no difference. + let members = dts_members( + "import * as i0 from './other';\n\ + import { Component } from '@angular/core';\n\ + interface U { x: i0.Type }\n\ + @Component({ selector: 'c', template: '

hi

', standalone: true })\n\ + class C { field!: i0.Type }\n\ + export { C };\n", + ); + + assert!(members.contains("i0_1.\u{0275}\u{0275}ComponentDeclaration"), "{members}"); + assert!(members.contains("i0_1.\u{0275}\u{0275}FactoryDeclaration"), "{members}"); +} + +#[test] +fn dts_members_dedupe_against_all_file_identifiers() { + // An unexported `const i0` still appears in the original file's + // `SourceFile.identifiers`, so upstream's dts ImportManager picks + // `i0_1` — identical to the JS emit's choice. + let members = dts_members(&format!("const i0 = 1;\n{COMPONENT}")); + + assert!(members.contains("i0_1.\u{0275}\u{0275}ComponentDeclaration"), "{members}"); + assert!(!members.contains("i0.\u{0275}"), "{members}"); +} + +#[test] +fn dts_input_transform_type_uses_the_uniquified_alias() { + // `ngAcceptInputType_*` types print `@angular/core` names through the + // same uniquified alias as the other members — a user `i0` must not + // capture `i0.Signal`. + let source = "\ +import { Component, Input, Signal } from '@angular/core'; +const i0 = 1; +@Component({ selector: 'c', template: '', standalone: true }) +export class C { + @Input({ transform: (v: Signal) => v }) t: any; +} +"; + let allocator = Allocator::default(); + let options = + TransformOptions { compilation_mode: CompilationMode::Full, ..Default::default() }; + let result = transform_angular_file(&allocator, "test.ts", source, Some(&options), None); + assert!(!result.has_errors(), "{:?}", result.diagnostics); + let members: String = result.dts_declarations.iter().map(|d| d.members.as_str()).collect(); + + assert!(members.contains("ngAcceptInputType_t: i0_1.Signal"), "{members}"); + assert!(!members.contains("i0.Signal"), "{members}"); + // The alias's module is recorded so the consumer emits its import. + assert_eq!( + result.dts_declarations[0].namespace_imports.get("i0_1").map(String::as_str), + Some("@angular/core"), + "{:?}", + result.dts_declarations[0].namespace_imports + ); +} + +#[test] +fn user_i0_still_resolves_in_metadata() { + // A user value named `i0` referenced inside decorator metadata must keep + // resolving to the user binding, not the generated namespace. + let out = compile( + "import { Component } from '@angular/core';\n\ + const i0 = { provide: 'x' };\n\ + @Component({ selector: 'c', template: '', standalone: true, providers: [i0] })\n\ + export class C {}\n", + ); + + assert!(out.contains("import * as i0_1 from '@angular/core';"), "{out}"); + // `providers: [i0]` in the emitted definition still reads the user const. + assert!(out.replace(char::is_whitespace, "").contains("providers:[i0]"), "{out}"); +} diff --git a/crates/oxc_angular_compiler/tests/partial_class_metadata_async_test.rs b/crates/oxc_angular_compiler/tests/partial_class_metadata_async_test.rs index 6087e7d6e..5c65c6c72 100644 --- a/crates/oxc_angular_compiler/tests/partial_class_metadata_async_test.rs +++ b/crates/oxc_angular_compiler/tests/partial_class_metadata_async_test.rs @@ -74,7 +74,7 @@ fn async_class_metadata_emits_async_call_with_18_0_min_version() { is_default_import: false, }]; - let expr = compile_declare_class_metadata_async(&allocator, &cmp_meta, &deps); + let expr = compile_declare_class_metadata_async(&allocator, "i0", &cmp_meta, &deps); let js = emit(&expr); assert!( @@ -107,7 +107,7 @@ fn async_class_metadata_emits_resolve_deferred_deps_and_resolve_metadata() { }, ]; - let expr = compile_declare_class_metadata_async(&allocator, &cmp_meta, &deps); + let expr = compile_declare_class_metadata_async(&allocator, "i0", &cmp_meta, &deps); let js = emit(&expr); // resolveDeferredDeps: arrow returning dynamic imports for each dep. @@ -148,7 +148,7 @@ fn async_class_metadata_emits_null_for_missing_ctor_params() { is_default_import: false, }]; - let expr = compile_declare_class_metadata_async(&allocator, &cmp_meta, &deps); + let expr = compile_declare_class_metadata_async(&allocator, "i0", &cmp_meta, &deps); let js = emit(&expr); assert!( @@ -173,7 +173,7 @@ fn default_import_uses_m_default_in_resolver() { is_default_import: true, }]; - let expr = compile_declare_class_metadata_async(&allocator, &cmp_meta, &deps); + let expr = compile_declare_class_metadata_async(&allocator, "i0", &cmp_meta, &deps); let js = emit(&expr); assert!( @@ -188,7 +188,7 @@ fn dispatch_helper_falls_back_to_sync_when_no_deferred_deps() { let cmp_meta = make_class_metadata(&allocator, "MyCmp", "Component"); let empty: [R3DeferPerComponentDependency<'_>; 0] = []; - let expr = compile_component_declare_class_metadata(&allocator, &cmp_meta, &empty); + let expr = compile_component_declare_class_metadata(&allocator, "i0", &cmp_meta, &empty); let js = emit(&expr); // Sync form — NOT async. @@ -216,7 +216,7 @@ fn dispatch_helper_picks_async_when_deferred_deps_present() { is_default_import: false, }]; - let expr = compile_component_declare_class_metadata(&allocator, &cmp_meta, &deps); + let expr = compile_component_declare_class_metadata(&allocator, "i0", &cmp_meta, &deps); let js = emit(&expr); assert!( @@ -317,7 +317,7 @@ fn unit_field_order_matches_upstream() { import_path: Ident::from("./lazy"), is_default_import: false, }]; - let expr = compile_declare_class_metadata_async(&allocator, &cmp_meta, &deps); + let expr = compile_declare_class_metadata_async(&allocator, "i0", &cmp_meta, &deps); let js = emit(&expr); let positions = [ diff --git a/crates/oxc_angular_compiler/tests/partial_directive_test.rs b/crates/oxc_angular_compiler/tests/partial_directive_test.rs index 4e7532804..2a1ecf2e7 100644 --- a/crates/oxc_angular_compiler/tests/partial_directive_test.rs +++ b/crates/oxc_angular_compiler/tests/partial_directive_test.rs @@ -35,7 +35,7 @@ fn minimal_directive_emits_required_fields_only() { .selector(Ident::from("[myDir]")) .build() .unwrap(); - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"minVersion:"14.0.0""#), "base minVersion 14.0.0, got: {js}"); @@ -59,7 +59,7 @@ fn legacy_inputs_string_when_names_match() { .add_input(R3InputMetadata::simple(Ident::from("value"))) .build() .unwrap(); - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"inputs:{value:"value"}"#), "expected string-shape input, got: {js}"); @@ -78,7 +78,7 @@ fn legacy_inputs_tuple_when_names_differ() { .add_input(input) .build() .unwrap(); - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); let compact: String = js.chars().filter(|c| !c.is_whitespace()).collect(); @@ -98,7 +98,7 @@ fn signal_input_uses_new_format_and_bumps_minversion_17_1() { .add_input(R3InputMetadata::signal(Ident::from("value"))) .build() .unwrap(); - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"minVersion:"17.1.0""#), "expected bumped minVersion 17.1.0, got: {js}"); @@ -126,7 +126,7 @@ fn signal_query_bumps_minversion_17_2() { .add_view_query(q) .build() .unwrap(); - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"minVersion:"17.2.0""#), "expected bumped minVersion 17.2.0, got: {js}"); @@ -148,7 +148,7 @@ fn host_listeners_and_properties_emitted_raw() { .host(host) .build() .unwrap(); - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); // Listeners and properties carried as raw unparsed strings — the linker @@ -222,7 +222,7 @@ fn host_directive_with_forward_ref_wraps_directive() { v }, }; - let expr = compile_declare_directive_from_metadata(&allocator, &manual_meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &manual_meta); let js = emit(&expr); assert!( js.contains("forwardRef(function") && js.contains("return FwdHostDir"), @@ -258,7 +258,7 @@ fn outputs_emitted_as_object_map() { is_signal: false, host_directives: Vec::new_in(&&allocator), }; - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!( js.contains(r#"outputs:{valueChange:"valueChange",internalEvent:"publicEvent"}"#), @@ -302,7 +302,7 @@ fn directive_uses_inheritance_and_on_changes() { is_signal: false, host_directives: Vec::new_in(&&allocator), }; - let expr = compile_declare_directive_from_metadata(&allocator, &manual); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &manual); let js = emit(&expr); assert!(js.contains("usesInheritance:true"), "expected usesInheritance, got: {js}"); assert!(js.contains("usesOnChanges:true"), "expected usesOnChanges, got: {js}"); @@ -335,7 +335,7 @@ fn directive_export_as_emitted_as_string_array() { is_signal: false, host_directives: Vec::new_in(&&allocator), }; - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"exportAs:["alias1","alias2"]"#), "expected exportAs array, got: {js}"); } @@ -350,7 +350,7 @@ fn ng_import_is_last_field() { .selector(Ident::from("[myDir]")) .build() .unwrap(); - let expr = compile_declare_directive_from_metadata(&allocator, &meta); + let expr = compile_declare_directive_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); let ng_idx = js.find("ngImport").expect("ngImport should be present"); let selector_idx = js.find("selector:").expect("selector should be present"); diff --git a/crates/oxc_angular_compiler/tests/partial_factory_test.rs b/crates/oxc_angular_compiler/tests/partial_factory_test.rs index 206c05f58..730b5b37f 100644 --- a/crates/oxc_angular_compiler/tests/partial_factory_test.rs +++ b/crates/oxc_angular_compiler/tests/partial_factory_test.rs @@ -50,7 +50,7 @@ fn simple_injectable_factory_with_one_dep() { let meta = make_meta(&allocator, "MyService", R3FactoryDeps::Valid(deps), FactoryTarget::Injectable); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); insta::assert_snapshot!(emit(&expr)); } @@ -59,7 +59,7 @@ fn simple_injectable_factory_with_one_dep() { fn factory_with_no_deps_emits_null() { let allocator = Allocator::default(); let meta = make_meta(&allocator, "MyService", R3FactoryDeps::None, FactoryTarget::Injectable); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("deps:null"), "deps should be null, got: {js}"); @@ -71,7 +71,7 @@ fn factory_with_invalid_deps_emits_string_invalid() { let allocator = Allocator::default(); let meta = make_meta(&allocator, "MyService", R3FactoryDeps::Invalid, FactoryTarget::Injectable); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"deps:"invalid""#), "deps should be \"invalid\", got: {js}"); @@ -90,7 +90,7 @@ fn factory_with_type_only_invalid_dep_coerces_to_invalid() { let meta = make_meta(&allocator, "MyService", R3FactoryDeps::Valid(deps), FactoryTarget::Injectable); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"deps:"invalid""#), "deps should coerce to \"invalid\", got: {js}"); @@ -111,7 +111,7 @@ fn factory_with_flag_deps_emits_only_set_flags() { R3FactoryDeps::Valid(deps), FactoryTarget::Injectable, ); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let js = emit(&expr); // Set flags appear. @@ -130,7 +130,7 @@ fn factory_with_flag_deps_emits_only_set_flags() { fn factory_target_pipe() { let allocator = Allocator::default(); let meta = make_meta(&allocator, "TitlePipe", R3FactoryDeps::None, FactoryTarget::Pipe); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("ɵɵFactoryTarget.Pipe"), "expected Pipe target, got: {js}"); @@ -140,7 +140,7 @@ fn factory_target_pipe() { fn factory_target_directive() { let allocator = Allocator::default(); let meta = make_meta(&allocator, "MyDir", R3FactoryDeps::None, FactoryTarget::Directive); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("ɵɵFactoryTarget.Directive"), "expected Directive target, got: {js}"); @@ -150,7 +150,7 @@ fn factory_target_directive() { fn factory_target_component() { let allocator = Allocator::default(); let meta = make_meta(&allocator, "MyCmp", R3FactoryDeps::None, FactoryTarget::Component); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("ɵɵFactoryTarget.Component"), "expected Component target, got: {js}"); @@ -160,7 +160,7 @@ fn factory_target_component() { fn factory_target_ng_module() { let allocator = Allocator::default(); let meta = make_meta(&allocator, "MyMod", R3FactoryDeps::None, FactoryTarget::NgModule); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("ɵɵFactoryTarget.NgModule"), "expected NgModule target, got: {js}"); @@ -180,7 +180,7 @@ fn round_trip_through_linker_to_full_factory() { let meta = make_meta(&allocator, "MyService", R3FactoryDeps::Valid(deps), FactoryTarget::Injectable); - let expr = compile_declare_factory_function(&allocator, &meta); + let expr = compile_declare_factory_function(&allocator, "i0", &meta); let source = format!( "import * as i0 from \"@angular/core\";\nexport class MyService {{}}\nMyService.\u{0275}fac = {};", diff --git a/crates/oxc_angular_compiler/tests/partial_injectable_test.rs b/crates/oxc_angular_compiler/tests/partial_injectable_test.rs index 553385db1..f782efde5 100644 --- a/crates/oxc_angular_compiler/tests/partial_injectable_test.rs +++ b/crates/oxc_angular_compiler/tests/partial_injectable_test.rs @@ -33,7 +33,7 @@ fn simple_root_injectable() { .provided_in_root() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); insta::assert_snapshot!(emit(&expr)); } @@ -47,7 +47,7 @@ fn injectable_with_use_class() { .provided_in_root() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!( @@ -70,7 +70,7 @@ fn injectable_with_use_class_forward_ref_wraps() { .provided_in_root() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!( @@ -90,7 +90,7 @@ fn injectable_with_use_existing() { .provided_in_root() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("useExisting:RealToken"), "expected useExisting:RealToken, got: {js}"); @@ -107,7 +107,7 @@ fn injectable_with_use_value() { .provided_in_root() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("useValue:configObject"), "expected useValue:configObject, got: {js}"); @@ -130,7 +130,7 @@ fn injectable_with_use_factory_and_deps() { .provided_in_root() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("useFactory:createService"), "useFactory should be raw, got: {js}"); @@ -150,7 +150,7 @@ fn provided_in_omitted_when_none() { .r#type(read_var(&allocator, "ScopedService")) .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(!js.contains("providedIn"), "providedIn should be omitted when None, got: {js}"); @@ -165,7 +165,7 @@ fn provided_in_platform() { .provided_in_platform() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"providedIn:"platform""#), "expected providedIn:\"platform\", got: {js}"); @@ -180,7 +180,7 @@ fn provided_in_any() { .provided_in_any() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"providedIn:"any""#), "expected providedIn:\"any\", got: {js}"); @@ -195,7 +195,7 @@ fn provided_in_module() { .provided_in_module(read_var(&allocator, "FeatureModule")) .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!( @@ -213,7 +213,7 @@ fn injectable_field_order_matches_upstream() { .provided_in_root() .build() .unwrap(); - let expr = compile_declare_injectable_from_metadata(&allocator, &meta); + let expr = compile_declare_injectable_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); let min_idx = js.find("minVersion").unwrap(); @@ -248,7 +248,7 @@ fn round_trip_partial_injectable_and_factory_through_linker() { .provided_in_root() .build() .unwrap(); - let prov_expr = compile_declare_injectable_from_metadata(&allocator, &injectable_meta); + let prov_expr = compile_declare_injectable_from_metadata(&allocator, "i0", &injectable_meta); let factory_meta = R3FactoryMetadata::Constructor(R3ConstructorFactoryMetadata { name: Ident::from("ApiService"), @@ -262,7 +262,7 @@ fn round_trip_partial_injectable_and_factory_through_linker() { }, target: FactoryTarget::Injectable, }); - let fac_expr = compile_declare_factory_function(&allocator, &factory_meta); + let fac_expr = compile_declare_factory_function(&allocator, "i0", &factory_meta); let source = format!( "import * as i0 from \"@angular/core\";\nexport class ApiService {{}}\nApiService.\u{0275}fac = {};\nApiService.\u{0275}prov = {};", diff --git a/crates/oxc_angular_compiler/tests/partial_ng_module_test.rs b/crates/oxc_angular_compiler/tests/partial_ng_module_test.rs index ace12b00e..994b305c6 100644 --- a/crates/oxc_angular_compiler/tests/partial_ng_module_test.rs +++ b/crates/oxc_angular_compiler/tests/partial_ng_module_test.rs @@ -33,7 +33,7 @@ fn empty_module_emits_only_type() { .selector_scope_mode(R3SelectorScopeMode::Omit) .build() .unwrap(); - let expr = compile_declare_ng_module_from_metadata(&allocator, &meta); + let expr = compile_declare_ng_module_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("type:EmptyModule"), "expected type:EmptyModule, got: {js}"); @@ -62,7 +62,7 @@ fn module_with_declarations_and_imports() { .add_import(R3Reference::value_only(read_var(&allocator, "CommonModule"))) .build() .unwrap(); - let expr = compile_declare_ng_module_from_metadata(&allocator, &meta); + let expr = compile_declare_ng_module_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("declarations:[MyComp,MyDir]"), "expected plain array, got: {js}"); @@ -84,7 +84,7 @@ fn module_with_forward_decls_wraps_lists_in_arrow() { .add_export(R3Reference::value_only(read_var(&allocator, "LaterComp"))) .build() .unwrap(); - let expr = compile_declare_ng_module_from_metadata(&allocator, &meta); + let expr = compile_declare_ng_module_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); // Normalize whitespace and search — emitter wraps lines and adjusts @@ -108,7 +108,7 @@ fn module_with_bootstrap_and_schemas() { .add_schema(R3Reference::value_only(read_var(&allocator, "CUSTOM_ELEMENTS_SCHEMA"))) .build() .unwrap(); - let expr = compile_declare_ng_module_from_metadata(&allocator, &meta); + let expr = compile_declare_ng_module_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("bootstrap:[AppComponent]"), "expected bootstrap, got: {js}"); @@ -125,7 +125,7 @@ fn injector_with_providers_only() { .providers(providers) .build() .unwrap(); - let expr = compile_declare_injector_from_metadata(&allocator, &meta); + let expr = compile_declare_injector_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("providers:MY_PROVIDERS"), "expected providers expr, got: {js}"); @@ -145,7 +145,7 @@ fn injector_without_providers_omits_field() { .r#type(read_var(&allocator, "EmptyModule")) .build() .unwrap(); - let expr = compile_declare_injector_from_metadata(&allocator, &meta); + let expr = compile_declare_injector_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(!js.contains("providers"), "providers field should be omitted entirely, got: {js}"); @@ -161,7 +161,7 @@ fn injector_with_imports_array() { .add_import(read_var(&allocator, "HttpClientModule")) .build() .unwrap(); - let expr = compile_declare_injector_from_metadata(&allocator, &meta); + let expr = compile_declare_injector_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!( @@ -186,7 +186,7 @@ fn injector_raw_imports_preserved_over_per_element_imports() { .raw_imports(raw_imports) .build() .unwrap(); - let expr = compile_declare_injector_from_metadata(&allocator, &meta); + let expr = compile_declare_injector_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("imports:EXTERNAL_IMPORTS_ARRAY"), "expected raw imports, got: {js}"); diff --git a/crates/oxc_angular_compiler/tests/partial_pipe_test.rs b/crates/oxc_angular_compiler/tests/partial_pipe_test.rs index 1b061a321..073f2e964 100644 --- a/crates/oxc_angular_compiler/tests/partial_pipe_test.rs +++ b/crates/oxc_angular_compiler/tests/partial_pipe_test.rs @@ -32,7 +32,7 @@ fn standalone_pure_named_pipe_omits_optional_fields() { .pure(true) .is_standalone(true) .build(); - let expr = compile_declare_pipe_from_metadata(&allocator, &meta); + let expr = compile_declare_pipe_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"name:"upper""#), "expected name:\"upper\", got: {js}"); @@ -50,7 +50,7 @@ fn impure_pipe_emits_pure_false() { .pure(false) .is_standalone(true) .build(); - let expr = compile_declare_pipe_from_metadata(&allocator, &meta); + let expr = compile_declare_pipe_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("pure:false"), "expected pure:false, got: {js}"); @@ -66,7 +66,7 @@ fn non_standalone_pipe_emits_isstandalone_false() { .pure(true) .is_standalone(false) .build(); - let expr = compile_declare_pipe_from_metadata(&allocator, &meta); + let expr = compile_declare_pipe_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains("isStandalone:false"), "expected isStandalone:false, got: {js}"); @@ -86,7 +86,7 @@ fn pipe_uses_pipe_name_over_class_name() { .pure(true) .is_standalone(true) .build(); - let expr = compile_declare_pipe_from_metadata(&allocator, &meta); + let expr = compile_declare_pipe_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"name:"currency""#), "expected name:\"currency\", got: {js}"); @@ -100,7 +100,7 @@ fn pipe_falls_back_to_class_name_when_pipe_name_missing() { .pure(true) .is_standalone(true) .build(); - let expr = compile_declare_pipe_from_metadata(&allocator, &meta); + let expr = compile_declare_pipe_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); assert!(js.contains(r#"name:"MyPipe""#), "expected name:\"MyPipe\", got: {js}"); @@ -114,7 +114,7 @@ fn field_order_matches_upstream() { .pure(false) .is_standalone(false) .build(); - let expr = compile_declare_pipe_from_metadata(&allocator, &meta); + let expr = compile_declare_pipe_from_metadata(&allocator, "i0", &meta); let js = emit(&expr); let min_idx = js.find("minVersion").unwrap(); diff --git a/napi/angular-compiler/index.d.ts b/napi/angular-compiler/index.d.ts index bd196d071..2b2989a62 100644 --- a/napi/angular-compiler/index.d.ts +++ b/napi/angular-compiler/index.d.ts @@ -212,6 +212,13 @@ export interface DtsDeclaration { * Newline-separated `static` property declarations. */ members: string + /** + * Module specifier for every namespace alias the members reference + * (alias → specifier, `@angular/core` included). Consumers must emit + * `import * as from ""` for each — or rewrite the + * member heads to their own canonical aliases. + */ + namespaceImports: Record } /** @@ -946,9 +953,13 @@ export interface TransformResult { * This enables library builds to include proper Ivy type declarations * for template type-checking by consumers. * - * The declarations use `i0` as the namespace alias for `@angular/core`. - * Consumers must ensure their `.d.ts` files include: - * `import * as i0 from "@angular/core";` + * The declarations reference `@angular/core` through the same + * namespace alias the JS emit used — `i0` uniquified (`i0_1`, …) + * against every identifier in the original `.ts` file, matching + * upstream's declaration `ImportManager`, which never reuses an + * existing import. Consumers must ensure their `.d.ts` files include + * a matching `import * as from "@angular/core";` — the alias can + * be read back from the member text (`.ɵɵ…Declaration`). */ dtsDeclarations: Array } diff --git a/napi/angular-compiler/package.json b/napi/angular-compiler/package.json index 4e0740f9d..8842df2b4 100644 --- a/napi/angular-compiler/package.json +++ b/napi/angular-compiler/package.json @@ -65,7 +65,8 @@ "prepublishOnly": "napi pre-publish -t npm" }, "dependencies": { - "obug": "^3.0.0" + "obug": "^3.0.0", + "oxc-parser": "^0.153.0" }, "devDependencies": { "@angular/compiler": "^22.0.0-rc.2", diff --git a/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs index c75f19fae..a3f80b74f 100644 --- a/napi/angular-compiler/src/lib.rs +++ b/napi/angular-compiler/src/lib.rs @@ -346,6 +346,12 @@ pub struct DtsDeclaration { /// The static member declarations to add to the class body in `.d.ts`. /// Newline-separated `static` property declarations. pub members: String, + /// Module specifier for every namespace alias the members reference + /// (alias → specifier, `@angular/core` included). Consumers must emit + /// `import * as from ""` for each — or rewrite the + /// member heads to their own canonical aliases. + #[napi(ts_type = "Record")] + pub namespace_imports: HashMap, } /// Result of transforming an Angular file. @@ -387,9 +393,13 @@ pub struct TransformResult { /// This enables library builds to include proper Ivy type declarations /// for template type-checking by consumers. /// - /// The declarations use `i0` as the namespace alias for `@angular/core`. - /// Consumers must ensure their `.d.ts` files include: - /// `import * as i0 from "@angular/core";` + /// The declarations reference `@angular/core` through the same + /// namespace alias the JS emit used — `i0` uniquified (`i0_1`, …) + /// against every identifier in the original `.ts` file, matching + /// upstream's declaration `ImportManager`, which never reuses an + /// existing import. Consumers must ensure their `.d.ts` files include + /// a matching `import * as from "@angular/core";` — the alias can + /// be read back from the member text (`.ɵɵ…Declaration`). pub dts_declarations: Vec, } @@ -1171,7 +1181,11 @@ impl Task for TransformAngularFileTask { dts_declarations: result .dts_declarations .into_iter() - .map(|d| DtsDeclaration { class_name: d.class_name, members: d.members }) + .map(|d| DtsDeclaration { + class_name: d.class_name, + members: d.members, + namespace_imports: d.namespace_imports, + }) .collect(), }) } @@ -1423,7 +1437,7 @@ pub fn compile_pipe_sync( } let r3_metadata = builder.build(); - let result = compile_pipe(&allocator, &r3_metadata); + let result = compile_pipe(&allocator, "i0", &r3_metadata); let emitter = JsEmitter::new(); let code = emitter.emit_expression(&result.expression); @@ -1970,7 +1984,7 @@ pub fn compile_injector_sync(input: InjectorCompileInput) -> InjectorNapiCompile }; }; - let result = compile_injector(&allocator, &metadata); + let result = compile_injector(&allocator, "i0", &metadata); let emitter = JsEmitter::new(); let code = emitter.emit_expression(&result.expression); @@ -2135,7 +2149,11 @@ pub fn compile_class_metadata_sync( }; // Compile to output expression - let result = compile_class_metadata(&allocator, &metadata); + let result = compile_class_metadata( + &allocator, + namespace_registry.angular_core_ns().as_str(), + &metadata, + ); // Emit to JavaScript let emitter = JsEmitter::new(); @@ -2384,7 +2402,7 @@ fn compile_factory_impl(input: FactoryCompileInput) -> FactoryNapiCompileResult }); // Compile and emit - let result = compile_factory_function(&allocator, &metadata, &factory_name); + let result = compile_factory_function(&allocator, "i0", &metadata, &factory_name); let emitter = JsEmitter::new(); let code = emitter.emit_expression(&result.expression); diff --git a/napi/angular-compiler/test/dts.test.ts b/napi/angular-compiler/test/dts.test.ts index dcc354dce..db29398c8 100644 --- a/napi/angular-compiler/test/dts.test.ts +++ b/napi/angular-compiler/test/dts.test.ts @@ -59,6 +59,21 @@ describe('injectDtsDeclarations', () => { expect(out.match(/@angular\/core/g)).toHaveLength(1) }) + it('appends members at the end of a class that already has members', () => { + // Upstream adds the Ivy members after the existing ones + // ([...members, ...newMembers]). + const source = + 'export declare class Foo {\n constructor(x: number);\n field: string;\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + const ctorIdx = out.indexOf('constructor') + const facIdx = out.indexOf('ɵfac') + const closeIdx = out.lastIndexOf('}') + expect(ctorIdx).toBeLessThan(facIdx) + expect(facIdx).toBeLessThan(closeIdx) + }) + it('keeps the i0 import after leading triple-slash references', () => { const source = '/// \nexport declare class Foo {\n}\n' const out = injectDtsDeclarations(source, [ @@ -74,6 +89,256 @@ describe('injectDtsDeclarations', () => { ]) expect(out).toBe(source) }) + + it('dedupes the alias against identifiers in the emitted .d.ts', () => { + // The bundle still binds `i0`, so members are normalized to the free + // `i0_1` alias and the injected import matches (#509). + const source = 'export declare const i0: 1;\nexport declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0_1.ɵɵFactoryDeclaration;' }, + ]) + expect(out).toContain('import * as i0_1 from "@angular/core";') + expect(out).toContain('static ɵfac: i0_1.ɵɵFactoryDeclaration;') + expect(out).not.toContain('import * as i0 from') + }) + + it('normalizes mixed per-file aliases to one bundle namespace', () => { + // Members compiled from different source files can carry different + // aliases — `i0` where it was free, `i0_1` where a binding collided. + // A bundled .d.ts needs one import, so every member is rewritten to the + // namespace chosen for this file. + const source = 'export declare class A {\n}\nexport declare class B {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'A', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + { className: 'B', members: 'static ɵfac: i0_1.ɵɵFactoryDeclaration;' }, + ]) + expect(out.match(/@angular\/core/g)).toHaveLength(1) + expect(out).toContain('import * as i0 from "@angular/core";') + expect(out).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;') + expect(out).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;') + expect(out).not.toContain('i0_1') + }) + + it('never reuses an existing differently-aliased @angular/core import', () => { + // ngtsc's declaration ImportManager mints its own `i0` even when the + // file already has `import * as ng from "@angular/core"`. + const source = 'import * as ng from "@angular/core";\nexport declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + expect(out.match(/@angular\/core/g)).toHaveLength(2) + expect(out).toContain('import * as ng from "@angular/core";') + expect(out).toContain('import * as i0 from "@angular/core";') + expect(out).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;') + }) + + it('mints i0 even when members arrive with a hand-written ng head', () => { + // The compiler never emits `ng` heads, but a declaration that has them + // (older binding, hand-written input) is still normalized to the + // canonical `i0` — upstream's ImportManager would never reuse `ng` + // either, it dedupes `i0` against the file's identifiers. + const source = 'import * as ng from "@angular/core";\nexport declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'Foo', + members: 'static ɵfac: ng.ɵɵFactoryDeclaration;', + namespaceImports: { ng: '@angular/core' }, + }, + ]) + expect(out).toContain('import * as ng from "@angular/core";') + expect(out).toContain('import * as i0 from "@angular/core";') + expect(out).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;') + }) + + it('emits a namespace import for non-core member aliases', () => { + // `typeof i1.SomeDirective` (host directives, ctor deps) resolves through + // `namespaceImports` — `i1` → `./dir` — and the matching import is added. + const source = 'export declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'Foo', + members: + 'static ɵcmp: i0.ɵɵDirectiveDeclaration;', + namespaceImports: { i1: './dir' }, + }, + ]) + expect(out).toContain('import * as i0 from "@angular/core";') + expect(out).toContain('import * as i1 from "./dir";') + expect(out).toContain('typeof i1.SomeDirective') + }) + + it('uniquifies non-core aliases that collide with file identifiers', () => { + const source = 'export declare const i1: number;\nexport declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'Foo', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i1: './dep' }, + }, + ]) + expect(out).toContain('import * as i1_1 from "./dep";') + expect(out).toContain('typeof i1_1.M') + expect(out).toContain('export declare const i1: number;') + }) + + it('canonicalizes the same alias to different modules per declaration', () => { + // Members merged from two source files: `i1` meant `./dep` in one and + // `./other` in the other — each module gets its own canonical alias. + const source = 'export declare class A {\n}\nexport declare class B {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'A', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i1: './dep' }, + }, + { + className: 'B', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i1: './other' }, + }, + ]) + expect(out).toContain('import * as i1 from "./dep";') + expect(out).toContain('import * as i1_1 from "./other";') + expect(out).toContain('typeof i1.D') + expect(out).toContain('typeof i1_1.O') + }) + + it('keeps a trailing comment attached to the last import', () => { + const source = 'import type { A } from "./a"; // keep me\nexport declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + const lines = out.split('\n') + expect(lines[0]).toBe('import type { A } from "./a"; // keep me') + expect(lines[1]).toBe('import * as i0 from "@angular/core";') + }) + + it('keeps a multiline trailing comment attached to the last import', () => { + // The newline inside the block comment must not be chosen as the + // insertion point — the import would land inside the comment. + const source = + 'import type { A } from "./a"; /* keep\nme */\nexport declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + const lines = out.split('\n') + expect(lines[0]).toBe('import type { A } from "./a"; /* keep') + expect(lines[1]).toBe('me */') + expect(lines[2]).toBe('import * as i0 from "@angular/core";') + }) + + it('is idempotent when the incoming alias differs from the first pass', () => { + // Members compiled against `i0_1` normalize to `i0` on the first pass; + // the second pass sees `i0` occupied and would mint `i0_1` — the member + // check is structural (name already in the class), so nothing is added. + const source = 'export declare class Foo {\n}\n' + const decls = [ + { className: 'Foo', members: 'static ɵfac: i0_1.ɵɵFactoryDeclaration;' }, + ] + const once = injectDtsDeclarations(source, decls) + const twice = injectDtsDeclarations(once, decls) + expect(twice).toBe(once) + expect(once.match(/ɵfac/g)).toHaveLength(1) + expect(once.match(/@angular\/core/g)).toHaveLength(1) + }) + + it('injects only the members the class is missing', () => { + // A class that already has `ɵfac` (e.g. hand-authored or from an earlier + // pass) still gets the missing `ɵcmp`, without duplicating `ɵfac`. + const source = + 'import * as i0 from "@angular/core";\nexport declare class Foo {\n static ɵfac: i0.ɵɵFactoryDeclaration;\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'Foo', + members: + 'static ɵfac: i0.ɵɵFactoryDeclaration;\n' + + 'static ɵcmp: i0.ɵɵComponentDeclaration;', + }, + ]) + expect(out.match(/ɵfac/g)).toHaveLength(1) + expect(out.match(/ɵcmp/g)).toHaveLength(1) + expect(out.match(/@angular\/core/g)).toHaveLength(1) + }) + + it('emits the core alias for non-ɵ members via namespaceImports', () => { + // `ngAcceptInputType_x` types like `i0.Signal` aren't `ɵ` heads; + // they resolve through `namespaceImports`. + const source = 'export declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'Foo', + members: 'static ngAcceptInputType_x: i0.Signal;', + namespaceImports: { i0: '@angular/core' }, + }, + ]) + expect(out).toContain('import * as i0 from "@angular/core";') + expect(out).toContain('static ngAcceptInputType_x: i0.Signal;') + }) + + it('canonicalizes identical relative specifiers under different source dirs as distinct modules', () => { + // `src/a/foo.ts` and `src/b/foo.ts` both import `"./dep"` — different + // modules, so they get different aliases even though the raw specifier + // strings are identical. + const source = 'export declare class A {\n}\nexport declare class B {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'A', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i0: '@angular/core', i1: './dep' }, + sourceFile: 'src/a/foo.ts', + }, + { + className: 'B', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i0: '@angular/core', i1: './dep' }, + sourceFile: 'src/b/foo.ts', + }, + ]) + expect(out).toContain('import * as i1 from "./dep";') + expect(out).toContain('import * as i1_1 from "./dep";') + expect(out).toContain('typeof i1.D') + expect(out).toContain('typeof i1_1.O') + }) + + it('skips declarations whose member text does not parse', () => { + const source = 'export declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + { className: 'Foo', members: 'this is not valid typescript {{' }, + ]) + // The well-formed declaration still lands; the malformed one is skipped + // rather than injected with unnormalized aliases. + expect(out).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;') + expect(out).not.toContain('this is not valid') + }) + + it('injects into a class whose type-param constraint contains a brace', () => { + // The AST locates the class body after ``; the old + // regex stopped at the constraint's `{`. + const source = 'export declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + expect(out).toContain('import * as i0 from "@angular/core";') + const facIdx = out.indexOf('ɵfac') + const classIdx = out.indexOf('class Foo') + const closeIdx = out.lastIndexOf('}') + expect(classIdx).toBeLessThan(facIdx) + expect(facIdx).toBeLessThan(closeIdx) + }) + + it('injects into a class nested in a declare module block', () => { + const source = 'declare module "pkg" {\n export class Foo {\n }\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + expect(out).toContain('import * as i0 from "@angular/core";') + const facIdx = out.indexOf('ɵfac') + const classIdx = out.indexOf('class Foo') + const closeIdx = out.lastIndexOf('}') + expect(classIdx).toBeLessThan(facIdx) + expect(facIdx).toBeLessThan(closeIdx) + }) }) describe('angular() dts plugin (#104)', () => { diff --git a/napi/angular-compiler/vite-plugin/index.ts b/napi/angular-compiler/vite-plugin/index.ts index 1ad31b88f..b377e0fb3 100644 --- a/napi/angular-compiler/vite-plugin/index.ts +++ b/napi/angular-compiler/vite-plugin/index.ts @@ -404,7 +404,14 @@ export function angular(options: PluginOptions = {}): Plugin[] { // a decorator — which makes the quick decorator check early-return, skipping // the transform entirely — would leave the old `ɵfac`/`ɵcmp` entries in place // and `generateBundle` would re-inject Ivy metadata into a now-plain class. - const collectedDtsDeclarations = new Map>() + const collectedDtsDeclarations = new Map< + string, + Array<{ + className: string + members: string + namespaceImports: Record + }> + >() function getMinifyComponentStyles(context?: { environment?: { config?: { build?: ResolvedConfig['build'] } } @@ -1050,6 +1057,7 @@ export function angular(options: PluginOptions = {}): Plugin[] { result.dtsDeclarations.map((decl) => ({ className: decl.className, members: decl.members, + namespaceImports: decl.namespaceImports, })), ) } @@ -1425,20 +1433,29 @@ export function angular(options: PluginOptions = {}): Plugin[] { // Flatten every module's declarations into a class-name-keyed list. // A library publishes one class per name; if names ever collide the // last module wins, matching the previous (class-name-keyed) behavior. - const byClassName = new Map() - for (const moduleDecls of collectedDtsDeclarations.values()) { + // Keep each declaration's source module id: relative specifiers in + // `namespaceImports` ("./dep") mean different modules under + // different directories, so canonicalization keys on the resolved + // identity, not the raw string. + const byClassName = new Map< + string, + { + className: string + members: string + namespaceImports: Record + sourceFile: string + } + >() + for (const [moduleId, moduleDecls] of collectedDtsDeclarations) { for (const decl of moduleDecls) { - byClassName.set(decl.className, decl.members) + byClassName.set(decl.className, { ...decl, sourceFile: moduleId }) } } - const declarations = Array.from(byClassName, ([className, members]) => ({ - className, - members, - })) + const declarations = Array.from(byClassName.values()) for (const file of Object.values(bundle)) { if (file.type !== 'asset') continue - if (!file.fileName.endsWith('.d.ts')) continue + if (!/\.d\.[cm]?ts$/.test(file.fileName)) continue const source = typeof file.source === 'string' diff --git a/napi/angular-compiler/vite-plugin/utils/dts.ts b/napi/angular-compiler/vite-plugin/utils/dts.ts index 9b32e529e..0c5a49423 100644 --- a/napi/angular-compiler/vite-plugin/utils/dts.ts +++ b/napi/angular-compiler/vite-plugin/utils/dts.ts @@ -11,70 +11,311 @@ * Vite/Rolldown don't emit `.d.ts` themselves — a separate declaration * generator (rolldown-plugin-dts, vite-plugin-dts, tsdown, `tsc`) produces * the base declarations. This helper is a post-processing pass that splices - * the Angular members into those already-generated `.d.ts`, and ensures the - * `i0` namespace import the members reference is present. + * the Angular members into those already-generated `.d.ts`, and emits the + * namespace imports the members reference. * - * Known limitation: class bodies are located with a regex that stops at the - * first `{` after `class `. A `{` inside a type-parameter constraint or - * default (e.g. `class C`) would be mistaken for the body - * brace. Emitted library declarations for Angular classes don't use such - * generics, so this is accepted in exchange for not pulling in a parser. + * ngtsc never reuses an existing namespace import in declaration emit — its + * `ImportManager` (`presetImportManagerForceNamespaceImports`) mints `i0`, + * `i0_1`, … deduped against the ORIGINAL source file's identifiers. The + * compiler mirrors that: each declaration's members carry the alias the + * per-source-file registry picked, and `namespaceImports` records which + * module each alias stands for. A bundled `.d.ts` merges declarations from + * many source files whose aliases can disagree (`i1` → `./dep` in one, + * `./other` in another), so this pass canonicalizes: one alias per module + * identity, collision-free against every identifier in the emitted file. + * The emitted `.d.ts` is parsed once with the oxc parser; class bodies are + * located through the AST (so type-parameter constraints containing `{` are + * no issue) and member alias heads are rewritten by splicing the + * `TSQualifiedName` leftmost identifier the parser reports. */ +import { + parseSync, + Visitor, + type ClassBody, + type Directive, + type Program, + type Statement, +} from 'oxc-parser' /** A single class's `.d.ts` static member declarations. */ export interface DtsClassDeclaration { /** The class the members belong to. */ className: string - /** Newline-separated `static …;` member declarations, `i0`-prefixed. */ + /** + * Newline-separated `static …;` member declarations. Namespace-qualified + * type names (`i0.ɵɵComponentDeclaration`, `typeof i1.SomeDirective`) use + * the alias the compiler picked for the member's module in that source + * file; `namespaceImports` maps each such alias to its module specifier. + */ members: string + /** + * alias → module specifier for every namespace the members reference: + * `i0` → `"@angular/core"` for `i0.ɵɵX`/`i0.Signal`, `i1` → `"./dep"` + * for `typeof i1.SomeDirective` host-directive references, imported + * ctor-dep types, and `ngAcceptInputType_*` transform types alike. + * Relative specifiers (`"./dep"`) are expected already rebased to the + * emitted file by the caller, or resolved against `sourceFile`. + */ + namespaceImports?: Record + /** + * The source module the declaration was compiled from (the transform's + * module id). Used to resolve relative specifiers in `namespaceImports` + * so two files in different directories that each import `"./dep"` + * canonicalize to different aliases instead of silently sharing one. + */ + sourceFile?: string } -const I0_IMPORT = 'import * as i0 from "@angular/core";' +/** One namespace-alias head found in a member body. */ +interface MemberHead { + /** `(start, end)` of the head inside the member's own text. */ + start: number + end: number + /** The head's alias as compiled (what `text.slice(start, end)` reads). */ + alias: string + /** The module specifier the alias refers to (what gets emitted). */ + module: string + /** The canonical identity for deduping — the specifier resolved against + * the declaration's source file, so `"./dep"` under different directories + * is two modules, not one. */ + resolved: string +} -const I0_IMPORT_RE = /import\s+\*\s+as\s+i0\s+from\s+['"]@angular\/core['"]/ +/** + * The alias → specifier map of every `import * as ns` the file already has. + */ +function namespaceImportsOf(program: Program): Map { + const aliases = new Map() + for (const stmt of program.body) { + if (stmt.type !== 'ImportDeclaration') { + continue + } + for (const spec of stmt.specifiers) { + if (spec.type === 'ImportNamespaceSpecifier') { + aliases.set(spec.local.name, String(stmt.source.value)) + } + } + } + return aliases +} -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +/** Every identifier name in the file (`Identifier` covers all flavors). */ +function collectIdentifiers(program: Program): Set { + const names = new Set() + new Visitor({ + Identifier(node) { + names.add(node.name) + }, + }).visit(program) + return names } /** - * Insert the `i0` namespace import, keeping it after any leading triple-slash - * reference directives and leading comments (which must stay at the top of a - * `.d.ts`). + * `i0`, `i0_1`, `i0_2`, … — the first name not colliding with `names`. + * Matches `check_unique_identifier_name` in ngtsc's ImportManager. */ -function ensureI0Import(source: string): string { - if (I0_IMPORT_RE.test(source)) { - return source +function uniquifyIdentifier(base: string, names: Set): string { + if (!names.has(base)) { + return base + } + let counter = 1 + while (names.has(`${base}_${counter}`)) { + counter += 1 + } + return `${base}_${counter}` +} + +/** + * The `ClassBody` of the class named `className`, searching through export + * wrappers and ambient module blocks (the latter covers `declare module` + * declarations a bundler may wrap library classes in). + */ +function findClassBody( + statements: ReadonlyArray, + className: string, +): ClassBody | null { + for (const stmt of statements) { + // `export declare class`, `export default class`, `export declare module`. + const node = + stmt.type === 'ExportNamedDeclaration' || stmt.type === 'ExportDefaultDeclaration' + ? stmt.declaration + : stmt + if (node === null) { + continue + } + if (node.type === 'ClassDeclaration' && node.id?.name === className) { + return node.body + } + if (node.type === 'TSModuleDeclaration' && node.body?.type === 'TSModuleBlock') { + const found = findClassBody(node.body.body, className) + if (found !== null) { + return found + } + } } + return null +} - const lines = source.split('\n') - let insertAt = 0 - for (let i = 0; i < lines.length; i++) { - const trimmed = lines[i].trim() - if ( - trimmed === '' || - trimmed.startsWith('///') || - trimmed.startsWith('//') || - trimmed.startsWith('/*') || - trimmed.startsWith('*') - ) { - insertAt = i + 1 +/** Every non-computed member name in `body`, as written. */ +function existingMemberNames(body: ClassBody): Set { + const names = new Set() + for (const element of body.body) { + const member = element as { + computed?: boolean + key?: { type: string; name?: string; value?: unknown } + } + if (member.computed || member.key === undefined) { continue } - break + if (member.key.type === 'Identifier' && member.key.name !== undefined) { + names.add(member.key.name) + } else if (member.key.type === 'Literal') { + names.add(String(member.key.value)) + } + } + return names +} + +/** + * Parse `members` (wrapped in a dummy `declare class`) into per-member + * entries: the member's name, its exact text slice, and its namespace heads. + * Each head is the leftmost `Identifier` of a `TSQualifiedName` (`i0.ɵɵX` + * and `typeof i1.SomeDirective` alike — a nested `a.b.c` chain is visited as + * its inner `a.b` too, so the head is always found). + * + * Returns `null` when the member text doesn't parse; the caller skips the + * declaration rather than splice members whose aliases might be wrong. + */ +function parseMembers( + members: string, + namespaceImports: Readonly>, + resolve: (specifier: string) => string, +): { name: string | null; text: string; heads: MemberHead[] }[] | null { + const prefix = 'declare class X {\n' + const wrapped = prefix + members + '\n}' + const { program, errors } = parseSync('members.d.ts', wrapped, { lang: 'dts' }) + if (errors.length > 0) { + return null + } + const cls = program.body[0] + if (cls.type !== 'ClassDeclaration') { + return [] } + // Collect every namespace head in one program-wide pass (the Visitor only + // accepts a Program), binned into the member that contains it. + const collected: { pos: number; alias: string; module: string; resolved: string }[] = [] + new Visitor({ + TSQualifiedName(node) { + if (node.left.type !== 'Identifier') { + return + } + const alias = node.left.name + const module = namespaceImports[alias] + if (module === undefined && !node.right.name.startsWith('ɵ')) { + // Not a generated namespace reference — resolve against whatever + // the `.d.ts` already imports and leave the head untouched. + return + } + const specifier = module ?? '@angular/core' + collected.push({ + pos: node.left.start - prefix.length, + alias, + module: specifier, + resolved: resolve(specifier), + }) + }, + }).visit(program) - lines.splice(insertAt, 0, I0_IMPORT) - return lines.join('\n') + return cls.body.body.map((element) => { + // Spans are relative to `wrapped`; shift them back into `members`. + const start = element.start - prefix.length + const end = element.end - prefix.length + const member = element as { + computed?: boolean + key?: { type: string; name?: string; value?: unknown } + } + const name = member.computed + ? null + : member.key === undefined + ? null + : member.key.type === 'Identifier' + ? (member.key.name ?? null) + : member.key.type === 'Literal' + ? String(member.key.value) + : null + const heads: MemberHead[] = collected + .filter((head) => head.pos >= start && head.pos < end) + .map((head) => ({ + start: head.pos - start, + end: head.pos - start + head.alias.length, + alias: head.alias, + module: head.module, + resolved: head.resolved, + })) + return { name, text: members.slice(start, end), heads } + }) +} + +/** + * The position just past `position`'s line, skipping newlines that fall + * inside comments — `parseSync`'s `comments` carry spans for both `//` and + * `/* ... *\/` so a multiline trailing comment can't split the insertion. + */ +function endOfLineAfter( + source: string, + position: number, + comments: ReadonlyArray<{ start: number; end: number }>, +): number { + let cursor = position + for (;;) { + const newline = source.indexOf('\n', cursor) + const end = newline === -1 ? source.length : newline + const covering = comments.find((c) => c.start <= end && end < c.end) + if (covering === undefined) { + return newline === -1 ? source.length : newline + 1 + } + cursor = covering.end + } +} + +/** + * Resolve `dir + specifier` the way Node would see it — collapsing `..` and + * `.` segments — so `"./dep"` under different directories canonicalizes to + * different identities while `../dep` from a nested file resolves back to + * the same one. Purely textual; no filesystem access. + */ +function resolvePath(path: string): string { + const out: string[] = [] + for (const segment of path.split('/')) { + if (segment === '' || segment === '.') { + continue + } + if (segment === '..') { + out.pop() + } else { + out.push(segment) + } + } + return out.join('/') } /** * Splice each declaration's static members into the matching class body in - * `source`, and ensure the `i0` import is present when anything was injected. + * `source`, and emit the namespace imports the injected members reference — + * one `import * as ns from "module"` per module identity, canonicalized + * across all injected declarations. * - * The pass is idempotent: a declaration whose first member already appears in - * `source` is skipped, so re-running over an already-augmented file is a - * no-op. A declaration whose class isn't found is silently skipped. + * Like ngtsc's declaration ImportManager, an existing `import * as ns` is + * reused only when it already binds the same alias and specifier — never + * for a different one, and never to borrow the user's `@angular/core` + * import: the members' compiled alias is kept when free, otherwise + * uniquified (`i0_1`, …). + * + * The pass is idempotent: a member whose name already appears in the target + * class is skipped — structurally, on the member name, so a second pass + * that canonicalizes to a different alias can't inject the same member + * twice. A declaration whose class isn't found, whose members don't parse, + * or a file the parser rejects is silently skipped. */ export function injectDtsDeclarations( source: string, @@ -84,40 +325,155 @@ export function injectDtsDeclarations( return source } - let output = source - let injected = false - - for (const { className, members } of declarations) { - const memberLines = members - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - if (memberLines.length === 0) { + const { program, errors, comments } = parseSync('file.d.ts', source, { lang: 'dts' }) + if (errors.length > 0) { + return source + } + + const fileNames = collectIdentifiers(program) + const existingImports = namespaceImportsOf(program) + + // Identity resolution is relative to each declaration's source module — + // the same "./dep" in two directories is two modules. Emitted specifiers + // stay verbatim (as the compiler recorded them); when `sourceFile` is + // known, relative specifiers are resolved for identity only. + const resolveFor = (sourceFile?: string) => { + if (sourceFile === undefined) { + return (specifier: string) => specifier + } + const dir = sourceFile.replace(/[^/\\]*$/, '') + return (specifier: string) => + specifier.startsWith('./') || specifier.startsWith('../') + ? resolvePath(dir + specifier) + : specifier + } + + // Canonical module identity → [rawSpecifier, alias] for this file. + const canonical = new Map() + const canonicalAlias = (head: MemberHead): string => { + const known = canonical.get(head.resolved) + if (known !== undefined) { + return known[1] + } + const preferred = head.module === '@angular/core' ? 'i0' : head.alias + // Reuse an existing import only when it binds this alias AND specifier — + // same module under the same name (which is exactly what an idempotent + // re-run sees on its second pass), never a different module. + const existing = existingImports.get(preferred) + const alias = + existing !== undefined && existing === head.module + ? preferred + : uniquifyIdentifier(preferred, fileNames) + canonical.set(head.resolved, [head.module, alias]) + fileNames.add(alias) + return alias + } + + const splices: { start: number; end: number; text: string }[] = [] + + for (const declaration of declarations) { + const members = parseMembers( + declaration.members, + declaration.namespaceImports ?? {}, + resolveFor(declaration.sourceFile), + ) + if (members === null || members.length === 0) { continue } - // Idempotency: if the members are already present, don't inject again. - if (output.includes(memberLines[0])) { + const body = findClassBody(program.body, declaration.className) + if (body === null) { continue } + const existing = existingMemberNames(body) - // Match `(export )?(declare )?(abstract )?class …{`, capturing up - // to and including the opening brace of the class body. - const classBodyOpen = new RegExp( - `(?:export\\s+)?(?:declare\\s+)?(?:abstract\\s+)?class\\s+${escapeRegExp( - className, - )}\\b[^{]*\\{`, - ) - const match = classBodyOpen.exec(output) - if (!match) { + // Per-member idempotency: skip members the class already declares. + // Names — not normalized text — decide, so a re-run whose canonical + // alias differs (the first pass's import now occupies `i0`) doesn't + // inject the same `static ɵfac` a second time. + const lines: string[] = [] + for (const member of members) { + if (member.name !== null && existing.has(member.name)) { + continue + } + let text = member.text + for (const head of member.heads.sort((a, b) => b.start - a.start)) { + const alias = canonicalAlias(head) + text = text.slice(0, head.start) + alias + text.slice(head.end) + } + for (const line of text.split('\n')) { + const trimmed = line.trim() + if (trimmed.length > 0) { + lines.push(trimmed) + } + } + } + if (lines.length === 0) { continue } - const insertAt = match.index + match[0].length - const body = '\n' + memberLines.map((line) => ` ${line}`).join('\n') - output = output.slice(0, insertAt) + body + output.slice(insertAt) - injected = true + // `ClassBody.end` is just past the closing brace; inserting before it + // appends the members, matching upstream's `[...members, ...newMembers]` + // (declaration.ts) — a class that already has members (constructor + // overloads, declared fields) keeps them first. + const insertAt = body.end - 1 + const needsLeadingNl = insertAt > 0 && source[insertAt - 1] !== '\n' + splices.push({ + start: insertAt, + end: insertAt, + text: + (needsLeadingNl ? '\n' : '') + + lines.map((line) => ` ${line}`).join('\n') + + '\n', + }) } - return injected ? ensureI0Import(output) : output + if (splices.length === 0) { + return source + } + + // Emit an import for every canonical module the file doesn't already + // have, sorted by alias — the same order the compiler's own namespace + // registry generates them in. + const importLines = [...canonical.values()] + .filter(([specifier, alias]) => existingImports.get(alias) !== specifier) + .sort(([, a], [, b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map(([specifier, alias]) => `import * as ${alias} from "${specifier}";`) + + if (importLines.length > 0) { + const text = importLines.join('\n') + let lastImportEnd: number | null = null + for (const stmt of program.body) { + if (stmt.type === 'ImportDeclaration') { + lastImportEnd = stmt.end + } + } + if (lastImportEnd !== null) { + // End of the last import's line, skipping multiline trailing comments + // (`import x from "m"; /* keep\nme */`) so the comment keeps its line + // and the new import lands on the next one. + const position = endOfLineAfter(source, lastImportEnd, comments) + splices.push({ start: position, end: position, text: `${text}\n` }) + } else if (program.body.length > 0) { + // Before the first statement keeps leading comments and triple-slash + // reference directives at the top of the file. + splices.push({ + start: program.body[0].start, + end: program.body[0].start, + text: `${text}\n`, + }) + } else { + splices.push({ + start: source.length, + end: source.length, + text: `${source.endsWith('\n') || source === '' ? '' : '\n'}${text}\n`, + }) + } + } + + let output = source + for (const { start, end, text } of splices.sort((a, b) => b.start - a.start)) { + output = output.slice(0, start) + text + output.slice(end) + } + return output } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24ee40e69..f95532f7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: obug: specifier: ^3.0.0 version: 3.0.0 + oxc-parser: + specifier: ^0.153.0 + version: 0.153.0 devDependencies: '@angular/compiler': specifier: ^22.0.0-rc.2 From 250e58731626487f1a6fbde69e74bcf8105a347c Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 7 Oct 2026 21:51:00 +0800 Subject: [PATCH 2/8] style: oxfmt dts plugin files --- napi/angular-compiler/test/dts.test.ts | 6 ++---- napi/angular-compiler/vite-plugin/utils/dts.ts | 5 +---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/napi/angular-compiler/test/dts.test.ts b/napi/angular-compiler/test/dts.test.ts index db29398c8..8f1d546d4 100644 --- a/napi/angular-compiler/test/dts.test.ts +++ b/napi/angular-compiler/test/dts.test.ts @@ -62,8 +62,7 @@ describe('injectDtsDeclarations', () => { it('appends members at the end of a class that already has members', () => { // Upstream adds the Ivy members after the existing ones // ([...members, ...newMembers]). - const source = - 'export declare class Foo {\n constructor(x: number);\n field: string;\n}\n' + const source = 'export declare class Foo {\n constructor(x: number);\n field: string;\n}\n' const out = injectDtsDeclarations(source, [ { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, ]) @@ -216,8 +215,7 @@ describe('injectDtsDeclarations', () => { it('keeps a multiline trailing comment attached to the last import', () => { // The newline inside the block comment must not be chosen as the // insertion point — the import would land inside the comment. - const source = - 'import type { A } from "./a"; /* keep\nme */\nexport declare class Foo {\n}\n' + const source = 'import type { A } from "./a"; /* keep\nme */\nexport declare class Foo {\n}\n' const out = injectDtsDeclarations(source, [ { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, ]) diff --git a/napi/angular-compiler/vite-plugin/utils/dts.ts b/napi/angular-compiler/vite-plugin/utils/dts.ts index 0c5a49423..32a785dae 100644 --- a/napi/angular-compiler/vite-plugin/utils/dts.ts +++ b/napi/angular-compiler/vite-plugin/utils/dts.ts @@ -421,10 +421,7 @@ export function injectDtsDeclarations( splices.push({ start: insertAt, end: insertAt, - text: - (needsLeadingNl ? '\n' : '') + - lines.map((line) => ` ${line}`).join('\n') + - '\n', + text: (needsLeadingNl ? '\n' : '') + lines.map((line) => ` ${line}`).join('\n') + '\n', }) } From fb0563ab99603fdb800a689a4b1acd55717c673f Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 7 Oct 2026 22:23:05 +0800 Subject: [PATCH 3/8] fix(vite-plugin): rebase relative dts imports to the output path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on PR #558: - P1: relative specifiers ("./dep") were emitted verbatim into the .d.ts asset, resolving against the OUTPUT directory. Two source files in different dirs canonicalized to i1/i1_1 but both emitted "./dep". injectDtsDeclarations now takes outputFile and rebases relative specifiers to the asset's directory; canonical entries store the emitted specifier and the reuse check compares against it. - P2: the idempotency check counted instance members — an instance `ɵfac: string` would suppress the generated `static ɵfac`. Only static member names now count as previously injected. - P2: parseMembers dereferenced program.body[0] without an undefined guard; empty/comment-only member strings are now handled explicitly. --- napi/angular-compiler/test/dts.test.ts | 96 ++++++++++++++++++ napi/angular-compiler/vite-plugin/index.ts | 9 +- .../angular-compiler/vite-plugin/utils/dts.ts | 97 +++++++++++++------ 3 files changed, 174 insertions(+), 28 deletions(-) diff --git a/napi/angular-compiler/test/dts.test.ts b/napi/angular-compiler/test/dts.test.ts index 8f1d546d4..7162dea04 100644 --- a/napi/angular-compiler/test/dts.test.ts +++ b/napi/angular-compiler/test/dts.test.ts @@ -298,6 +298,102 @@ describe('injectDtsDeclarations', () => { expect(out).toContain('typeof i1_1.O') }) + it('rebases relative specifiers to the output file directory', () => { + // `"./dep"` compiled under `src/a/` must not be emitted verbatim into + // `dist/index.d.ts` — from there it resolves to a module that does not + // exist. With `outputFile` known, the emitted specifier reaches back to + // the source-resolved target. + const source = 'export declare class A {\n}\n' + const out = injectDtsDeclarations( + source, + [ + { + className: 'A', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i0: '@angular/core', i1: './dep' }, + sourceFile: '/repo/src/a/foo.ts', + }, + ], + 'dist/index.d.ts', + ) + expect(out).toContain('import * as i1 from "../repo/src/a/dep";') + expect(out).toContain('import * as i0 from "@angular/core";') + expect(out).toContain('typeof i1.D') + }) + + it('keeps distinct rebased specifiers for same-named relative modules', () => { + // Two source dirs, same raw specifier: two identities AND two emitted + // specifiers — `i1` → ../src/a/dep, `i1_1` → ../src/b/dep. + const source = 'export declare class A {\n}\nexport declare class B {\n}\n' + const out = injectDtsDeclarations( + source, + [ + { + className: 'A', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i0: '@angular/core', i1: './dep' }, + sourceFile: '/repo/src/a/foo.ts', + }, + { + className: 'B', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i0: '@angular/core', i1: './dep' }, + sourceFile: '/repo/src/b/foo.ts', + }, + ], + 'dist/index.d.ts', + ) + expect(out).toContain('import * as i1 from "../repo/src/a/dep";') + expect(out).toContain('import * as i1_1 from "../repo/src/b/dep";') + expect(out).toContain('typeof i1.D') + expect(out).toContain('typeof i1_1.O') + }) + + it('reuses an existing import when the rebased specifier matches', () => { + // Reuse compares the EMITTED specifier: an existing `import * as i1 + // from "../repo/src/a/dep"` already binds exactly what this member + // needs after rebasing, so no second import is added. + const source = 'import * as i1 from "../repo/src/a/dep";\nexport declare class A {\n}\n' + const out = injectDtsDeclarations( + source, + [ + { + className: 'A', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i0: '@angular/core', i1: './dep' }, + sourceFile: '/repo/src/a/foo.ts', + }, + ], + 'dist/index.d.ts', + ) + expect(out.match(/from "\.\.\/repo\/src\/a\/dep"/g)).toHaveLength(1) + expect(out).toContain('typeof i1.D') + }) + + it('still injects a static member when an instance member has the same name', () => { + // TypeScript permits `ɵfac` and `static ɵfac` side by side; the + // idempotency check must only count statics, otherwise a partial build + // loses the generated factory declaration. + const source = 'export declare class Foo {\n ɵfac: string;\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + expect(out).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;') + expect(out.match(/ɵfac/g)).toHaveLength(2) + }) + + it('handles declarations with empty or comment-only members', () => { + // The wrapper always yields a class, but nothing should be spliced — + // no crash, no members, no imports. + const source = 'export declare class Foo {\n}\n' + const decls = [ + { className: 'Foo', members: '' }, + { className: 'Foo', members: ' \n ' }, + { className: 'Foo', members: '/* no members */ // only comments' }, + ] + expect(injectDtsDeclarations(source, decls)).toBe(source) + }) + it('skips declarations whose member text does not parse', () => { const source = 'export declare class Foo {\n}\n' const out = injectDtsDeclarations(source, [ diff --git a/napi/angular-compiler/vite-plugin/index.ts b/napi/angular-compiler/vite-plugin/index.ts index b377e0fb3..75053340c 100644 --- a/napi/angular-compiler/vite-plugin/index.ts +++ b/napi/angular-compiler/vite-plugin/index.ts @@ -1462,7 +1462,14 @@ export function angular(options: PluginOptions = {}): Plugin[] { ? file.source : Buffer.from(file.source).toString('utf-8') - const augmented = injectDtsDeclarations(source, declarations) + // The asset's own path lets `injectDtsDeclarations` rebase + // relative namespace specifiers ("./dep" collected under src/a/) + // so they resolve from wherever the .d.ts is emitted. + const outputFile = _outputOptions.dir + ? `${_outputOptions.dir}/${file.fileName}` + : _outputOptions.file + + const augmented = injectDtsDeclarations(source, declarations, outputFile) if (augmented !== source) { file.source = augmented } diff --git a/napi/angular-compiler/vite-plugin/utils/dts.ts b/napi/angular-compiler/vite-plugin/utils/dts.ts index 32a785dae..e60bdb61f 100644 --- a/napi/angular-compiler/vite-plugin/utils/dts.ts +++ b/napi/angular-compiler/vite-plugin/utils/dts.ts @@ -53,8 +53,9 @@ export interface DtsClassDeclaration { * `i0` → `"@angular/core"` for `i0.ɵɵX`/`i0.Signal`, `i1` → `"./dep"` * for `typeof i1.SomeDirective` host-directive references, imported * ctor-dep types, and `ngAcceptInputType_*` transform types alike. - * Relative specifiers (`"./dep"`) are expected already rebased to the - * emitted file by the caller, or resolved against `sourceFile`. + * Relative specifiers (`"./dep"`) are resolved against `sourceFile` for + * identity and rebased to the output file's directory when `outputFile` + * is passed to `injectDtsDeclarations`. */ namespaceImports?: Record /** @@ -73,8 +74,12 @@ interface MemberHead { end: number /** The head's alias as compiled (what `text.slice(start, end)` reads). */ alias: string - /** The module specifier the alias refers to (what gets emitted). */ + /** The module specifier the alias refers to, as recorded by the compiler. */ module: string + /** The specifier emitted in the import statement — rebased to the output + * file's directory when it is relative (`"./dep"` compiled under `src/a` + * becomes `"../src/a/dep"` in `dist/index.d.ts`). */ + emit: string /** The canonical identity for deduping — the specifier resolved against * the declaration's source file, so `"./dep"` under different directories * is two modules, not one. */ @@ -156,15 +161,18 @@ function findClassBody( return null } -/** Every non-computed member name in `body`, as written. */ +/** Every non-computed STATIC member name in `body`, as written. Instance + * members don't count: TypeScript permits `ɵfac` and `static ɵfac` to + * coexist, and only a previously generated static is idempotent. */ function existingMemberNames(body: ClassBody): Set { const names = new Set() for (const element of body.body) { const member = element as { computed?: boolean + static?: boolean key?: { type: string; name?: string; value?: unknown } } - if (member.computed || member.key === undefined) { + if (member.static !== true || member.computed || member.key === undefined) { continue } if (member.key.type === 'Identifier' && member.key.name !== undefined) { @@ -189,7 +197,7 @@ function existingMemberNames(body: ClassBody): Set { function parseMembers( members: string, namespaceImports: Readonly>, - resolve: (specifier: string) => string, + paths: { resolve: (specifier: string) => string; emit: (specifier: string) => string }, ): { name: string | null; text: string; heads: MemberHead[] }[] | null { const prefix = 'declare class X {\n' const wrapped = prefix + members + '\n}' @@ -198,12 +206,18 @@ function parseMembers( return null } const cls = program.body[0] - if (cls.type !== 'ClassDeclaration') { + if (cls === undefined || cls.type !== 'ClassDeclaration') { return [] } // Collect every namespace head in one program-wide pass (the Visitor only // accepts a Program), binned into the member that contains it. - const collected: { pos: number; alias: string; module: string; resolved: string }[] = [] + const collected: { + pos: number + alias: string + module: string + emit: string + resolved: string + }[] = [] new Visitor({ TSQualifiedName(node) { if (node.left.type !== 'Identifier') { @@ -221,7 +235,8 @@ function parseMembers( pos: node.left.start - prefix.length, alias, module: specifier, - resolved: resolve(specifier), + emit: paths.emit(specifier), + resolved: paths.resolve(specifier), }) }, }).visit(program) @@ -250,6 +265,7 @@ function parseMembers( end: head.pos - start + head.alias.length, alias: head.alias, module: head.module, + emit: head.emit, resolved: head.resolved, })) return { name, text: members.slice(start, end), heads } @@ -299,6 +315,18 @@ function resolvePath(path: string): string { return out.join('/') } +/** The `./…` specifier `from`'s directory needs to reach `to`. */ +function relativeSpecifier(fromDir: string, to: string): string { + const from = fromDir.split('/').filter((s) => s !== '' && s !== '.') + const target = to.split('/').filter((s) => s !== '' && s !== '.') + let common = 0 + while (common < from.length && common < target.length && from[common] === target[common]) { + common += 1 + } + const rel = [...Array(from.length - common).fill('..'), ...target.slice(common)].join('/') + return rel.startsWith('..') ? rel : `./${rel}` +} + /** * Splice each declaration's static members into the matching class body in * `source`, and emit the namespace imports the injected members reference — @@ -311,15 +339,20 @@ function resolvePath(path: string): string { * import: the members' compiled alias is kept when free, otherwise * uniquified (`i0_1`, …). * - * The pass is idempotent: a member whose name already appears in the target - * class is skipped — structurally, on the member name, so a second pass - * that canonicalizes to a different alias can't inject the same member + * The pass is idempotent: a member whose STATIC name already appears in the + * target class is skipped — structurally, on the member name, so a second + * pass that canonicalizes to a different alias can't inject the same member * twice. A declaration whose class isn't found, whose members don't parse, * or a file the parser rejects is silently skipped. + * + * `outputFile` is the emitted asset's own path (as the caller knows it). + * Relative specifiers in `namespaceImports` are rebased to its directory so + * `"./dep"` compiled under `src/a/` resolves correctly from `dist/`. */ export function injectDtsDeclarations( source: string, declarations: readonly DtsClassDeclaration[], + outputFile?: string, ): string { if (declarations.length === 0) { return source @@ -334,21 +367,30 @@ export function injectDtsDeclarations( const existingImports = namespaceImportsOf(program) // Identity resolution is relative to each declaration's source module — - // the same "./dep" in two directories is two modules. Emitted specifiers - // stay verbatim (as the compiler recorded them); when `sourceFile` is - // known, relative specifiers are resolved for identity only. - const resolveFor = (sourceFile?: string) => { + // the same "./dep" in two directories is two modules. Emitted relative + // specifiers are rebased from the source module's directory to the + // output file's directory when both are known: `src/a/foo.ts` importing + // `"./dep"` lands in `dist/index.d.ts` as `"../src/a/dep"`. + const outDir = outputFile?.replace(/[^/\\]*$/, '') + const pathsFor = (sourceFile?: string) => { if (sourceFile === undefined) { - return (specifier: string) => specifier + const identity = (specifier: string) => specifier + return { resolve: identity, emit: identity } } const dir = sourceFile.replace(/[^/\\]*$/, '') - return (specifier: string) => + const isRelative = (specifier: string) => specifier.startsWith('./') || specifier.startsWith('../') - ? resolvePath(dir + specifier) - : specifier + return { + resolve: (specifier: string) => + isRelative(specifier) ? resolvePath(dir + specifier) : specifier, + emit: (specifier: string) => + isRelative(specifier) && outDir !== undefined + ? relativeSpecifier(outDir, resolvePath(dir + specifier)) + : specifier, + } } - // Canonical module identity → [rawSpecifier, alias] for this file. + // Canonical module identity → [emittedSpecifier, alias] for this file. const canonical = new Map() const canonicalAlias = (head: MemberHead): string => { const known = canonical.get(head.resolved) @@ -356,15 +398,16 @@ export function injectDtsDeclarations( return known[1] } const preferred = head.module === '@angular/core' ? 'i0' : head.alias - // Reuse an existing import only when it binds this alias AND specifier — - // same module under the same name (which is exactly what an idempotent - // re-run sees on its second pass), never a different module. + // Reuse an existing import only when it binds this alias AND the + // specifier we're about to emit — same module under the same name + // (which is exactly what an idempotent re-run sees on its second + // pass), never a different module. const existing = existingImports.get(preferred) const alias = - existing !== undefined && existing === head.module + existing !== undefined && existing === head.emit ? preferred : uniquifyIdentifier(preferred, fileNames) - canonical.set(head.resolved, [head.module, alias]) + canonical.set(head.resolved, [head.emit, alias]) fileNames.add(alias) return alias } @@ -375,7 +418,7 @@ export function injectDtsDeclarations( const members = parseMembers( declaration.members, declaration.namespaceImports ?? {}, - resolveFor(declaration.sourceFile), + pathsFor(declaration.sourceFile), ) if (members === null || members.length === 0) { continue From 0d2850baa28f6eac0e4db95a8274b41e7f15f73b Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 7 Oct 2026 22:37:17 +0800 Subject: [PATCH 4/8] fix(vite-plugin): emit dts relative specifiers verbatim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the previous commit: rebasing to the source tree was wrong — a published package ships dist/ only, and structure-preserving declaration emit (the dominant mode for vite-plugin-dts / rolldown-plugin-dts / tsdown) mirrors the source layout, so "./dep" compiled under src/a/ correctly reaches dist/a/dep.d.ts. Rebasing would have pointed imports at files outside the package. Relative specifiers are still resolved against sourceFile for identity dedup; under fully-bundled declaration output, two same-named relative modules cannot both resolve — documented as a dts-generator limitation. --- napi/angular-compiler/test/dts.test.ts | 93 ++++++------------- napi/angular-compiler/vite-plugin/index.ts | 14 ++- .../angular-compiler/vite-plugin/utils/dts.ts | 80 ++++++---------- 3 files changed, 61 insertions(+), 126 deletions(-) diff --git a/napi/angular-compiler/test/dts.test.ts b/napi/angular-compiler/test/dts.test.ts index 7162dea04..a88e5c6fe 100644 --- a/napi/angular-compiler/test/dts.test.ts +++ b/napi/angular-compiler/test/dts.test.ts @@ -298,75 +298,40 @@ describe('injectDtsDeclarations', () => { expect(out).toContain('typeof i1_1.O') }) - it('rebases relative specifiers to the output file directory', () => { - // `"./dep"` compiled under `src/a/` must not be emitted verbatim into - // `dist/index.d.ts` — from there it resolves to a module that does not - // exist. With `outputFile` known, the emitted specifier reaches back to - // the source-resolved target. + it('emits relative specifiers verbatim for structure-preserving emit', () => { + // ngtsc and structure-preserving declaration emit place the emitted + // `.d.ts` in the source's relative layout (`src/a/foo.ts` → + // `dist/a/foo.d.ts`), so `"./dep"` verbatim reaches `dist/a/dep.d.ts`. + // Rebasing to the source tree would point outside the published + // package; under fully-bundled declaration output two same-named + // relative modules can never both resolve regardless. const source = 'export declare class A {\n}\n' - const out = injectDtsDeclarations( - source, - [ - { - className: 'A', - members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', - namespaceImports: { i0: '@angular/core', i1: './dep' }, - sourceFile: '/repo/src/a/foo.ts', - }, - ], - 'dist/index.d.ts', - ) - expect(out).toContain('import * as i1 from "../repo/src/a/dep";') + const out = injectDtsDeclarations(source, [ + { + className: 'A', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i0: '@angular/core', i1: './dep' }, + sourceFile: '/repo/src/a/foo.ts', + }, + ]) + expect(out).toContain('import * as i1 from "./dep";') expect(out).toContain('import * as i0 from "@angular/core";') expect(out).toContain('typeof i1.D') }) - it('keeps distinct rebased specifiers for same-named relative modules', () => { - // Two source dirs, same raw specifier: two identities AND two emitted - // specifiers — `i1` → ../src/a/dep, `i1_1` → ../src/b/dep. - const source = 'export declare class A {\n}\nexport declare class B {\n}\n' - const out = injectDtsDeclarations( - source, - [ - { - className: 'A', - members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', - namespaceImports: { i0: '@angular/core', i1: './dep' }, - sourceFile: '/repo/src/a/foo.ts', - }, - { - className: 'B', - members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', - namespaceImports: { i0: '@angular/core', i1: './dep' }, - sourceFile: '/repo/src/b/foo.ts', - }, - ], - 'dist/index.d.ts', - ) - expect(out).toContain('import * as i1 from "../repo/src/a/dep";') - expect(out).toContain('import * as i1_1 from "../repo/src/b/dep";') - expect(out).toContain('typeof i1.D') - expect(out).toContain('typeof i1_1.O') - }) - - it('reuses an existing import when the rebased specifier matches', () => { - // Reuse compares the EMITTED specifier: an existing `import * as i1 - // from "../repo/src/a/dep"` already binds exactly what this member - // needs after rebasing, so no second import is added. - const source = 'import * as i1 from "../repo/src/a/dep";\nexport declare class A {\n}\n' - const out = injectDtsDeclarations( - source, - [ - { - className: 'A', - members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', - namespaceImports: { i0: '@angular/core', i1: './dep' }, - sourceFile: '/repo/src/a/foo.ts', - }, - ], - 'dist/index.d.ts', - ) - expect(out.match(/from "\.\.\/repo\/src\/a\/dep"/g)).toHaveLength(1) + it('reuses an existing import bound to the same verbatim specifier', () => { + // An existing `import * as i1 from "./dep"` already binds the specifier + // this member needs (verbatim emit), so no second import is added. + const source = 'import * as i1 from "./dep";\nexport declare class A {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'A', + members: 'static ɵmod: i0.ɵɵNgModuleDeclaration;', + namespaceImports: { i0: '@angular/core', i1: './dep' }, + sourceFile: '/repo/src/a/foo.ts', + }, + ]) + expect(out.match(/from "\.\/dep"/g)).toHaveLength(1) expect(out).toContain('typeof i1.D') }) diff --git a/napi/angular-compiler/vite-plugin/index.ts b/napi/angular-compiler/vite-plugin/index.ts index 75053340c..10972e2b4 100644 --- a/napi/angular-compiler/vite-plugin/index.ts +++ b/napi/angular-compiler/vite-plugin/index.ts @@ -1427,6 +1427,11 @@ export function angular(options: PluginOptions = {}): Plugin[] { name: '@oxc-angular/vite-dts', enforce: 'post', generateBundle(_outputOptions, bundle) { + // `injectDtsDeclarations` emits relative specifiers verbatim — the + // right behavior under structure-preserving declaration emit (the + // dominant mode), where dist mirrors src. Bundled declarations that + // collapse directories can't represent two same-named relative + // modules regardless; that is a dts-generator limitation. if (pluginOptions.compilationMode !== 'partial') return if (collectedDtsDeclarations.size === 0) return @@ -1462,14 +1467,7 @@ export function angular(options: PluginOptions = {}): Plugin[] { ? file.source : Buffer.from(file.source).toString('utf-8') - // The asset's own path lets `injectDtsDeclarations` rebase - // relative namespace specifiers ("./dep" collected under src/a/) - // so they resolve from wherever the .d.ts is emitted. - const outputFile = _outputOptions.dir - ? `${_outputOptions.dir}/${file.fileName}` - : _outputOptions.file - - const augmented = injectDtsDeclarations(source, declarations, outputFile) + const augmented = injectDtsDeclarations(source, declarations) if (augmented !== source) { file.source = augmented } diff --git a/napi/angular-compiler/vite-plugin/utils/dts.ts b/napi/angular-compiler/vite-plugin/utils/dts.ts index e60bdb61f..69f9dadc8 100644 --- a/napi/angular-compiler/vite-plugin/utils/dts.ts +++ b/napi/angular-compiler/vite-plugin/utils/dts.ts @@ -54,8 +54,9 @@ export interface DtsClassDeclaration { * for `typeof i1.SomeDirective` host-directive references, imported * ctor-dep types, and `ngAcceptInputType_*` transform types alike. * Relative specifiers (`"./dep"`) are resolved against `sourceFile` for - * identity and rebased to the output file's directory when `outputFile` - * is passed to `injectDtsDeclarations`. + * identity (so the same specifier under different directories is two + * modules) and emitted verbatim — structure-preserving declaration emit + * places `dist/a/foo.d.ts` next to `dist/a/dep.d.ts`, same as the source. */ namespaceImports?: Record /** @@ -74,12 +75,10 @@ interface MemberHead { end: number /** The head's alias as compiled (what `text.slice(start, end)` reads). */ alias: string - /** The module specifier the alias refers to, as recorded by the compiler. */ + /** The module specifier the alias refers to — emitted verbatim, matching + * ngtsc and structure-preserving declaration emit (`src/a/foo.ts` → + * `dist/a/foo.d.ts`, where `"./dep"` reaches `dist/a/dep.d.ts`). */ module: string - /** The specifier emitted in the import statement — rebased to the output - * file's directory when it is relative (`"./dep"` compiled under `src/a` - * becomes `"../src/a/dep"` in `dist/index.d.ts`). */ - emit: string /** The canonical identity for deduping — the specifier resolved against * the declaration's source file, so `"./dep"` under different directories * is two modules, not one. */ @@ -197,7 +196,7 @@ function existingMemberNames(body: ClassBody): Set { function parseMembers( members: string, namespaceImports: Readonly>, - paths: { resolve: (specifier: string) => string; emit: (specifier: string) => string }, + resolve: (specifier: string) => string, ): { name: string | null; text: string; heads: MemberHead[] }[] | null { const prefix = 'declare class X {\n' const wrapped = prefix + members + '\n}' @@ -215,7 +214,6 @@ function parseMembers( pos: number alias: string module: string - emit: string resolved: string }[] = [] new Visitor({ @@ -235,8 +233,7 @@ function parseMembers( pos: node.left.start - prefix.length, alias, module: specifier, - emit: paths.emit(specifier), - resolved: paths.resolve(specifier), + resolved: resolve(specifier), }) }, }).visit(program) @@ -265,7 +262,6 @@ function parseMembers( end: head.pos - start + head.alias.length, alias: head.alias, module: head.module, - emit: head.emit, resolved: head.resolved, })) return { name, text: members.slice(start, end), heads } @@ -315,18 +311,6 @@ function resolvePath(path: string): string { return out.join('/') } -/** The `./…` specifier `from`'s directory needs to reach `to`. */ -function relativeSpecifier(fromDir: string, to: string): string { - const from = fromDir.split('/').filter((s) => s !== '' && s !== '.') - const target = to.split('/').filter((s) => s !== '' && s !== '.') - let common = 0 - while (common < from.length && common < target.length && from[common] === target[common]) { - common += 1 - } - const rel = [...Array(from.length - common).fill('..'), ...target.slice(common)].join('/') - return rel.startsWith('..') ? rel : `./${rel}` -} - /** * Splice each declaration's static members into the matching class body in * `source`, and emit the namespace imports the injected members reference — @@ -344,15 +328,10 @@ function relativeSpecifier(fromDir: string, to: string): string { * pass that canonicalizes to a different alias can't inject the same member * twice. A declaration whose class isn't found, whose members don't parse, * or a file the parser rejects is silently skipped. - * - * `outputFile` is the emitted asset's own path (as the caller knows it). - * Relative specifiers in `namespaceImports` are rebased to its directory so - * `"./dep"` compiled under `src/a/` resolves correctly from `dist/`. */ export function injectDtsDeclarations( source: string, declarations: readonly DtsClassDeclaration[], - outputFile?: string, ): string { if (declarations.length === 0) { return source @@ -367,30 +346,24 @@ export function injectDtsDeclarations( const existingImports = namespaceImportsOf(program) // Identity resolution is relative to each declaration's source module — - // the same "./dep" in two directories is two modules. Emitted relative - // specifiers are rebased from the source module's directory to the - // output file's directory when both are known: `src/a/foo.ts` importing - // `"./dep"` lands in `dist/index.d.ts` as `"../src/a/dep"`. - const outDir = outputFile?.replace(/[^/\\]*$/, '') - const pathsFor = (sourceFile?: string) => { + // the same "./dep" in two directories is two modules. Emitted specifiers + // stay verbatim, matching ngtsc and structure-preserving declaration + // emit (`src/a/foo.ts` → `dist/a/foo.d.ts`, where "./dep" reaches + // `dist/a/dep.d.ts`); there is no dts-generator source→output mapping to + // rebase against, so under fully-bundled declaration output two same-named + // relative modules remain a documented limitation. + const resolveFor = (sourceFile?: string) => { if (sourceFile === undefined) { - const identity = (specifier: string) => specifier - return { resolve: identity, emit: identity } + return (specifier: string) => specifier } const dir = sourceFile.replace(/[^/\\]*$/, '') - const isRelative = (specifier: string) => + return (specifier: string) => specifier.startsWith('./') || specifier.startsWith('../') - return { - resolve: (specifier: string) => - isRelative(specifier) ? resolvePath(dir + specifier) : specifier, - emit: (specifier: string) => - isRelative(specifier) && outDir !== undefined - ? relativeSpecifier(outDir, resolvePath(dir + specifier)) - : specifier, - } + ? resolvePath(dir + specifier) + : specifier } - // Canonical module identity → [emittedSpecifier, alias] for this file. + // Canonical module identity → [rawSpecifier, alias] for this file. const canonical = new Map() const canonicalAlias = (head: MemberHead): string => { const known = canonical.get(head.resolved) @@ -398,16 +371,15 @@ export function injectDtsDeclarations( return known[1] } const preferred = head.module === '@angular/core' ? 'i0' : head.alias - // Reuse an existing import only when it binds this alias AND the - // specifier we're about to emit — same module under the same name - // (which is exactly what an idempotent re-run sees on its second - // pass), never a different module. + // Reuse an existing import only when it binds this alias AND specifier — + // same module under the same name (which is exactly what an idempotent + // re-run sees on its second pass), never a different module. const existing = existingImports.get(preferred) const alias = - existing !== undefined && existing === head.emit + existing !== undefined && existing === head.module ? preferred : uniquifyIdentifier(preferred, fileNames) - canonical.set(head.resolved, [head.emit, alias]) + canonical.set(head.resolved, [head.module, alias]) fileNames.add(alias) return alias } @@ -418,7 +390,7 @@ export function injectDtsDeclarations( const members = parseMembers( declaration.members, declaration.namespaceImports ?? {}, - pathsFor(declaration.sourceFile), + resolveFor(declaration.sourceFile), ) if (members === null || members.length === 0) { continue From 80de508d8f995de85941f9242acf860421b6bfe4 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 7 Oct 2026 23:02:37 +0800 Subject: [PATCH 5/8] refactor(vite-plugin): move .d.ts injection to Rust MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dts post-processing pass parsed the emitted declaration file with the JS oxc-parser package — a runtime dependency duplicating what the compiler already carries in Rust. injectDtsDeclarations is now a napi export of dts_inject::inject_dts_declarations in oxc_angular_compiler (SourceType::d_ts + oxc_ast_visit), and vite-plugin/utils/dts.ts is a thin typed wrapper. The oxc-parser dependency is removed from napi/angular-compiler (the e2e/compare harness keeps its own copy). Same contract as before: verbatim specifier emission, resolved-identity dedup, static-only idempotency, multiline-comment-aware import insertion. --- crates/oxc_angular_compiler/src/dts_inject.rs | 601 ++++++++++++++++++ crates/oxc_angular_compiler/src/lib.rs | 4 + napi/angular-compiler/index.d.ts | 28 + napi/angular-compiler/index.js | 2 + napi/angular-compiler/package.json | 3 +- napi/angular-compiler/src/lib.rs | 36 ++ .../angular-compiler/vite-plugin/utils/dts.ts | 428 +------------ pnpm-lock.yaml | 3 - 8 files changed, 683 insertions(+), 422 deletions(-) create mode 100644 crates/oxc_angular_compiler/src/dts_inject.rs diff --git a/crates/oxc_angular_compiler/src/dts_inject.rs b/crates/oxc_angular_compiler/src/dts_inject.rs new file mode 100644 index 000000000..312fe8ec4 --- /dev/null +++ b/crates/oxc_angular_compiler/src/dts_inject.rs @@ -0,0 +1,601 @@ +//! Inject Angular's Ivy `.d.ts` type declarations into emitted declaration +//! files for library builds. +//! +//! The compiler returns, per Angular class, the static member type +//! declarations that should live in the class's `.d.ts` body — e.g. +//! `static ɵcmp: i0.ɵɵComponentDeclaration<…>;`. Those members are what +//! Angular's template type-checker reads from a pre-compiled library, and +//! they mirror what ngtsc's `IvyDeclarationDtsTransform` would have +//! written. +//! +//! Vite/Rolldown don't emit `.d.ts` themselves — a separate declaration +//! generator (rolldown-plugin-dts, vite-plugin-dts, tsdown, `tsc`) produces +//! the base declarations. This is a post-processing pass that splices the +//! Angular members into those already-generated `.d.ts`, and emits the +//! namespace imports the members reference. +//! +//! ngtsc never reuses an existing namespace import in declaration emit — +//! its `ImportManager` (`presetImportManagerForceNamespaceImports`) mints +//! `i0`, `i0_1`, … deduped against the ORIGINAL source file's identifiers. +//! The compiler mirrors that: each declaration's members carry the alias +//! the per-source-file registry picked, and `namespaceImports` records +//! which module each alias stands for. A bundled `.d.ts` merges +//! declarations from many source files whose aliases can disagree (`i1` → +//! `./dep` in one, `./other` in another), so this pass canonicalizes: one +//! alias per module identity, collision-free against every identifier in +//! the emitted file. + +use rustc_hash::{FxHashMap, FxHashSet}; + +use oxc_allocator::Allocator; +use oxc_ast::ast::{ + ClassBody, ClassElement, Declaration, Expression, ImportDeclarationSpecifier, Program, + PropertyKey, Statement, TSNamespaceDeclarationBody, TSQualifiedName, TSTypeName, +}; +use oxc_ast_visit::{Visit, walk}; +use oxc_parser::Parser; +use oxc_span::{GetSpan, SourceType, Span}; + +/// One class's `.d.ts` static member declarations. +pub struct DtsInjectDeclaration { + /// The class the members belong to. + pub class_name: String, + /// Newline-separated `static …;` member declarations. + /// Namespace-qualified type names (`i0.ɵɵComponentDeclaration`, + /// `typeof i1.SomeDirective`) use the alias the compiler picked for the + /// member's module in that source file; `namespace_imports` maps each + /// such alias to its module specifier. + pub members: String, + /// alias → module specifier for every namespace the members reference: + /// `i0` → `"@angular/core"` for `i0.ɵɵX`/`i0.Signal`, `i1` → `"./dep"` + /// for `typeof i1.SomeDirective` host-directive references, imported + /// ctor-dep types, and `ngAcceptInputType_*` transform types alike. + /// Relative specifiers (`"./dep"`) are resolved against `source_file` + /// for identity (so the same specifier under different directories is + /// two modules) and emitted verbatim — structure-preserving + /// declaration emit places `dist/a/foo.d.ts` next to `dist/a/dep.d.ts`, + /// same as the source. + pub namespace_imports: FxHashMap, + /// The source module the declaration was compiled from (the + /// transform's module id). Used to resolve relative specifiers in + /// `namespace_imports` so two files in different directories that each + /// import `"./dep"` canonicalize to different aliases instead of + /// silently sharing one. + pub source_file: Option, +} + +/// One namespace-alias head found in a member body. +struct MemberHead { + /// `(start, end)` of the head inside the member's own text. + start: usize, + end: usize, + /// The head's alias as compiled (what `member[start..end]` reads). + alias: String, + /// The module specifier the alias refers to — emitted verbatim, + /// matching ngtsc and structure-preserving declaration emit. + module: String, + /// The canonical identity for deduping — the specifier resolved against + /// the declaration's source file, so `"./dep"` under different + /// directories is two modules, not one. + resolved: String, +} + +/// `i0`, `i0_1`, `i0_2`, … — the first name not colliding with `names`. +/// Matches `check_unique_identifier_name` in ngtsc's ImportManager. +fn uniquify_identifier(base: &str, names: &FxHashSet) -> String { + if !names.contains(base) { + return base.to_string(); + } + let mut counter = 1; + while names.contains(&format!("{base}_{counter}")) { + counter += 1; + } + format!("{base}_{counter}") +} + +/// The alias → specifier map of every `import * as ns` the file already +/// has. +fn namespace_imports_of(program: &Program<'_>) -> FxHashMap { + let mut aliases = FxHashMap::default(); + for stmt in &program.body { + let Statement::ImportDeclaration(decl) = stmt else { continue }; + let Some(specifiers) = &decl.specifiers else { continue }; + for spec in specifiers { + if let ImportDeclarationSpecifier::ImportNamespaceSpecifier(ns) = spec { + aliases.insert(ns.local.name.to_string(), decl.source.value.to_string()); + } + } + } + aliases +} + +/// Every identifier name in the file — the same set the JS `Identifier` +/// visitor collected (`IdentifierName`, `IdentifierReference`, +/// `BindingIdentifier`, `LabelIdentifier`). +struct IdentifierCollector<'a> { + names: &'a mut FxHashSet, +} + +impl<'a> Visit<'a> for IdentifierCollector<'_> { + fn visit_identifier_name(&mut self, it: &oxc_ast::ast::IdentifierName<'a>) { + self.names.insert(it.name.to_string()); + walk::walk_identifier_name(self, it); + } + + fn visit_identifier_reference(&mut self, it: &oxc_ast::ast::IdentifierReference<'a>) { + self.names.insert(it.name.to_string()); + walk::walk_identifier_reference(self, it); + } + + fn visit_binding_identifier(&mut self, it: &oxc_ast::ast::BindingIdentifier<'a>) { + self.names.insert(it.name.to_string()); + walk::walk_binding_identifier(self, it); + } + + fn visit_label_identifier(&mut self, it: &oxc_ast::ast::LabelIdentifier<'a>) { + self.names.insert(it.name.to_string()); + walk::walk_label_identifier(self, it); + } +} + +fn collect_identifiers(program: &Program<'_>) -> FxHashSet { + let mut names = FxHashSet::default(); + let mut collector = IdentifierCollector { names: &mut names }; + collector.visit_program(program); + names +} + +/// The `ClassBody` of the class named `class_name`, searching through +/// export wrappers and ambient module blocks (the latter covers +/// `declare module` declarations a bundler may wrap library classes in). +fn find_class_body<'a, 'b>( + statements: &'b [Statement<'a>], + class_name: &str, +) -> Option<&'b ClassBody<'a>> { + fn from_namespace<'a, 'b>( + ns: &'b oxc_ast::ast::TSNamespaceDeclaration<'a>, + class_name: &str, + ) -> Option<&'b ClassBody<'a>> { + match &ns.body { + TSNamespaceDeclarationBody::TSModuleBlock(block) => { + find_class_body(&block.body, class_name) + } + TSNamespaceDeclarationBody::TSNamespaceDeclaration(inner) => { + from_namespace(inner, class_name) + } + } + } + + fn from_declaration<'a, 'b>( + decl: &'b Declaration<'a>, + class_name: &str, + ) -> Option<&'b ClassBody<'a>> { + match decl { + Declaration::ClassDeclaration(class) => { + if class.id.as_ref().map(|id| id.name.as_str()) == Some(class_name) { + Some(&*class.body) + } else { + None + } + } + // `declare module "pkg" { export class … }` + Declaration::TSExternalModuleDeclaration(module) => { + module.body.as_deref().and_then(|block| find_class_body(&block.body, class_name)) + } + Declaration::TSNamespaceDeclaration(ns) => from_namespace(ns, class_name), + Declaration::TSGlobalDeclaration(global) => { + find_class_body(&global.body.body, class_name) + } + _ => None, + } + } + + for stmt in statements { + let found = match stmt { + // `export declare class` — oxc splits export-with-declaration + // into `ExportDeclaration`, while `export { a }` is + // `ExportNamedDeclaration` (which never wraps a declaration). + Statement::ExportDeclaration(export) => { + from_declaration(&export.declaration, class_name) + } + Statement::ExportDefaultDeclaration(export) => match &export.declaration { + oxc_ast::ast::ExportDefaultDeclarationKind::ClassDeclaration(class) + if class.id.as_ref().map(|id| id.name.as_str()) == Some(class_name) => + { + Some(&*class.body) + } + _ => None, + }, + other => other.as_declaration().and_then(|decl| from_declaration(decl, class_name)), + }; + if found.is_some() { + return found; + } + } + None +} + +/// The member's name as written, if it is a non-computed static or string +/// literal key. +fn property_key_name(key: &PropertyKey<'_>) -> Option { + match key { + PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()), + _ => match key.as_expression() { + Some(Expression::StringLiteral(lit)) => Some(lit.value.to_string()), + Some(Expression::NumericLiteral(lit)) => Some(lit.value.to_string()), + _ => None, + }, + } +} + +fn class_element_span(element: &ClassElement<'_>) -> Span { + match element { + ClassElement::StaticBlock(el) => el.span, + ClassElement::MethodDefinition(el) => el.span, + ClassElement::PropertyDefinition(el) => el.span, + ClassElement::AccessorProperty(el) => el.span, + ClassElement::TSIndexSignature(el) => el.span, + } +} + +fn class_element_parts<'a, 'b>( + element: &'b ClassElement<'a>, +) -> (bool, bool, Option<&'b PropertyKey<'a>>) { + match element { + ClassElement::StaticBlock(_) | ClassElement::TSIndexSignature(_) => (false, false, None), + ClassElement::MethodDefinition(el) => (el.r#static, el.computed, Some(&el.key)), + ClassElement::PropertyDefinition(el) => (el.r#static, el.computed, Some(&el.key)), + ClassElement::AccessorProperty(el) => (el.r#static, el.computed, Some(&el.key)), + } +} + +/// Every non-computed STATIC member name in `body`, as written. Instance +/// members don't count: TypeScript permits `ɵfac` and `static ɵfac` to +/// coexist, and only a previously generated static is idempotent. +fn existing_member_names(body: &ClassBody<'_>) -> FxHashSet { + let mut names = FxHashSet::default(); + for element in &body.body { + let (is_static, computed, key) = class_element_parts(element); + if !is_static || computed { + continue; + } + if let Some(Some(name)) = key.map(property_key_name) { + names.insert(name); + } + } + names +} + +/// One parsed member: its name (if a static identifier/literal key), its +/// exact text slice, and its namespace heads. +struct ParsedMember { + name: Option, + text: String, + heads: Vec, +} + +/// The `TSQualifiedName` heads inside `members`, collected in one pass over +/// the wrapped program (mirroring the JS Visitor's always-descend +/// semantics: children are walked even after a head is recorded). +struct HeadCollector<'ns, 'r> { + namespace_imports: &'ns FxHashMap, + resolve: &'r dyn Fn(&str) -> String, + offset: usize, + collected: Vec<(usize, String, String, String)>, +} + +impl<'a> Visit<'a> for HeadCollector<'_, '_> { + fn visit_ts_qualified_name(&mut self, node: &TSQualifiedName<'a>) { + if let TSTypeName::IdentifierReference(left) = &node.left { + let alias = left.name.to_string(); + let module = self.namespace_imports.get(&alias); + if module.is_some() || node.right.name.starts_with('ɵ') { + let specifier = module.cloned().unwrap_or_else(|| "@angular/core".to_string()); + self.collected.push(( + left.span.start as usize - self.offset, + alias, + specifier.clone(), + (self.resolve)(&specifier), + )); + } + } + // Always descend: `a.b.c` also visits `a.b`, whose head is `a` — + // dedup happens implicitly because only the innermost node has an + // identifier `left`. + walk::walk_ts_qualified_name(self, node); + } +} + +/// Parse `members` (wrapped in a dummy `declare class`) into per-member +/// entries. Each head is the leftmost identifier of a `TSQualifiedName` +/// (`i0.ɵɵX` and `typeof i1.SomeDirective` alike — a nested `a.b.c` chain +/// is visited as its inner `a.b` too, so the head is always found). +/// +/// Returns `None` when the member text doesn't parse; the caller skips the +/// declaration rather than splice members whose aliases might be wrong. +fn parse_members( + members: &str, + namespace_imports: &FxHashMap, + resolve: &dyn Fn(&str) -> String, +) -> Option> { + const PREFIX: &str = "declare class X {\n"; + let wrapped = format!("{PREFIX}{members}\n}}"); + + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, &wrapped, SourceType::d_ts()).parse(); + if !ret.diagnostics.is_empty() { + return None; + } + let Some(Statement::ClassDeclaration(cls)) = ret.program.body.first() else { + return Some(Vec::new()); + }; + + let mut collector = + HeadCollector { namespace_imports, resolve, offset: PREFIX.len(), collected: Vec::new() }; + collector.visit_program(&ret.program); + let collected = collector.collected; + + let parsed: Vec = cls + .body + .body + .iter() + .map(|element| { + // Spans are relative to `wrapped`; shift them back into `members`. + let span = class_element_span(element); + let start = span.start as usize - PREFIX.len(); + let end = span.end as usize - PREFIX.len(); + let (_, computed, key) = class_element_parts(element); + let name = if computed { None } else { key.and_then(property_key_name) }; + let heads: Vec = collected + .iter() + .filter(|(pos, ..)| *pos >= start && *pos < end) + .map(|(pos, alias, module, resolved)| MemberHead { + start: pos - start, + end: pos - start + alias.len(), + alias: alias.clone(), + module: module.clone(), + resolved: resolved.clone(), + }) + .collect(); + ParsedMember { + name, + text: members.get(start..end).unwrap_or_default().to_string(), + heads, + } + }) + .collect(); + Some(parsed) +} + +/// The position just past `position`'s line, skipping newlines that fall +/// inside comments — a multiline trailing comment can't split the +/// insertion. +fn end_of_line_after(source: &str, position: usize, comment_spans: &[(usize, usize)]) -> usize { + let bytes = source.as_bytes(); + let mut cursor = position; + loop { + let newline = bytes[cursor..].iter().position(|&b| b == b'\n').map(|i| cursor + i); + let end = newline.unwrap_or(source.len()); + let covering = comment_spans.iter().find(|(start, cend)| *start <= end && end < *cend); + match covering { + None => return newline.map_or(source.len(), |n| n + 1), + Some(&(_, cend)) => cursor = cend, + } + } +} + +/// Resolve `dir + specifier` the way Node would see it — collapsing `..` +/// and `.` segments — so `"./dep"` under different directories +/// canonicalizes to different identities while `../dep` from a nested file +/// resolves back to the same one. Purely textual; no filesystem access. +fn resolve_path(path: &str) -> String { + let mut out: Vec<&str> = Vec::new(); + for segment in path.split('/') { + if segment.is_empty() || segment == "." { + continue; + } + if segment == ".." { + out.pop(); + } else { + out.push(segment); + } + } + out.join("/") +} + +/// Splice each declaration's static members into the matching class body. +/// +/// Emits the namespace imports the injected members reference — one +/// `import * as ns from "module"` per module identity, canonicalized +/// across all injected declarations. +/// +/// Like ngtsc's declaration ImportManager, an existing `import * as ns` is +/// reused only when it already binds the same alias and specifier — never +/// for a different one, and never to borrow the user's `@angular/core` +/// import: the members' compiled alias is kept when free, otherwise +/// uniquified (`i0_1`, …). +/// +/// The pass is idempotent: a member whose STATIC name already appears in +/// the target class is skipped — structurally, on the member name, so a +/// second pass that canonicalizes to a different alias can't inject the +/// same member twice. A declaration whose class isn't found, whose members +/// don't parse, or a file the parser rejects is silently skipped. +pub fn inject_dts_declarations(source: &str, declarations: &[DtsInjectDeclaration]) -> String { + if declarations.is_empty() { + return source.to_string(); + } + + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source, SourceType::d_ts()).parse(); + if !ret.diagnostics.is_empty() { + return source.to_string(); + } + let program = &ret.program; + + let mut file_names = collect_identifiers(program); + let existing_imports = namespace_imports_of(program); + + // Identity resolution is relative to each declaration's source module — + // the same "./dep" in two directories is two modules. Emitted + // specifiers stay verbatim, matching ngtsc and structure-preserving + // declaration emit (`src/a/foo.ts` → `dist/a/foo.d.ts`, where "./dep" + // reaches `dist/a/dep.d.ts`); there is no dts-generator source→output + // mapping to rebase against, so under fully-bundled declaration output + // two same-named relative modules remain a documented limitation. + let resolve_for = |source_file: Option<&str>| -> Box String> { + match source_file { + None => Box::new(|specifier: &str| specifier.to_string()), + Some(file) => { + let dir_end = file.rfind(['/', '\\']).map_or(0, |i| i + 1); + let dir = file[..dir_end].to_string(); + Box::new(move |specifier: &str| { + if specifier.starts_with("./") || specifier.starts_with("../") { + resolve_path(&format!("{dir}{specifier}")) + } else { + specifier.to_string() + } + }) + } + } + }; + + // Canonical module identity → [rawSpecifier, alias] for this file. + let mut canonical: FxHashMap = FxHashMap::default(); + let mut canonical_alias = |head: &MemberHead| -> String { + if let Some((_, alias)) = canonical.get(&head.resolved) { + return alias.clone(); + } + let preferred = if head.module == "@angular/core" { "i0" } else { head.alias.as_str() }; + // Reuse an existing import only when it binds this alias AND + // specifier — same module under the same name (which is exactly + // what an idempotent re-run sees on its second pass), never a + // different module. + let alias = match existing_imports.get(preferred) { + Some(existing) if existing == &head.module => preferred.to_string(), + _ => uniquify_identifier(preferred, &file_names), + }; + canonical.insert(head.resolved.clone(), (head.module.clone(), alias.clone())); + file_names.insert(alias.clone()); + alias + }; + + let mut splices: Vec<(usize, usize, String)> = Vec::new(); + + for declaration in declarations { + let resolve = resolve_for(declaration.source_file.as_deref()); + let Some(members) = + parse_members(&declaration.members, &declaration.namespace_imports, &*resolve) + else { + continue; + }; + if members.is_empty() { + continue; + } + + let Some(body) = find_class_body(&program.body, &declaration.class_name) else { + continue; + }; + let existing = existing_member_names(body); + + // Per-member idempotency: skip members the class already declares. + // Names — not normalized text — decide, so a re-run whose + // canonical alias differs (the first pass's import now occupies + // `i0`) doesn't inject the same `static ɵfac` a second time. + let mut lines: Vec = Vec::new(); + for member in &members { + if let Some(name) = &member.name + && existing.contains(name) + { + continue; + } + let mut text = member.text.clone(); + let mut heads: Vec<&MemberHead> = member.heads.iter().collect(); + heads.sort_by_key(|head| std::cmp::Reverse(head.start)); + for head in heads { + let alias = canonical_alias(head); + text.replace_range(head.start..head.end, &alias); + } + for line in text.split('\n') { + let trimmed = line.trim(); + if !trimmed.is_empty() { + lines.push(trimmed.to_string()); + } + } + } + if lines.is_empty() { + continue; + } + + // `ClassBody.span.end` is just past the closing brace; inserting + // before it appends the members, matching upstream's + // `[...members, ...newMembers]` (declaration.ts) — a class that + // already has members keeps them first. + let insert_at = body.span.end as usize - 1; + let needs_leading_nl = insert_at > 0 && source.as_bytes()[insert_at - 1] != b'\n'; + let text = format!( + "{}{}\n", + if needs_leading_nl { "\n" } else { "" }, + lines.iter().map(|line| format!(" {line}")).collect::>().join("\n") + ); + splices.push((insert_at, insert_at, text)); + } + + if splices.is_empty() { + return source.to_string(); + } + + // Emit an import for every canonical module the file doesn't already + // have, sorted by alias — the same order the compiler's own namespace + // registry generates them in. + let mut import_entries: Vec<(&String, &String)> = canonical + .values() + .filter(|(specifier, alias)| existing_imports.get(alias.as_str()) != Some(specifier)) + .map(|(specifier, alias)| (alias, specifier)) + .collect(); + import_entries.sort(); + let import_lines: Vec = import_entries + .iter() + .map(|(alias, specifier)| format!("import * as {alias} from \"{specifier}\";")) + .collect(); + + if !import_lines.is_empty() { + let text = import_lines.join("\n"); + let last_import_end = program.body.iter().rev().find_map(|stmt| match stmt { + Statement::ImportDeclaration(decl) => Some(decl.span.end as usize), + _ => None, + }); + if let Some(end) = last_import_end { + // End of the last import's line, skipping multiline trailing + // comments (`import x from "m"; /* keep\nme */`) so the comment + // keeps its line and the new import lands on the next one. + let comment_spans: Vec<(usize, usize)> = program + .comments + .iter() + .map(|c| (c.span.start as usize, c.span.end as usize)) + .collect(); + let position = end_of_line_after(source, end, &comment_spans); + splices.push((position, position, format!("{text}\n"))); + } else if let Some(first) = program.body.first() { + // Before the first statement keeps leading comments and + // triple-slash reference directives at the top of the file. + let start = first.span().start as usize; + splices.push((start, start, format!("{text}\n"))); + } else { + splices.push(( + source.len(), + source.len(), + format!( + "{}{text}\n", + if source.ends_with('\n') || source.is_empty() { "" } else { "\n" } + ), + )); + } + } + + let mut output = source.to_string(); + splices.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start)); + for (start, end, text) in splices { + output.replace_range(start..end, &text); + } + output +} diff --git a/crates/oxc_angular_compiler/src/lib.rs b/crates/oxc_angular_compiler/src/lib.rs index e8b13c627..00d756263 100644 --- a/crates/oxc_angular_compiler/src/lib.rs +++ b/crates/oxc_angular_compiler/src/lib.rs @@ -33,6 +33,7 @@ pub mod compilation_mode; pub mod component; pub mod directive; pub mod dts; +pub mod dts_inject; pub mod factory; pub mod hmr; pub mod i18n; @@ -166,6 +167,9 @@ pub use dts::{ generate_ng_module_dts, generate_pipe_dts, }; +// Re-export the .d.ts injection pass used by bundler plugins +pub use dts_inject::{DtsInjectDeclaration, inject_dts_declarations}; + // Re-export linker types pub use linker::{LinkResult, link}; diff --git a/napi/angular-compiler/index.d.ts b/napi/angular-compiler/index.d.ts index 2b2989a62..52666dc58 100644 --- a/napi/angular-compiler/index.d.ts +++ b/napi/angular-compiler/index.d.ts @@ -221,6 +221,21 @@ export interface DtsDeclaration { namespaceImports: Record } +/** One class's `.d.ts` declarations for [`inject_dts_declarations`]. */ +export interface DtsInjectDeclaration { + /** The class the members belong to. */ + className: string + /** Newline-separated `static …;` member declarations. */ + members: string + /** alias → module specifier for every namespace the members reference. */ + namespaceImports?: Record + /** + * The source module the declaration was compiled from; used to resolve + * relative specifiers in `namespace_imports` for identity. + */ + sourceFile?: string +} + /** * Encapsulate CSS styles for a component using attribute selectors. * @@ -559,6 +574,19 @@ export interface HostMetadataInput { styleAttr?: string } +/** + * Inject Ivy `.d.ts` member declarations into an emitted declaration file. + * + * Splices each declaration's `static` members into the matching class body + * and emits the namespace imports they reference, canonicalized one alias + * per module identity. See `dts_inject` in `oxc_angular_compiler` for the + * full contract. + */ +export declare function injectDtsDeclarations( + source: string, + declarations: Array, +): string + /** * Input for compiling an injector. * diff --git a/napi/angular-compiler/index.js b/napi/angular-compiler/index.js index 96b5ce828..7977f9a62 100644 --- a/napi/angular-compiler/index.js +++ b/napi/angular-compiler/index.js @@ -919,6 +919,7 @@ const { extractComponentUrls, generateHmrModule, generateStyleModule, + injectDtsDeclarations, linkAngularPackage, linkAngularPackageSync, optimizeAngularPackage, @@ -940,6 +941,7 @@ export { extractComponentMetadataSync } export { extractComponentUrls } export { generateHmrModule } export { generateStyleModule } +export { injectDtsDeclarations } export { linkAngularPackage } export { linkAngularPackageSync } export { optimizeAngularPackage } diff --git a/napi/angular-compiler/package.json b/napi/angular-compiler/package.json index 8842df2b4..4e0740f9d 100644 --- a/napi/angular-compiler/package.json +++ b/napi/angular-compiler/package.json @@ -65,8 +65,7 @@ "prepublishOnly": "napi pre-publish -t npm" }, "dependencies": { - "obug": "^3.0.0", - "oxc-parser": "^0.153.0" + "obug": "^3.0.0" }, "devDependencies": { "@angular/compiler": "^22.0.0-rc.2", diff --git a/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs index a3f80b74f..32930eec3 100644 --- a/napi/angular-compiler/src/lib.rs +++ b/napi/angular-compiler/src/lib.rs @@ -403,6 +403,42 @@ pub struct TransformResult { pub dts_declarations: Vec, } +/// One class's `.d.ts` declarations for [`inject_dts_declarations`]. +#[derive(Default)] +#[napi(object)] +pub struct DtsInjectDeclaration { + /// The class the members belong to. + pub class_name: String, + /// Newline-separated `static …;` member declarations. + pub members: String, + /// alias → module specifier for every namespace the members reference. + #[napi(ts_type = "Record")] + pub namespace_imports: Option>, + /// The source module the declaration was compiled from; used to resolve + /// relative specifiers in `namespace_imports` for identity. + pub source_file: Option, +} + +/// Inject Ivy `.d.ts` member declarations into an emitted declaration file. +/// +/// Splices each declaration's `static` members into the matching class body +/// and emits the namespace imports they reference, canonicalized one alias +/// per module identity. See `dts_inject` in `oxc_angular_compiler` for the +/// full contract. +#[napi] +pub fn inject_dts_declarations(source: String, declarations: Vec) -> String { + let declarations: Vec = declarations + .into_iter() + .map(|d| oxc_angular_compiler::DtsInjectDeclaration { + class_name: d.class_name, + members: d.members, + namespace_imports: d.namespace_imports.unwrap_or_default().into_iter().collect(), + source_file: d.source_file, + }) + .collect(); + oxc_angular_compiler::inject_dts_declarations(&source, &declarations) +} + /// Compile an Angular template to JavaScript. /// /// This compiles a template string to a template function that can be diff --git a/napi/angular-compiler/vite-plugin/utils/dts.ts b/napi/angular-compiler/vite-plugin/utils/dts.ts index 69f9dadc8..061f7e8de 100644 --- a/napi/angular-compiler/vite-plugin/utils/dts.ts +++ b/napi/angular-compiler/vite-plugin/utils/dts.ts @@ -2,6 +2,11 @@ * Inject Angular's Ivy `.d.ts` type declarations into emitted declaration * files for library builds. * + * The implementation lives in Rust (`dts_inject` in + * `oxc_angular_compiler`, exported over napi as `injectDtsDeclarations`): + * the compiler already carries oxc, so the emitted `.d.ts` is parsed with + * the same engine rather than a JS-side parser dependency. + * * The Rust compiler returns, per Angular class, the static member type * declarations that should live in the class's `.d.ts` body — e.g. * `static ɵcmp: i0.ɵɵComponentDeclaration<…>;`. Those members are what @@ -21,21 +26,10 @@ * per-source-file registry picked, and `namespaceImports` records which * module each alias stands for. A bundled `.d.ts` merges declarations from * many source files whose aliases can disagree (`i1` → `./dep` in one, - * `./other` in another), so this pass canonicalizes: one alias per module + * `./other` in another), so the pass canonicalizes: one alias per module * identity, collision-free against every identifier in the emitted file. - * The emitted `.d.ts` is parsed once with the oxc parser; class bodies are - * located through the AST (so type-parameter constraints containing `{` are - * no issue) and member alias heads are rewritten by splicing the - * `TSQualifiedName` leftmost identifier the parser reports. */ -import { - parseSync, - Visitor, - type ClassBody, - type Directive, - type Program, - type Statement, -} from 'oxc-parser' +import { injectDtsDeclarations as injectDtsDeclarationsNative } from '#binding' /** A single class's `.d.ts` static member declarations. */ export interface DtsClassDeclaration { @@ -68,261 +62,12 @@ export interface DtsClassDeclaration { sourceFile?: string } -/** One namespace-alias head found in a member body. */ -interface MemberHead { - /** `(start, end)` of the head inside the member's own text. */ - start: number - end: number - /** The head's alias as compiled (what `text.slice(start, end)` reads). */ - alias: string - /** The module specifier the alias refers to — emitted verbatim, matching - * ngtsc and structure-preserving declaration emit (`src/a/foo.ts` → - * `dist/a/foo.d.ts`, where `"./dep"` reaches `dist/a/dep.d.ts`). */ - module: string - /** The canonical identity for deduping — the specifier resolved against - * the declaration's source file, so `"./dep"` under different directories - * is two modules, not one. */ - resolved: string -} - -/** - * The alias → specifier map of every `import * as ns` the file already has. - */ -function namespaceImportsOf(program: Program): Map { - const aliases = new Map() - for (const stmt of program.body) { - if (stmt.type !== 'ImportDeclaration') { - continue - } - for (const spec of stmt.specifiers) { - if (spec.type === 'ImportNamespaceSpecifier') { - aliases.set(spec.local.name, String(stmt.source.value)) - } - } - } - return aliases -} - -/** Every identifier name in the file (`Identifier` covers all flavors). */ -function collectIdentifiers(program: Program): Set { - const names = new Set() - new Visitor({ - Identifier(node) { - names.add(node.name) - }, - }).visit(program) - return names -} - -/** - * `i0`, `i0_1`, `i0_2`, … — the first name not colliding with `names`. - * Matches `check_unique_identifier_name` in ngtsc's ImportManager. - */ -function uniquifyIdentifier(base: string, names: Set): string { - if (!names.has(base)) { - return base - } - let counter = 1 - while (names.has(`${base}_${counter}`)) { - counter += 1 - } - return `${base}_${counter}` -} - -/** - * The `ClassBody` of the class named `className`, searching through export - * wrappers and ambient module blocks (the latter covers `declare module` - * declarations a bundler may wrap library classes in). - */ -function findClassBody( - statements: ReadonlyArray, - className: string, -): ClassBody | null { - for (const stmt of statements) { - // `export declare class`, `export default class`, `export declare module`. - const node = - stmt.type === 'ExportNamedDeclaration' || stmt.type === 'ExportDefaultDeclaration' - ? stmt.declaration - : stmt - if (node === null) { - continue - } - if (node.type === 'ClassDeclaration' && node.id?.name === className) { - return node.body - } - if (node.type === 'TSModuleDeclaration' && node.body?.type === 'TSModuleBlock') { - const found = findClassBody(node.body.body, className) - if (found !== null) { - return found - } - } - } - return null -} - -/** Every non-computed STATIC member name in `body`, as written. Instance - * members don't count: TypeScript permits `ɵfac` and `static ɵfac` to - * coexist, and only a previously generated static is idempotent. */ -function existingMemberNames(body: ClassBody): Set { - const names = new Set() - for (const element of body.body) { - const member = element as { - computed?: boolean - static?: boolean - key?: { type: string; name?: string; value?: unknown } - } - if (member.static !== true || member.computed || member.key === undefined) { - continue - } - if (member.key.type === 'Identifier' && member.key.name !== undefined) { - names.add(member.key.name) - } else if (member.key.type === 'Literal') { - names.add(String(member.key.value)) - } - } - return names -} - -/** - * Parse `members` (wrapped in a dummy `declare class`) into per-member - * entries: the member's name, its exact text slice, and its namespace heads. - * Each head is the leftmost `Identifier` of a `TSQualifiedName` (`i0.ɵɵX` - * and `typeof i1.SomeDirective` alike — a nested `a.b.c` chain is visited as - * its inner `a.b` too, so the head is always found). - * - * Returns `null` when the member text doesn't parse; the caller skips the - * declaration rather than splice members whose aliases might be wrong. - */ -function parseMembers( - members: string, - namespaceImports: Readonly>, - resolve: (specifier: string) => string, -): { name: string | null; text: string; heads: MemberHead[] }[] | null { - const prefix = 'declare class X {\n' - const wrapped = prefix + members + '\n}' - const { program, errors } = parseSync('members.d.ts', wrapped, { lang: 'dts' }) - if (errors.length > 0) { - return null - } - const cls = program.body[0] - if (cls === undefined || cls.type !== 'ClassDeclaration') { - return [] - } - // Collect every namespace head in one program-wide pass (the Visitor only - // accepts a Program), binned into the member that contains it. - const collected: { - pos: number - alias: string - module: string - resolved: string - }[] = [] - new Visitor({ - TSQualifiedName(node) { - if (node.left.type !== 'Identifier') { - return - } - const alias = node.left.name - const module = namespaceImports[alias] - if (module === undefined && !node.right.name.startsWith('ɵ')) { - // Not a generated namespace reference — resolve against whatever - // the `.d.ts` already imports and leave the head untouched. - return - } - const specifier = module ?? '@angular/core' - collected.push({ - pos: node.left.start - prefix.length, - alias, - module: specifier, - resolved: resolve(specifier), - }) - }, - }).visit(program) - - return cls.body.body.map((element) => { - // Spans are relative to `wrapped`; shift them back into `members`. - const start = element.start - prefix.length - const end = element.end - prefix.length - const member = element as { - computed?: boolean - key?: { type: string; name?: string; value?: unknown } - } - const name = member.computed - ? null - : member.key === undefined - ? null - : member.key.type === 'Identifier' - ? (member.key.name ?? null) - : member.key.type === 'Literal' - ? String(member.key.value) - : null - const heads: MemberHead[] = collected - .filter((head) => head.pos >= start && head.pos < end) - .map((head) => ({ - start: head.pos - start, - end: head.pos - start + head.alias.length, - alias: head.alias, - module: head.module, - resolved: head.resolved, - })) - return { name, text: members.slice(start, end), heads } - }) -} - -/** - * The position just past `position`'s line, skipping newlines that fall - * inside comments — `parseSync`'s `comments` carry spans for both `//` and - * `/* ... *\/` so a multiline trailing comment can't split the insertion. - */ -function endOfLineAfter( - source: string, - position: number, - comments: ReadonlyArray<{ start: number; end: number }>, -): number { - let cursor = position - for (;;) { - const newline = source.indexOf('\n', cursor) - const end = newline === -1 ? source.length : newline - const covering = comments.find((c) => c.start <= end && end < c.end) - if (covering === undefined) { - return newline === -1 ? source.length : newline + 1 - } - cursor = covering.end - } -} - -/** - * Resolve `dir + specifier` the way Node would see it — collapsing `..` and - * `.` segments — so `"./dep"` under different directories canonicalizes to - * different identities while `../dep` from a nested file resolves back to - * the same one. Purely textual; no filesystem access. - */ -function resolvePath(path: string): string { - const out: string[] = [] - for (const segment of path.split('/')) { - if (segment === '' || segment === '.') { - continue - } - if (segment === '..') { - out.pop() - } else { - out.push(segment) - } - } - return out.join('/') -} - /** * Splice each declaration's static members into the matching class body in * `source`, and emit the namespace imports the injected members reference — * one `import * as ns from "module"` per module identity, canonicalized * across all injected declarations. * - * Like ngtsc's declaration ImportManager, an existing `import * as ns` is - * reused only when it already binds the same alias and specifier — never - * for a different one, and never to borrow the user's `@angular/core` - * import: the members' compiled alias is kept when free, otherwise - * uniquified (`i0_1`, …). - * * The pass is idempotent: a member whose STATIC name already appears in the * target class is skipped — structurally, on the member name, so a second * pass that canonicalizes to a different alias can't inject the same member @@ -333,159 +78,8 @@ export function injectDtsDeclarations( source: string, declarations: readonly DtsClassDeclaration[], ): string { - if (declarations.length === 0) { - return source - } - - const { program, errors, comments } = parseSync('file.d.ts', source, { lang: 'dts' }) - if (errors.length > 0) { - return source - } - - const fileNames = collectIdentifiers(program) - const existingImports = namespaceImportsOf(program) - - // Identity resolution is relative to each declaration's source module — - // the same "./dep" in two directories is two modules. Emitted specifiers - // stay verbatim, matching ngtsc and structure-preserving declaration - // emit (`src/a/foo.ts` → `dist/a/foo.d.ts`, where "./dep" reaches - // `dist/a/dep.d.ts`); there is no dts-generator source→output mapping to - // rebase against, so under fully-bundled declaration output two same-named - // relative modules remain a documented limitation. - const resolveFor = (sourceFile?: string) => { - if (sourceFile === undefined) { - return (specifier: string) => specifier - } - const dir = sourceFile.replace(/[^/\\]*$/, '') - return (specifier: string) => - specifier.startsWith('./') || specifier.startsWith('../') - ? resolvePath(dir + specifier) - : specifier - } - - // Canonical module identity → [rawSpecifier, alias] for this file. - const canonical = new Map() - const canonicalAlias = (head: MemberHead): string => { - const known = canonical.get(head.resolved) - if (known !== undefined) { - return known[1] - } - const preferred = head.module === '@angular/core' ? 'i0' : head.alias - // Reuse an existing import only when it binds this alias AND specifier — - // same module under the same name (which is exactly what an idempotent - // re-run sees on its second pass), never a different module. - const existing = existingImports.get(preferred) - const alias = - existing !== undefined && existing === head.module - ? preferred - : uniquifyIdentifier(preferred, fileNames) - canonical.set(head.resolved, [head.module, alias]) - fileNames.add(alias) - return alias - } - - const splices: { start: number; end: number; text: string }[] = [] - - for (const declaration of declarations) { - const members = parseMembers( - declaration.members, - declaration.namespaceImports ?? {}, - resolveFor(declaration.sourceFile), - ) - if (members === null || members.length === 0) { - continue - } - - const body = findClassBody(program.body, declaration.className) - if (body === null) { - continue - } - const existing = existingMemberNames(body) - - // Per-member idempotency: skip members the class already declares. - // Names — not normalized text — decide, so a re-run whose canonical - // alias differs (the first pass's import now occupies `i0`) doesn't - // inject the same `static ɵfac` a second time. - const lines: string[] = [] - for (const member of members) { - if (member.name !== null && existing.has(member.name)) { - continue - } - let text = member.text - for (const head of member.heads.sort((a, b) => b.start - a.start)) { - const alias = canonicalAlias(head) - text = text.slice(0, head.start) + alias + text.slice(head.end) - } - for (const line of text.split('\n')) { - const trimmed = line.trim() - if (trimmed.length > 0) { - lines.push(trimmed) - } - } - } - if (lines.length === 0) { - continue - } - - // `ClassBody.end` is just past the closing brace; inserting before it - // appends the members, matching upstream's `[...members, ...newMembers]` - // (declaration.ts) — a class that already has members (constructor - // overloads, declared fields) keeps them first. - const insertAt = body.end - 1 - const needsLeadingNl = insertAt > 0 && source[insertAt - 1] !== '\n' - splices.push({ - start: insertAt, - end: insertAt, - text: (needsLeadingNl ? '\n' : '') + lines.map((line) => ` ${line}`).join('\n') + '\n', - }) - } - - if (splices.length === 0) { - return source - } - - // Emit an import for every canonical module the file doesn't already - // have, sorted by alias — the same order the compiler's own namespace - // registry generates them in. - const importLines = [...canonical.values()] - .filter(([specifier, alias]) => existingImports.get(alias) !== specifier) - .sort(([, a], [, b]) => (a < b ? -1 : a > b ? 1 : 0)) - .map(([specifier, alias]) => `import * as ${alias} from "${specifier}";`) - - if (importLines.length > 0) { - const text = importLines.join('\n') - let lastImportEnd: number | null = null - for (const stmt of program.body) { - if (stmt.type === 'ImportDeclaration') { - lastImportEnd = stmt.end - } - } - if (lastImportEnd !== null) { - // End of the last import's line, skipping multiline trailing comments - // (`import x from "m"; /* keep\nme */`) so the comment keeps its line - // and the new import lands on the next one. - const position = endOfLineAfter(source, lastImportEnd, comments) - splices.push({ start: position, end: position, text: `${text}\n` }) - } else if (program.body.length > 0) { - // Before the first statement keeps leading comments and triple-slash - // reference directives at the top of the file. - splices.push({ - start: program.body[0].start, - end: program.body[0].start, - text: `${text}\n`, - }) - } else { - splices.push({ - start: source.length, - end: source.length, - text: `${source.endsWith('\n') || source === '' ? '' : '\n'}${text}\n`, - }) - } - } - - let output = source - for (const { start, end, text } of splices.sort((a, b) => b.start - a.start)) { - output = output.slice(0, start) + text + output.slice(end) - } - return output + return injectDtsDeclarationsNative( + source, + declarations.map((d) => ({ ...d })), + ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f95532f7d..24ee40e69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,9 +68,6 @@ importers: obug: specifier: ^3.0.0 version: 3.0.0 - oxc-parser: - specifier: ^0.153.0 - version: 0.153.0 devDependencies: '@angular/compiler': specifier: ^22.0.0-rc.2 From 0a63595aa55e3a3350346a996374877e5a3a0f8b Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 7 Oct 2026 23:17:58 +0800 Subject: [PATCH 6/8] fix(vite-plugin): keep generated imports out of same-line class bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on the previous commit (lines are stale — the logic now lives in crates/oxc_angular_compiler/src/dts_inject.rs): - P2: end_of_line_after scanned past syntax sharing the last import's line ("import …; export declare class Foo {") and spliced the new import inside the class body. import_insert_position now advances only over trailing comments and the splice opens its own line, so same-line declarations stay below the new import. Also fixes "import …; // note" at EOF previously inserting inside the line comment. - P2: dotted ambient namespaces (declare namespace Outer.Inner) already recurse in the Rust port (TSNamespaceDeclarationBody nesting); added a regression test so the finding is covered either way. --- crates/oxc_angular_compiler/src/dts_inject.rs | 53 +++++++++++++------ napi/angular-compiler/test/dts.test.ts | 41 ++++++++++++++ 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/crates/oxc_angular_compiler/src/dts_inject.rs b/crates/oxc_angular_compiler/src/dts_inject.rs index 312fe8ec4..2234013b3 100644 --- a/crates/oxc_angular_compiler/src/dts_inject.rs +++ b/crates/oxc_angular_compiler/src/dts_inject.rs @@ -367,19 +367,31 @@ fn parse_members( Some(parsed) } -/// The position just past `position`'s line, skipping newlines that fall -/// inside comments — a multiline trailing comment can't split the -/// insertion. -fn end_of_line_after(source: &str, position: usize, comment_spans: &[(usize, usize)]) -> usize { +/// The position where a new import can be spliced: just past `position` +/// and any comments that begin on the same line (trailing comments, +/// multiline included). Code sharing the import's line +/// (`import …; export declare class X {`) must stay below the new import, +/// so the scan never advances past non-comment syntax. +fn import_insert_position( + source: &str, + position: usize, + comment_spans: &[(usize, usize)], +) -> usize { let bytes = source.as_bytes(); let mut cursor = position; loop { - let newline = bytes[cursor..].iter().position(|&b| b == b'\n').map(|i| cursor + i); - let end = newline.unwrap_or(source.len()); - let covering = comment_spans.iter().find(|(start, cend)| *start <= end && end < *cend); - match covering { - None => return newline.map_or(source.len(), |n| n + 1), - Some(&(_, cend)) => cursor = cend, + let line_end = + bytes[cursor..].iter().position(|&b| b == b'\n').map_or(source.len(), |i| cursor + i); + // A comment beginning on this line at-or-after `cursor` is a + // trailing comment of the import; keep it on the import's line. + let next = comment_spans + .iter() + .filter(|&&(start, _)| start >= cursor && start < line_end) + .map(|&(_, end)| end) + .min(); + match next { + Some(comment_end) => cursor = comment_end, + None => return cursor, } } } @@ -565,16 +577,27 @@ pub fn inject_dts_declarations(source: &str, declarations: &[DtsInjectDeclaratio _ => None, }); if let Some(end) = last_import_end { - // End of the last import's line, skipping multiline trailing - // comments (`import x from "m"; /* keep\nme */`) so the comment - // keeps its line and the new import lands on the next one. + // After the last import and any trailing comments on its line + // (`import x from "m"; /* keep\nme */` keeps the comment with + // it; `import x; export class` doesn't spill inside the class). let comment_spans: Vec<(usize, usize)> = program .comments .iter() .map(|c| (c.span.start as usize, c.span.end as usize)) .collect(); - let position = end_of_line_after(source, end, &comment_spans); - splices.push((position, position, format!("{text}\n"))); + let position = import_insert_position(source, end, &comment_spans); + // Source's own newline at `position` separates the new import; + // otherwise we open a line — and close ours too when code + // follows on the same line. + let at_newline = source.as_bytes().get(position) == Some(&b'\n'); + splices.push(( + position, + position, + format!( + "\n{text}{}", + if at_newline || position == source.len() { "" } else { "\n" } + ), + )); } else if let Some(first) = program.body.first() { // Before the first statement keeps leading comments and // triple-slash reference directives at the top of the file. diff --git a/napi/angular-compiler/test/dts.test.ts b/napi/angular-compiler/test/dts.test.ts index a88e5c6fe..2124b3ff0 100644 --- a/napi/angular-compiler/test/dts.test.ts +++ b/napi/angular-compiler/test/dts.test.ts @@ -347,6 +347,47 @@ describe('injectDtsDeclarations', () => { expect(out.match(/ɵfac/g)).toHaveLength(2) }) + it('keeps the generated import outside a class declared on the import line', () => { + // `import …; export declare class Foo {` on one line must not let the + // import insertion scan past `{` — that would splice the import into + // the class body. + const source = 'import type { A } from "./a"; export declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + const importIdx = out.indexOf('import * as i0') + const classIdx = out.indexOf('export declare class Foo') + const openBrace = out.indexOf('{', classIdx) + expect(importIdx).toBeGreaterThan(-1) + expect(importIdx).toBeLessThan(classIdx) + expect(importIdx).toBeLessThan(openBrace) + }) + + it('injects into a class in a dotted ambient namespace', () => { + // `declare namespace Outer.Inner {}` parses as nested module + // declarations — the search must descend through them. + const source = 'declare namespace Outer.Inner {\n export class Foo {\n }\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + expect(out).toContain('import * as i0 from "@angular/core";') + const facIdx = out.indexOf('ɵfac') + const classIdx = out.indexOf('class Foo') + const closeIdx = out.lastIndexOf('}') + expect(classIdx).toBeLessThan(facIdx) + expect(facIdx).toBeLessThan(closeIdx) + }) + + it('keeps a line comment on the last import with it', () => { + // `import …; // note` at EOF has no newline after the comment — the + // new import must not land inside the comment. + const source = 'import type { A } from "./a"; // note\nexport declare class Foo {\n}\n' + const out = injectDtsDeclarations(source, [ + { className: 'Foo', members: 'static ɵfac: i0.ɵɵFactoryDeclaration;' }, + ]) + expect(out).toContain('// note\nimport * as i0 from "@angular/core";') + }) + it('handles declarations with empty or comment-only members', () => { // The wrapper always yields a class, but nothing should be spliced — // no crash, no members, no imports. From c4696ee152b6c130b422e7c8e347b2a9759ec3fd Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 7 Oct 2026 23:29:39 +0800 Subject: [PATCH 7/8] fix(vite-plugin): infer unmapped i0 aliases as @angular/core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review: when a caller omits namespaceImports, an unmapped non-ɵ head like i0.Signal was skipped — the member lands with i0 unresolvable, where the pre-canonicalization helper always emitted the i0 import. The registry reserves i0/i0_N for @angular/core, so heads matching that convention are now inferred as core when the map is empty. i1+ aliases stay untouched: without the map their module is unknowable. --- crates/oxc_angular_compiler/src/dts_inject.rs | 16 +++++++++- napi/angular-compiler/test/dts.test.ts | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/oxc_angular_compiler/src/dts_inject.rs b/crates/oxc_angular_compiler/src/dts_inject.rs index 2234013b3..82d397ae6 100644 --- a/crates/oxc_angular_compiler/src/dts_inject.rs +++ b/crates/oxc_angular_compiler/src/dts_inject.rs @@ -284,12 +284,26 @@ struct HeadCollector<'ns, 'r> { collected: Vec<(usize, String, String, String)>, } +/// `i0`, `i0_1`, `i0_2`, … — the aliases the namespace registry prefers +/// for `@angular/core`. When `namespace_imports` is absent these are the +/// only unmapped heads safe to infer as core, preserving the pre- +/// canonicalization behavior of always emitting the `i0` import. `i1`+ +/// aliases can stand for any module and stay untouched. +fn is_core_alias_convention(alias: &str) -> bool { + alias == "i0" + || alias + .strip_prefix("i0_") + .is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())) +} + impl<'a> Visit<'a> for HeadCollector<'_, '_> { fn visit_ts_qualified_name(&mut self, node: &TSQualifiedName<'a>) { if let TSTypeName::IdentifierReference(left) = &node.left { let alias = left.name.to_string(); let module = self.namespace_imports.get(&alias); - if module.is_some() || node.right.name.starts_with('ɵ') { + let unmapped_core = + self.namespace_imports.is_empty() && is_core_alias_convention(&alias); + if module.is_some() || node.right.name.starts_with('ɵ') || unmapped_core { let specifier = module.cloned().unwrap_or_else(|| "@angular/core".to_string()); self.collected.push(( left.span.start as usize - self.offset, diff --git a/napi/angular-compiler/test/dts.test.ts b/napi/angular-compiler/test/dts.test.ts index 2124b3ff0..3337a997a 100644 --- a/napi/angular-compiler/test/dts.test.ts +++ b/napi/angular-compiler/test/dts.test.ts @@ -388,6 +388,36 @@ describe('injectDtsDeclarations', () => { expect(out).toContain('// note\nimport * as i0 from "@angular/core";') }) + it('infers unmapped i0 aliases as @angular/core when namespaceImports is absent', () => { + // Legacy callers pass no `namespaceImports`; the compiler's registry + // reserves `i0`/`i0_N` for `@angular/core`, so an unmapped `i0.Signal` + // head must still emit the core import — previously the member was + // injected with `i0` unresolvable. + const source = 'export declare class Dir {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'Dir', + members: 'static ngAcceptInputType_x: i0.Signal;', + }, + ]) + expect(out).toContain('import * as i0 from "@angular/core";') + expect(out).toContain('i0.Signal') + }) + + it('does not infer unmapped i1+ aliases without namespaceImports', () => { + // `i1` isn't reserved for core — it could be a relative dep. Without a + // map the head is left alone rather than guessing a module. + const source = 'export declare class Dir {\n}\n' + const out = injectDtsDeclarations(source, [ + { + className: 'Dir', + members: 'static ngAcceptInputType_x: i1.Signal;', + }, + ]) + expect(out).not.toContain('@angular/core') + expect(out).toContain('i1.Signal') + }) + it('handles declarations with empty or comment-only members', () => { // The wrapper always yields a class, but nothing should be spliced — // no crash, no members, no imports. From 7ae6f82ae77e96ea5fc5227a456c725ec4a1c1bc Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 7 Oct 2026 23:40:49 +0800 Subject: [PATCH 8/8] refactor(compiler): centralize the i0 / @angular/core literals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CORE module name and CORE_ALIAS preferred namespace alias now live in r3::identifiers (next to the other Angular runtime identifiers) and are re-exported from crate::r3. Every real-code literal — the registry's mint site, emitter/linker/HMR fallbacks, the host-bindings call, directive initializer-API tables, dts_type matches, and dts_inject's core-alias convention check — references the constants. Test-only literals are left as-is (they assert on the string values). --- .../src/class_metadata/builders.rs | 2 +- .../src/component/namespace_registry.rs | 6 +++--- .../src/component/transform.rs | 17 +++++++++-------- .../src/directive/decorator.rs | 15 ++++++++------- .../src/directive/dts_type.rs | 5 +++-- .../src/directive/property_decorators.rs | 16 ++++++++-------- crates/oxc_angular_compiler/src/dts_inject.rs | 19 +++++++++++-------- .../src/hmr/initializer.rs | 4 ++-- crates/oxc_angular_compiler/src/linker/mod.rs | 4 ++-- .../src/output/emitter.rs | 2 +- .../src/pipeline/compilation.rs | 4 ++-- .../oxc_angular_compiler/src/pipeline/emit.rs | 14 +++++++------- .../src/r3/identifiers.rs | 6 ++++++ crates/oxc_angular_compiler/src/r3/mod.rs | 9 +++++---- 14 files changed, 68 insertions(+), 55 deletions(-) diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index fa75acae7..510e2cf96 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/builders.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/builders.rs @@ -803,7 +803,7 @@ fn build_core_decorator<'a>( decorator_name: &'static str, args: AllocVec<'a, OutputExpression<'a>>, ) -> OutputExpression<'a> { - let core_namespace = namespace_registry.get_or_assign(&Ident::from("@angular/core")); + let core_namespace = namespace_registry.get_or_assign(&Ident::from(crate::r3::CORE)); let type_expr = OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in( diff --git a/crates/oxc_angular_compiler/src/component/namespace_registry.rs b/crates/oxc_angular_compiler/src/component/namespace_registry.rs index a356da08d..6b95a48d7 100644 --- a/crates/oxc_angular_compiler/src/component/namespace_registry.rs +++ b/crates/oxc_angular_compiler/src/component/namespace_registry.rs @@ -57,13 +57,13 @@ impl<'a> NamespaceRegistry<'a> { allocator, }; // Pre-register @angular/core - let core_module = Ident::from("@angular/core"); + let core_module = Ident::from(crate::r3::identifiers::CORE); let core_alias = match reused_core_alias { Some(alias) => { registry.reused_modules.insert(core_module); alias } - None => registry.unique_name("i0"), + None => registry.unique_name(crate::r3::identifiers::CORE_ALIAS), }; registry.modules.insert(core_module, core_alias); registry @@ -91,7 +91,7 @@ impl<'a> NamespaceRegistry<'a> { /// The alias generated `@angular/core` references use (usually `i0`). pub fn angular_core_ns(&self) -> Ident<'a> { // @angular/core is always pre-registered by the constructors. - self.modules[&Ident::from("@angular/core")] + self.modules[&Ident::from(crate::r3::identifiers::CORE)] } /// Get the namespace alias for a module, assigning one if not yet assigned. diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 5c29ca62b..def0ba48f 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -16,6 +16,8 @@ use oxc_diagnostics::OxcDiagnostic; use oxc_parser::Parser; use oxc_span::{GetSpan, SourceType, Span}; use oxc_str::Ident; + +use crate::r3::CORE; use rustc_hash::FxHashMap; use crate::optimizer::{Edit, apply_edits, apply_edits_with_sourcemap}; @@ -1015,7 +1017,7 @@ pub(crate) fn is_angular_core_export( exported_name: &str, ) -> bool { let Some(info) = import_map.get(&Ident::from(local_name)) else { return false }; - if info.source_module.as_str() != "@angular/core" { + if info.source_module.as_str() != CORE { return false; } match &info.imported_name { @@ -1031,8 +1033,7 @@ pub(crate) fn is_angular_core_export( pub(crate) fn is_angular_core_namespace(import_map: &ImportMap<'_>, local_name: &str) -> bool { import_map .get(&Ident::from(local_name)) - .map(|info| info.source_module.as_str() == "@angular/core" && !info.is_named_import) - .unwrap_or(false) + .is_some_and(|info| info.source_module.as_str() == CORE && !info.is_named_import) } /// Return the name of the first non-`Service` `@angular/core` decorator on @@ -1452,7 +1453,7 @@ fn classify_initializer_api( /// `i0.Input` / `i0.Output` / etc. backed by `import * as i0 from "@angular/core"`. /// Without this prefixing, the synthesized `static propDecorators` would reference /// an undefined identifier and throw `ReferenceError` at module-evaluation time. -pub(crate) const JIT_ANGULAR_CORE_NS: &str = "i0"; +pub(crate) const JIT_ANGULAR_CORE_NS: &str = crate::r3::CORE_ALIAS; /// Identifiers referenced inside constructor parameters — types and /// decorators. Used for jit-forced classes, whose parameters are re-emitted @@ -1557,7 +1558,7 @@ fn reused_angular_core_namespace<'a>( ) -> Option> { for stmt in program.body.iter().rev() { let Statement::ImportDeclaration(import) = stmt else { continue }; - if import.source.value != "@angular/core" || import.import_kind.is_type() { + if import.source.value != CORE || import.import_kind.is_type() { continue; } let namespace = import.specifiers.iter().flatten().find_map(|spec| match spec { @@ -4981,8 +4982,8 @@ fn compile_component_full<'a>( // Add the @angular/core namespace dependency under whatever alias the // file's registry assigned (`i0`, a uniquified `i0_1`, or a reused // `import * as ns`). - let core_ns = namespace_registry.get_or_assign(&Ident::from("@angular/core")); - hmr_meta.add_namespace_dependency(Ident::from("@angular/core"), core_ns); + let core_ns = namespace_registry.get_or_assign(&Ident::from(CORE)); + hmr_meta.add_namespace_dependency(Ident::from(CORE), core_ns); // Generate the HMR initializer expression let hmr_expr = compile_hmr_initializer(allocator, &hmr_meta); @@ -5356,7 +5357,7 @@ pub fn compile_template_to_js_with_options<'a>( if let Some(ref host_input) = options.host { if let Some(host_result) = compile_host_bindings_from_input( &allocator, - "i0", + crate::r3::CORE_ALIAS, host_input, component_name, options.selector.as_deref(), diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index 1f2fb7b32..2da04b76d 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -23,6 +23,7 @@ use super::metadata::{ use crate::factory::R3DependencyMetadata; use crate::output::ast::{OutputAstBuilder, OutputExpression, ReadVarExpr}; use crate::output::oxc_converter::convert_oxc_expression; +use crate::r3::CORE; use crate::util::is_metadata_property; /// Find the @Directive decorator in a list of decorators. With the file's @@ -1467,18 +1468,18 @@ fn output_decorator_error<'a>( /// An initializer API: its function name and the module exporting it. pub(crate) type InitializerApi = (&'static str, &'static str); -pub(crate) const INPUT_API: InitializerApi = ("input", "@angular/core"); -pub(crate) const MODEL_API: InitializerApi = ("model", "@angular/core"); -pub(crate) const OUTPUT_API: InitializerApi = ("output", "@angular/core"); +pub(crate) const INPUT_API: InitializerApi = ("input", CORE); +pub(crate) const MODEL_API: InitializerApi = ("model", CORE); +pub(crate) const OUTPUT_API: InitializerApi = ("output", CORE); pub(crate) const OUTPUT_FROM_OBSERVABLE_API: InitializerApi = ("outputFromObservable", "@angular/core/rxjs-interop"); /// Angular's signal query functions (ngtsc's `QUERY_INITIALIZER_FNS`). pub(crate) const QUERY_APIS: [InitializerApi; 4] = [ - ("viewChild", "@angular/core"), - ("viewChildren", "@angular/core"), - ("contentChild", "@angular/core"), - ("contentChildren", "@angular/core"), + ("viewChild", CORE), + ("viewChildren", CORE), + ("contentChild", CORE), + ("contentChildren", CORE), ]; /// Whether `value` calls one of `apis` (see [`initializer_api_call`]). diff --git a/crates/oxc_angular_compiler/src/directive/dts_type.rs b/crates/oxc_angular_compiler/src/directive/dts_type.rs index f26231069..2d233d75f 100644 --- a/crates/oxc_angular_compiler/src/directive/dts_type.rs +++ b/crates/oxc_angular_compiler/src/directive/dts_type.rs @@ -21,6 +21,7 @@ use oxc_span::GetSpan; use super::evaluator::{AliasTarget, FileScope}; use crate::output::emitter::format_number_like_js; +use crate::r3::CORE; pub(crate) struct TypePrinter<'s, 'a> { pub scope: &'s FileScope<'a>, @@ -252,7 +253,7 @@ impl<'a> TypePrinter<'_, 'a> { match target { // `import Core = require('@angular/core')`: `Core.X` is // `i0.X`, like a namespace import's member. - AliasTarget::Module("@angular/core") => { + AliasTarget::Module(CORE) => { return Some(Resolved::Core( parts[len..].iter().map(|m| format!(".{m}")).collect(), )); @@ -286,7 +287,7 @@ impl<'a> TypePrinter<'_, 'a> { /// other module, as written otherwise. fn value_name(&mut self, head: &str, rest: &str) -> String { match self.scope.import(head) { - Some(import) if import.module == "@angular/core" => match import.imported { + Some(import) if import.module == CORE => match import.imported { Some(imported) => format!("{}.{imported}{rest}", self.core_ns), None => format!("{}{rest}", self.core_ns), }, diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index b869ac95a..4f36fd6ef 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -23,6 +23,7 @@ use super::evaluator::{Evaluator, Value}; use super::metadata::{QueryPredicate, R3InputMetadata, R3QueryMetadata}; use crate::output::ast::OutputExpression; use crate::output::oxc_converter::{convert_oxc_expression, make_raw_source}; +use crate::r3::CORE; use crate::util::is_metadata_property; // ============================================================================ @@ -38,7 +39,7 @@ use crate::util::is_metadata_property; fn is_core_namespace(consts: Option<&super::StringConsts<'_>>, name: &str) -> bool { consts .and_then(|consts| consts.scope().import(name)) - .is_some_and(|import| import.module == "@angular/core" && import.imported.is_none()) + .is_some_and(|import| import.module == CORE && import.imported.is_none()) } /// Angular's member decorators, which are compiled into the definition. @@ -77,7 +78,7 @@ pub(crate) fn angular_core_decorator( Expression::Identifier(id) => match consts { Some(consts) => { let import = consts.scope().import(&id.name)?; - if import.module != "@angular/core" { + if import.module != CORE { return None; } import.imported? @@ -117,10 +118,9 @@ pub(crate) fn is_angular_core_decorator( }; match callee { Expression::Identifier(id) => match consts { - Some(consts) => consts - .scope() - .import(&id.name) - .is_some_and(|import| import.module == "@angular/core"), + Some(consts) => { + consts.scope().import(&id.name).is_some_and(|import| import.module == CORE) + } None => true, }, Expression::StaticMemberExpression(m) => match &m.object { @@ -287,7 +287,7 @@ fn try_unwrap_forward_ref<'a>( let is_forward_ref = match &call.callee { Expression::Identifier(id) => match consts { Some(consts) => consts.scope().import(&id.name).is_some_and(|import| { - import.module == "@angular/core" && import.imported == Some("forwardRef") + import.module == CORE && import.imported == Some("forwardRef") }), None => id.name == "forwardRef", }, @@ -1882,7 +1882,7 @@ pub(crate) fn parse_decorator_queries<'a>( Expression::Identifier(id) => consts .scope() .import(id.name.as_str()) - .filter(|i| i.module == "@angular/core") + .filter(|i| i.module == CORE) .and_then(|i| i.imported), Expression::StaticMemberExpression(m) => match &m.object { Expression::Identifier(ns) if is_core_namespace(Some(consts), &ns.name) => { diff --git a/crates/oxc_angular_compiler/src/dts_inject.rs b/crates/oxc_angular_compiler/src/dts_inject.rs index 82d397ae6..dbb8578f4 100644 --- a/crates/oxc_angular_compiler/src/dts_inject.rs +++ b/crates/oxc_angular_compiler/src/dts_inject.rs @@ -36,6 +36,8 @@ use oxc_ast_visit::{Visit, walk}; use oxc_parser::Parser; use oxc_span::{GetSpan, SourceType, Span}; +use crate::r3::{CORE, CORE_ALIAS}; + /// One class's `.d.ts` static member declarations. pub struct DtsInjectDeclaration { /// The class the members belong to. @@ -285,14 +287,15 @@ struct HeadCollector<'ns, 'r> { } /// `i0`, `i0_1`, `i0_2`, … — the aliases the namespace registry prefers -/// for `@angular/core`. When `namespace_imports` is absent these are the -/// only unmapped heads safe to infer as core, preserving the pre- -/// canonicalization behavior of always emitting the `i0` import. `i1`+ -/// aliases can stand for any module and stay untouched. +/// for `@angular/core` (`CORE_ALIAS` uniquified). When `namespace_imports` +/// is absent these are the only unmapped heads safe to infer as core, +/// preserving the pre-canonicalization behavior of always emitting the +/// `i0` import. `i1`+ aliases can stand for any module and stay untouched. fn is_core_alias_convention(alias: &str) -> bool { - alias == "i0" + let uniquified_prefix = format!("{CORE_ALIAS}_"); + alias == CORE_ALIAS || alias - .strip_prefix("i0_") + .strip_prefix(uniquified_prefix.as_str()) .is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())) } @@ -304,7 +307,7 @@ impl<'a> Visit<'a> for HeadCollector<'_, '_> { let unmapped_core = self.namespace_imports.is_empty() && is_core_alias_convention(&alias); if module.is_some() || node.right.name.starts_with('ɵ') || unmapped_core { - let specifier = module.cloned().unwrap_or_else(|| "@angular/core".to_string()); + let specifier = module.cloned().unwrap_or_else(|| CORE.to_string()); self.collected.push(( left.span.start as usize - self.offset, alias, @@ -491,7 +494,7 @@ pub fn inject_dts_declarations(source: &str, declarations: &[DtsInjectDeclaratio if let Some((_, alias)) = canonical.get(&head.resolved) { return alias.clone(); } - let preferred = if head.module == "@angular/core" { "i0" } else { head.alias.as_str() }; + let preferred = if head.module == CORE { CORE_ALIAS } else { head.alias.as_str() }; // Reuse an existing import only when it binds this alias AND // specifier — same module under the same name (which is exactly // what an idempotent re-run sees on its second pass), never a diff --git a/crates/oxc_angular_compiler/src/hmr/initializer.rs b/crates/oxc_angular_compiler/src/hmr/initializer.rs index a33436f52..3f26eeb0c 100644 --- a/crates/oxc_angular_compiler/src/hmr/initializer.rs +++ b/crates/oxc_angular_compiler/src/hmr/initializer.rs @@ -64,9 +64,9 @@ pub fn compile_hmr_initializer<'a>( let core_ns = meta .namespace_dependencies .iter() - .find(|dep| dep.module_name.as_str() == "@angular/core") + .find(|dep| dep.module_name.as_str() == crate::r3::CORE) .map(|dep| dep.assigned_name) - .unwrap_or_else(|| Ident::from("i0")); + .unwrap_or_else(|| Ident::from(crate::r3::CORE_ALIAS)); // i0.ɵɵreplaceMetadata(Comp, m.default, [...namespaces], [...locals], import.meta, id) let replace_call = invoke_fn( diff --git a/crates/oxc_angular_compiler/src/linker/mod.rs b/crates/oxc_angular_compiler/src/linker/mod.rs index 37c053fd8..2fd8369a8 100644 --- a/crates/oxc_angular_compiler/src/linker/mod.rs +++ b/crates/oxc_angular_compiler/src/linker/mod.rs @@ -390,9 +390,9 @@ fn get_ng_import_namespace<'a>(call: &'a CallExpression<'a>) -> &'a str { if let Expression::Identifier(ident) = &member.object { return ident.name.as_str(); } - "i0" + crate::r3::CORE_ALIAS } - _ => "i0", + _ => crate::r3::CORE_ALIAS, } } diff --git a/crates/oxc_angular_compiler/src/output/emitter.rs b/crates/oxc_angular_compiler/src/output/emitter.rs index 389902f0d..a39c920eb 100644 --- a/crates/oxc_angular_compiler/src/output/emitter.rs +++ b/crates/oxc_angular_compiler/src/output/emitter.rs @@ -656,7 +656,7 @@ impl JsEmitter { // Variable references are key for source mapping - map the variable name let var_span = e.source_span.and_then(|span| ctx.span_to_source_span(span)); let name = match &self.core_namespace { - Some(ns) if e.name == "i0" => ns.as_str(), + Some(ns) if e.name == crate::r3::CORE_ALIAS => ns.as_str(), _ => e.name.as_str(), }; ctx.print_with_span(name, var_span); diff --git a/crates/oxc_angular_compiler/src/pipeline/compilation.rs b/crates/oxc_angular_compiler/src/pipeline/compilation.rs index 7952f7df5..3717b6e14 100644 --- a/crates/oxc_angular_compiler/src/pipeline/compilation.rs +++ b/crates/oxc_angular_compiler/src/pipeline/compilation.rs @@ -279,7 +279,7 @@ impl<'a> ComponentCompilationJob<'a> { content_selectors: None, angular_version: None, legacy_optional_chaining: None, - core_namespace: Ident::from("i0"), + core_namespace: Ident::from(crate::r3::CORE_ALIAS), diagnostics: std::vec::Vec::new(), } } @@ -730,7 +730,7 @@ impl<'a> HostBindingCompilationJob<'a> { diagnostics: std::vec::Vec::new(), angular_version: None, legacy_optional_chaining: None, - core_namespace: Ident::from("i0"), + core_namespace: Ident::from(crate::r3::CORE_ALIAS), } } diff --git a/crates/oxc_angular_compiler/src/pipeline/emit.rs b/crates/oxc_angular_compiler/src/pipeline/emit.rs index 777ac0e42..2907b3438 100644 --- a/crates/oxc_angular_compiler/src/pipeline/emit.rs +++ b/crates/oxc_angular_compiler/src/pipeline/emit.rs @@ -13,7 +13,7 @@ use crate::output::ast::{ LiteralExpr, LiteralValue, OutputExpression, OutputStatement, ReadVarExpr, StmtModifier, clone_output_statement, }; -use crate::r3::{Identifiers, get_interpolate_instruction, get_pipe_bind_instruction}; +use crate::r3::{CORE, Identifiers, get_interpolate_instruction, get_pipe_bind_instruction}; use oxc_allocator::Box; use oxc_str::Ident; @@ -533,7 +533,7 @@ fn convert_pure_function_body<'a>( OutputExpression::External(Box::new_in( ExternalExpr { value: ExternalReference { - module_name: Some(Ident::from("@angular/core")), + module_name: Some(Ident::from(CORE)), name: Some(Ident::from(allocator.alloc_str(instruction))), }, source_span: None, @@ -600,7 +600,7 @@ fn convert_pure_function_body<'a>( OutputExpression::External(Box::new_in( ExternalExpr { value: ExternalReference { - module_name: Some(Ident::from("@angular/core")), + module_name: Some(Ident::from(CORE)), name: Some(Ident::from(fn_name)), }, source_span: None, @@ -643,7 +643,7 @@ fn convert_pure_function_body<'a>( OutputExpression::External(Box::new_in( ExternalExpr { value: ExternalReference { - module_name: Some(Ident::from("@angular/core")), + module_name: Some(Ident::from(CORE)), name: Some(Ident::from(Identifiers::PIPE_BIND_V)), }, source_span: None, @@ -1005,7 +1005,7 @@ fn convert_pure_function_body<'a>( OutputExpression::External(Box::new_in( ExternalExpr { value: ExternalReference { - module_name: Some(Ident::from("@angular/core")), + module_name: Some(Ident::from(CORE)), name: Some(Ident::from(Identifiers::TWO_WAY_BINDING_SET)), }, source_span: None, @@ -1038,7 +1038,7 @@ fn convert_pure_function_body<'a>( OutputExpression::External(Box::new_in( ExternalExpr { value: ExternalReference { - module_name: Some(Ident::from("@angular/core")), + module_name: Some(Ident::from(CORE)), name: Some(Ident::from(Identifiers::READ_CONTEXT_LET)), }, source_span: None, @@ -1066,7 +1066,7 @@ fn convert_pure_function_body<'a>( OutputExpression::External(Box::new_in( ExternalExpr { value: ExternalReference { - module_name: Some(Ident::from("@angular/core")), + module_name: Some(Ident::from(CORE)), name: Some(Ident::from(Identifiers::STORE_LET)), }, source_span: None, diff --git a/crates/oxc_angular_compiler/src/r3/identifiers.rs b/crates/oxc_angular_compiler/src/r3/identifiers.rs index 38ae09ea7..4b89ff0f0 100644 --- a/crates/oxc_angular_compiler/src/r3/identifiers.rs +++ b/crates/oxc_angular_compiler/src/r3/identifiers.rs @@ -10,6 +10,12 @@ /// The Angular core module name. pub const CORE: &str = "@angular/core"; +/// The preferred namespace alias for `@angular/core` imports (`i0`, +/// uniquified to `i0_1`, `i0_2`, … when a source file already binds `i0`). +/// Matches ngtsc's declaration `ImportManager`, which reserves `i0` for +/// core references. +pub const CORE_ALIAS: &str = "i0"; + /// Angular runtime identifiers used in code generation. /// /// Each identifier corresponds to a runtime instruction exported from `@angular/core`. diff --git a/crates/oxc_angular_compiler/src/r3/mod.rs b/crates/oxc_angular_compiler/src/r3/mod.rs index f0567961c..cdb881c40 100644 --- a/crates/oxc_angular_compiler/src/r3/mod.rs +++ b/crates/oxc_angular_compiler/src/r3/mod.rs @@ -6,8 +6,9 @@ pub mod identifiers; pub use identifiers::{ - Identifiers, get_attribute_interpolate_instruction, get_class_map_interpolate_instruction, - get_interpolate_instruction, get_pipe_bind_instruction, get_property_interpolate_instruction, - get_pure_function_instruction, get_style_map_interpolate_instruction, - get_style_prop_interpolate_instruction, get_text_interpolate_instruction, + CORE, CORE_ALIAS, Identifiers, get_attribute_interpolate_instruction, + get_class_map_interpolate_instruction, get_interpolate_instruction, get_pipe_bind_instruction, + get_property_interpolate_instruction, get_pure_function_instruction, + get_style_map_interpolate_instruction, get_style_prop_interpolate_instruction, + get_text_interpolate_instruction, };