Skip to content

Commit

Permalink
refactor(core): change component emit to 'dependencies' (#45672)
Browse files Browse the repository at this point in the history
Previously, the compiler would represent template dependencies of a
component in its component definition through separate fields (`directives`,
`pipes`).

This commit refactors the compiler/runtime interface to use a single field
(`dependencies`). The runtime component definition object still has separate
`directiveDefs` and `pipeDefs`, which are calculated from the `dependencies`
when the definition is evaluated.

This change is also reflected in partially compiled declarations. To ensure
compatibility with partially compiled code already on NPM, the linker
will still honor the old form of declaration (with separate fields).

PR Close #45672
  • Loading branch information
alxhub committed Apr 20, 2022
1 parent 046dad1 commit 1527e8f
Show file tree
Hide file tree
Showing 91 changed files with 659 additions and 444 deletions.
4 changes: 2 additions & 2 deletions goldens/size-tracking/aio-payloads.json
Expand Up @@ -12,11 +12,11 @@
"aio-local": {
"uncompressed": {
"runtime": 4343,
"main": 452837,
"main": 453341,
"polyfills": 33980,
"styles": 71714,
"light-theme": 78213,
"dark-theme": 78318
}
}
}
}
8 changes: 4 additions & 4 deletions integration/ngcc/run_test.sh
Expand Up @@ -214,8 +214,8 @@ ngc -p tsconfig-app.json
assertSucceeded "Expected the app to successfully compile with the ngcc-processed libraries."

# Did it compile the main.ts correctly (including the ngIf and MatButton directives)?
grep "directives: \[.*\.NgIf.*\]" dist/src/main.js
assertSucceeded "Expected the compiled app's 'main.ts' to list 'NgIf' in 'directives'."
grep "dependencies: \[.*\.NgIf.*\]" dist/src/main.js
assertSucceeded "Expected the compiled app's 'main.ts' to list 'NgIf' in 'dependencies'."

grep "directives: \[.*\.MatButton.*\]" dist/src/main.js
assertSucceeded "Expected the compiled app's 'main.ts' to list 'MatButton' in 'directives'."
grep "dependencies: \[.*\.MatButton.*\]" dist/src/main.js
assertSucceeded "Expected the compiled app's 'main.ts' to list 'MatButton' in 'dependencies'."
Expand Up @@ -5,7 +5,7 @@
* 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 {ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareUsedDirectiveMetadata, R3PartialDeclaration, R3UsedDirectiveMetadata, ViewEncapsulation} from '@angular/compiler';
import {ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TemplateDependencyKind, R3TemplateDependencyMetadata, ViewEncapsulation} from '@angular/compiler';

import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
import {Range} from '../../ast/ast_host';
Expand All @@ -17,6 +17,28 @@ import {toR3DirectiveMeta} from './partial_directive_linker_1';
import {LinkedDefinition, PartialLinker} from './partial_linker';
import {extractForwardRef} from './util';

function makeDirectiveMetadata<TExpression>(
directiveExpr: AstObject<R3DeclareDirectiveDependencyMetadata, TExpression>,
typeExpr: o.WrappedNodeExpr<TExpression>,
isComponentByDefault: true|null = null): R3DirectiveDependencyMetadata {
return {
kind: R3TemplateDependencyKind.Directive,
isComponent: isComponentByDefault ||
(directiveExpr.has('kind') && directiveExpr.getString('kind') === 'component'),
type: typeExpr,
selector: directiveExpr.getString('selector'),
inputs: directiveExpr.has('inputs') ?
directiveExpr.getArray('inputs').map(input => input.getString()) :
[],
outputs: directiveExpr.has('outputs') ?
directiveExpr.getArray('outputs').map(input => input.getString()) :
[],
exportAs: directiveExpr.has('exportAs') ?
directiveExpr.getArray('exportAs').map(exportAs => exportAs.getString()) :
null,
};
}

/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareComponent()` call expressions.
*/
Expand All @@ -37,7 +59,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
* This function derives the `R3ComponentMetadata` from the provided AST object.
*/
private toR3ComponentMeta(metaObj: AstObject<R3DeclareComponentMetadata, TExpression>):
R3ComponentMetadata {
R3ComponentMetadata<R3TemplateDependencyMetadata> {
const interpolation = parseInterpolationConfig(metaObj);
const templateSource = metaObj.getValue('template');
const isInline = metaObj.has('isInline') ? metaObj.getBoolean('isInline') : false;
Expand All @@ -61,51 +83,74 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements

let declarationListEmitMode = DeclarationListEmitMode.Direct;

const collectUsedDirectives =
(directives: AstValue<R3DeclareUsedDirectiveMetadata, TExpression>[]) => {
return directives.map(directive => {
const directiveExpr = directive.getObject();
const type = directiveExpr.getValue('type');
const selector = directiveExpr.getString('selector');
const extractDeclarationTypeExpr =
(type: AstValue<o.Expression|(() => o.Expression), TExpression>) => {
const {expression, forwardRef} = extractForwardRef(type);
if (forwardRef === ForwardRefHandling.Unwrapped) {
declarationListEmitMode = DeclarationListEmitMode.Closure;
}
return expression;
};

const {expression: typeExpr, forwardRef} = extractForwardRef(type);
if (forwardRef === ForwardRefHandling.Unwrapped) {
declarationListEmitMode = DeclarationListEmitMode.Closure;
}
let declarations: R3TemplateDependencyMetadata[] = [];

return {
type: typeExpr,
selector: selector,
inputs: directiveExpr.has('inputs') ?
directiveExpr.getArray('inputs').map(input => input.getString()) :
[],
outputs: directiveExpr.has('outputs') ?
directiveExpr.getArray('outputs').map(input => input.getString()) :
[],
exportAs: directiveExpr.has('exportAs') ?
directiveExpr.getArray('exportAs').map(exportAs => exportAs.getString()) :
null,
};
});
};
// There are two ways that declarations (directives/pipes) can be represented in declare
// metadata. The "old style" uses separate fields for each (arrays for components/directives and
// an object literal for pipes). The "new style" uses a unified `dependencies` array. For
// backwards compatibility, both are processed and unified here:

let directives: R3UsedDirectiveMetadata[] = [];
// Process the old style fields:
if (metaObj.has('components')) {
directives.push(...collectUsedDirectives(metaObj.getArray('components')));
declarations.push(...metaObj.getArray('components').map(dir => {
const dirExpr = dir.getObject();
const typeExpr = extractDeclarationTypeExpr(dirExpr.getValue('type'));
return makeDirectiveMetadata(dirExpr, typeExpr, /* isComponentByDefault */ true);
}));
}
if (metaObj.has('directives')) {
directives.push(...collectUsedDirectives(metaObj.getArray('directives')));
declarations.push(...metaObj.getArray('directives').map(dir => {
const dirExpr = dir.getObject();
const typeExpr = extractDeclarationTypeExpr(dirExpr.getValue('type'));
return makeDirectiveMetadata(dirExpr, typeExpr);
}));
}

let pipes = new Map<string, o.Expression>();
if (metaObj.has('pipes')) {
pipes = metaObj.getObject('pipes').toMap(pipe => {
const {expression: pipeType, forwardRef} = extractForwardRef(pipe);
if (forwardRef === ForwardRefHandling.Unwrapped) {
declarationListEmitMode = DeclarationListEmitMode.Closure;
const pipes = metaObj.getObject('pipes').toMap(pipe => pipe);
for (const [name, type] of pipes) {
const typeExpr = extractDeclarationTypeExpr(type);
declarations.push({
kind: R3TemplateDependencyKind.Pipe,
name,
type: typeExpr,
});
}
}

// Process the new style field:
if (metaObj.has('dependencies')) {
for (const dep of metaObj.getArray('dependencies')) {
const depObj = dep.getObject();
const typeExpr = extractDeclarationTypeExpr(depObj.getValue('type'));

switch (depObj.getString('kind')) {
case 'directive':
case 'component':
declarations.push(makeDirectiveMetadata(depObj, typeExpr));
break;
case 'pipe':
const pipeObj =
depObj as AstObject<R3DeclarePipeDependencyMetadata&{kind: 'pipe'}, TExpression>;
declarations.push({
kind: R3TemplateDependencyKind.Pipe,
name: pipeObj.getString('name'),
type: typeExpr,
});
break;
default:
// Skip unknown types of dependencies.
continue;
}
return pipeType;
});
}
}

return {
Expand All @@ -128,8 +173,7 @@ export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null,
relativeContextFilePath: this.sourceUrl,
i18nUseExternalIds: false,
pipes,
directives,
declarations,
};
}

Expand Down
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {AnimationTriggerNames, compileClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, FactoryTarget, makeBindingParser, R3ComponentMetadata, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, ViewEncapsulation, WrappedNodeExpr} from '@angular/compiler';
import {AnimationTriggerNames, compileClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, FactoryTarget, makeBindingParser, R3ComponentMetadata, R3DirectiveDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, ViewEncapsulation, WrappedNodeExpr} from '@angular/compiler';
import ts from 'typescript';

import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles';
Expand Down Expand Up @@ -541,11 +541,11 @@ export class ComponentDecoratorHandler implements
}

const context = getSourceFile(node);
const metadata = analysis.meta as Readonly<R3ComponentMetadata>;
const metadata = analysis.meta as Readonly<R3ComponentMetadata<R3TemplateDependencyMetadata>>;


const data: ComponentResolutionData = {
directives: EMPTY_ARRAY,
pipes: EMPTY_MAP,
declarations: EMPTY_ARRAY,
declarationListEmitMode: DeclarationListEmitMode.Direct,
};
const diagnostics: ts.Diagnostic[] = [];
Expand Down Expand Up @@ -600,13 +600,14 @@ export class ComponentDecoratorHandler implements
const bound = binder.bind({template: metadata.template.nodes});

// The BoundTarget knows which directives and pipes matched the template.
type UsedDirective =
R3UsedDirectiveMetadata&{ref: Reference<ClassDeclaration>, importedFile: ImportedFile};
type UsedDirective = R3DirectiveDependencyMetadata&
{ref: Reference<ClassDeclaration>, importedFile: ImportedFile};
const usedDirectives: UsedDirective[] = bound.getUsedDirectives().map(directive => {
const type = this.refEmitter.emit(directive.ref, context);
assertSuccessfulReferenceEmit(
type, node.name, directive.isComponent ? 'component' : 'directive');
return {
kind: R3TemplateDependencyKind.Directive,
ref: directive.ref,
type: type.expression,
importedFile: type.importedFile,
Expand All @@ -618,10 +619,8 @@ export class ComponentDecoratorHandler implements
};
});

type UsedPipe = {
type UsedPipe = R3PipeDependencyMetadata&{
ref: Reference<ClassDeclaration>,
pipeName: string,
expression: Expression,
importedFile: ImportedFile,
};
const usedPipes: UsedPipe[] = [];
Expand All @@ -633,18 +632,18 @@ export class ComponentDecoratorHandler implements
const type = this.refEmitter.emit(pipe, context);
assertSuccessfulReferenceEmit(type, node.name, 'pipe');
usedPipes.push({
kind: R3TemplateDependencyKind.Pipe,
type: type.expression,
name: pipeName,
ref: pipe,
pipeName,
expression: type.expression,
importedFile: type.importedFile,
});
}
if (this.semanticDepGraphUpdater !== null) {
symbol.usedDirectives = usedDirectives.map(
dir => this.semanticDepGraphUpdater!.getSemanticReference(dir.ref.node, dir.type));
symbol.usedPipes = usedPipes.map(
pipe =>
this.semanticDepGraphUpdater!.getSemanticReference(pipe.ref.node, pipe.expression));
pipe => this.semanticDepGraphUpdater!.getSemanticReference(pipe.ref.node, pipe.type));
}

// Scan through the directives/pipes actually used in the template and check whether any
Expand All @@ -659,8 +658,7 @@ export class ComponentDecoratorHandler implements
}
const cyclesFromPipes = new Map<UsedPipe, Cycle>();
for (const usedPipe of usedPipes) {
const cycle =
this._checkForCyclicImport(usedPipe.importedFile, usedPipe.expression, context);
const cycle = this._checkForCyclicImport(usedPipe.importedFile, usedPipe.type, context);
if (cycle !== null) {
cyclesFromPipes.set(usedPipe, cycle);
}
Expand All @@ -673,8 +671,8 @@ export class ComponentDecoratorHandler implements
for (const {type, importedFile} of usedDirectives) {
this._recordSyntheticImport(importedFile, type, context);
}
for (const {expression, importedFile} of usedPipes) {
this._recordSyntheticImport(importedFile, expression, context);
for (const {type, importedFile} of usedPipes) {
this._recordSyntheticImport(importedFile, type, context);
}

// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
Expand All @@ -683,11 +681,12 @@ export class ComponentDecoratorHandler implements
const wrapDirectivesAndPipesInClosure =
usedDirectives.some(
dir => isExpressionForwardReference(dir.type, node.name, context)) ||
usedPipes.some(
pipe => isExpressionForwardReference(pipe.expression, node.name, context));
usedPipes.some(pipe => isExpressionForwardReference(pipe.type, node.name, context));

data.directives = usedDirectives;
data.pipes = new Map(usedPipes.map(pipe => [pipe.pipeName, pipe.expression]));
data.declarations = [
...usedDirectives,
...usedPipes,
];
data.declarationListEmitMode = wrapDirectivesAndPipesInClosure ?
DeclarationListEmitMode.Closure :
DeclarationListEmitMode.Direct;
Expand Down Expand Up @@ -813,7 +812,7 @@ export class ComponentDecoratorHandler implements
if (analysis.template.errors !== null && analysis.template.errors.length > 0) {
return [];
}
const meta: R3ComponentMetadata = {...analysis.meta, ...resolution};
const meta: R3ComponentMetadata<R3TemplateDependency> = {...analysis.meta, ...resolution};
const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component));
const def = compileComponentFromMetadata(meta, pool, makeBindingParser());
const classMetadata = analysis.classMetadata !== null ?
Expand All @@ -836,7 +835,8 @@ export class ComponentDecoratorHandler implements
new WrappedNodeExpr(analysis.template.sourceMapping.node) :
null,
};
const meta: R3ComponentMetadata = {...analysis.meta, ...resolution};
const meta:
R3ComponentMetadata<R3TemplateDependencyMetadata> = {...analysis.meta, ...resolution};
const fac = compileDeclareFactory(toFactoryMetadata(meta, FactoryTarget.Component));
const def = compileDeclareComponentFromMetadata(meta, analysis.template, templateInfo);
const classMetadata = analysis.classMetadata !== null ?
Expand Down
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {AnimationTriggerNames, R3ClassMetadata, R3ComponentMetadata} from '@angular/compiler';
import {AnimationTriggerNames, R3ClassMetadata, R3ComponentMetadata, R3TemplateDependency, R3TemplateDependencyMetadata} from '@angular/compiler';
import ts from 'typescript';

import {Reference} from '../../../imports';
Expand All @@ -22,15 +22,15 @@ import {ParsedTemplateWithSource, StyleUrlMeta} from './resources';
* The `keyof R3ComponentMetadata &` condition ensures that only fields of `R3ComponentMetadata` can
* be included here.
*/
export type ComponentMetadataResolvedFields =
SubsetOfKeys<R3ComponentMetadata, 'directives'|'pipes'|'declarationListEmitMode'>;
export type ComponentMetadataResolvedFields = SubsetOfKeys<
R3ComponentMetadata<R3TemplateDependencyMetadata>, 'declarations'|'declarationListEmitMode'>;

export interface ComponentAnalysisData {
/**
* `meta` includes those fields of `R3ComponentMetadata` which are calculated at `analyze` time
* (not during resolve).
*/
meta: Omit<R3ComponentMetadata, ComponentMetadataResolvedFields>;
meta: Omit<R3ComponentMetadata<R3TemplateDependencyMetadata>, ComponentMetadataResolvedFields>;
baseClass: Reference<ClassDeclaration>|'dynamic'|null;
typeCheckMeta: DirectiveTypeCheckMeta;
template: ParsedTemplateWithSource;
Expand Down Expand Up @@ -72,4 +72,5 @@ export interface ComponentAnalysisData {
}|null;
}

export type ComponentResolutionData = Pick<R3ComponentMetadata, ComponentMetadataResolvedFields>;
export type ComponentResolutionData =
Pick<R3ComponentMetadata<R3TemplateDependencyMetadata>, ComponentMetadataResolvedFields>;

0 comments on commit 1527e8f

Please sign in to comment.