diff --git a/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts b/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts
index d1448e8c5a268..4e38f058af932 100644
--- a/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts
@@ -1146,5 +1146,218 @@ runInEachFileSystem(() => {
'inputs: { x: [i0.ɵɵInputFlags.HasDecoratorInputTransform, "x", "x", v => v + \'TRANSFORMED!\'] }');
});
});
+
+ describe('@defer', () => {
+ it('should handle `@Component.deferredImports` field', () => {
+ env.write('deferred-a.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-a',
+ template: 'DeferredCmpA contents',
+ })
+ export class DeferredCmpA {
+ }
+ `);
+
+ env.write('deferred-b.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-b',
+ template: 'DeferredCmpB contents',
+ })
+ export class DeferredCmpB {
+ }
+ `);
+
+ env.write('test.ts', `
+ import {Component} from '@angular/core';
+ import {DeferredCmpA} from './deferred-a';
+ import {DeferredCmpB} from './deferred-b';
+ @Component({
+ standalone: true,
+ deferredImports: [DeferredCmpA, DeferredCmpB],
+ template: \`
+ @defer {
+
+ }
+ @defer {
+
+ }
+ \`,
+ })
+ export class AppCmp {
+ }
+ `);
+
+ env.driveMain();
+ const jsContents = env.getContents('test.js');
+
+ // Expect that all deferrableImports in local compilation mode
+ // are located in a single function (since we can't detect in
+ // the local mode which components belong to which block).
+ expect(jsContents)
+ .toContain(
+ 'const AppCmp_DeferFn = () => [' +
+ 'import("./deferred-a").then(m => m.DeferredCmpA), ' +
+ 'import("./deferred-b").then(m => m.DeferredCmpB)];');
+
+ // Make sure there are no eager imports present in the output.
+ expect(jsContents).not.toContain(`from './deferred-a'`);
+ expect(jsContents).not.toContain(`from './deferred-b'`);
+
+ // All defer instructions use the same dependency function.
+ expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_DeferFn);');
+ expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_DeferFn);');
+
+ // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
+ expect(jsContents)
+ .toContain(
+ 'ɵsetClassMetadataAsync(AppCmp, () => [' +
+ 'import("./deferred-a").then(m => m.DeferredCmpA), ' +
+ 'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
+ '(DeferredCmpA, DeferredCmpB) => {');
+ });
+
+ it('should handle `@Component.imports` field', () => {
+ env.write('deferred-a.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-a',
+ template: 'DeferredCmpA contents',
+ })
+ export class DeferredCmpA {
+ }
+ `);
+
+ env.write('test.ts', `
+ import {Component} from '@angular/core';
+ import {DeferredCmpA} from './deferred-a';
+ @Component({
+ standalone: true,
+ imports: [DeferredCmpA],
+ template: \`
+ @defer {
+
+ }
+ \`,
+ })
+ export class AppCmp {
+ }
+ `);
+
+ env.driveMain();
+ const jsContents = env.getContents('test.js');
+
+ // In local compilation mode we can't detect which components
+ // belong to `@defer` blocks, thus can't determine whether corresponding
+ // classes can be defer-loaded. In this case we retain eager imports
+ // and do not generate defer dependency functions for `@defer` instructions.
+
+ // Eager imports are retained in the output.
+ expect(jsContents).toContain(`from './deferred-a'`);
+
+ // Defer instructions do not have a dependency function,
+ // since all dependencies were defined in `@Component.imports`.
+ expect(jsContents).toContain('ɵɵdefer(1, 0);');
+
+ // Expect `ɵsetClassMetadata` (sync) to be generated.
+ expect(jsContents).toContain('ɵsetClassMetadata(AppCmp,');
+ });
+
+ it('should handle defer blocks that rely on deps from `deferredImports` and `imports`',
+ () => {
+ env.write('eager-a.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'eager-cmp-a',
+ template: 'EagerCmpA contents',
+ })
+ export class EagerCmpA {
+ }
+ `);
+
+ env.write('deferred-a.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-a',
+ template: 'DeferredCmpA contents',
+ })
+ export class DeferredCmpA {
+ }
+ `);
+
+ env.write('deferred-b.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-b',
+ template: 'DeferredCmpB contents',
+ })
+ export class DeferredCmpB {
+ }
+ `);
+
+ env.write('test.ts', `
+ import {Component} from '@angular/core';
+ import {DeferredCmpA} from './deferred-a';
+ import {DeferredCmpB} from './deferred-b';
+ import {EagerCmpA} from './eager-a';
+ @Component({
+ standalone: true,
+ imports: [EagerCmpA],
+ deferredImports: [DeferredCmpA, DeferredCmpB],
+ template: \`
+ @defer {
+
+
+ }
+ @defer {
+
+
+ }
+ \`,
+ })
+ export class AppCmp {
+ }
+ `);
+
+ env.driveMain();
+ const jsContents = env.getContents('test.js');
+
+ // Expect that all deferrableImports in local compilation mode
+ // are located in a single function (since we can't detect in
+ // the local mode which components belong to which block).
+ // Eager dependencies are **not* included here.
+ expect(jsContents)
+ .toContain(
+ 'const AppCmp_DeferFn = () => [' +
+ 'import("./deferred-a").then(m => m.DeferredCmpA), ' +
+ 'import("./deferred-b").then(m => m.DeferredCmpB)];');
+
+ // Make sure there are no eager imports present in the output.
+ expect(jsContents).not.toContain(`from './deferred-a'`);
+ expect(jsContents).not.toContain(`from './deferred-b'`);
+
+ // Eager dependencies retain their imports.
+ expect(jsContents).toContain(`from './eager-a';`);
+
+ // All defer instructions use the same dependency function.
+ expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_DeferFn);');
+ expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_DeferFn);');
+
+ // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
+ expect(jsContents)
+ .toContain(
+ 'ɵsetClassMetadataAsync(AppCmp, () => [' +
+ 'import("./deferred-a").then(m => m.DeferredCmpA), ' +
+ 'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
+ '(DeferredCmpA, DeferredCmpB) => {');
+ });
+ });
});
});
diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
index 2f49ee6505afd..fbaac611217fa 100644
--- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
@@ -9544,6 +9544,358 @@ function allTests(os: string) {
expect(jsContents).toContain('dependencies: [TestPipe]');
});
+ describe('@Component.deferredImports', () => {
+ beforeEach(() => {
+ env.tsconfig({onlyExplicitDeferDependencyImports: true});
+ });
+
+ it('should handle `@Component.deferredImports` field', () => {
+ env.write('deferred-a.ts', `
+ import {Component} from '@angular/core';
+
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-a',
+ template: 'DeferredCmpA contents',
+ })
+ export class DeferredCmpA {
+ }
+ `);
+
+ env.write('deferred-b.ts', `
+ import {Component} from '@angular/core';
+
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-b',
+ template: 'DeferredCmpB contents',
+ })
+ export class DeferredCmpB {
+ }
+ `);
+
+ env.write('test.ts', `
+ import {Component} from '@angular/core';
+ import {DeferredCmpA} from './deferred-a';
+ import {DeferredCmpB} from './deferred-b';
+
+ @Component({
+ standalone: true,
+ deferredImports: [DeferredCmpA, DeferredCmpB],
+ template: \`
+ @defer {
+
+ }
+ @defer {
+
+ }
+ \`,
+ })
+ export class AppCmp {
+ }
+ `);
+
+ env.driveMain();
+ const jsContents = env.getContents('test.js');
+
+ // Expect that all deferrableImports become dynamic imports.
+ expect(jsContents)
+ .toContain(
+ 'const AppCmp_Defer_1_DepsFn = () => [' +
+ 'import("./deferred-a").then(m => m.DeferredCmpA)];');
+ expect(jsContents)
+ .toContain(
+ 'const AppCmp_Defer_4_DepsFn = () => [' +
+ 'import("./deferred-b").then(m => m.DeferredCmpB)];');
+
+ // Make sure there are no eager imports present in the output.
+ expect(jsContents).not.toContain(`from './deferred-a'`);
+ expect(jsContents).not.toContain(`from './deferred-b'`);
+
+ // Defer instructions have different dependency functions in full mode.
+ expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_Defer_1_DepsFn);');
+ expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_Defer_4_DepsFn);');
+
+ // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
+ expect(jsContents)
+ .toContain(
+ 'ɵsetClassMetadataAsync(AppCmp, () => [' +
+ 'import("./deferred-a").then(m => m.DeferredCmpA), ' +
+ 'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
+ '(DeferredCmpA, DeferredCmpB) => {');
+ });
+
+ it('should handle defer blocks that rely on deps from `deferredImports` and `imports`',
+ () => {
+ env.write('eager-a.ts', `
+ import {Component} from '@angular/core';
+
+ @Component({
+ standalone: true,
+ selector: 'eager-cmp-a',
+ template: 'EagerCmpA contents',
+ })
+ export class EagerCmpA {
+ }
+ `);
+
+ env.write('deferred-a.ts', `
+ import {Component} from '@angular/core';
+
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-a',
+ template: 'DeferredCmpA contents',
+ })
+ export class DeferredCmpA {
+ }
+ `);
+
+ env.write('deferred-b.ts', `
+ import {Component} from '@angular/core';
+
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-b',
+ template: 'DeferredCmpB contents',
+ })
+ export class DeferredCmpB {
+ }
+ `);
+
+ env.write('test.ts', `
+ import {Component} from '@angular/core';
+ import {DeferredCmpA} from './deferred-a';
+ import {DeferredCmpB} from './deferred-b';
+ import {EagerCmpA} from './eager-a';
+
+ @Component({
+ standalone: true,
+ imports: [EagerCmpA],
+ deferredImports: [DeferredCmpA, DeferredCmpB],
+ template: \`
+ @defer {
+
+
+ }
+ @defer {
+
+
+ }
+ \`,
+ })
+ export class AppCmp {
+ }
+ `);
+
+ env.driveMain();
+ const jsContents = env.getContents('test.js');
+
+ // Expect that all deferrableImports to become dynamic imports.
+ // Other imported symbols remain eager.
+ expect(jsContents)
+ .toContain(
+ 'const AppCmp_Defer_1_DepsFn = () => [' +
+ 'import("./deferred-a").then(m => m.DeferredCmpA), ' +
+ 'EagerCmpA];');
+ expect(jsContents)
+ .toContain(
+ 'const AppCmp_Defer_4_DepsFn = () => [' +
+ 'import("./deferred-b").then(m => m.DeferredCmpB), ' +
+ 'EagerCmpA];');
+
+ // Make sure there are no eager imports present in the output.
+ expect(jsContents).not.toContain(`from './deferred-a'`);
+ expect(jsContents).not.toContain(`from './deferred-b'`);
+
+ // Eager dependencies retain their imports.
+ expect(jsContents).toContain(`from './eager-a';`);
+
+ // Defer blocks would have their own dependency functions in full mode.
+ expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_Defer_1_DepsFn);');
+ expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_Defer_4_DepsFn);');
+
+ // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
+ expect(jsContents)
+ .toContain(
+ 'ɵsetClassMetadataAsync(AppCmp, () => [' +
+ 'import("./deferred-a").then(m => m.DeferredCmpA), ' +
+ 'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
+ '(DeferredCmpA, DeferredCmpB) => {');
+ });
+
+ describe('error handling', () => {
+ it('should produce an error when unsupported type (@Injectable) is used in `deferredImports`',
+ () => {
+ env.write('test.ts', `
+ import {Component, Injectable} from '@angular/core';
+ @Injectable()
+ class MyInjectable {}
+ @Component({
+ standalone: true,
+ deferredImports: [MyInjectable],
+ template: '',
+ })
+ export class AppCmp {
+ }
+ `);
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT));
+ });
+
+ it('should produce an error when unsupported type (@NgModule) is used in `deferredImports`',
+ () => {
+ env.write('test.ts', `
+ import {Component, NgModule} from '@angular/core';
+ @NgModule()
+ class MyModule {}
+ @Component({
+ standalone: true,
+ deferredImports: [MyModule],
+ template: '',
+ })
+ export class AppCmp {
+ }
+ `);
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT));
+ });
+
+ it('should produce an error when components from `deferredImports` are used outside of defer blocks',
+ () => {
+ env.write('deferred-a.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-a',
+ template: 'DeferredCmpA contents',
+ })
+ export class DeferredCmpA {
+ }
+ `);
+
+ env.write('deferred-b.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-b',
+ template: 'DeferredCmpB contents',
+ })
+ export class DeferredCmpB {
+ }
+ `);
+
+ env.write('test.ts', `
+ import {Component} from '@angular/core';
+ import {DeferredCmpA} from './deferred-a';
+ import {DeferredCmpB} from './deferred-b';
+ @Component({
+ standalone: true,
+ deferredImports: [DeferredCmpA, DeferredCmpB],
+ template: \`
+
+ @defer {
+
+ }
+ \`,
+ })
+ export class AppCmp {
+ }
+ `);
+
+ const diags = env.driveDiagnostics();
+
+ expect(diags.length).toBe(1);
+ expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY));
+ });
+
+ it('should produce an error the same component is referenced in both `deferredImports` and `imports`',
+ () => {
+ env.write('deferred-a.ts', `
+ import {Component} from '@angular/core';
+ @Component({
+ standalone: true,
+ selector: 'deferred-cmp-a',
+ template: 'DeferredCmpA contents',
+ })
+ export class DeferredCmpA {
+ }
+ `);
+
+ env.write('test.ts', `
+ import {Component} from '@angular/core';
+ import {DeferredCmpA} from './deferred-a';
+ @Component({
+ standalone: true,
+ deferredImports: [DeferredCmpA],
+ imports: [DeferredCmpA],
+ template: \`
+ @defer {
+
+ }
+ \`,
+ })
+ export class AppCmp {}
+ `);
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].code)
+ .toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY));
+ });
+
+ it('should produce an error when pipes from `deferredImports` are used outside of defer blocks',
+ () => {
+ env.write('deferred-pipe-a.ts', `
+ import {Pipe} from '@angular/core';
+ @Pipe({
+ standalone: true,
+ name: 'deferredPipeA'
+ })
+ export class DeferredPipeA {
+ transform() {}
+ }
+ `);
+
+ env.write('deferred-pipe-b.ts', `
+ import {Pipe} from '@angular/core';
+ @Pipe({
+ standalone: true,
+ name: 'deferredPipeB'
+ })
+ export class DeferredPipeB {
+ transform() {}
+ }
+ `);
+
+ env.write('test.ts', `
+ import {Component} from '@angular/core';
+ import {DeferredPipeA} from './deferred-pipe-a';
+ import {DeferredPipeB} from './deferred-pipe-b';
+ @Component({
+ standalone: true,
+ deferredImports: [DeferredPipeA, DeferredPipeB],
+ template: \`
+ {{ 'Eager' | deferredPipeA }}
+ @defer {
+ {{ 'Deferred' | deferredPipeB }}
+ }
+ \`,
+ })
+ export class AppCmp {}
+ `);
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY));
+ });
+ });
+ });
+
describe('setClassMetadataAsync', () => {
it('should generate setClassMetadataAsync for components with defer blocks', () => {
env.write('cmp-a.ts', `