Skip to content

Commit a524a50

Browse files
alxhubthePunderWoman
authored andcommitted
fix(compiler-cli): handle standalone components with cycles (#46029)
The Angular compiler performs cycle detection when generating imports within component files. This was previously necessary as reifying dependencies discovered via NgModules into the component output could add imports that weren't present in the original component and potentially create cycles. Doing this could cause order-of-execution issues with existing user imports, so the compiler detects this case and falls back to an alternative way of specifying component dependencies that doesn't risk creating cycles. For standalone components, Angular does not need to add new imports to the component file as the user has already explicitly referenced dependencies in the `@Component.imports`. As a result, the cycle detection can be skipped. Correctly authoring a program with import cycles is always challenging. One side of a cyclic import will always initially evaluate to `undefined`, and this can result in errors in the component definition when this happens within component `imports`. Our compiler _could_ detect the cycle and choose to wrap the component dependencies in an automatic closure instead, avoiding any issues with `undefined` during an eager evaluation. However, this commit makes an active choice not to do that as it only serves to mask the problems with cyclic imports. Future refactorings may cause the "other half" of the cycle to break. Users should instead be aware of the potential problems with cycles and explicitly defer evaluations with `forwardRef` where needed. This ensures that future implementations of Angular compilation which may not be able to automatically detect import cycles and correct accordingly can still compile such components. PR Close #46029
1 parent 37dbe55 commit a524a50

File tree

2 files changed

+58
-14
lines changed

2 files changed

+58
-14
lines changed

packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -717,20 +717,26 @@ export class ComponentDecoratorHandler implements
717717
symbol.usedPipes = declarations.filter(isUsedPipe).map(getSemanticReference);
718718
}
719719

720-
// Scan through the directives/pipes actually used in the template and check whether any
721-
// import which needs to be generated would create a cycle.
722720
const cyclesFromDirectives = new Map<UsedDirective, Cycle>();
723721
const cyclesFromPipes = new Map<UsedPipe, Cycle>();
724-
for (const usedDep of declarations) {
725-
const cycle = this._checkForCyclicImport(usedDep.importedFile, usedDep.type, context);
726-
if (cycle !== null) {
727-
switch (usedDep.kind) {
728-
case R3TemplateDependencyKind.Directive:
729-
cyclesFromDirectives.set(usedDep, cycle);
730-
break;
731-
case R3TemplateDependencyKind.Pipe:
732-
cyclesFromPipes.set(usedDep, cycle);
733-
break;
722+
723+
// Scan through the directives/pipes actually used in the template and check whether any
724+
// import which needs to be generated would create a cycle. This check is skipped for
725+
// standalone components as the dependencies of a standalone component have already been
726+
// imported directly by the user, so Angular won't introduce any imports that aren't already
727+
// in the user's program.
728+
if (!metadata.isStandalone) {
729+
for (const usedDep of declarations) {
730+
const cycle = this._checkForCyclicImport(usedDep.importedFile, usedDep.type, context);
731+
if (cycle !== null) {
732+
switch (usedDep.kind) {
733+
case R3TemplateDependencyKind.Directive:
734+
cyclesFromDirectives.set(usedDep, cycle);
735+
break;
736+
case R3TemplateDependencyKind.Pipe:
737+
cyclesFromPipes.set(usedDep, cycle);
738+
break;
739+
}
734740
}
735741
}
736742
}
@@ -740,7 +746,7 @@ export class ComponentDecoratorHandler implements
740746
// No cycle was detected. Record the imports that need to be created in the cycle detector
741747
// so that future cyclic import checks consider their production.
742748
for (const {type, importedFile} of declarations) {
743-
this._recordSyntheticImport(importedFile, type, context);
749+
this.maybeRecordSyntheticImport(importedFile, type, context);
744750
}
745751

746752
// Check whether the dependencies arrays in ɵcmp need to be wrapped in a closure.
@@ -932,7 +938,7 @@ export class ComponentDecoratorHandler implements
932938
return this.cycleAnalyzer.wouldCreateCycle(origin, imported);
933939
}
934940

935-
private _recordSyntheticImport(
941+
private maybeRecordSyntheticImport(
936942
importedFile: ImportedFile, expr: Expression, origin: ts.SourceFile): void {
937943
const imported = resolveImportedFile(this.moduleResolver, importedFile, expr, origin);
938944
if (imported === null) {

packages/compiler-cli/test/ngtsc/standalone_spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,44 @@ runInEachFileSystem(() => {
9393
expect(dtsCode).toContain('i0.ɵɵPipeDeclaration<TestPipe, "test", true>');
9494
});
9595

96+
it('should compile a standalone component even in the presence of cycles', () => {
97+
env.write('dep.ts', `
98+
import {Directive, Input} from '@angular/core';
99+
100+
// This import creates a cycle, since 'test.ts' imports 'dir.ts'.
101+
import {TestType} from './test';
102+
103+
@Directive({
104+
standalone: true,
105+
selector: '[dir]',
106+
})
107+
export class TestDir {
108+
@Input() value?: TestType;
109+
}
110+
`);
111+
env.write('test.ts', `
112+
import {Component} from '@angular/core';
113+
import {TestDir} from './dep';
114+
115+
export interface TestType {
116+
value: string;
117+
}
118+
119+
@Component({
120+
standalone: true,
121+
imports: [TestDir],
122+
selector: 'test-cmp',
123+
template: '<div dir></div>',
124+
})
125+
export class TestCmp {}
126+
`);
127+
128+
env.driveMain();
129+
130+
const jsContents = env.getContents('test.js');
131+
expect(jsContents).toContain('dependencies: [i1.TestDir]');
132+
});
133+
96134
it('should error when a non-standalone component tries to use imports', () => {
97135
env.write('test.ts', `
98136
import {Component, Directive} from '@angular/core';

0 commit comments

Comments
 (0)