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.
This commit is contained in:
Matthieu Riegler
2026-08-27 22:15:17 +02:00
committed by Matthew Beck
parent ad22e89d4d
commit 4c55a36a87
3 changed files with 65 additions and 3 deletions
@@ -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);
}
}
}
}
}
@@ -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) {
<div dirA></div>
}
@defer (name blockB) {
<div dirA></div>
}
\`,
})
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',
);
});
});
});
});
@@ -85,10 +85,16 @@ export function compileComponentClassMetadata(
return compileClassMetadata(metadata);
}
const uniqueDeps = new Map<string, R3DeferPerComponentDependency>();
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),
);
}