Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support deferred blocks in partial compilation #54908

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/compiler-cli/linker/babel/src/ast/babel_ast_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ export class BabelAstHost implements AstHost<t.Expression> {
return result;
}

isFunctionExpression(node: t.Expression): node is Extract<t.Function, t.Expression> {
return t.isFunction(node);
isFunctionExpression(node: t.Expression):
node is Extract<t.Function|t.ArrowFunctionExpression, t.Expression> {
return t.isFunction(node) || t.isArrowFunctionExpression(node);
}

parseReturnValue(fn: t.Expression): t.Expression {
Expand Down Expand Up @@ -114,6 +115,14 @@ export class BabelAstHost implements AstHost<t.Expression> {
return stmt.argument;
}

parseParameters(fn: t.Expression): t.Expression[] {
assert(fn, this.isFunctionExpression, 'a function');
return fn.params.map(param => {
assert(param, t.isIdentifier, 'an identifier');
return param;
});
}

isCallExpression = t.isCallExpression;
parseCallee(call: t.Expression): t.Expression {
assert(call, t.isCallExpression, 'a call expression');
Expand Down
17 changes: 17 additions & 0 deletions packages/compiler-cli/linker/babel/test/ast/babel_ast_host_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,23 @@ describe('BabelAstHost', () => {
});
});

describe('parseParameters()', () => {
it('should return the parameters as an array of expressions', () => {
expect(host.parseParameters(rhs('x = function(a, b) {}'))).toEqual([expr('a'), expr('b')]);
expect(host.parseParameters(rhs('x = (a, b) => {}'))).toEqual([expr('a'), expr('b')]);
});

it('should error if the node is not a function declaration or arrow function', () => {
expect(() => host.parseParameters(expr('[]')))
.toThrowError('Unsupported syntax, expected a function.');
});

it('should error if a parameter uses spread syntax', () => {
expect(() => host.parseParameters(rhs('x = function(a, ...other) {}')))
.toThrowError('Unsupported syntax, expected an identifier.');
});
});

describe('isCallExpression()', () => {
it('should return true if the expression is a call expression', () => {
expect(host.isCallExpression(expr('foo()'))).toBe(true);
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler-cli/linker/src/ast/ast_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export interface AstHost<TExpression> {
*/
parseReturnValue(fn: TExpression): TExpression;

/**
* Returns the parameter expressions for the function, or throw if it is not a function.
*/
parseParameters(fn: TExpression): TExpression[];

/**
* Return true if the given expression is a call expression, or false otherwise.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/compiler-cli/linker/src/ast/ast_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,14 @@ export class AstValue<T, TExpression> {
return new AstValue(this.host.parseReturnValue(this.expression), this.host);
}

/**
* Extract the parameters from this value as a function expression, or error if it is not a
* function expression.
*/
getFunctionParameters<R>(this: ConformsTo<this, T, Function>): AstValue<R, TExpression>[] {
return this.host.parseParameters(this.expression).map(param => new AstValue(param, this.host));
}

isCallExpression(): boolean {
return this.host.isCallExpression(this.expression);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ export class TypeScriptAstHost implements AstHost<ts.Expression> {
return stmt.expression;
}

parseParameters(fn: ts.Expression): ts.Expression[] {
assert(fn, this.isFunctionExpression, 'a function');
return fn.parameters.map(param => {
assert(param.name, ts.isIdentifier, 'an identifier');
if (param.dotDotDotToken) {
throw new FatalLinkerError(fn.body, 'Unsupported syntax, expected an identifier.');
}
return param.name;
});
}

isCallExpression = ts.isCallExpression;

parseCallee(call: ts.Expression): ts.Expression {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {compileOpaqueAsyncClassMetadata, ConstantPool, R3ClassMetadata, R3DeclareClassMetadataAsync} from '@angular/compiler';

import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';

import {LinkedDefinition, PartialLinker} from './partial_linker';

/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareClassMetadataAsync()` call expressions.
*/
export class PartialClassMetadataAsyncLinkerVersion1<TExpression> implements
PartialLinker<TExpression> {
linkPartialDeclaration(
constantPool: ConstantPool,
metaObj: AstObject<R3DeclareClassMetadataAsync, TExpression>): LinkedDefinition {
const resolveMetadataKey = 'resolveMetadata';
const resolveMetadata =
metaObj.getValue(resolveMetadataKey) as unknown as AstValue<Function, TExpression>;

if (!resolveMetadata.isFunction()) {
throw new FatalLinkerError(
resolveMetadata, `Unsupported \`${resolveMetadataKey}\` value. Expected a function.`);
}

const dependencyResolverFunction = metaObj.getOpaque('resolveDeferredDeps');
const deferredSymbolNames =
resolveMetadata.getFunctionParameters().map(p => p.getSymbolName()!);
const returnValue = resolveMetadata.getFunctionReturnValue<R3ClassMetadata>().getObject();
const metadata: R3ClassMetadata = {
type: metaObj.getOpaque('type'),
decorators: returnValue.getOpaque('decorators'),
ctorParameters: returnValue.getOpaque('ctorParameters'),
propDecorators: returnValue.getOpaque('propDecorators'),
};

return {
expression: compileOpaqueAsyncClassMetadata(
metadata, dependencyResolverFunction, deferredSymbolNames),
statements: [],
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
declarationListEmitMode,
styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) :
[],
defer: this.createR3ComponentDeferMetadata(boundTarget),
defer: this.createR3ComponentDeferMetadata(metaObj, boundTarget),
encapsulation: metaObj.has('encapsulation') ?
parseEncapsulation(metaObj.getValue('encapsulation')) :
ViewEncapsulation.Emulated,
Expand Down Expand Up @@ -257,13 +257,24 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
};
}

private createR3ComponentDeferMetadata(boundTarget: BoundTarget<any>): R3ComponentDeferMetadata {
private createR3ComponentDeferMetadata(
metaObj: AstObject<R3DeclareComponentMetadata, TExpression>,
boundTarget: BoundTarget<any>): R3ComponentDeferMetadata {
const deferredBlocks = boundTarget.getDeferBlocks();
const blocks = new Map<TmplAstDeferredBlock, o.ArrowFunctionExpr|null>();

for (const block of deferredBlocks) {
// TODO: leaving `deps` empty for now, to be implemented as one of the next steps.
blocks.set(block, null);
const blocks = new Map<TmplAstDeferredBlock, o.Expression|null>();
const dependencies =
metaObj.has('deferBlockDependencies') ? metaObj.getArray('deferBlockDependencies') : null;

for (let i = 0; i < deferredBlocks.length; i++) {
const matchingDependencyFn = dependencies?.[i];

if (matchingDependencyFn == null) {
blocks.set(deferredBlocks[i], null);
} else {
blocks.set(
deferredBlocks[i],
matchingDependencyFn.isNull() ? null : matchingDependencyFn.getOpaque());
}
}

return {mode: DeferBlockDepsEmitMode.PerBlock, blocks};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {Logger} from '../../../../src/ngtsc/logging';
import {createGetSourceFile} from '../get_source_file';
import {LinkerEnvironment} from '../linker_environment';

import {PartialClassMetadataAsyncLinkerVersion1} from './partial_class_metadata_async_linker_1';
import {PartialClassMetadataLinkerVersion1} from './partial_class_metadata_linker_1';
import {PartialComponentLinkerVersion1} from './partial_component_linker_1';
import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1';
Expand All @@ -31,9 +32,11 @@ export const ɵɵngDeclareInjectable = 'ɵɵngDeclareInjectable';
export const ɵɵngDeclareInjector = 'ɵɵngDeclareInjector';
export const ɵɵngDeclareNgModule = 'ɵɵngDeclareNgModule';
export const ɵɵngDeclarePipe = 'ɵɵngDeclarePipe';
export const ɵɵngDeclareClassMetadataAsync = 'ɵɵngDeclareClassMetadataAsync';
export const declarationFunctions = [
ɵɵngDeclareDirective, ɵɵngDeclareClassMetadata, ɵɵngDeclareComponent, ɵɵngDeclareFactory,
ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule, ɵɵngDeclarePipe
ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule, ɵɵngDeclarePipe,
ɵɵngDeclareClassMetadataAsync
];

export interface LinkerRange<TExpression> {
Expand Down Expand Up @@ -74,6 +77,9 @@ export function createLinkerMap<TStatement, TExpression>(
linkers.set(ɵɵngDeclareDirective, [
{range: LATEST_VERSION_RANGE, linker: new PartialDirectiveLinkerVersion1(sourceUrl, code)},
]);
linkers.set(ɵɵngDeclareClassMetadataAsync, [
{range: LATEST_VERSION_RANGE, linker: new PartialClassMetadataAsyncLinkerVersion1()},
]);
linkers.set(ɵɵngDeclareClassMetadata, [
{range: LATEST_VERSION_RANGE, linker: new PartialClassMetadataLinkerVersion1()},
]);
Expand Down
15 changes: 15 additions & 0 deletions packages/compiler-cli/linker/test/ast/ast_value_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,21 @@ describe('AstValue', () => {
});
});

describe('getFunctionParameters', () => {
it('should return the parameters of a function expression', () => {
const funcExpr = factory.createFunctionExpression('foo', ['a', 'b'], factory.createBlock([]));
expect(createAstValue<Function>(funcExpr).getFunctionParameters()).toEqual([
'a', 'b'
].map(name => createAstValue(factory.createIdentifier(name))));
});

it('should throw an error if the property is not a function declaration', () => {
// @ts-expect-error
expect(() => createAstValue<number>(factory.createLiteral(42)).getFunctionParameters())
.toThrowError('Unsupported syntax, expected a function.');
});
});

describe('isCallExpression', () => {
it('should return true if the value represents a call expression', () => {
const callExpr = factory.createCallExpression(factory.createIdentifier('foo'), [], false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import ts from 'typescript';

import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host';

describe('TypeScriptAstHost', () => {
Expand Down Expand Up @@ -269,6 +270,25 @@ describe('TypeScriptAstHost', () => {
});
});

describe('parseParameters()', () => {
it('should return the parameters as an array of expressions', () => {
const arg1 = jasmine.objectContaining({text: 'a', kind: ts.SyntaxKind.Identifier});
const arg2 = jasmine.objectContaining({text: 'b', kind: ts.SyntaxKind.Identifier});
expect(host.parseParameters(rhs('x = function (a, b) {}'))).toEqual([arg1, arg2]);
expect(host.parseParameters(rhs('x = (a, b) => {}'))).toEqual([arg1, arg2]);
});

it('should error if the node is not a function declaration or arrow function', () => {
expect(() => host.parseParameters(expr('[]')))
.toThrowError('Unsupported syntax, expected a function.');
});

it('should error if a parameter uses spread syntax', () => {
expect(() => host.parseParameters(rhs('x = function(a, ...other) {}')))
.toThrowError('Unsupported syntax, expected an identifier.');
});
});

describe('isCallExpression()', () => {
it('should return true if the expression is a call expression', () => {
expect(host.isCallExpression(expr('foo()'))).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, compileDeferResolverFunction, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, ExternalExpr, FactoryTarget, makeBindingParser, outputAst as o, R3ComponentDeferMetadata, R3ComponentMetadata, R3DeferPerComponentDependency, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, ViewEncapsulation} from '@angular/compiler';
import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentDeclareClassMetadata, compileComponentFromMetadata, compileDeclareComponentFromMetadata, compileDeferResolverFunction, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, ExternalExpr, FactoryTarget, makeBindingParser, outputAst as o, R3ComponentDeferMetadata, R3ComponentMetadata, R3DeferPerComponentDependency, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, ViewEncapsulation} from '@angular/compiler';
import ts from 'typescript';

import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles';
Expand Down Expand Up @@ -1143,21 +1143,20 @@ export class ComponentDecoratorHandler implements
return [];
}

const deferrableTypes = this.collectDeferredSymbols(resolution);

const perComponentDeferredDeps = this.resolveAllDeferredDependencies(resolution);
const meta: R3ComponentMetadata<R3TemplateDependency> = {
...analysis.meta,
...resolution,
defer: this.compileDeferBlocks(resolution),
};
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));

removeDeferrableTypesFromComponentDecorator(analysis, deferrableTypes);
removeDeferrableTypesFromComponentDecorator(analysis, perComponentDeferredDeps);

const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
const inputTransformFields = compileInputTransformFields(analysis.inputs);
const classMetadata = analysis.classMetadata !== null ?
compileComponentClassMetadata(analysis.classMetadata, deferrableTypes).toStmt() :
compileComponentClassMetadata(analysis.classMetadata, perComponentDeferredDeps).toStmt() :
null;
const debugInfo = analysis.classDebugInfo !== null ?
compileClassDebugInfo(analysis.classDebugInfo).toStmt() :
Expand All @@ -1182,6 +1181,7 @@ export class ComponentDecoratorHandler implements
null,
};

const perComponentDeferredDeps = this.resolveAllDeferredDependencies(resolution);
const meta: R3ComponentMetadata<R3TemplateDependencyMetadata> = {
...analysis.meta,
...resolution,
Expand All @@ -1191,11 +1191,11 @@ export class ComponentDecoratorHandler implements
const inputTransformFields = compileInputTransformFields(analysis.inputs);
const def = compileDeclareComponentFromMetadata(meta, analysis.template, templateInfo);
const classMetadata = analysis.classMetadata !== null ?
compileDeclareClassMetadata(analysis.classMetadata).toStmt() :
compileComponentDeclareClassMetadata(analysis.classMetadata, perComponentDeferredDeps)
.toStmt() :
null;

return compileResults(
fac, def, classMetadata, 'ɵcmp', inputTransformFields, null /* deferrableImports */);
const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls();
return compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports);
}

compileLocal(
Expand Down Expand Up @@ -1256,7 +1256,8 @@ export class ComponentDecoratorHandler implements
* Computes a list of deferrable symbols based on dependencies from
* the `@Component.imports` field and their usage in `@defer` blocks.
*/
private collectDeferredSymbols(resolution: Readonly<ComponentResolutionData>) {
private resolveAllDeferredDependencies(resolution: Readonly<ComponentResolutionData>):
R3DeferPerComponentDependency[] {
const deferrableTypes: R3DeferPerComponentDependency[] = [];
// Go over all dependencies of all defer blocks and update the value of
// the `isDeferrable` flag and the `importPath` to reflect the current
Expand Down Expand Up @@ -1489,7 +1490,7 @@ export class ComponentDecoratorHandler implements
'Internal error: deferPerBlockDependencies must be present when compiling in PerBlock mode');
}

const blocks = new Map<TmplAstDeferredBlock, o.ArrowFunctionExpr|null>();
const blocks = new Map<TmplAstDeferredBlock, o.Expression|null>();
for (const [block, dependencies] of perBlockDeps) {
blocks.set(
block,
Expand All @@ -1505,7 +1506,7 @@ export class ComponentDecoratorHandler implements
'Internal error: deferPerComponentDependencies must be present in PerComponent mode');
}
return {
mode: DeferBlockDepsEmitMode.PerComponent,
mode,
dependenciesFn: perComponentDeps.length === 0 ?
null :
compileDeferResolverFunction({mode, dependencies: perComponentDeps})
Expand Down