Skip to content

Commit

Permalink
fix(core): deduplicate imports of standalone components in JIT compil…
Browse files Browse the repository at this point in the history
…er (#46439)

During JIT compilation of standalone components the compiler did not deduplicate
declarations in the imports array, unlike the AOT compiler. This may result in
runtime errors during directive matching, when the same component is found
multiple times resulting in NG0300, for example:

> NG0300: Multiple components match node with tagname mat-form-field: MatFormField and MatFormField.

This commit fixes the issue by deduplicating imports in the JIT compiler.

Relates to #46109 (comment)

Closes #46109

PR Close #46439
  • Loading branch information
JoostK authored and AndrewKushnir committed Jun 21, 2022
1 parent 3dd7bb3 commit c086653
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 2 deletions.
23 changes: 21 additions & 2 deletions packages/core/src/render3/jit/directive.ts
Expand Up @@ -246,16 +246,23 @@ function getStandaloneDefFunctions(type: Type<any>, imports: Type<any>[]): {
// Standalone components are always able to self-reference, so include the component's own
// definition in its `directiveDefs`.
cachedDirectiveDefs = [getComponentDef(type)!];
const seen = new Set<Type<unknown>>();

for (const rawDep of imports) {
ngDevMode && verifyStandaloneImport(rawDep, type);

const dep = resolveForwardRef(rawDep);
if (seen.has(dep)) {
continue;
}
seen.add(dep);

if (!!getNgModuleDef(dep)) {
const scope = transitiveScopesFor(dep);
for (const dir of scope.exported.directives) {
const def = getComponentDef(dir) || getDirectiveDef(dir);
if (def) {
if (def && !seen.has(dir)) {
seen.add(dir);
cachedDirectiveDefs.push(def);
}
}
Expand All @@ -273,12 +280,24 @@ function getStandaloneDefFunctions(type: Type<any>, imports: Type<any>[]): {
const pipeDefs = () => {
if (cachedPipeDefs === null) {
cachedPipeDefs = [];
const seen = new Set<Type<unknown>>();

for (const rawDep of imports) {
const dep = resolveForwardRef(rawDep);
if (seen.has(dep)) {
continue;
}
seen.add(dep);

if (!!getNgModuleDef(dep)) {
const scope = transitiveScopesFor(dep);
cachedPipeDefs.push(...Array.from(scope.exported.pipes).map(pipe => getPipeDef(pipe)!));
for (const pipe of scope.exported.pipes) {
const def = getPipeDef(pipe);
if (def && !seen.has(pipe)) {
seen.add(pipe);
cachedPipeDefs.push(def);
}
}
} else {
const def = getPipeDef(dep);
if (def) {
Expand Down
36 changes: 36 additions & 0 deletions packages/core/test/acceptance/standalone_spec.ts
Expand Up @@ -453,6 +453,42 @@ describe('standalone components, directives and pipes', () => {
expect(fixture.nativeElement.innerHTML).toBe('<div red="true">blue</div>');
});

it('should deduplicate declarations', () => {
@Component({selector: 'test-red', standalone: true, template: 'red(<ng-content></ng-content>)'})
class RedComponent {
}

@Component({selector: 'test-blue', template: 'blue(<ng-content></ng-content>)'})
class BlueComponent {
}

@NgModule({declarations: [BlueComponent], exports: [BlueComponent]})
class BlueModule {
}

@NgModule({exports: [BlueModule]})
class BlueAModule {
}

@NgModule({exports: [BlueModule]})
class BlueBModule {
}

@Component({
selector: 'standalone',
standalone: true,
template: `<test-red><test-blue>orange</test-blue></test-red>`,
imports: [RedComponent, RedComponent, BlueAModule, BlueBModule],
})
class TestComponent {
}

const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toBe('<test-red>red(<test-blue>blue(orange)</test-blue>)</test-red>');
});

it('should error when forwardRef does not resolve to a truthy value', () => {
@Component({
selector: 'test',
Expand Down

0 comments on commit c086653

Please sign in to comment.