Skip to content

Commit a8f351b

Browse files
authored
fix(compiler): dedupe generated i0 namespace imports against all file identifiers (#558)
* fix(compiler): dedupe generated i0 namespace imports against all file identifiers 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 * style: oxfmt dts plugin files * fix(vite-plugin): rebase relative dts imports to the output path 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. * fix(vite-plugin): emit dts relative specifiers verbatim 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. * refactor(vite-plugin): move .d.ts injection to Rust 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. * fix(vite-plugin): keep generated imports out of same-line class bodies 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. * fix(vite-plugin): infer unmapped i0 aliases as @angular/core Codex review: when a caller omits namespaceImports, an unmapped non-ɵ head like i0.Signal<number> 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. * refactor(compiler): centralize the i0 / @angular/core literals 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).
1 parent 3a05105 commit a8f351b

69 files changed

Lines changed: 3681 additions & 900 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/oxc_angular_compiler/src/class_debug_info/compiler.rs‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ use crate::r3::Identifiers;
3333
/// Ported from Angular's `compileClassDebugInfo` function.
3434
pub fn compile_class_debug_info<'a>(
3535
allocator: &'a Allocator,
36+
core_namespace: &'a str,
3637
debug_info: &R3ClassDebugInfo<'a>,
3738
) -> OutputExpression<'a> {
38-
let fn_call = internal_compile_class_debug_info(allocator, debug_info);
39+
let fn_call = internal_compile_class_debug_info(allocator, core_namespace, debug_info);
3940
let guarded = dev_only_guarded_expression(allocator, fn_call);
4041
let stmt = expr_stmt(allocator, guarded);
4142
create_arrow_iife(allocator, stmt)
@@ -44,9 +45,10 @@ pub fn compile_class_debug_info<'a>(
4445
/// Compiles the internal `setClassDebugInfo` call without wrappers.
4546
fn internal_compile_class_debug_info<'a>(
4647
allocator: &'a Allocator,
48+
core_namespace: &'a str,
4749
debug_info: &R3ClassDebugInfo<'a>,
4850
) -> OutputExpression<'a> {
49-
let import = import_expr(allocator, Identifiers::SET_CLASS_DEBUG_INFO);
51+
let import = import_expr(allocator, core_namespace, Identifiers::SET_CLASS_DEBUG_INFO);
5052

5153
// Build the debug info object
5254
// Always include className
@@ -161,13 +163,17 @@ fn dev_only_guarded_expression<'a>(
161163
))
162164
}
163165

164-
/// Creates an import expression: i0.identifier
165-
fn import_expr<'a>(allocator: &'a Allocator, identifier: &'static str) -> OutputExpression<'a> {
166+
/// Creates an import expression: ns.identifier
167+
fn import_expr<'a>(
168+
allocator: &'a Allocator,
169+
core_namespace: &'a str,
170+
identifier: &'static str,
171+
) -> OutputExpression<'a> {
166172
OutputExpression::ReadProp(Box::new_in(
167173
ReadPropExpr {
168174
receiver: Box::new_in(
169175
OutputExpression::ReadVar(Box::new_in(
170-
ReadVarExpr { name: Ident::from("i0"), source_span: None },
176+
ReadVarExpr { name: Ident::from(core_namespace), source_span: None },
171177
&allocator,
172178
)),
173179
&allocator,

‎crates/oxc_angular_compiler/src/class_metadata/builders.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ fn build_core_decorator<'a>(
803803
decorator_name: &'static str,
804804
args: AllocVec<'a, OutputExpression<'a>>,
805805
) -> OutputExpression<'a> {
806-
let core_namespace = namespace_registry.get_or_assign(&Ident::from("@angular/core"));
806+
let core_namespace = namespace_registry.get_or_assign(&Ident::from(crate::r3::CORE));
807807
let type_expr = OutputExpression::ReadProp(Box::new_in(
808808
ReadPropExpr {
809809
receiver: Box::new_in(

‎crates/oxc_angular_compiler/src/class_metadata/compiler.rs‎

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ use crate::r3::Identifiers;
3131
/// Ported from Angular's `compileClassMetadata` function.
3232
pub fn compile_class_metadata<'a>(
3333
allocator: &'a Allocator,
34+
core_namespace: &'a str,
3435
metadata: &R3ClassMetadata<'a>,
3536
) -> OutputExpression<'a> {
36-
let fn_call = internal_compile_class_metadata(allocator, metadata);
37+
let fn_call = internal_compile_class_metadata(allocator, core_namespace, metadata);
3738
let guarded = dev_only_guarded_expression(allocator, fn_call);
3839
let stmt = expr_stmt(allocator, guarded);
3940
let arrow = create_arrow_iife(allocator, stmt);
@@ -48,13 +49,14 @@ pub fn compile_class_metadata<'a>(
4849
/// Ported from Angular's `compileComponentClassMetadata` function.
4950
pub fn compile_component_class_metadata<'a>(
5051
allocator: &'a Allocator,
52+
core_namespace: &'a str,
5153
metadata: &R3ClassMetadata<'a>,
5254
dependencies: Option<&[R3DeferPerComponentDependency<'a>]>,
5355
) -> OutputExpression<'a> {
5456
match dependencies {
5557
None | Some([]) => {
5658
// No deferred dependencies - use regular setClassMetadata
57-
compile_class_metadata(allocator, metadata)
59+
compile_class_metadata(allocator, core_namespace, metadata)
5860
}
5961
Some(deps) => {
6062
// Has deferred dependencies - use setClassMetadataAsync
@@ -63,7 +65,13 @@ pub fn compile_component_class_metadata<'a>(
6365
params.push(FnParam { name: dep.param_name.clone() });
6466
}
6567
let resolver = compile_component_metadata_async_resolver(allocator, deps);
66-
internal_compile_set_class_metadata_async(allocator, metadata, params, resolver)
68+
internal_compile_set_class_metadata_async(
69+
allocator,
70+
core_namespace,
71+
metadata,
72+
params,
73+
resolver,
74+
)
6775
}
6876
}
6977
}
@@ -75,6 +83,7 @@ pub fn compile_component_class_metadata<'a>(
7583
/// Ported from Angular's `compileOpaqueAsyncClassMetadata` function.
7684
pub fn compile_opaque_async_class_metadata<'a>(
7785
allocator: &'a Allocator,
86+
core_namespace: &'a str,
7887
metadata: &R3ClassMetadata<'a>,
7988
defer_resolver: OutputExpression<'a>,
8089
deferred_dependency_names: &[Ident<'a>],
@@ -83,7 +92,13 @@ pub fn compile_opaque_async_class_metadata<'a>(
8392
for name in deferred_dependency_names {
8493
params.push(FnParam { name: name.clone() });
8594
}
86-
internal_compile_set_class_metadata_async(allocator, metadata, params, defer_resolver)
95+
internal_compile_set_class_metadata_async(
96+
allocator,
97+
core_namespace,
98+
metadata,
99+
params,
100+
defer_resolver,
101+
)
87102
}
88103

89104
/// Compiles the dependency resolver function for `setClassMetadataAsync`.
@@ -197,9 +212,10 @@ pub fn compile_component_metadata_async_resolver<'a>(
197212
/// Compiles the internal `setClassMetadata` call without wrappers.
198213
fn internal_compile_class_metadata<'a>(
199214
allocator: &'a Allocator,
215+
core_namespace: &'a str,
200216
metadata: &R3ClassMetadata<'a>,
201217
) -> OutputExpression<'a> {
202-
let import = import_expr(allocator, Identifiers::SET_CLASS_METADATA);
218+
let import = import_expr(allocator, core_namespace, Identifiers::SET_CLASS_METADATA);
203219

204220
let mut args = Vec::new_in(&allocator);
205221
args.push(metadata.r#type.clone_in(allocator));
@@ -234,12 +250,14 @@ fn internal_compile_class_metadata<'a>(
234250
/// Compiles `setClassMetadataAsync` with wrapper params and resolver.
235251
fn internal_compile_set_class_metadata_async<'a>(
236252
allocator: &'a Allocator,
253+
core_namespace: &'a str,
237254
metadata: &R3ClassMetadata<'a>,
238255
wrapper_params: Vec<'a, FnParam<'a>>,
239256
dependency_resolver_fn: OutputExpression<'a>,
240257
) -> OutputExpression<'a> {
241258
// Create the inner setClassMetadata call
242-
let set_class_metadata_call = internal_compile_class_metadata(allocator, metadata);
259+
let set_class_metadata_call =
260+
internal_compile_class_metadata(allocator, core_namespace, metadata);
243261

244262
// Create wrapper: (deps...) => { setClassMetadata(...); }
245263
let mut wrapper_stmts = Vec::new_in(&allocator);
@@ -255,7 +273,7 @@ fn internal_compile_set_class_metadata_async<'a>(
255273
));
256274

257275
// Create: setClassMetadataAsync(type, resolver, wrapper)
258-
let import = import_expr(allocator, Identifiers::SET_CLASS_METADATA_ASYNC);
276+
let import = import_expr(allocator, core_namespace, Identifiers::SET_CLASS_METADATA_ASYNC);
259277

260278
let mut args = Vec::new_in(&allocator);
261279
args.push(metadata.r#type.clone_in(allocator));
@@ -343,13 +361,17 @@ fn guarded_expression<'a>(
343361
))
344362
}
345363

346-
/// Creates an import expression: i0.identifier
347-
fn import_expr<'a>(allocator: &'a Allocator, identifier: &'static str) -> OutputExpression<'a> {
364+
/// Creates an import expression: ns.identifier
365+
fn import_expr<'a>(
366+
allocator: &'a Allocator,
367+
core_namespace: &'a str,
368+
identifier: &'static str,
369+
) -> OutputExpression<'a> {
348370
OutputExpression::ReadProp(Box::new_in(
349371
ReadPropExpr {
350372
receiver: Box::new_in(
351373
OutputExpression::ReadVar(Box::new_in(
352-
ReadVarExpr { name: Ident::from("i0"), source_span: None },
374+
ReadVarExpr { name: Ident::from(core_namespace), source_span: None },
353375
&allocator,
354376
)),
355377
&allocator,

0 commit comments

Comments
 (0)