feat(compiler): implement "enableIvy" compiler option (#21427)
The "enableIvy" compiler option is the initial implementation of the Render3 (or Ivy) code generation. This commit enables generation generating "Hello, World" (example in the test) but not much else. It is currenly only useful for internal Ivy testing as Ivy is in development. PR Close #21427
This commit is contained in:

committed by
Miško Hevery

parent
ce8b5877e2
commit
64d16dee02
@ -38,6 +38,8 @@ export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic {
|
||||
}
|
||||
|
||||
export interface CompilerOptions extends ts.CompilerOptions {
|
||||
// NOTE: These comments and aio/content/guides/aot-compiler.md should be kept in sync.
|
||||
|
||||
// Write statistics about compilation (e.g. total time, ...)
|
||||
// Note: this is the --diagnostics command line option from TS (which is @internal
|
||||
// on ts.CompilerOptions interface).
|
||||
@ -159,6 +161,17 @@ export interface CompilerOptions extends ts.CompilerOptions {
|
||||
*/
|
||||
enableSummariesForJit?: boolean;
|
||||
|
||||
/**
|
||||
* Tells the compiler to generate definitions using the Render3 style code generation.
|
||||
* This option defaults to `false`.
|
||||
*
|
||||
* Not all features are supported with this option enabled. It is only supported
|
||||
* for experimentation and testing of Render3 style code generation.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
enableIvy?: boolean;
|
||||
|
||||
/** @internal */
|
||||
collectAllErrors?: boolean;
|
||||
}
|
||||
|
@ -6,8 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
|
||||
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassMethod, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
import {error} from './util';
|
||||
|
||||
export interface Node { sourceSpan: ParseSourceSpan|null; }
|
||||
|
||||
@ -50,6 +51,98 @@ export class TypeScriptNodeEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given source file to include the changes specified in module.
|
||||
*
|
||||
* The module parameter is treated as a partial module meaning that the statements are added to
|
||||
* the module instead of replacing the module. Also, any classes are treated as partial classes
|
||||
* and the included members are added to the class with the same name instead of a new class
|
||||
* being created.
|
||||
*/
|
||||
export function updateSourceFile(
|
||||
sourceFile: ts.SourceFile, module: PartialModule,
|
||||
context: ts.TransformationContext): [ts.SourceFile, Map<ts.Node, Node>] {
|
||||
const converter = new _NodeEmitterVisitor();
|
||||
const prefixStatements = module.statements.filter(statement => !(statement instanceof ClassStmt));
|
||||
const classes =
|
||||
module.statements.filter(statement => statement instanceof ClassStmt) as ClassStmt[];
|
||||
const classMap = new Map(
|
||||
classes.map<[string, ClassStmt]>(classStatement => [classStatement.name, classStatement]));
|
||||
const classNames = new Set(classes.map(classStatement => classStatement.name));
|
||||
|
||||
const prefix =
|
||||
<ts.Statement[]>prefixStatements.map(statement => statement.visitStatement(converter, null));
|
||||
|
||||
// Add static methods to all the classes referenced in module.
|
||||
let newStatements = sourceFile.statements.map(node => {
|
||||
if (node.kind == ts.SyntaxKind.ClassDeclaration) {
|
||||
const classDeclaration = node as ts.ClassDeclaration;
|
||||
const name = classDeclaration.name;
|
||||
if (name) {
|
||||
const classStatement = classMap.get(name.text);
|
||||
if (classStatement) {
|
||||
classNames.delete(name.text);
|
||||
const classMemberHolder =
|
||||
converter.visitDeclareClassStmt(classStatement) as ts.ClassDeclaration;
|
||||
const newMethods =
|
||||
classMemberHolder.members.filter(member => member.kind !== ts.SyntaxKind.Constructor);
|
||||
const newMembers = [...classDeclaration.members, ...newMethods];
|
||||
|
||||
return ts.updateClassDeclaration(
|
||||
classDeclaration,
|
||||
/* decorators */ classDeclaration.decorators,
|
||||
/* modifiers */ classDeclaration.modifiers,
|
||||
/* name */ classDeclaration.name,
|
||||
/* typeParameters */ classDeclaration.typeParameters,
|
||||
/* heritageClauses */ classDeclaration.heritageClauses || [],
|
||||
/* members */ newMembers);
|
||||
}
|
||||
}
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
// Validate that all the classes have been generated
|
||||
classNames.size == 0 ||
|
||||
error(
|
||||
`${classNames.size == 1 ? 'Class' : 'Classes'} "${Array.from(classNames.keys()).join(', ')}" not generated`);
|
||||
|
||||
// Add imports to the module required by the new methods
|
||||
const imports = converter.getImports();
|
||||
if (imports && imports.length) {
|
||||
// Find where the new imports should go
|
||||
const index = firstAfter(
|
||||
newStatements, statement => statement.kind === ts.SyntaxKind.ImportDeclaration ||
|
||||
statement.kind === ts.SyntaxKind.ImportEqualsDeclaration);
|
||||
newStatements =
|
||||
[...newStatements.slice(0, index), ...imports, ...prefix, ...newStatements.slice(index)];
|
||||
} else {
|
||||
newStatements = [...prefix, ...newStatements];
|
||||
}
|
||||
|
||||
converter.updateSourceMap(newStatements);
|
||||
const newSourceFile = ts.updateSourceFileNode(sourceFile, newStatements);
|
||||
|
||||
return [newSourceFile, converter.getNodeMap()];
|
||||
}
|
||||
|
||||
// Return the index after the first value in `a` that doesn't match the predicate after a value that
|
||||
// does or 0 if no values match.
|
||||
function firstAfter<T>(a: T[], predicate: (value: T) => boolean) {
|
||||
let index = 0;
|
||||
const len = a.length;
|
||||
for (; index < len; index++) {
|
||||
const value = a[index];
|
||||
if (predicate(value)) break;
|
||||
}
|
||||
if (index >= len) return 0;
|
||||
for (; index < len; index++) {
|
||||
const value = a[index];
|
||||
if (!predicate(value)) break;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// A recorded node is a subtype of the node that is marked as being recoreded. This is used
|
||||
// to ensure that NodeEmitterVisitor.record has been called on all nodes returned by the
|
||||
// NodeEmitterVisitor
|
||||
@ -232,9 +325,12 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||
const modifiers = this.getModifiers(stmt);
|
||||
const fields = stmt.fields.map(
|
||||
field => ts.createProperty(
|
||||
/* decorators */ undefined, /* modifiers */ undefined, field.name,
|
||||
/* decorators */ undefined, /* modifiers */ translateModifiers(field.modifiers),
|
||||
field.name,
|
||||
/* questionToken */ undefined,
|
||||
/* type */ undefined, ts.createNull()));
|
||||
/* type */ undefined,
|
||||
field.initializer == null ? ts.createNull() :
|
||||
field.initializer.visitExpression(this, null)));
|
||||
const getters = stmt.getters.map(
|
||||
getter => ts.createGetAccessor(
|
||||
/* decorators */ undefined, /* modifiers */ undefined, getter.name, /* parameters */[],
|
||||
@ -256,7 +352,8 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||
const methods = stmt.methods.filter(method => method.name)
|
||||
.map(
|
||||
method => ts.createMethod(
|
||||
/* decorators */ undefined, /* modifiers */ undefined,
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ translateModifiers(method.modifiers),
|
||||
/* astriskToken */ undefined, method.name !/* guarded by filter */,
|
||||
/* questionToken */ undefined, /* typeParameters */ undefined,
|
||||
method.params.map(
|
||||
@ -547,3 +644,21 @@ function getMethodName(methodRef: {name: string | null; builtin: BuiltinMethod |
|
||||
}
|
||||
throw new Error('Unexpected method reference form');
|
||||
}
|
||||
|
||||
function modifierFromModifier(modifier: StmtModifier): ts.Modifier {
|
||||
switch (modifier) {
|
||||
case StmtModifier.Exported:
|
||||
return ts.createToken(ts.SyntaxKind.ExportKeyword);
|
||||
case StmtModifier.Final:
|
||||
return ts.createToken(ts.SyntaxKind.ConstKeyword);
|
||||
case StmtModifier.Private:
|
||||
return ts.createToken(ts.SyntaxKind.PrivateKeyword);
|
||||
case StmtModifier.Static:
|
||||
return ts.createToken(ts.SyntaxKind.StaticKeyword);
|
||||
}
|
||||
return error(`unknown statement modifier`);
|
||||
}
|
||||
|
||||
function translateModifiers(modifiers: StmtModifier[] | null): ts.Modifier[]|undefined {
|
||||
return modifiers == null ? undefined : modifiers !.map(modifierFromModifier);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
@ -6,7 +7,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
|
||||
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, 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';
|
||||
@ -18,7 +19,8 @@ import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, D
|
||||
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
|
||||
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
|
||||
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
||||
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused} from './util';
|
||||
import {getAngularClassTransformerFactory} from './r3_transform';
|
||||
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
|
||||
|
||||
|
||||
/**
|
||||
@ -65,9 +67,10 @@ class AngularCompilerProgram implements Program {
|
||||
private host: CompilerHost, oldProgram?: Program) {
|
||||
this.rootNames = [...rootNames];
|
||||
const [major, minor] = ts.version.split('.');
|
||||
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
|
||||
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
|
||||
}
|
||||
|
||||
Number(major) > 2 || (Number(major) === 2 && Number(minor) >= 4) ||
|
||||
userError('The Angular Compiler requires TypeScript >= 2.4.');
|
||||
|
||||
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
|
||||
if (oldProgram) {
|
||||
this.oldProgramLibrarySummaries = oldProgram.getLibrarySummaries();
|
||||
@ -79,8 +82,6 @@ class AngularCompilerProgram implements Program {
|
||||
const {host: bundleHost, indexName, errors} =
|
||||
createBundleIndexHost(options, this.rootNames, host);
|
||||
if (errors) {
|
||||
// TODO(tbosch): once we move MetadataBundler from tsc_wrapped into compiler_cli,
|
||||
// directly create ng.Diagnostic instead of using ts.Diagnostic here.
|
||||
this._optionsDiagnostics.push(...errors.map(e => ({
|
||||
category: e.category,
|
||||
messageText: e.messageText as string,
|
||||
@ -196,7 +197,55 @@ class AngularCompilerProgram implements Program {
|
||||
return this.compiler.listLazyRoutes(route, route ? undefined : this.analyzedModules);
|
||||
}
|
||||
|
||||
emit(
|
||||
emit(parameters: {
|
||||
emitFlags?: EmitFlags,
|
||||
cancellationToken?: ts.CancellationToken,
|
||||
customTransformers?: CustomTransformers,
|
||||
emitCallback?: TsEmitCallback
|
||||
} = {}): ts.EmitResult {
|
||||
return this.options.enableIvy === true ? this._emitRender3(parameters) :
|
||||
this._emitRender2(parameters);
|
||||
}
|
||||
|
||||
private _emitRender3(
|
||||
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
|
||||
emitCallback = defaultEmitCallback}: {
|
||||
emitFlags?: EmitFlags,
|
||||
cancellationToken?: ts.CancellationToken,
|
||||
customTransformers?: CustomTransformers,
|
||||
emitCallback?: TsEmitCallback
|
||||
} = {}): ts.EmitResult {
|
||||
const emitStart = Date.now();
|
||||
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Codegen)) ===
|
||||
0) {
|
||||
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
|
||||
}
|
||||
const modules = this.compiler.emitAllPartialModules(this.analyzedModules);
|
||||
|
||||
const writeTsFile: ts.WriteFileCallback =
|
||||
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
|
||||
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
|
||||
let genFile: GeneratedFile|undefined;
|
||||
this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles);
|
||||
};
|
||||
|
||||
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
|
||||
|
||||
const tsCustomTansformers = this.calculateTransforms(
|
||||
/* genFiles */ undefined, /* partialModules */ modules, customTransformers);
|
||||
|
||||
const emitResult = emitCallback({
|
||||
program: this.tsProgram,
|
||||
host: this.host,
|
||||
options: this.options,
|
||||
writeFile: writeTsFile, emitOnlyDtsFiles,
|
||||
customTransformers: tsCustomTansformers
|
||||
});
|
||||
|
||||
return emitResult;
|
||||
}
|
||||
|
||||
private _emitRender2(
|
||||
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
|
||||
emitCallback = defaultEmitCallback}: {
|
||||
emitFlags?: EmitFlags,
|
||||
@ -244,7 +293,8 @@ class AngularCompilerProgram implements Program {
|
||||
}
|
||||
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
|
||||
};
|
||||
const tsCustomTansformers = this.calculateTransforms(genFileByFileName, customTransformers);
|
||||
const tsCustomTansformers = this.calculateTransforms(
|
||||
genFileByFileName, /* partialModules */ 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.
|
||||
@ -400,13 +450,18 @@ class AngularCompilerProgram implements Program {
|
||||
}
|
||||
|
||||
private calculateTransforms(
|
||||
genFiles: Map<string, GeneratedFile>,
|
||||
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
|
||||
customTransformers?: CustomTransformers): ts.CustomTransformers {
|
||||
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
|
||||
if (!this.options.disableExpressionLowering) {
|
||||
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache, this.tsProgram));
|
||||
}
|
||||
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
|
||||
if (genFiles) {
|
||||
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
|
||||
}
|
||||
if (partialModules) {
|
||||
beforeTs.push(getAngularClassTransformerFactory(partialModules));
|
||||
}
|
||||
if (customTransformers && customTransformers.beforeTs) {
|
||||
beforeTs.push(...customTransformers.beforeTs);
|
||||
}
|
||||
|
36
packages/compiler-cli/src/transformers/r3_transform.ts
Normal file
36
packages/compiler-cli/src/transformers/r3_transform.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @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 {PartialModule, Statement, StaticSymbol} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {updateSourceFile} from './node_emitter';
|
||||
|
||||
export type Transformer = (sourceFile: ts.SourceFile) => ts.SourceFile;
|
||||
export type TransformerFactory = (context: ts.TransformationContext) => Transformer;
|
||||
|
||||
/**
|
||||
* Returns a transformer that adds the requested static methods specified by modules.
|
||||
*/
|
||||
export function getAngularClassTransformerFactory(modules: PartialModule[]): TransformerFactory {
|
||||
if (modules.length === 0) {
|
||||
// If no modules are specified, just return an identity transform.
|
||||
return () => sf => sf;
|
||||
}
|
||||
const moduleMap = new Map(modules.map<[string, PartialModule]>(m => [m.fileName, m]));
|
||||
return function(context: ts.TransformationContext) {
|
||||
return function(sourceFile: ts.SourceFile): ts.SourceFile {
|
||||
const module = moduleMap.get(sourceFile.fileName);
|
||||
if (module) {
|
||||
const [newSourceFile] = updateSourceFile(sourceFile, module, context);
|
||||
return newSourceFile;
|
||||
}
|
||||
return sourceFile;
|
||||
};
|
||||
};
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {syntaxError} from '@angular/compiler';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
@ -21,6 +22,14 @@ export function tsStructureIsReused(program: ts.Program): StructureIsReused {
|
||||
return (program as any).structureIsReused;
|
||||
}
|
||||
|
||||
export function error(msg: string): never {
|
||||
throw new Error(`Internal error: ${msg}`);
|
||||
}
|
||||
|
||||
export function userError(msg: string): never {
|
||||
throw syntaxError(msg);
|
||||
}
|
||||
|
||||
export function createMessageDiagnostic(messageText: string): ts.Diagnostic&Diagnostic {
|
||||
return {
|
||||
file: undefined,
|
||||
|
Reference in New Issue
Block a user