diff --git a/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts b/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts index a04f0449653..3bd544432ee 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts @@ -110,6 +110,13 @@ export class ImportGraph { continue; } + if (ts.isImportDeclaration(stmt) && stmt.importClause !== undefined && + stmt.importClause.isTypeOnly) { + // Exclude type-only imports as they are always elided, so they don't contribute to + // cycles. + continue; + } + const symbol = this.checker.getSymbolAtLocation(stmt.moduleSpecifier); if (symbol === undefined || symbol.valueDeclaration === undefined) { // No symbol could be found to skip over this import/export. diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts b/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts index 85b058dd8fc..29152580d69 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts @@ -70,6 +70,17 @@ runInEachFileSystem(() => { expect(cycle).toBeInstanceOf(Cycle); expect(importPath(cycle!.getPath())).toEqual('b,c,b'); }); + + it('should not consider type-only imports', () => { + const {program, analyzer} = makeAnalyzer('a:b,c!;b;c'); + const a = getSourceFileOrError(program, (_('/a.ts'))); + const b = getSourceFileOrError(program, (_('/b.ts'))); + const c = getSourceFileOrError(program, (_('/c.ts'))); + expect(analyzer.wouldCreateCycle(c, a)).toBe(null); + const cycle = analyzer.wouldCreateCycle(b, a); + expect(cycle).toBeInstanceOf(Cycle); + expect(importPath(cycle!.getPath())).toEqual('b,a,b'); + }); }); function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} { diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/util.ts b/packages/compiler-cli/src/ngtsc/cycles/test/util.ts index c4a8ca4ed42..67868b936e7 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/util.ts @@ -30,6 +30,8 @@ import {makeProgram} from '../../testing'; * "a:*b,c;b;c" * * represents a program where a.ts exports from b.ts and imports from c.ts. + * + * An import can be suffixed with ! to make it a type-only import. */ export function makeProgramFromGraph(fs: PathManipulation, graph: string): { program: ts.Program, @@ -43,6 +45,9 @@ export function makeProgramFromGraph(fs: PathManipulation, graph: string): { if (i.startsWith('*')) { const sym = i.substr(1); return `export {${sym}} from './${sym}';`; + } else if (i.endsWith('!')) { + const sym = i.substr(0, i.length - 1); + return `import type {${sym}} from './${sym}';`; } else { return `import {${i}} from './${i}';`; } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 494b6045de7..d8849461747 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -4836,6 +4836,41 @@ function allTests(os: string) { expect(jsContents).not.toContain('setComponentScope'); }); + it('should not consider type-only imports during cycle detection', () => { + env.write('test.ts', ` + import {NgModule} from '@angular/core'; + import {ACmp} from './a'; + import {BCmp} from './b'; + + @NgModule({declarations: [ACmp, BCmp]}) + export class Module {} + `); + env.write('a.ts', ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'a-cmp', + template: '', + }) + export class ACmp {} + `); + env.write('b.ts', ` + import {Component} from '@angular/core'; + import type {ACmp} from './a'; + + @Component({ + selector: 'b-cmp', + template: 'does not use a-cmp', + }) + export class BCmp { + a: ACmp; + } + `); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).not.toContain('setComponentScope'); + }); + it('should only pass components actually used to setComponentScope', () => { env.write('test.ts', ` import {Component, NgModule} from '@angular/core';