feat(ivy): @NgModule -> ngInjectorDef compilation (#22458)

This adds compilation of @NgModule providers and imports into
ngInjectorDef statements in generated code. All @NgModule annotations
will be compiled and the @NgModule decorators removed from the
resultant js output.

All @Injectables will also be compiled in Ivy mode, and the decorator
removed.

PR Close #22458
This commit is contained in:
Alex Rickabaugh
2018-02-16 08:45:21 -08:00
committed by Miško Hevery
parent 688096b7a3
commit 6ef9f2278f
48 changed files with 2044 additions and 206 deletions

View File

@ -208,7 +208,7 @@ interface MetadataAndLoweringRequests {
requests: RequestLocationMap;
}
function shouldLower(node: ts.Node | undefined): boolean {
function isEligibleForLowering(node: ts.Node | undefined): boolean {
if (node) {
switch (node.kind) {
case ts.SyntaxKind.SourceFile:
@ -226,7 +226,7 @@ function shouldLower(node: ts.Node | undefined): boolean {
// Avoid lowering expressions already in an exported variable declaration
return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) == 0;
}
return shouldLower(node.parent);
return isEligibleForLowering(node.parent);
}
return true;
}
@ -251,11 +251,14 @@ function isLiteralFieldNamed(node: ts.Node, names: Set<string>): boolean {
return false;
}
const LOWERABLE_FIELD_NAMES = new Set(['useValue', 'useFactory', 'data']);
export class LowerMetadataTransform implements RequestsMap, MetadataTransformer {
private cache: MetadataCache;
private requests = new Map<string, RequestLocationMap>();
private lowerableFieldNames: Set<string>;
constructor(lowerableFieldNames: string[]) {
this.lowerableFieldNames = new Set<string>(lowerableFieldNames);
}
// RequestMap
getRequests(sourceFile: ts.SourceFile): RequestLocationMap {
@ -312,17 +315,46 @@ export class LowerMetadataTransform implements RequestsMap, MetadataTransformer
return false;
};
const hasLowerableParentCache = new Map<ts.Node, boolean>();
const shouldBeLowered = (node: ts.Node | undefined): boolean => {
if (node === undefined) {
return false;
}
let lowerable: boolean = false;
if ((node.kind === ts.SyntaxKind.ArrowFunction ||
node.kind === ts.SyntaxKind.FunctionExpression) &&
isEligibleForLowering(node)) {
lowerable = true;
} else if (
isLiteralFieldNamed(node, this.lowerableFieldNames) && isEligibleForLowering(node) &&
!isExportedSymbol(node) && !isExportedPropertyAccess(node)) {
lowerable = true;
}
return lowerable;
};
const hasLowerableParent = (node: ts.Node | undefined): boolean => {
if (node === undefined) {
return false;
}
if (!hasLowerableParentCache.has(node)) {
hasLowerableParentCache.set(
node, shouldBeLowered(node.parent) || hasLowerableParent(node.parent));
}
return hasLowerableParentCache.get(node) !;
};
const isLowerable = (node: ts.Node | undefined): boolean => {
if (node === undefined) {
return false;
}
return shouldBeLowered(node) && !hasLowerableParent(node);
};
return (value: MetadataValue, node: ts.Node): MetadataValue => {
if (!isPrimitive(value) && !isRewritten(value)) {
if ((node.kind === ts.SyntaxKind.ArrowFunction ||
node.kind === ts.SyntaxKind.FunctionExpression) &&
shouldLower(node)) {
return replaceNode(node);
}
if (isLiteralFieldNamed(node, LOWERABLE_FIELD_NAMES) && shouldLower(node) &&
!isExportedSymbol(node) && !isExportedPropertyAccess(node)) {
return replaceNode(node);
}
if (!isPrimitive(value) && !isRewritten(value) && isLowerable(node)) {
return replaceNode(node);
}
return value;
};

View File

@ -7,7 +7,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, StaticSymbol, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
@ -22,9 +22,11 @@ import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './l
import {MetadataCache, MetadataTransformer} from './metadata_cache';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
import {StripDecoratorsMetadataTransformer, getDecoratorStripTransformerFactory} from './r3_strip_decorators';
import {getAngularClassTransformerFactory} from './r3_transform';
import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
// Closure compiler transforms the form `Service.ngInjectableDef = X` into
// `Service$ngInjectableDef = X`. To prevent this transformation, such assignments need to be
// annotated with @nocollapse. Unfortunately, a bug in Typescript where comments aren't propagated
@ -62,6 +64,25 @@ const R3_NOCOLLAPSE_DEFS = '$1\/** @nocollapse *\/ $2';
*/
const MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT = 20;
/**
* Fields to lower within metadata in render2 mode.
*/
const LOWER_FIELDS = ['useValue', 'useFactory', 'data'];
/**
* Fields to lower within metadata in render3 mode.
*/
const R3_LOWER_FIELDS = [...LOWER_FIELDS, 'providers', 'imports', 'exports'];
const R3_REIFIED_DECORATORS = [
'Component',
'Directive',
'Injectable',
'NgModule',
'Pipe',
];
const emptyModules: NgAnalyzedModules = {
ngModules: [],
ngModuleByPipeOrDirective: new Map(),
@ -96,6 +117,7 @@ class AngularCompilerProgram implements Program {
private _structuralDiagnostics: Diagnostic[]|undefined;
private _programWithStubs: ts.Program|undefined;
private _optionsDiagnostics: Diagnostic[] = [];
private _reifiedDecorators: Set<StaticSymbol>;
constructor(
rootNames: ReadonlyArray<string>, private options: CompilerOptions,
@ -129,7 +151,9 @@ class AngularCompilerProgram implements Program {
this.host = bundleHost;
}
}
this.loweringMetadataTransform = new LowerMetadataTransform();
this.loweringMetadataTransform =
new LowerMetadataTransform(options.enableIvy ? R3_LOWER_FIELDS : LOWER_FIELDS);
this.metadataCache = this.createMetadataCache([this.loweringMetadataTransform]);
}
@ -269,7 +293,11 @@ class AngularCompilerProgram implements Program {
0) {
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
}
const modules = this.compiler.emitAllPartialModules(this.analyzedModules);
// analyzedModules and analyzedInjectables are created together. If one exists, so does the
// other.
const modules =
this.compiler.emitAllPartialModules(this.analyzedModules, this._analyzedInjectables !);
const writeTsFile: ts.WriteFileCallback =
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
@ -285,7 +313,8 @@ class AngularCompilerProgram implements Program {
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
const tsCustomTransformers = this.calculateTransforms(
/* genFiles */ undefined, /* partialModules */ modules, customTransformers);
/* genFiles */ undefined, /* partialModules */ modules,
/* stripDecorators */ this.reifiedDecorators, customTransformers);
const emitResult = emitCallback({
program: this.tsProgram,
@ -356,8 +385,8 @@ class AngularCompilerProgram implements Program {
const modules = this._analyzedInjectables &&
this.compiler.emitAllPartialModules2(this._analyzedInjectables);
const tsCustomTransformers =
this.calculateTransforms(genFileByFileName, modules, customTransformers);
const tsCustomTransformers = this.calculateTransforms(
genFileByFileName, modules, /* stripDecorators */ undefined, customTransformers);
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
// Restore the original references before we emit so TypeScript doesn't emit
// a reference to the .d.ts file.
@ -512,8 +541,18 @@ class AngularCompilerProgram implements Program {
return this._tsProgram !;
}
private get reifiedDecorators(): Set<StaticSymbol> {
if (!this._reifiedDecorators) {
const reflector = this.compiler.reflector;
this._reifiedDecorators = new Set(
R3_REIFIED_DECORATORS.map(name => reflector.findDeclaration('@angular/core', name)));
}
return this._reifiedDecorators;
}
private calculateTransforms(
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
stripDecorators: Set<StaticSymbol>|undefined,
customTransformers?: CustomTransformers): ts.CustomTransformers {
const beforeTs: Array<ts.TransformerFactory<ts.SourceFile>> = [];
const metadataTransforms: MetadataTransformer[] = [];
@ -521,6 +560,7 @@ class AngularCompilerProgram implements Program {
beforeTs.push(getInlineResourcesTransformFactory(this.tsProgram, this.hostAdapter));
metadataTransforms.push(new InlineResourcesMetadataTransformer(this.hostAdapter));
}
if (!this.options.disableExpressionLowering) {
beforeTs.push(
getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram));
@ -536,6 +576,14 @@ class AngularCompilerProgram implements Program {
// the partial module transforms.
metadataTransforms.push(new PartialModuleMetadataTransformer(partialModules));
}
if (stripDecorators) {
beforeTs.push(getDecoratorStripTransformerFactory(
stripDecorators, this.compiler.reflector, this.getTsProgram().getTypeChecker()));
metadataTransforms.push(
new StripDecoratorsMetadataTransformer(stripDecorators, this.compiler.reflector));
}
if (customTransformers && customTransformers.beforeTs) {
beforeTs.push(...customTransformers.beforeTs);
}
@ -832,6 +880,7 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
preserveWhitespaces: options.preserveWhitespaces,
fullTemplateTypeCheck: options.fullTemplateTypeCheck,
allowEmptyCodegenFiles: options.allowEmptyCodegenFiles,
enableIvy: options.enableIvy,
};
}

View File

@ -0,0 +1,161 @@
/**
* @license
* Copyright Google Inc. 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 {StaticReflector, StaticSymbol} from '@angular/compiler';
import * as ts from 'typescript';
import {MetadataValue, isClassMetadata, isMetadataImportedSymbolReferenceExpression, isMetadataSymbolicCallExpression} from '../metadata';
import {MetadataTransformer, ValueTransform} from './metadata_cache';
export type Transformer = (sourceFile: ts.SourceFile) => ts.SourceFile;
export type TransformerFactory = (context: ts.TransformationContext) => Transformer;
export function getDecoratorStripTransformerFactory(
coreDecorators: Set<StaticSymbol>, reflector: StaticReflector,
checker: ts.TypeChecker): TransformerFactory {
return function(context: ts.TransformationContext) {
return function(sourceFile: ts.SourceFile): ts.SourceFile {
const stripDecoratorsFromClassDeclaration =
(node: ts.ClassDeclaration): ts.ClassDeclaration => {
if (node.decorators === undefined) {
return node;
}
const decorators = node.decorators.filter(decorator => {
const callExpr = decorator.expression;
if (ts.isCallExpression(callExpr)) {
const id = callExpr.expression;
if (ts.isIdentifier(id)) {
const symbol = resolveToStaticSymbol(id, sourceFile.fileName, reflector, checker);
return symbol && coreDecorators.has(symbol);
}
}
return true;
});
if (decorators.length !== node.decorators.length) {
return ts.updateClassDeclaration(
node, decorators, node.modifiers, node.name, node.typeParameters,
node.heritageClauses || [], node.members, );
}
return node;
};
const stripDecoratorPropertyAssignment = (node: ts.ClassDeclaration): ts.ClassDeclaration => {
return ts.visitEachChild(node, member => {
if (!ts.isPropertyDeclaration(member) || !isDecoratorAssignment(member) ||
!member.initializer || !ts.isArrayLiteralExpression(member.initializer)) {
return member;
}
const newInitializer = ts.visitEachChild(member.initializer, decorator => {
if (!ts.isObjectLiteralExpression(decorator)) {
return decorator;
}
const type = lookupProperty(decorator, 'type');
if (!type || !ts.isIdentifier(type)) {
return decorator;
}
const symbol = resolveToStaticSymbol(type, sourceFile.fileName, reflector, checker);
if (!symbol || !coreDecorators.has(symbol)) {
return decorator;
}
return undefined;
}, context);
if (newInitializer === member.initializer) {
return member;
} else if (newInitializer.elements.length === 0) {
return undefined;
} else {
return ts.updateProperty(
member, member.decorators, member.modifiers, member.name, member.questionToken,
member.type, newInitializer);
}
}, context);
};
return ts.visitEachChild(sourceFile, stmt => {
if (ts.isClassDeclaration(stmt)) {
let decl = stmt;
if (stmt.decorators) {
decl = stripDecoratorsFromClassDeclaration(stmt);
}
return stripDecoratorPropertyAssignment(decl);
}
return stmt;
}, context);
};
};
}
function isDecoratorAssignment(member: ts.ClassElement): boolean {
if (!ts.isPropertyDeclaration(member)) {
return false;
}
if (!member.modifiers ||
!member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
return false;
}
if (!ts.isIdentifier(member.name) || member.name.text !== 'decorators') {
return false;
}
if (!member.initializer || !ts.isArrayLiteralExpression(member.initializer)) {
return false;
}
return true;
}
function lookupProperty(expr: ts.ObjectLiteralExpression, prop: string): ts.Expression|undefined {
const decl = expr.properties.find(
elem => !!elem.name && ts.isIdentifier(elem.name) && elem.name.text === prop);
if (decl === undefined || !ts.isPropertyAssignment(decl)) {
return undefined;
}
return decl.initializer;
}
function resolveToStaticSymbol(
id: ts.Identifier, containingFile: string, reflector: StaticReflector,
checker: ts.TypeChecker): StaticSymbol|null {
const res = checker.getSymbolAtLocation(id);
if (!res || !res.declarations || res.declarations.length === 0) {
return null;
}
const decl = res.declarations[0];
if (!ts.isImportSpecifier(decl)) {
return null;
}
const moduleSpecifier = decl.parent !.parent !.parent !.moduleSpecifier;
if (!ts.isStringLiteral(moduleSpecifier)) {
return null;
}
return reflector.tryFindDeclaration(moduleSpecifier.text, id.text, containingFile);
}
export class StripDecoratorsMetadataTransformer implements MetadataTransformer {
constructor(private coreDecorators: Set<StaticSymbol>, private reflector: StaticReflector) {}
start(sourceFile: ts.SourceFile): ValueTransform|undefined {
return (value: MetadataValue, node: ts.Node): MetadataValue => {
if (isClassMetadata(value) && ts.isClassDeclaration(node) && value.decorators) {
value.decorators = value.decorators.filter(d => {
if (isMetadataSymbolicCallExpression(d) &&
isMetadataImportedSymbolReferenceExpression(d.expression)) {
const declaration = this.reflector.tryFindDeclaration(
d.expression.module, d.expression.name, sourceFile.fileName);
if (declaration && this.coreDecorators.has(declaration)) {
return false;
}
}
return true;
});
}
return value;
};
}
}