diff --git a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts index 52e2f14154f..3295f94fc84 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts @@ -44,12 +44,19 @@ export class DeferredSymbolTracker { throw new Error(`Provided import declaration doesn't have any symbols.`); } + // If the entire import is a type-only import, none of the symbols can be eager. + if (importDecl.importClause.isTypeOnly) { + return symbolMap; + } + if (importDecl.importClause.namedBindings !== undefined) { const bindings = importDecl.importClause.namedBindings; if (ts.isNamedImports(bindings)) { // Case 1: `import {a, b as B} from 'a'` for (const element of bindings.elements) { - symbolMap.set(element.name.text, AssumeEager); + if (!element.isTypeOnly) { + symbolMap.set(element.name.text, AssumeEager); + } } } else { // Case 2: `import X from 'a'` diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 20e49bc5430..bca4e1ddf5e 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -9107,6 +9107,230 @@ function allTests(os: string) { // via dynamic imports and an original import can be removed. expect(jsContents).not.toContain('import { CmpA }'); }); + + it('should drop imports when one is deferrable and the rest are type-only imports', () => { + env.write('cmp-a.ts', ` + import { Component } from '@angular/core'; + + export class Foo {} + + @Component({ + standalone: true, + selector: 'cmp-a', + template: 'CmpA!' + }) + export class CmpA {} + `); + + env.write('/test.ts', ` + import { Component } from '@angular/core'; + import { CmpA, type Foo } from './cmp-a'; + + export const foo: Foo = {}; + + @Component({ + selector: 'test-cmp', + standalone: true, + imports: [CmpA], + template: \` + @defer { + + } + \`, + }) + export class TestCmp {} + `); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); + expect(jsContents).not.toContain('import { CmpA }'); + }); + + it('should drop multiple imports to the same file when one is deferrable and the other has a single type-only element', + () => { + env.write('cmp-a.ts', ` + import { Component } from '@angular/core'; + + export class Foo {} + + @Component({ + standalone: true, + selector: 'cmp-a', + template: 'CmpA!' + }) + export class CmpA {} + `); + + env.write('/test.ts', ` + import { Component } from '@angular/core'; + import { CmpA } from './cmp-a'; + import { type Foo } from './cmp-a'; + + export const foo: Foo = {}; + + @Component({ + selector: 'test-cmp', + standalone: true, + imports: [CmpA], + template: \` + @defer { + + } + \`, + }) + export class TestCmp {} + `); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); + expect(jsContents).not.toContain('import { CmpA }'); + }); + + it('should drop multiple imports to the same file when one is deferrable and the other is type-only at the declaration level', + () => { + env.write('cmp-a.ts', ` + import { Component } from '@angular/core'; + + export class Foo {} + + @Component({ + standalone: true, + selector: 'cmp-a', + template: 'CmpA!' + }) + export class CmpA {} + `); + + env.write('/test.ts', ` + import { Component } from '@angular/core'; + import { CmpA } from './cmp-a'; + import type { Foo, CmpA as CmpAlias } from './cmp-a'; + + export const foo: Foo|CmpAlias = {}; + + @Component({ + selector: 'test-cmp', + standalone: true, + imports: [CmpA], + template: \` + @defer { + + } + \`, + }) + export class TestCmp {} + `); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); + expect(jsContents).not.toContain('import { CmpA }'); + }); + + it('should drop multiple imports to the same file when one is deferrable and the other is a type-only import of all symbols', + () => { + env.write('cmp-a.ts', ` + import { Component } from '@angular/core'; + + export class Foo {} + + @Component({ + standalone: true, + selector: 'cmp-a', + template: 'CmpA!' + }) + export class CmpA {} + `); + + env.write('/test.ts', ` + import { Component } from '@angular/core'; + import { CmpA } from './cmp-a'; + import type * as allCmpA from './cmp-a'; + + export const foo: allCmpA.Foo|allCmpA.CmpA = {}; + + @Component({ + selector: 'test-cmp', + standalone: true, + imports: [CmpA], + template: \` + @defer { + + } + \`, + }) + export class TestCmp {} + `); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); + expect(jsContents).not.toContain('import { CmpA }'); + }); + + it('should drop multiple imports of deferrable symbols from the same file', () => { + env.write('cmps.ts', ` + import { Component } from '@angular/core'; + + @Component({ + standalone: true, + selector: 'cmp-a', + template: 'CmpA!' + }) + export class CmpA {} + + @Component({ + standalone: true, + selector: 'cmp-b', + template: 'CmpB!' + }) + export class CmpB {} + `); + + env.write('/test.ts', ` + import { Component } from '@angular/core'; + import { CmpA } from './cmps'; + import { CmpB } from './cmps'; + + @Component({ + selector: 'test-cmp', + standalone: true, + imports: [CmpA, CmpB], + template: \` + @defer { + + + } + \`, + }) + export class TestCmp {} + `); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents) + .toContain( + '() => [import("./cmps").then(m => m.CmpA), import("./cmps").then(m => m.CmpB)]'); + expect(jsContents).not.toContain('import { CmpA }'); + expect(jsContents).not.toContain('import { CmpB }'); + }); }); it('should detect pipe used in the `when` trigger as an eager dependency', () => { @@ -9327,7 +9551,7 @@ function allTests(os: string) { })); env.write(`test.ts`, ` import {Component} from '@angular/core'; - + @Component({ template: '...', }) @@ -9350,7 +9574,7 @@ function allTests(os: string) { })); env.write(`test.ts`, ` import {Component} from '@angular/core'; - + @Component({ standalone: true, template: '...', @@ -9368,7 +9592,7 @@ function allTests(os: string) { () => { env.write(`test.ts`, ` import {Component} from '@angular/core'; - + @Component({ template: '...', })