From 4c55a36a87adbcdcfa72a66f7e74f89ae83eba03 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Thu, 27 Aug 2026 22:15:17 +0200 Subject: [PATCH] refactor(compiler): deduplicate explicitly deferred types to prevent syntax errors When `@Component.deferredImports` maps blocks to arrays of dependencies, an explicitly deferred dependency might be defined in multiple blocks (e.g. `block1: [CmpA], block2: [CmpA]`). Previously, these were appended to the `explicitlyDeferredTypes` array without deduplication. When generating the `setClassMetadataAsync` wrapper for development mode, the compiler used this array to generate callback parameters for dynamic imports. This resulted in duplicate parameter names in the callback signature `(CmpA, CmpA) => { ... }`, which causes an `Uncaught SyntaxError` when V8 parses the module in strict mode. This commit deduplicates `explicitlyDeferredTypes` in the `ngtsc` component handler, and adds a secondary deduplication check in the `r3_class_metadata_compiler` generator to ensure duplicate parameter names are never emitted. --- .../annotations/component/src/handler.ts | 8 ++- .../compiler-cli/test/ngtsc/defer_spec.ts | 50 +++++++++++++++++++ .../src/render3/r3_class_metadata_compiler.ts | 10 +++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index d849aa5f3fe..bd63b44a77c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -1005,7 +1005,13 @@ export class ComponentDecoratorHandler implements DecoratorHandler< const blockDeps = this.collectExplicitlyDeferredSymbols(node, initializer); explicitlyDeferredTypesByBlock.set(blockName, blockDeps); explicitlyDeferredTypes ??= []; - explicitlyDeferredTypes.push(...blockDeps); + for (const dep of blockDeps) { + if ( + !explicitlyDeferredTypes.some((existing) => existing.symbolName === dep.symbolName) + ) { + explicitlyDeferredTypes.push(dep); + } + } } } } diff --git a/packages/compiler-cli/test/ngtsc/defer_spec.ts b/packages/compiler-cli/test/ngtsc/defer_spec.ts index a5a2dba20df..2fb31f4b0c7 100644 --- a/packages/compiler-cli/test/ngtsc/defer_spec.ts +++ b/packages/compiler-cli/test/ngtsc/defer_spec.ts @@ -2774,6 +2774,56 @@ runInEachFileSystem(() => { const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); + + it('should not duplicate explicitly deferred dependencies in generated setClassMetadataAsync', () => { + env.write( + 'dirs.ts', + ` + import { Directive } from '@angular/core'; + @Directive({ selector: '[dirA]' }) + export class DirA {} + `, + ); + + env.write( + '/test.ts', + ` + import { Component } from '@angular/core'; + import { DirA } from './dirs'; + + @Component({ + selector: 'test-cmp', + // @ts-ignore + deferredImports: { + blockA: [DirA], + blockB: [DirA], + }, + template: \` + @defer (name blockA) { +
+ } + @defer (name blockB) { +
+ } + \`, + }) + export class TestCmp {} + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Make sure we do not generate a duplicate parameter like (DirA, DirA) + expect(jsContents).not.toContain('DirA, DirA'); + + // Ensure that setClassMetadataAsync has exactly one parameter for DirA + expect(cleanNewLines(jsContents)).toContain( + 'i0.ɵsetClassMetadataAsync(TestCmp, ' + + '() => [/* @ts-ignore */ import("./dirs").then(m => m.DirA)], ' + + 'DirA => { i0.ɵsetClassMetadata(TestCmp', + ); + }); }); }); }); diff --git a/packages/compiler/src/render3/r3_class_metadata_compiler.ts b/packages/compiler/src/render3/r3_class_metadata_compiler.ts index 07632fb2e4a..ef7641ee404 100644 --- a/packages/compiler/src/render3/r3_class_metadata_compiler.ts +++ b/packages/compiler/src/render3/r3_class_metadata_compiler.ts @@ -85,10 +85,16 @@ export function compileComponentClassMetadata( return compileClassMetadata(metadata); } + const uniqueDeps = new Map(); + for (const dep of dependencies) { + uniqueDeps.set(dep.symbolName, dep); + } + const dedupedDependencies = Array.from(uniqueDeps.values()); + return internalCompileSetClassMetadataAsync( metadata, - dependencies.map((dep) => new o.FnParam(dep.symbolName, o.DYNAMIC_TYPE)), - compileComponentMetadataAsyncResolver(dependencies), + dedupedDependencies.map((dep) => new o.FnParam(dep.symbolName, o.DYNAMIC_TYPE)), + compileComponentMetadataAsyncResolver(dedupedDependencies), ); }