Skip to content

Commit

Permalink
WIP: support local compilation for @defer blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewKushnir committed Dec 7, 2023
1 parent 6cd91c6 commit 0a04292
Show file tree
Hide file tree
Showing 17 changed files with 706 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements

// Defer blocks are not yet fully supported in partial compilation.
deferrableDeclToImportDecl: new Map(),
deferrableTypes: new Map(),

encapsulation: metaObj.has('encapsulation') ?
parseEncapsulation(metaObj.getValue('encapsulation')) :
Expand Down
276 changes: 190 additions & 86 deletions packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {ParsedTemplateWithSource, StyleUrlMeta} from './resources';
*/
export type ComponentMetadataResolvedFields = SubsetOfKeys<
R3ComponentMetadata<R3TemplateDependencyMetadata>,
'declarations'|'declarationListEmitMode'|'deferBlocks'|'deferrableDeclToImportDecl'>;
'declarations'|'declarationListEmitMode'|'deferBlocks'|'deferrableDeclToImportDecl'|
'deferrableTypes'>;

export interface ComponentAnalysisData {
/**
Expand Down Expand Up @@ -70,6 +71,7 @@ export interface ComponentAnalysisData {

rawImports: ts.Expression|null;
resolvedImports: Reference<ClassDeclaration>[]|null;
rawDeferredImports: ts.Expression|null;

schemas: SchemaMetadata[]|null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const animationTriggerResolver: ForeignFunctionResolver =
return res;
};

// TODO: update to use correct field name, i.e. `imports` vs `deferredImports`
export function validateAndFlattenComponentImports(imports: ResolvedValue, expr: ts.Expression): {
imports: Reference<ClassDeclaration>[],
diagnostics: ts.Diagnostic[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function setup(
hostDirectivesResolver,
true,
compilationMode,
new DeferredSymbolTracker(checker),
new DeferredSymbolTracker(checker, /* onlyExplicitDeferDependencyImports */ false),
/* forbidOrphanRenderering */ false,
/* enableBlockSyntax */ true,
);
Expand Down
11 changes: 11 additions & 0 deletions packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,17 @@ export interface BazelAndG3Options {
* Insert JSDoc type annotations needed by Closure Compiler
*/
annotateForClosureCompiler?: boolean;

/**
* Specifies whether Angular compiler should rely on explicit imports
* via `@Component.deferredImports` field for `@defer` blocks and generate
* dynamic imports only for types from that list.
*
* This flag is needed to enable stricter behavior internally to make sure
* that local compilation with specific internal configuration can support
* `@defer` blocks.
*/
onlyExplicitDeferDependencyImports?: boolean;
}

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,9 @@ export class NgCompiler {

const resourceRegistry = new ResourceRegistry();

const deferredSymbolsTracker = new DeferredSymbolTracker(this.inputProgram.getTypeChecker());
const deferredSymbolsTracker = new DeferredSymbolTracker(
this.inputProgram.getTypeChecker(),
this.options.onlyExplicitDeferDependencyImports ?? false);

// Cycles are handled in full compilation mode by "remote scoping".
// "Remote scoping" does not work well with tree shaking for libraries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export enum ErrorCode {
COMPONENT_IMPORT_NOT_STANDALONE = 2011,

/**
* Raised when a type in the `imports` of a component is not a directive, pipe, or NgModule.
* Raised when a type in the `imports` of a component is not a component, directive, pipe, or
* NgModule.
*/
COMPONENT_UNKNOWN_IMPORT = 2012,

Expand Down Expand Up @@ -116,6 +117,12 @@ export enum ErrorCode {
/** Raised when a component has both `styleUrls` and `styleUrl`. */
COMPONENT_INVALID_STYLE_URLS = 2021,

/**
* Raised when a type in the `deferredImports` of a component is not a component, directive or
* pipe.
*/
COMPONENT_UNKNOWN_DEFERRED_IMPORT = 2022,

SYMBOL_NOT_EXPORTED = 3001,
/**
* Raised when a relationship between directives and/or pipes would cause a cyclic import to be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ export class DeferredSymbolTracker {
private readonly imports =
new Map<ts.ImportDeclaration, Map<string, Set<ts.Identifier>|AssumeEager>>();

constructor(private readonly typeChecker: ts.TypeChecker) {}
// Set of import declarations used within the `@Component.imports` field.
// We need this information to detect whether a given reference can be defer-loadable,
// when we run in a mode when auto-extraction is disabled. In that mode we want to
// make sure that those symbols are *not* used within the `@Component.imports` field.
private readonly usedInRegularImports = new Set<ts.ImportDeclaration>();

constructor(
private readonly typeChecker: ts.TypeChecker,
private onlyExplicitDeferDependencyImports: boolean) {}

/**
* Given an import declaration node, extract the names of all imported symbols
Expand Down Expand Up @@ -75,13 +83,19 @@ export class DeferredSymbolTracker {
* Marks a given identifier and an associated import declaration as a candidate
* for defer loading.
*/
markAsDeferrableCandidate(identifier: ts.Identifier, importDecl: ts.ImportDeclaration): void {
markAsDeferrableCandidate(
identifier: ts.Identifier, importDecl: ts.ImportDeclaration,
usedInRegularImports: boolean): void {
// Do we come across this import for the first time?
if (!this.imports.has(importDecl)) {
const symbolMap = this.extractImportedSymbols(importDecl);
this.imports.set(importDecl, symbolMap);
}

if (usedInRegularImports) {
this.usedInRegularImports.add(importDecl);
}

const symbolMap = this.imports.get(importDecl)!;

if (!symbolMap.has(identifier.text)) {
Expand Down Expand Up @@ -120,6 +134,13 @@ export class DeferredSymbolTracker {
}
}

// If we only require explicit dependencies and symbols from this import
// declaration were used in the `@Component.imports` field - treat this
// import declaration as non-deferrable.
if (this.onlyExplicitDeferDependencyImports && this.usedInRegularImports.has(importDecl)) {
return false;
}

return true;
}

Expand Down
7 changes: 7 additions & 0 deletions packages/compiler-cli/src/ngtsc/scope/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@ export function makeUnknownComponentImportDiagnostic(
ErrorCode.COMPONENT_UNKNOWN_IMPORT, getDiagnosticNode(ref, rawExpr),
`Component imports must be standalone components, directives, pipes, or must be NgModules.`);
}

export function makeUnknownComponentDeferredImportDiagnostic(
ref: Reference<ClassDeclaration>, rawExpr: ts.Expression) {
return makeDiagnostic(
ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT, getDiagnosticNode(ref, rawExpr),
`Component deferred imports must be standalone components, directives or pipes.`);
}
180 changes: 178 additions & 2 deletions packages/compiler-cli/test/ngtsc/local_compilation_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,8 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = env.getContents('test.js');

// If there is no style, don't generate css selectors on elements by setting encapsulation
// to none (=2)
// If there is no style, don't generate css selectors on elements by setting
// encapsulation to none (=2)
expect(jsContents).toContain('encapsulation: 2');
});
});
Expand Down Expand Up @@ -1033,5 +1033,181 @@ runInEachFileSystem(() => {
.toContain('ɵɵsetNgModuleScope(AppModule, { declarations: [App] }); })();');
});
});

// TODO: make sure to copy those tests over to `ngtsc_spec.ts` and
// test the behavior in full compilation mode, make sure import graph
// remains the same.
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 {
<deferred-cmp-a />
}
@defer {
<deferred-cmp-b />
}
\`,
})
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 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 {
<eager-cmp-a />
<deferred-cmp-a />
}
@defer {
<eager-cmp-a />
<deferred-cmp-b />
}
\`,
})
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) => {');
});
});
});
});
Loading

0 comments on commit 0a04292

Please sign in to comment.