diff --git a/aio/content/guide/aot-compiler.md b/aio/content/guide/aot-compiler.md index 8b53f57585..8823ed5348 100644 --- a/aio/content/guide/aot-compiler.md +++ b/aio/content/guide/aot-compiler.md @@ -245,6 +245,16 @@ Tells the compiler to generate all the possible generated files even if they are how `bazel` rules track file dependencies. It is not recommended to use this option outside of the `bazel` rules. + ### *enableIvy* + + 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. + + *Note*: Is it not recommended to use this option as it is not yet feature complete with the Render2 code generation. + + ## Angular Metadata and AOT The Angular **AOT compiler** extracts and interprets **metadata** about the parts of the application that Angular is supposed to manage. diff --git a/karma-js.conf.js b/karma-js.conf.js index 4c5a80083f..9088752d23 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -59,6 +59,7 @@ module.exports = function(config) { 'dist/all/@angular/benchpress/**', 'dist/all/@angular/compiler-cli/**', 'dist/all/@angular/compiler/test/aot/**', + 'dist/all/@angular/compiler/test/render3/**', 'dist/all/@angular/examples/**/e2e_test/*', 'dist/all/@angular/language-service/**', 'dist/all/@angular/router/test/**', diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index be14b7b217..6679592f0d 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -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; } diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts index ae0eb2a663..8ca656ff90 100644 --- a/packages/compiler-cli/src/transformers/node_emitter.ts +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -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] { + 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 = + 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(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); +} \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index d5cca5a58d..9200819ad3 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -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, + genFiles: Map|undefined, partialModules: PartialModule[]|undefined, customTransformers?: CustomTransformers): ts.CustomTransformers { const beforeTs: ts.TransformerFactory[] = []; 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); } diff --git a/packages/compiler-cli/src/transformers/r3_transform.ts b/packages/compiler-cli/src/transformers/r3_transform.ts new file mode 100644 index 0000000000..c162c62c7c --- /dev/null +++ b/packages/compiler-cli/src/transformers/r3_transform.ts @@ -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; + }; + }; +} diff --git a/packages/compiler-cli/src/transformers/util.ts b/packages/compiler-cli/src/transformers/util.ts index 7becf7547a..858be64594 100644 --- a/packages/compiler-cli/src/transformers/util.ts +++ b/packages/compiler-cli/src/transformers/util.ts @@ -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, diff --git a/packages/compiler-cli/test/mocks.ts b/packages/compiler-cli/test/mocks.ts index 9c867bd94f..9183b5181d 100644 --- a/packages/compiler-cli/test/mocks.ts +++ b/packages/compiler-cli/test/mocks.ts @@ -13,7 +13,9 @@ export type Entry = string | Directory; export interface Directory { [name: string]: Entry; } export class MockAotContext { - constructor(public currentDirectory: string, private files: Entry) {} + private files: Entry[]; + + constructor(public currentDirectory: string, ...files: Entry[]) { this.files = files; } fileExists(fileName: string): boolean { return typeof this.getEntry(fileName) === 'string'; } @@ -53,19 +55,7 @@ export class MockAotContext { } parts.shift(); parts = normalize(parts); - let current = this.files; - while (parts.length) { - const part = parts.shift() !; - if (typeof current === 'string') { - return undefined; - } - const next = (current)[part]; - if (next === undefined) { - return undefined; - } - current = next; - } - return current; + return first(this.files, files => getEntryFromFiles(parts, files)); } getDirectories(path: string): string[] { @@ -76,6 +66,31 @@ export class MockAotContext { return Object.keys(dir).filter(key => typeof dir[key] === 'object'); } } + + override(files: Entry) { return new MockAotContext(this.currentDirectory, files, ...this.files); } +} + +function first(a: T[], cb: (value: T) => T | undefined): T|undefined { + for (const value of a) { + const result = cb(value); + if (result != null) return result; + } +} + +function getEntryFromFiles(parts: string[], files: Entry) { + let current = files; + while (parts.length) { + const part = parts.shift() !; + if (typeof current === 'string') { + return undefined; + } + const next = (current)[part]; + if (next === undefined) { + return undefined; + } + current = next; + } + return current; } function normalize(parts: string[]): string[] { diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 65bc967120..1a1d9984f2 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -1807,4 +1807,42 @@ describe('ngc transformer command-line', () => { `); }); }); + + describe('ivy', () => { + function emittedFile(name: string): string { + const outputName = path.resolve(outDir, name); + expect(fs.existsSync(outputName)).toBe(true); + return fs.readFileSync(outputName, {encoding: 'UTF-8'}); + } + + it('should emit the hello world example', () => { + write('tsconfig.json', `{ + "extends": "./tsconfig-base.json", + "files": ["hello-world.ts"], + "angularCompilerOptions": { + "enableIvy": true + } + }`); + + write('hello-world.ts', ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'hello-world', + template: 'Hello, world!' + }) + export class HelloWorldComponent { + + } + + @NgModule({ + declarations: [HelloWorldComponent] + }) + export class HelloWorldModule {} + `); + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]); + expect(exitCode).toBe(0, 'Compile failed'); + expect(emittedFile('hello-world.js')).toContain('ngComponentDef'); + }); + }); }); diff --git a/packages/compiler-cli/test/transformers/r3_transform_spec.ts b/packages/compiler-cli/test/transformers/r3_transform_spec.ts new file mode 100644 index 0000000000..7659f9171a --- /dev/null +++ b/packages/compiler-cli/test/transformers/r3_transform_spec.ts @@ -0,0 +1,163 @@ +/** + * @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} from '@angular/compiler'; +import * as o from '@angular/compiler/src/output/output_ast'; +import {MockAotCompilerHost} from 'compiler/test/aot/test_util'; +import {initDomAdapter} from 'platform-browser/src/browser'; +import * as ts from 'typescript'; + +import {getAngularClassTransformerFactory} from '../../src/transformers/r3_transform'; +import {Directory, MockAotContext, MockCompilerHost} from '../mocks'; + +const someGenFilePath = '/somePackage/someGenFile'; +const someGenFileName = someGenFilePath + '.ts'; + +describe('r3_transform_spec', () => { + let context: MockAotContext; + let host: MockCompilerHost; + + beforeEach(() => { + context = new MockAotContext('/', FILES); + host = new MockCompilerHost(context); + }); + + it('should be able to generate a simple identity function', () => { + expect(emitStaticMethod(new o.ReturnStatement(o.variable('v')), ['v'])) + .toContain('static someMethod(v) { return v; }'); + }); + + it('should be able to generate a static field declaration', + () => { expect(emitStaticField(o.literal(10))).toContain('SomeClass.someField = 10'); }); + + it('should be able to import a symbol', () => { + expect(emitStaticMethod(new o.ReturnStatement( + o.importExpr(new o.ExternalReference('@angular/core', 'Component'))))) + .toContain('static someMethod() { return i0.Component; } }'); + }); + + it('should be able to modify multiple classes in the same module', () => { + const result = emit(getAngularClassTransformerFactory([{ + fileName: someGenFileName, + statements: [ + classMethod(new o.ReturnStatement(o.variable('v')), ['v'], 'someMethod', 'SomeClass'), + classMethod( + new o.ReturnStatement(o.variable('v')), ['v'], 'someOtherMethod', 'SomeOtherClass') + ] + }])); + expect(result).toContain('static someMethod(v) { return v; }'); + expect(result).toContain('static someOtherMethod(v) { return v; }'); + }); + + it('should insert imports after existing imports', () => { + context = context.override({ + somePackage: { + 'someGenFile.ts': ` + import {Component} from '@angular/core'; + + @Component({selector: 'some-class', template: 'hello!'}) + export class SomeClass {} + + export class SomeOtherClass {} + ` + } + }); + host = new MockCompilerHost(context); + + expect(emitStaticMethod(new o.ReturnStatement( + o.importExpr(new o.ExternalReference('@angular/core', 'Component'))))) + .toContain('const core_1 = require("@angular/core"); const i0 = require("@angular/core");'); + }); + + function emit(factory: ts.TransformerFactory): string { + let result: string = ''; + const program = ts.createProgram( + [someGenFileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host); + const moduleSourceFile = program.getSourceFile(someGenFileName); + const transformers: ts.CustomTransformers = {before: [factory]}; + const emitResult = program.emit( + moduleSourceFile, (fileName, data, writeByteOrderMark, onError, sourceFiles) => { + if (fileName.startsWith(someGenFilePath)) { + result = data; + } + }, undefined, undefined, transformers); + return normalizeResult(result); + } + + function emitStaticMethod( + stmt: o.Statement | o.Statement[], parameters: string[] = [], + methodName: string = 'someMethod', className: string = 'SomeClass'): string { + const module: PartialModule = { + fileName: someGenFileName, + statements: [classMethod(stmt, parameters, methodName, className)] + }; + return emit(getAngularClassTransformerFactory([module])); + } + + function emitStaticField( + initializer: o.Expression, fieldName: string = 'someField', + className: string = 'SomeClass'): string { + const module: PartialModule = { + fileName: someGenFileName, + statements: [classField(initializer, fieldName, className)] + }; + return emit(getAngularClassTransformerFactory([module])); + } +}); + +const FILES: Directory = { + somePackage: { + 'someGenFile.ts': ` + + export class SomeClass {} + + export class SomeOtherClass {} +` + } +}; + +function classMethod( + stmt: o.Statement | o.Statement[], parameters: string[] = [], methodName: string = 'someMethod', + className: string = 'SomeClass'): o.ClassStmt { + const statements = Array.isArray(stmt) ? stmt : [stmt]; + return new o.ClassStmt( + /* name */ className, + /* parent */ null, + /* fields */[], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[new o.ClassMethod( + methodName, parameters.map(name => new o.FnParam(name)), statements, null, + [o.StmtModifier.Static])]); +} + +function classField( + initializer: o.Expression, fieldName: string = 'someField', + className: string = 'SomeClass'): o.ClassStmt { + return new o.ClassStmt( + /* name */ className, + /* parent */ null, + /* fields */[new o.ClassField(fieldName, null, [o.StmtModifier.Static], initializer)], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[]); +} + +function normalizeResult(result: string): string { + // Remove TypeScript prefixes + // Remove new lines + // Squish adjacent spaces + // Remove prefix and postfix spaces + return result.replace('"use strict";', ' ') + .replace('exports.__esModule = true;', ' ') + .replace('Object.defineProperty(exports, "__esModule", { value: true });', ' ') + .replace(/\n/g, ' ') + .replace(/ +/g, ' ') + .replace(/^ /g, '') + .replace(/ $/g, ''); +} diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 15496acec8..66610497a0 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -8,6 +8,7 @@ import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, flatten, identifierName, templateSourceUrl, tokenReference} from '../compile_metadata'; import {CompilerConfig} from '../config'; +import {ConstantPool} from '../constant_pool'; import {ViewEncapsulation} from '../core'; import {MessageBundle} from '../i18n/message_bundle'; import {Identifiers, createTokenForExternalReference} from '../identifiers'; @@ -18,11 +19,12 @@ import {NgModuleCompiler} from '../ng_module_compiler'; import {OutputEmitter} from '../output/abstract_emitter'; import * as o from '../output/output_ast'; import {ParseError} from '../parse_util'; +import {compileComponent as compileIvyComponent} from '../render3/r3_view_compiler'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; import {SummaryResolver} from '../summary_resolver'; import {TemplateAst} from '../template_parser/template_ast'; import {TemplateParser} from '../template_parser/template_parser'; -import {OutputContext, ValueVisitor, syntaxError, visitValue} from '../util'; +import {OutputContext, ValueVisitor, error, syntaxError, visitValue} from '../util'; import {TypeCheckCompiler} from '../view_compiler/type_check_compiler'; import {ViewCompileResult, ViewCompiler} from '../view_compiler/view_compiler'; @@ -30,6 +32,7 @@ import {AotCompilerHost} from './compiler_host'; import {AotCompilerOptions} from './compiler_options'; import {GeneratedFile} from './generated_file'; import {LazyRoute, listLazyRoutes, parseLazyRoute} from './lazy_routes'; +import {PartialModule} from './partial_module'; import {StaticReflector} from './static_reflector'; import {StaticSymbol} from './static_symbol'; import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver'; @@ -304,6 +307,45 @@ export class AotCompiler { return messageBundle; } + emitAllPartialModules({ngModuleByPipeOrDirective, files}: NgAnalyzedModules): PartialModule[] { + // Using reduce like this is a select many pattern (where map is a select pattern) + return files.reduce((r, file) => { + r.push(...this._emitPartialModule( + file.fileName, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules, + file.injectables)); + return r; + }, []); + } + + private _emitPartialModule( + fileName: string, ngModuleByPipeOrDirective: Map, + directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[], + injectables: StaticSymbol[]): PartialModule[] { + const classes: o.ClassStmt[] = []; + + const context = this._createOutputContext(fileName); + + // Process all components + directives.forEach(directiveType => { + const directiveMetadata = this._metadataResolver.getDirectiveMetadata(directiveType); + if (directiveMetadata.isComponent) { + const module = ngModuleByPipeOrDirective.get(directiveType) !; + module || + error( + `Cannot determine the module for component '${identifierName(directiveMetadata.type)}'`); + + const {template: parsedTemplate} = + this._parseTemplate(directiveMetadata, module, module.transitiveModule.directives); + compileIvyComponent(context, directiveMetadata, parsedTemplate, this._reflector); + } + }); + + if (context.statements) { + return [{fileName, statements: [...context.constantPool.statements, ...context.statements]}]; + } + return []; + } + emitAllImpls(analyzeResult: NgAnalyzedModules): GeneratedFile[] { const {ngModuleByPipeOrDirective, files} = analyzeResult; const sourceModules = files.map( @@ -547,7 +589,7 @@ export class AotCompiler { new o.ExternalReference(moduleName, name, null), allTypeParams)); }; - return {statements: [], genFilePath, importExpr}; + return {statements: [], genFilePath, importExpr, constantPool: new ConstantPool()}; } private _fileNameToModuleName(importedFilePath: string, containingFilePath: string): string { diff --git a/packages/compiler/src/aot/partial_module.ts b/packages/compiler/src/aot/partial_module.ts new file mode 100644 index 0000000000..7e205bfdf6 --- /dev/null +++ b/packages/compiler/src/aot/partial_module.ts @@ -0,0 +1,14 @@ +/** + * @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 * as o from '../output/output_ast'; + +export interface PartialModule { + fileName: string; + statements: o.Statement[]; +} diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 1cf8496156..960c40a288 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -36,6 +36,7 @@ export * from './aot/generated_file'; export * from './aot/compiler_options'; export * from './aot/compiler_host'; export * from './aot/formatted_error'; +export * from './aot/partial_module'; export * from './aot/static_reflector'; export * from './aot/static_symbol'; export * from './aot/static_symbol_resolver'; @@ -66,7 +67,7 @@ export * from './ml_parser/html_tags'; export * from './ml_parser/interpolation_config'; export * from './ml_parser/tags'; export {NgModuleCompiler} from './ng_module_compiler'; -export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, collectExternalReferences} from './output/output_ast'; +export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassMethod, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, collectExternalReferences} from './output/output_ast'; export {EmitterVisitorContext} from './output/abstract_emitter'; export * from './output/ts_emitter'; export * from './parse_util'; diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 59aa5bdfd0..0b942328e7 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ - import * as cdAst from '../expression_parser/ast'; import {Identifiers} from '../identifiers'; import * as o from '../output/output_ast'; @@ -19,13 +18,15 @@ export class ConvertActionBindingResult { constructor(public stmts: o.Statement[], public allowDefault: o.ReadVarExpr) {} } +export type InterpolationFunction = (args: o.Expression[]) => o.Expression; + /** * Converts the given expression AST into an executable output AST, assuming the expression is * used in an action binding (e.g. an event handler). */ export function convertActionBinding( localResolver: LocalResolver | null, implicitReceiver: o.Expression, action: cdAst.AST, - bindingId: string): ConvertActionBindingResult { + bindingId: string, interpolationFunction?: InterpolationFunction): ConvertActionBindingResult { if (!localResolver) { localResolver = new DefaultLocalResolver(); } @@ -52,7 +53,8 @@ export function convertActionBinding( }, action); - const visitor = new _AstToIrVisitor(localResolver, implicitReceiver, bindingId); + const visitor = + new _AstToIrVisitor(localResolver, implicitReceiver, bindingId, interpolationFunction); const actionStmts: o.Statement[] = []; flattenStatements(actionWithoutBuiltins.visit(visitor, _Mode.Statement), actionStmts); prependTemporaryDecls(visitor.temporaryCount, bindingId, actionStmts); @@ -98,6 +100,7 @@ export enum BindingForm { // otherise generate a general binding TrySimple, } + /** * Converts the given expression AST into an executable output AST, assuming the expression * is used in property binding. The expression has to be preprocessed via @@ -105,14 +108,15 @@ export enum BindingForm { */ export function convertPropertyBinding( localResolver: LocalResolver | null, implicitReceiver: o.Expression, - expressionWithoutBuiltins: cdAst.AST, bindingId: string, - form: BindingForm): ConvertPropertyBindingResult { + expressionWithoutBuiltins: cdAst.AST, bindingId: string, form: BindingForm, + interpolationFunction?: InterpolationFunction): ConvertPropertyBindingResult { if (!localResolver) { localResolver = new DefaultLocalResolver(); } const currValExpr = createCurrValueExpr(bindingId); const stmts: o.Statement[] = []; - const visitor = new _AstToIrVisitor(localResolver, implicitReceiver, bindingId); + const visitor = + new _AstToIrVisitor(localResolver, implicitReceiver, bindingId, interpolationFunction); const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression); if (visitor.temporaryCount) { @@ -200,7 +204,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { constructor( private _localResolver: LocalResolver, private _implicitReceiver: o.Expression, - private bindingId: string) {} + private bindingId: string, private interpolationFunction: InterpolationFunction|undefined) {} visitBinary(ast: cdAst.Binary, mode: _Mode): any { let op: o.BinaryOperator; @@ -303,6 +307,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } args.push(o.literal(ast.strings[ast.strings.length - 1])); + if (this.interpolationFunction) { + return this.interpolationFunction(args); + } return ast.expressions.length <= 9 ? o.importExpr(Identifiers.inlineInterpolate).callFn(args) : o.importExpr(Identifiers.interpolate).callFn([args[0], o.literalArr(args.slice(1))]); diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts new file mode 100644 index 0000000000..1284c7d905 --- /dev/null +++ b/packages/compiler/src/constant_pool.ts @@ -0,0 +1,141 @@ +/** + * @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 * as o from './output/output_ast'; +import {OutputContext, error} from './util'; + +export const enum DefinitionKind {Injector, Directive, Component} + +/** + * A node that is a place-holder that allows the node to be replaced when the actual + * node is known. + * + * This allows the constant pool to change an expression from a direct reference to + * a constant to a shared constant. It returns a fix-up node that is later allowed to + * change the referenced expression. + */ +class FixupExpression extends o.Expression { + constructor(public resolved: o.Expression) { super(resolved.type); } + + shared: boolean; + + visitExpression(visitor: o.ExpressionVisitor, context: any): any { + this.resolved.visitExpression(visitor, context); + } + + isEquivalent(e: o.Expression): boolean { + return e instanceof FixupExpression && this.resolved.isEquivalent(e.resolved); + } + + fixup(expression: o.Expression) { + this.resolved = expression; + this.shared = true; + } +} + +/** + * A constant pool allows a code emitter to share constant in an output context. + * + * The constant pool also supports sharing access to ivy definitions references. + */ +export class ConstantPool { + statements: o.Statement[] = []; + private literals = new Map(); + private injectorDefinitions = new Map(); + private directiveDefinitions = new Map(); + private componentDefintions = new Map(); + + private nextNameIndex = 0; + + getConstLiteral(literal: o.Expression): o.Expression { + const key = this.keyOf(literal); + let fixup = this.literals.get(key); + if (!fixup) { + fixup = new FixupExpression(literal); + this.literals.set(key, fixup); + } else if (!fixup.shared) { + // Replace the expression with a variable + const name = this.freshName(); + this.statements.push( + o.variable(name).set(literal).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final])); + fixup.fixup(o.variable(name)); + } + return fixup; + } + + getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext): o.Expression { + const declarations = kind == DefinitionKind.Component ? + this.componentDefintions : + kind == DefinitionKind.Directive ? this.directiveDefinitions : this.injectorDefinitions; + let fixup = declarations.get(type); + if (!fixup) { + const property = kind == DefinitionKind.Component ? + 'ngComponentDef' : + kind == DefinitionKind.Directive ? 'ngDirectiveDef' : 'ngInjectorDef'; + fixup = new FixupExpression(ctx.importExpr(type).prop(property)); + declarations.set(type, fixup); + } else if (!fixup.shared) { + const name = this.freshName(); + this.statements.push( + o.variable(name).set(fixup.resolved).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final])); + fixup.fixup(o.variable(name)); + } + return fixup; + } + + /** + * Produce a unique name. + * + * The name might be unique among different prefixes if any of the prefixes end in + * a digit so the prefix should be a constant string (not based on user input) and + * must not end in a digit. + */ + uniqueName(prefix: string): string { return `${prefix}${this.nextNameIndex++}`; } + + private freshName(): string { return this.uniqueName(`_$`); } + + private keyOf(expression: o.Expression) { + return expression.visitExpression(new KeyVisitor(), null); + } +} + +class KeyVisitor implements o.ExpressionVisitor { + visitLiteralExpr(ast: o.LiteralExpr): string { return `${ast.value}`; } + visitLiteralArrayExpr(ast: o.LiteralArrayExpr): string { + return ast.entries.map(entry => entry.visitExpression(this, null)).join(','); + } + + visitLiteralMapExpr(ast: o.LiteralMapExpr): string { + const entries = + ast.entries.map(entry => `${entry.key}:${entry.value.visitExpression(this, null)}`); + return `{${entries.join(',')}`; + } + + visitReadVarExpr = invalid; + visitWriteVarExpr = invalid; + visitWriteKeyExpr = invalid; + visitWritePropExpr = invalid; + visitInvokeMethodExpr = invalid; + visitInvokeFunctionExpr = invalid; + visitInstantiateExpr = invalid; + visitExternalExpr = invalid; + visitConditionalExpr = invalid; + visitNotExpr = invalid; + visitAssertNotNullExpr = invalid; + visitCastExpr = invalid; + visitFunctionExpr = invalid; + visitBinaryOperatorExpr = invalid; + visitReadPropExpr = invalid; + visitReadKeyExpr = invalid; + visitCommaExpr = invalid; +} + +function invalid(arg: o.Expression | o.Statement): never { + throw new Error( + `Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`); +} diff --git a/packages/compiler/src/jit/compiler.ts b/packages/compiler/src/jit/compiler.ts index ff8636c2dc..814d5b557c 100644 --- a/packages/compiler/src/jit/compiler.ts +++ b/packages/compiler/src/jit/compiler.ts @@ -9,6 +9,7 @@ import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileTypeSummary, ProviderMeta, ProxyClass, identifierName, ngModuleJitUrl, sharedStylesheetJitUrl, templateJitUrl, templateSourceUrl} from '../compile_metadata'; import {CompileReflector} from '../compile_reflector'; import {CompilerConfig} from '../config'; +import {ConstantPool} from '../constant_pool'; import {Type} from '../core'; import {CompileMetadataResolver} from '../metadata_resolver'; import {NgModuleCompiler} from '../ng_module_compiler'; @@ -355,5 +356,5 @@ function assertComponent(meta: CompileDirectiveMetadata) { function createOutputContext(): OutputContext { const importExpr = (symbol: any) => ir.importExpr({name: identifierName(symbol), moduleName: null, runtime: symbol}); - return {statements: [], genFilePath: '', importExpr}; + return {statements: [], genFilePath: '', importExpr, constantPool: new ConstantPool()}; } diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index ab0e561d56..81bd23efa2 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -8,6 +8,7 @@ import {ParseSourceSpan} from '../parse_util'; +import {error} from '../util'; //// Types export enum TypeModifier { @@ -644,7 +645,8 @@ export const TYPED_NULL_EXPR = new LiteralExpr(null, INFERRED_TYPE, null); export enum StmtModifier { Final, Private, - Exported + Exported, + Static, } export abstract class Statement { @@ -739,7 +741,9 @@ export class AbstractClassPart { } export class ClassField extends AbstractClassPart { - constructor(public name: string, type?: Type|null, modifiers: StmtModifier[]|null = null) { + constructor( + public name: string, type?: Type|null, modifiers: StmtModifier[]|null = null, + public initializer?: Expression) { super(type, modifiers); } isEquivalent(f: ClassField) { return this.name === f.name; } @@ -1370,6 +1374,10 @@ export function fn( return new FunctionExpr(params, body, type, sourceSpan); } +export function ifStmt(condition: Expression, thenClause: Statement[], elseClause?: Statement[]) { + return new IfStmt(condition, thenClause, elseClause); +} + export function literal( value: any, type?: Type | null, sourceSpan?: ParseSourceSpan | null): LiteralExpr { return new LiteralExpr(value, type, sourceSpan); diff --git a/packages/compiler/src/output/ts_emitter.ts b/packages/compiler/src/output/ts_emitter.ts index 6f3b138c1e..58d5921891 100644 --- a/packages/compiler/src/output/ts_emitter.ts +++ b/packages/compiler/src/output/ts_emitter.ts @@ -216,8 +216,15 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor // comment out as a workaround for #10967 ctx.print(null, `/*private*/ `); } + if (field.hasModifier(o.StmtModifier.Static)) { + ctx.print(null, 'static '); + } ctx.print(null, field.name); this._printColonType(field.type, ctx); + if (field.initializer) { + ctx.print(null, ' = '); + field.initializer.visitExpression(this, ctx); + } ctx.println(null, `;`); } diff --git a/packages/compiler/src/parse_util.ts b/packages/compiler/src/parse_util.ts index 307cb57d4d..12dd1fb4e5 100644 --- a/packages/compiler/src/parse_util.ts +++ b/packages/compiler/src/parse_util.ts @@ -7,6 +7,7 @@ */ import * as chars from './chars'; import {CompileIdentifierMetadata, identifierModuleUrl, identifierName} from './compile_metadata'; +import {error} from './util'; export class ParseLocation { constructor( diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts new file mode 100644 index 0000000000..089d9b4283 --- /dev/null +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -0,0 +1,85 @@ +/** + * @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 * as o from '../output/output_ast'; + +const CORE = '@angular/core'; + +// Copied from core and must be in sync with the value in the runtime. +export const enum LifeCycleGuard {ON_INIT = 1, ON_DESTROY = 2, ON_CHANGES = 4} + +// TODO: Include assignments that use the enum literals +// e.g. { let a: core.LifeCycleGuard.ON_INIT = LifeCycleGuard.ON_INIT; ...} +// Ensure these get removed in bundling. + +export class Identifiers { + /* Methods */ + static NEW_METHOD = 'n'; + static HOST_BINDING_METHOD = 'h'; + static REFRESH_METHOD = 'r'; + + /* Instructions */ + static createElement: o.ExternalReference = {name: 'ɵE', moduleName: CORE}; + + static elementEnd: o.ExternalReference = {name: 'ɵe', moduleName: CORE}; + + static elementProperty: o.ExternalReference = {name: 'ɵp', moduleName: CORE}; + + static elementAttribute: o.ExternalReference = {name: 'ɵa', moduleName: CORE}; + + static elementClass: o.ExternalReference = {name: 'ɵk', moduleName: CORE}; + + static elementStyle: o.ExternalReference = {name: 'ɵs', moduleName: CORE}; + + static containerCreate: o.ExternalReference = {name: 'ɵC', moduleName: CORE}; + + static containerEnd: o.ExternalReference = {name: 'ɵc', moduleName: CORE}; + + static directiveCreate: o.ExternalReference = {name: 'ɵD', moduleName: CORE}; + + static text: o.ExternalReference = {name: 'ɵT', moduleName: CORE}; + + static directiveInput: o.ExternalReference = {name: 'ɵi', moduleName: CORE}; + + static textCreateBound: o.ExternalReference = {name: 'ɵt', moduleName: CORE}; + + static bind: o.ExternalReference = {name: 'ɵb', moduleName: CORE}; + + static bind1: o.ExternalReference = {name: 'ɵb1', moduleName: CORE}; + static bind2: o.ExternalReference = {name: 'ɵb2', moduleName: CORE}; + static bind3: o.ExternalReference = {name: 'ɵb3', moduleName: CORE}; + static bind4: o.ExternalReference = {name: 'ɵb4', moduleName: CORE}; + static bind5: o.ExternalReference = {name: 'ɵb5', moduleName: CORE}; + static bind6: o.ExternalReference = {name: 'ɵb6', moduleName: CORE}; + static bind7: o.ExternalReference = {name: 'ɵb7', moduleName: CORE}; + static bind8: o.ExternalReference = {name: 'ɵb8', moduleName: CORE}; + static bind9: o.ExternalReference = {name: 'ɵb9', moduleName: CORE}; + static bindV: o.ExternalReference = {name: 'ɵbV', moduleName: CORE}; + + static refreshComponent: o.ExternalReference = {name: 'ɵr', moduleName: CORE}; + + static directiveLifeCycle: o.ExternalReference = {name: 'ɵl', moduleName: CORE}; + + static injectElementRef: o.ExternalReference = {name: 'ɵinjectElementRef', moduleName: CORE}; + + static injectTemplateRef: o.ExternalReference = {name: 'ɵinjectTemplateRef', moduleName: CORE}; + + static injectViewContainerRef: + o.ExternalReference = {name: 'ɵinjectViewContainerRef', moduleName: CORE}; + + static inject: o.ExternalReference = {name: 'ɵinject', moduleName: CORE}; + + static defineComponent: o.ExternalReference = {name: 'ɵdefineComponent', moduleName: CORE}; + + static defineDirective: o.ExternalReference = { + name: 'ɵdefineDirective', + moduleName: CORE, + }; + + static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; +} \ No newline at end of file diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts new file mode 100644 index 0000000000..a8c5633469 --- /dev/null +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -0,0 +1,432 @@ +/** + * @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 {CompileDirectiveMetadata, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; +import {CompileReflector} from '../compile_reflector'; +import {BindingForm, BuiltinConverter, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; +import {ConstantPool, DefinitionKind} from '../constant_pool'; +import {AST} from '../expression_parser/ast'; +import {Identifiers} from '../identifiers'; +import * as o from '../output/output_ast'; +import {ParseSourceSpan} from '../parse_util'; +import {CssSelector} from '../selector'; +import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast'; +import {OutputContext, error} from '../util'; + +import {Identifiers as R3} from './r3_identifiers'; + + +/** Name of the context parameter passed into a template function */ +const CONTEXT_NAME = 'ctx'; + +/** Name of the creation mode flag passed into a template function */ +const CREATION_MODE_FLAG = 'cm'; + +/** Name of the temporary to use during data binding */ +const TEMPORARY_NAME = '_t'; + +export function compileComponent( + outputCtx: OutputContext, component: CompileDirectiveMetadata, template: TemplateAst[], + reflector: CompileReflector) { + const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; + + // e.g. `type: MyApp` + definitionMapValues.push( + {key: 'type', value: outputCtx.importExpr(component.type.reference), quoted: false}); + + // e.g. `tag: 'my-app' + // This is optional and only included if the first selector of a component has element. + const selector = component.selector && CssSelector.parse(component.selector); + const firstSelector = selector && selector[0]; + if (firstSelector && firstSelector.hasElementSelector()) { + definitionMapValues.push({key: 'tag', value: o.literal(firstSelector.element), quoted: false}); + } + + // e.g. `attr: ["class", ".my.app"] + // This is optional an only included if the first selector of a component specifies attributes. + if (firstSelector) { + const selectorAttributes = firstSelector.getAttrs(); + if (selectorAttributes.length) { + definitionMapValues.push({ + key: 'attrs', + value: outputCtx.constantPool.getConstLiteral(o.literalArr(selectorAttributes.map( + value => value != null ? o.literal(value) : o.literal(undefined)))), + quoted: false + }); + } + } + + // e.g. `template: function(_ctx, _cm) {...}` + const templateFunctionExpression = + new TemplateDefinitionBuilder(outputCtx, outputCtx.constantPool, CONTEXT_NAME) + .buildTemplateFunction(template); + definitionMapValues.push({key: 'template', value: templateFunctionExpression, quoted: false}); + + + // e.g. `factory: () => new MyApp(injectElementRef())` + const templateFactory = createFactory(component.type, outputCtx, reflector); + definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); + + const className = identifierName(component.type) !; + className || error(`Cannot resolver the name of ${component.type}`); + + // Create the partial class to be merged with the actual class. + outputCtx.statements.push(new o.ClassStmt( + /* name */ className, + /* parent */ null, + /* fields */[new o.ClassField( + /* name */ 'ngComponentDef', + /* type */ o.INFERRED_TYPE, + /* modifiers */[o.StmtModifier.Static], + /* initializer */ o.importExpr(R3.defineComponent).callFn([o.literalMap( + definitionMapValues)]))], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[])); +} + + +// TODO: Remove these when the things are fully supported +function unknown(arg: o.Expression | o.Statement | TemplateAst): never { + throw new Error(`Builder ${this.constructor.name} is unable to handle ${o.constructor.name} yet`); +} +function unsupported(feature: string): never { + if (this) { + throw new Error(`Builder ${this.constructor.name} doesn't support ${feature} yet`); + } + throw new Error(`Feature ${feature} is supported yet`); +} + +const BINDING_INSTRUCTION_MAP: {[index: number]: o.ExternalReference | undefined} = { + [PropertyBindingType.Property]: R3.elementProperty, + [PropertyBindingType.Attribute]: R3.elementAttribute, + [PropertyBindingType.Class]: R3.elementClass, + [PropertyBindingType.Style]: R3.elementStyle +}; + +function interpolate(args: o.Expression[]): o.Expression { + args = args.slice(1); // Ignore the length prefix added for render2 + switch (args.length) { + case 3: + return o.importExpr(R3.bind1).callFn(args); + case 5: + return o.importExpr(R3.bind2).callFn(args); + case 7: + return o.importExpr(R3.bind3).callFn(args); + case 9: + return o.importExpr(R3.bind4).callFn(args); + case 11: + return o.importExpr(R3.bind5).callFn(args); + case 13: + return o.importExpr(R3.bind6).callFn(args); + case 15: + return o.importExpr(R3.bind7).callFn(args); + case 17: + return o.importExpr(R3.bind8).callFn(args); + case 19: + return o.importExpr(R3.bind9).callFn(args); + } + (args.length > 19 && args.length % 2 == 1) || + error(`Invalid interpolation argument length ${args.length}`); + return o.importExpr(R3.bindV).callFn(args); +} + +class TemplateDefinitionBuilder implements TemplateAstVisitor { + private _dataIndex = 0; + private _bindingContext = 0; + private _temporaryAllocated = false; + private _prefix: o.Statement[] = []; + private _creationMode: o.Statement[] = []; + private _bindingMode: o.Statement[] = []; + private _hostMode: o.Statement[] = []; + private _refreshMode: o.Statement[] = []; + private _postfix: o.Statement[] = []; + private unsupported = unsupported; + private invalid = invalid; + + constructor( + private outputCtx: OutputContext, private constantPool: ConstantPool, + private contextParameter: string, private level = 0) {} + + buildTemplateFunction(asts: TemplateAst[]): o.FunctionExpr { + templateVisitAll(this, asts); + + return o.fn( + [ + new o.FnParam(this.contextParameter, null), new o.FnParam(CREATION_MODE_FLAG, o.BOOL_TYPE) + ], + [ + // Temporary variable declarations (i.e. let _t: any;) + ...this._prefix, + + // Creating mode (i.e. if (cm) { ... }) + o.ifStmt(o.variable(CREATION_MODE_FLAG), this._creationMode), + + // Binding mode (i.e. ɵp(...)) + ...this._bindingMode, + + // Host mode (i.e. Comp.h(...)) + ...this._hostMode, + + // Refesh mode (i.e. Comp.r(...)) + ...this._refreshMode, + + // Nested templates (i.e. function CompTemplate() {}) + ...this._postfix + ], + o.INFERRED_TYPE); + } + + // TODO(chuckj): Implement ng-content + visitNgContent = unknown; + + visitElement(ast: ElementAst) { + let bindingCount = 0; + const elementIndex = this.allocateNode(); + + // Element creation mode + const component = findComponent(ast.directives); + const parameters: o.Expression[] = [o.literal(elementIndex)]; + if (component) { + parameters.push(this.typeReference(component.directive.type.reference)); + } else { + parameters.push(o.literal(ast.name)); + } + + const attributes: o.Expression[] = []; + for (let attr of ast.attrs) { + attributes.push(o.literal(attr.name), o.literal(attr.value)); + } + + if (attributes.length !== 0) { + parameters.push(this.constantPool.getConstLiteral(o.literalArr(attributes))); + } + + this.instruction(this._creationMode, ast.sourceSpan, R3.createElement, ...parameters); + + const implicit = o.variable(this.contextParameter); + + // Generate element input bindings + for (let input of ast.inputs) { + if (input.isAnimation) { + this.unsupported('animations'); + } + // TODO(chuckj): Builtins transform? + const convertedBinding = convertPropertyBinding( + null, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); + this._bindingMode.push(...convertedBinding.stmts); + const parameters = + [o.literal(elementIndex), o.literal(input.name), convertedBinding.currValExpr]; + const instruction = BINDING_INSTRUCTION_MAP[input.type]; + if (instruction) { + // TODO(chuckj): runtime: security context? + this.instruction( + this._bindingMode, input.sourceSpan, instruction, o.literal(elementIndex), + o.literal(input.name), convertedBinding.currValExpr); + } else { + this.unsupported(`binding ${PropertyBindingType[input.type]}`); + } + } + + // Generate directives input bindings + this._visitDirectives(ast.directives, implicit, elementIndex); + + // Traverse element child nodes + templateVisitAll(this, ast.children); + + + // Finish element construction mode. + this.instruction(this._creationMode, ast.endSourceSpan || ast.sourceSpan, R3.elementEnd); + } + + private _visitDirectives(directives: DirectiveAst[], implicit: o.Expression, nodeIndex: number) { + for (let directive of directives) { + const directiveIndex = this.allocateDirective(); + + // Creation mode + // e.g. D(0, TodoComponentDef.n(), TodoComponentDef); + const directiveType = directive.directive.type.reference; + const kind = + directive.directive.isComponent ? DefinitionKind.Component : DefinitionKind.Directive; + + // Note: *do not cache* calls to this.directiveOf() as the constant pool needs to know if the + // node is referenced multiple times to know that it must generate the reference into a + // temporary. + + this.instruction( + this._creationMode, directive.sourceSpan, R3.directiveCreate, o.literal(directiveIndex), + this.definitionOf(directiveType, kind) + .callMethod(R3.NEW_METHOD, [], directive.sourceSpan), + this.definitionOf(directiveType, kind)); + + // Bindings + for (const input of directive.inputs) { + const convertedBinding = convertPropertyBinding( + null, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); + this._bindingMode.push(...convertedBinding.stmts); + this.instruction( + this._bindingMode, directive.sourceSpan, R3.elementProperty, + o.literal(input.templateName), o.literal(nodeIndex), convertedBinding.currValExpr); + } + + // e.g. TodoComponentDef.h(0, 0); + this._hostMode.push( + this.definitionOf(directiveType, kind) + .callMethod(R3.HOST_BINDING_METHOD, [o.literal(directiveIndex), o.literal(nodeIndex)]) + .toStmt()); + + // e.g. TodoComponentDef.r(0, 0); + this._refreshMode.push( + this.definitionOf(directiveType, kind) + .callMethod(R3.REFRESH_METHOD, [o.literal(directiveIndex), o.literal(nodeIndex)]) + .toStmt()); + } + } + + visitEmbeddedTemplate(ast: EmbeddedTemplateAst) { + const templateIndex = this.allocateNode(); + + const templateName = `C${templateIndex}Template`; + const templateContext = `ctx${this.level}`; + + // TODO(chuckj): attrs? + + // e.g. C(1, C1Template) + this.instruction( + this._creationMode, ast.sourceSpan, R3.containerCreate, o.literal(templateIndex), + o.variable(templateName)); + + // Generate directies + this._visitDirectives( + ast.directives, o.variable(this.contextParameter), + // TODO(chuckj): This should be the element index of the element that contained the template + templateIndex); + + // Create the template function + const templateVisitor = new TemplateDefinitionBuilder( + this.outputCtx, this.constantPool, templateContext, this.level + 1); + const templateFunctionExpr = templateVisitor.buildTemplateFunction(ast.children); + this._postfix.push(templateFunctionExpr.toDeclStmt(templateName, null)); + + // Terminate the definition + this.instruction(this._creationMode, ast.sourceSpan, R3.containerEnd); + } + + // These should be handled in the template or element directly. + readonly visitReference = invalid; + readonly visitVariable = invalid; + readonly visitEvent = invalid; + readonly visitElementProperty = invalid; + readonly visitAttr = invalid; + + visitBoundText(ast: BoundTextAst) { + const nodeIndex = this.allocateNode(); + + // Creation mode + this.instruction(this._creationMode, ast.sourceSpan, R3.text, o.literal(nodeIndex)); + + // Refresh mode + this.instruction( + this._refreshMode, ast.sourceSpan, R3.textCreateBound, o.literal(nodeIndex), + this.bind(o.variable(this.contextParameter), ast.value, ast.sourceSpan)); + } + + visitText(ast: TextAst) { + // Text is defined in creation mode only. + this.instruction(this._creationMode, ast.sourceSpan, R3.text, o.literal(ast.value)); + } + + // These should be handled in the template or element directly + readonly visitDirective = invalid; + readonly visitDirectiveProperty = invalid; + + private allocateDirective() { return this._dataIndex++; } + private allocateNode() { return this._dataIndex++; } + private bindingContext() { return `${this._bindingContext++}`; } + + private instruction( + statements: o.Statement[], span: ParseSourceSpan, reference: o.ExternalReference, + ...params: o.Expression[]) { + statements.push(o.importExpr(reference, null, span).callFn(params, span).toStmt()); + } + + private typeReference(type: any): o.Expression { return this.outputCtx.importExpr(type); } + + private definitionOf(type: any, kind: DefinitionKind): o.Expression { + return this.constantPool.getDefinition(type, kind, this.outputCtx); + } + + private temp(): o.ReadVarExpr { + if (!this._temporaryAllocated) { + this._prefix.push(o.variable(TEMPORARY_NAME, o.DYNAMIC_TYPE,  null) + .set(o.literal(undefined)) + .toDeclStmt(o.DYNAMIC_TYPE)); + this._temporaryAllocated = true; + } + return o.variable(TEMPORARY_NAME); + } + + private convertPropertyBinding(implicit: o.Expression, value: AST): o.Expression { + const convertedPropertyBinding = convertPropertyBinding( + null, implicit, value, this.bindingContext(), BindingForm.TrySimple, interpolate); + this._refreshMode.push(...convertedPropertyBinding.stmts); + return convertedPropertyBinding.currValExpr; + } + + private bind(implicit: o.Expression, value: AST, sourceSpan: ParseSourceSpan): o.Expression { + return o.importExpr(R3.bind).callFn([this.convertPropertyBinding(implicit, value)]); + } +} + +function createFactory( + type: CompileTypeMetadata, outputCtx: OutputContext, + reflector: CompileReflector): o.FunctionExpr { + let args: o.Expression[] = []; + + const elementRef = reflector.resolveExternalReference(Identifiers.ElementRef); + const templateRef = reflector.resolveExternalReference(Identifiers.TemplateRef); + const viewContainerRef = reflector.resolveExternalReference(Identifiers.ViewContainerRef); + + for (let dependency of type.diDeps) { + if (dependency.isValue) { + unsupported('value dependencies'); + } + if (dependency.isHost) { + unsupported('host dependencies'); + } + const token = dependency.token; + if (token) { + const tokenRef = tokenReference(token); + if (tokenRef === elementRef) { + args.push(o.importExpr(R3.injectElementRef).callFn([])); + } else if (tokenRef === templateRef) { + args.push(o.importExpr(R3.injectTemplateRef).callFn([])); + } else if (tokenRef === viewContainerRef) { + args.push(o.importExpr(R3.injectViewContainerRef).callFn([])); + } else { + args.push(o.importExpr(R3.inject).callFn([outputCtx.importExpr(token)])); + } + } else { + unsupported('dependency without a token'); + } + } + + return o.fn( + [], + [new o.ReturnStatement(new o.InstantiateExpr(outputCtx.importExpr(type.reference), args))], + o.INFERRED_TYPE); +} + +function invalid(arg: o.Expression | o.Statement | TemplateAst): never { + throw new Error( + `Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`); +} + +function findComponent(directives: DirectiveAst[]): DirectiveAst|undefined { + return directives.filter(directive => directive.directive.isComponent)[0]; +} diff --git a/packages/compiler/src/selector.ts b/packages/compiler/src/selector.ts index f5e7c56ad1..4576efaed9 100644 --- a/packages/compiler/src/selector.ts +++ b/packages/compiler/src/selector.ts @@ -104,6 +104,14 @@ export class CssSelector { `<${tagName}${classAttr}${attrs}>`; } + getAttrs(): string[] { + const result: string[] = []; + if (this.classNames.length > 0) { + result.push('class', this.classNames.join(' ')); + } + return result.concat(this.attrs); + } + addAttribute(name: string, value: string = '') { this.attrs.push(name, value && value.toLowerCase() || ''); } diff --git a/packages/compiler/src/util.ts b/packages/compiler/src/util.ts index 064c703030..6dc2cf8da1 100644 --- a/packages/compiler/src/util.ts +++ b/packages/compiler/src/util.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {ConstantPool} from './constant_pool'; + import * as o from './output/output_ast'; import {ParseError} from './parse_util'; @@ -90,6 +92,10 @@ export const SyncAsync = { } }; +export function error(msg: string): never { + throw new Error(`Internal Error: ${msg}`); +} + export function syntaxError(msg: string, parseErrors?: ParseError[]): Error { const error = Error(msg); (error as any)[ERROR_SYNTAX_ERROR] = true; @@ -152,6 +158,7 @@ export function utf8Encode(str: string): string { export interface OutputContext { genFilePath: string; statements: o.Statement[]; + constantPool: ConstantPool; importExpr(reference: any, typeParams?: o.Type[]|null, useSummaries?: boolean): o.Expression; } diff --git a/packages/compiler/test/aot/summary_resolver_spec.ts b/packages/compiler/test/aot/summary_resolver_spec.ts index c431d863dc..75ad8f4195 100644 --- a/packages/compiler/test/aot/summary_resolver_spec.ts +++ b/packages/compiler/test/aot/summary_resolver_spec.ts @@ -8,6 +8,7 @@ import {AotSummaryResolver, AotSummaryResolverHost, CompileSummaryKind, CompileTypeSummary, ResolvedStaticSymbol, StaticSymbol, StaticSymbolCache, StaticSymbolResolver} from '@angular/compiler'; import {deserializeSummaries, serializeSummaries} from '@angular/compiler/src/aot/summary_serializer'; +import {ConstantPool} from '@angular/compiler/src/constant_pool'; import * as o from '@angular/compiler/src/output/output_ast'; import {OutputContext} from '@angular/compiler/src/util'; import * as path from 'path'; @@ -127,5 +128,10 @@ export class MockAotSummaryResolverHost implements AotSummaryResolverHost { } export function createMockOutputContext(): OutputContext { - return {statements: [], genFilePath: 'someGenFilePath', importExpr: () => o.NULL_EXPR}; + return { + statements: [], + genFilePath: 'someGenFilePath', + importExpr: () => o.NULL_EXPR, + constantPool: new ConstantPool() + }; } \ No newline at end of file diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index b8ba9b1889..969d696157 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -538,10 +538,12 @@ const minCoreIndex = ` export * from './src/codegen_private_exports'; `; -export function setup(options: {compileAngular: boolean, compileAnimations: boolean} = { - compileAngular: true, - compileAnimations: true, -}) { +export function setup( + options: {compileAngular: boolean, compileAnimations: boolean, compileCommon?: boolean} = { + compileAngular: true, + compileAnimations: true, + compileCommon: false, + }) { let angularFiles = new Map(); beforeAll(() => { @@ -552,6 +554,13 @@ export function setup(options: {compileAngular: boolean, compileAnimations: bool emittingProgram.emit(); emittingHost.writtenAngularFiles(angularFiles); } + if (options.compileCommon) { + const emittingHost = + new EmittingCompilerHost(['@angular/common/index.ts'], {emitMetadata: true}); + const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost); + emittingProgram.emit(); + emittingHost.writtenAngularFiles(angularFiles); + } if (options.compileAnimations) { const emittingHost = new EmittingCompilerHost(['@angular/animations/index.ts'], {emitMetadata: true}); diff --git a/packages/compiler/test/render3/README.md b/packages/compiler/test/render3/README.md new file mode 100644 index 0000000000..7c522a1396 --- /dev/null +++ b/packages/compiler/test/render3/README.md @@ -0,0 +1 @@ +Tests in this directory are excluded from running in the browser and only run in node. \ No newline at end of file diff --git a/packages/compiler/test/render3/r3_view_compiler_spec.ts b/packages/compiler/test/render3/r3_view_compiler_spec.ts new file mode 100644 index 0000000000..359cca05af --- /dev/null +++ b/packages/compiler/test/render3/r3_view_compiler_spec.ts @@ -0,0 +1,212 @@ +/** + * @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 {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler'; +import {ViewEncapsulation} from '@angular/core'; +import * as ts from 'typescript'; + +import {ConstantPool} from '../../src/constant_pool'; +import * as o from '../../src/output/output_ast'; +import {compileComponent} from '../../src/render3/r3_view_compiler'; +import {OutputContext} from '../../src/util'; +import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, setup, toMockFileArray} from '../aot/test_util'; + +describe('r3_view_compiler', () => { + const angularFiles = setup({ + compileAngular: true, + compileAnimations: false, + compileCommon: true, + }); + + describe('hello world', () => { + it('should be able to generate the hello world component', () => { + const files: MockDirectory = { + app: { + 'hello.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'hello-world', + template: 'Hello, world!' + }) + export class HelloWorldComponent { + + } + + @NgModule({ + declarations: [HelloWorldComponent] + }) + export class HelloWorldModule {} + ` + } + }; + compile(files, angularFiles); + }); + }); + + it('should be able to generate the example', () => { + const files: MockDirectory = { + app: { + 'example.ts': ` + import {Component, OnInit, OnDestroy, ElementRef, Input, NgModule} from '@angular/core'; + import {CommonModule} from '@angular/common'; + + @Component({ + selector: 'my-app', + template: '' + }) + export class MyApp implements OnInit { + + list: any[] = []; + + constructor(public elementRef: ElementRef) {} + + ngOnInit(): void { + } + } + + @Component({ + selector: 'todo', + template: '
  • {{data}}
' + }) + export class TodoComponent implements OnInit, OnDestroy { + + @Input() + data: any[] = []; + + myTitle: string; + + constructor(public elementRef: ElementRef) {} + + ngOnInit(): void {} + + ngOnDestroy(): void {} + } + + @NgModule({ + declarations: [TodoComponent, MyApp], + imports: [CommonModule] + }) + export class TodoModule{} + ` + } + }; + const result = compile(files, angularFiles); + expect(result.source).toContain('@angular/core'); + }); +}); + +function compile( + data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {}, + errorCollector: (error: any, fileName?: string) => void = error => { throw error; }) { + const testFiles = toMockFileArray(data); + const scripts = testFiles.map(entry => entry.fileName); + const angularFilesArray = toMockFileArray(angularFiles); + const files = arrayToMockDir([...testFiles, ...angularFilesArray]); + const mockCompilerHost = new MockCompilerHost(scripts, files); + const compilerHost = new MockAotCompilerHost(mockCompilerHost); + + const program = ts.createProgram(scripts, {...settings}, mockCompilerHost); + + // TODO(chuckj): Replace with a variant of createAotCompiler() when the r3_view_compiler is + // integrated + const translations = options.translations || ''; + + const urlResolver = createAotUrlResolver(compilerHost); + const symbolCache = new StaticSymbolCache(); + const summaryResolver = new AotSummaryResolver(compilerHost, symbolCache); + const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver); + const staticReflector = + new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector); + const htmlParser = new I18NHtmlParser( + new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console); + const config = new CompilerConfig({ + defaultEncapsulation: ViewEncapsulation.Emulated, + useJit: false, + enableLegacyTemplate: options.enableLegacyTemplate === true, + missingTranslation: options.missingTranslation, + preserveWhitespaces: options.preserveWhitespaces, + strictInjectionParameters: options.strictInjectionParameters, + }); + const normalizer = new DirectiveNormalizer( + {get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config); + const expressionParser = new Parser(new Lexer()); + const elementSchemaRegistry = new DomElementSchemaRegistry(); + const templateParser = new TemplateParser( + config, staticReflector, expressionParser, elementSchemaRegistry, htmlParser, console, []); + const resolver = new CompileMetadataResolver( + config, htmlParser, new NgModuleResolver(staticReflector), + new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver, + elementSchemaRegistry, normalizer, console, symbolCache, staticReflector, errorCollector); + + + + // Create the TypeScript program + const sourceFiles = program.getSourceFiles().map(sf => sf.fileName); + + // Analyze the modules + // TODO(chuckj): Eventually this should not be necessary as the ts.SourceFile should be sufficient + // to generate a template definition. + const analyzedModules = analyzeNgModules(sourceFiles, compilerHost, symbolResolver, resolver); + + const directives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys()); + + const fakeOuputContext: OutputContext = { + genFilePath: 'fakeFactory.ts', + statements: [], + importExpr(symbol: StaticSymbol, typeParams: o.Type[]) { + if (!(symbol instanceof StaticSymbol)) { + if (!symbol) { + throw new Error('Invalid: undefined passed to as a symbol'); + } + throw new Error(`Invalid: ${(symbol as any).constructor.name} is not a symbol`); + } + return (symbol.members || []) + .reduce( + (expr, member) => expr.prop(member), + o.importExpr(new o.ExternalReference(symbol.filePath, symbol.name))); + }, + constantPool: new ConstantPool() + }; + + // Load All directives + for (const directive of directives) { + const module = analyzedModules.ngModuleByPipeOrDirective.get(directive) !; + resolver.loadNgModuleDirectiveAndPipeMetadata(module.type.reference, true); + } + + // Compile the directives. + for (const directive of directives) { + const module = analyzedModules.ngModuleByPipeOrDirective.get(directive) !; + if (resolver.isDirective(directive)) { + const metadata = resolver.getDirectiveMetadata(directive); + if (metadata.isComponent) { + const fakeUrl = 'ng://fake-template-url.html'; + const htmlAst = htmlParser.parse(metadata.template !.template !, fakeUrl); + + const directives = module.transitiveModule.directives.map( + dir => resolver.getDirectiveSummary(dir.reference)); + const pipes = + module.transitiveModule.pipes.map(pipe => resolver.getPipeSummary(pipe.reference)); + const parsedTemplate = templateParser.parse( + metadata, htmlAst, directives, pipes, module.schemas, fakeUrl, false); + + compileComponent(fakeOuputContext, metadata, parsedTemplate.template, staticReflector); + } + } + } + + fakeOuputContext.statements.unshift(...fakeOuputContext.constantPool.statements); + + const emitter = new TypeScriptEmitter(); + + const result = emitter.emitStatementsAndContext( + fakeOuputContext.genFilePath, fakeOuputContext.statements, '', false); + + return {source: result.sourceText, outputContext: fakeOuputContext}; +} \ No newline at end of file