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:

committed by
Miško Hevery

parent
688096b7a3
commit
6ef9f2278f
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
161
packages/compiler-cli/src/transformers/r3_strip_decorators.ts
Normal file
161
packages/compiler-cli/src/transformers/r3_strip_decorators.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user