diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 6ebb98a1d3..54491bf563 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -21,6 +21,7 @@ import {OutputEmitter} from '../output/abstract_emitter'; import * as o from '../output/output_ast'; import {ParseError} from '../parse_util'; import {compilePipe as compileIvyPipe} from '../render3/r3_pipe_compiler'; +import {OutputMode} from '../render3/r3_types'; import {compileComponent as compileIvyComponent, compileDirective as compileIvyDirective} from '../render3/r3_view_compiler'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; import {SummaryResolver} from '../summary_resolver'; @@ -170,7 +171,7 @@ export class AotCompiler { _createEmptyStub(outputCtx); } // Note: for the stubs, we don't need a property srcFileUrl, - // as lateron in emitAllImpls we will create the proper GeneratedFiles with the + // as later on in emitAllImpls we will create the proper GeneratedFiles with the // correct srcFileUrl. // This is good as e.g. for .ngstyle.ts files we can't derive // the url of components based on the genFileUrl. @@ -223,7 +224,7 @@ export class AotCompiler { let componentId = 0; file.ngModules.forEach((ngModuleMeta, ngModuleIndex) => { // Note: the code below needs to executed for StubEmitFlags.Basic and StubEmitFlags.TypeCheck, - // so we don't change the .ngfactory file too much when adding the typecheck block. + // so we don't change the .ngfactory file too much when adding the type-check block. // create exports that user code can reference this._ngModuleCompiler.createStub(outputCtx, ngModuleMeta.type.reference); @@ -256,7 +257,7 @@ export class AotCompiler { }); if (emitFlags & StubEmitFlags.TypeCheck) { - // add the typecheck block for all components of the NgModule + // add the type-check block for all components of the NgModule ngModuleMeta.declaredDirectives.forEach((dirId) => { const compMeta = this._metadataResolver.getDirectiveMetadata(dirId.reference); if (!compMeta.isComponent) { @@ -366,16 +367,18 @@ export class AotCompiler { this._parseTemplate(directiveMetadata, module, module.transitiveModule.directives); compileIvyComponent( context, directiveMetadata, parsedPipes, parsedTemplate, this._reflector, - hostBindingParser); + hostBindingParser, OutputMode.PartialClass); } else { - compileIvyDirective(context, directiveMetadata, this._reflector, hostBindingParser); + compileIvyDirective( + context, directiveMetadata, this._reflector, hostBindingParser, + OutputMode.PartialClass); } }); pipes.forEach(pipeType => { const pipeMetadata = this._metadataResolver.getPipeMetadata(pipeType); if (pipeMetadata) { - compileIvyPipe(context, pipeMetadata, this._reflector); + compileIvyPipe(context, pipeMetadata, this._reflector, OutputMode.PartialClass); } }); diff --git a/packages/compiler/src/render3/r3_back_patch_compiler.ts b/packages/compiler/src/render3/r3_back_patch_compiler.ts new file mode 100644 index 0000000000..c16db2c41b --- /dev/null +++ b/packages/compiler/src/render3/r3_back_patch_compiler.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {StaticReflector} from '../aot/static_reflector'; +import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeSummary, CompileTypeMetadata} from '../compile_metadata'; +import {DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Lexer, ParseError, Parser} from '../compiler'; +import {CompileMetadataResolver} from '../metadata_resolver'; +import * as o from '../output/output_ast'; +import {BindingParser} from '../template_parser/binding_parser'; +import {TemplateAst} from '../template_parser/template_ast'; +import {OutputContext} from '../util'; + +import {compilePipe} from './r3_pipe_compiler'; +import {BUILD_OPTIMIZER_REMOVE, OutputMode} from './r3_types'; +import {compileComponent, compileDirective} from './r3_view_compiler'; + +export const enum ModuleKind { + Renderer2, + Renderer3, +} + +/** + * Produce the back-patching function for the given module to the output context. + */ +export function compileModuleBackPatch( + outputCtx: OutputContext, name: string, module: CompileNgModuleMetadata, kind: ModuleKind, + backPatchReferenceOf: (module: CompileTypeMetadata) => o.Expression, + parseTemplate: ( + compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, + directiveIdentifiers: CompileIdentifierMetadata[]) => { + template: TemplateAst[], + pipes: CompilePipeSummary[] + }, + reflector: StaticReflector, resolver: CompileMetadataResolver) { + const imports: o.Statement[] = []; + let statements: o.Statement[] = []; + + // Call dependent back patching + for (const importedModule of module.importedModules) { + const importBackPatchFunction = backPatchReferenceOf(importedModule.type); + + // e.g. // @BUILD_OPTIMIZER_REMOVE + imports.push(new o.CommentStmt(BUILD_OPTIMIZER_REMOVE)); + + // e.g. ngBackPatch_some_other_module_Module(); + imports.push(importBackPatchFunction.callFn([]).toStmt()); + } + + // The local output context allows collecting the back-patch statements that + // are generated by the various compilers which allows putting placing them + // into the body of a function instead of at global scope. + const localCtx: OutputContext = { + statements, + constantPool: outputCtx.constantPool, + genFilePath: outputCtx.genFilePath, + importExpr: outputCtx.importExpr + }; + + // e.g. export function ngBackPatch_some_module_Lib1Module() + if (kind === ModuleKind.Renderer2) { + // For all Renderer2 modules generate back-patching code for all the components, directives, + // pipes, and injectables as well as the injector def for the module itself. + + const expressionParser = new Parser(new Lexer()); + const elementSchemaRegistry = new DomElementSchemaRegistry(); + const errors: ParseError[] = []; + const hostBindingParser = new BindingParser( + expressionParser, DEFAULT_INTERPOLATION_CONFIG, elementSchemaRegistry, [], errors); + + // Back-patch all declared directive and components + for (const declaredDirective of module.declaredDirectives) { + const declaredDirectiveMetadata = resolver.getDirectiveMetadata(declaredDirective.reference); + if (declaredDirectiveMetadata.isComponent) { + const {template: parsedTemplate, pipes: parsedPipes} = + parseTemplate(declaredDirectiveMetadata, module, module.transitiveModule.directives); + compileComponent( + localCtx, declaredDirectiveMetadata, parsedPipes, parsedTemplate, reflector, + hostBindingParser, OutputMode.BackPatch); + } else { + compileDirective( + localCtx, declaredDirectiveMetadata, reflector, hostBindingParser, + OutputMode.BackPatch); + } + } + + // Back-patch all pipes declared in the module. + for (const pipeType of module.declaredPipes) { + const pipeMetadata = resolver.getPipeMetadata(pipeType.reference); + if (pipeMetadata) { + compilePipe(localCtx, pipeMetadata, reflector, OutputMode.BackPatch); + } + } + + if (errors.length) { + throw new Error(errors.map(e => e.toString()).join('\n')); + } + } + + outputCtx.statements.push(new o.DeclareFunctionStmt( + name, [], [...imports, ...statements], o.INFERRED_TYPE, [o.StmtModifier.Exported])); +} diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index b25cf60046..3aeaf6bf8e 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -10,18 +10,12 @@ 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 TRANSFORM_METHOD = 'transform'; + static PATCH_DEPS = 'patchedDeps'; /* Instructions */ static createElement: o.ExternalReference = {name: 'ɵE', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_module_factory_compiler.ts b/packages/compiler/src/render3/r3_module_factory_compiler.ts new file mode 100644 index 0000000000..36aae89480 --- /dev/null +++ b/packages/compiler/src/render3/r3_module_factory_compiler.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {StaticReflector} from '../aot/static_reflector'; +import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeSummary, CompileTypeMetadata, identifierName} from '../compile_metadata'; +import {CompileMetadataResolver} from '../metadata_resolver'; +import * as o from '../output/output_ast'; +import {OutputContext} from '../util'; + +import {Identifiers as R3} from './r3_identifiers'; + +/** + * Write a Renderer2 compatibility module factory to the output context. + */ +export function compileModuleFactory( + outputCtx: OutputContext, module: CompileNgModuleMetadata, + backPatchReferenceOf: (module: CompileTypeMetadata) => o.Expression, + resolver: CompileMetadataResolver) { + const ngModuleFactoryVar = `${identifierName(module.type)}NgFactory`; + + const parentInjector = 'parentInjector'; + const createFunction = o.fn( + [new o.FnParam(parentInjector, o.DYNAMIC_TYPE)], + [new o.IfStmt( + o.THIS_EXPR.prop(R3.PATCH_DEPS).notIdentical(o.literal(true, o.INFERRED_TYPE)), + [ + o.THIS_EXPR.prop(R3.PATCH_DEPS).set(o.literal(true, o.INFERRED_TYPE)).toStmt(), + backPatchReferenceOf(module.type).callFn([]).toStmt() + ])], + o.INFERRED_TYPE, null, `${ngModuleFactoryVar}_Create`); + + const moduleFactoryLiteral = o.literalMap([ + {key: 'moduleType', value: outputCtx.importExpr(module.type.reference), quoted: false}, + {key: 'create', value: createFunction, quoted: false} + ]); + + outputCtx.statements.push( + o.variable(ngModuleFactoryVar).set(moduleFactoryLiteral).toDeclStmt(o.DYNAMIC_TYPE, [ + o.StmtModifier.Exported, o.StmtModifier.Final + ])); +} diff --git a/packages/compiler/src/render3/r3_pipe_compiler.ts b/packages/compiler/src/render3/r3_pipe_compiler.ts index e0b189a755..60b7b93565 100644 --- a/packages/compiler/src/render3/r3_pipe_compiler.ts +++ b/packages/compiler/src/render3/r3_pipe_compiler.ts @@ -13,10 +13,15 @@ import * as o from '../output/output_ast'; import {OutputContext, error} from '../util'; import {Identifiers as R3} from './r3_identifiers'; +import {BUILD_OPTIMIZER_COLOCATE, OutputMode} from './r3_types'; import {createFactory} from './r3_view_compiler'; +/** + * Write a pipe definition to the output context. + */ export function compilePipe( - outputCtx: OutputContext, pipe: CompilePipeMetadata, reflector: CompileReflector) { + outputCtx: OutputContext, pipe: CompilePipeMetadata, reflector: CompileReflector, + mode: OutputMode) { const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; // e.g. 'type: MyPipe` @@ -35,16 +40,29 @@ export function compilePipe( const className = identifierName(pipe.type) !; className || error(`Cannot resolve the name of ${pipe.type}`); - outputCtx.statements.push(new o.ClassStmt( - /* name */ className, - /* parent */ null, - /* fields */[new o.ClassField( - /* name */ outputCtx.constantPool.propertyNameOf(DefinitionKind.Pipe), - /* type */ o.INFERRED_TYPE, - /* modifiers */[o.StmtModifier.Static], - /* initializer */ o.importExpr(R3.definePipe).callFn([o.literalMap( - definitionMapValues)]))], - /* getters */[], - /* constructorMethod */ new o.ClassMethod(null, [], []), - /* methods */[])); + const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Pipe); + const definitionFunction = + o.importExpr(R3.definePipe).callFn([o.literalMap(definitionMapValues)]); + + if (mode === OutputMode.PartialClass) { + outputCtx.statements.push(new o.ClassStmt( + /* name */ className, + /* parent */ null, + /* fields */[new o.ClassField( + /* name */ definitionField, + /* type */ o.INFERRED_TYPE, + /* modifiers */[o.StmtModifier.Static], + /* initializer */ definitionFunction)], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[])); + } else { + // Create back-patch definition. + const classReference = outputCtx.importExpr(pipe.type.reference); + + // Create the back-patch statement + outputCtx.statements.push( + new o.CommentStmt(BUILD_OPTIMIZER_COLOCATE), + classReference.prop(definitionField).set(definitionFunction).toStmt()); + } } \ No newline at end of file diff --git a/packages/compiler/src/render3/r3_types.ts b/packages/compiler/src/render3/r3_types.ts new file mode 100644 index 0000000000..87f0f1257a --- /dev/null +++ b/packages/compiler/src/render3/r3_types.ts @@ -0,0 +1,25 @@ +/** + * @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 + */ + +/** + * The statement mode for the render, either as a class back-patch or as a partial class + */ +export const enum OutputMode { + PartialClass, + BackPatch, +} + +/** + * Comment to insert above back-patch + */ +export const BUILD_OPTIMIZER_COLOCATE = '@__BUILD_OPTIMIZER_COLOCATE__'; + +/** + * Comment to mark removable expressions + */ +export const BUILD_OPTIMIZER_REMOVE = '@__BUILD_OPTIMIZER_REMOVE__'; diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index 4a3da8db0a..2838568de4 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -21,6 +21,7 @@ import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventA import {OutputContext, error} from '../util'; import {Identifiers as R3} from './r3_identifiers'; +import {BUILD_OPTIMIZER_COLOCATE, OutputMode} from './r3_types'; @@ -41,7 +42,7 @@ const IMPLICIT_REFERENCE = '$implicit'; export function compileDirective( outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector, - bindingParser: BindingParser) { + bindingParser: BindingParser, mode: OutputMode) { const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; const field = (key: string, value: o.Expression | null) => { @@ -68,24 +69,38 @@ export function compileDirective( const className = identifierName(directive.type) !; className || error(`Cannot resolver the name of ${directive.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 */ outputCtx.constantPool.propertyNameOf(DefinitionKind.Directive), - /* type */ o.INFERRED_TYPE, - /* modifiers */[o.StmtModifier.Static], - /* initializer */ o.importExpr(R3.defineDirective).callFn([o.literalMap( - definitionMapValues)]))], - /* getters */[], - /* constructorMethod */ new o.ClassMethod(null, [], []), - /* methods */[])); + const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Directive); + const definitionFunction = + o.importExpr(R3.defineDirective).callFn([o.literalMap(definitionMapValues)]); + + if (mode === OutputMode.PartialClass) { + // 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 */ definitionField, + /* type */ o.INFERRED_TYPE, + /* modifiers */[o.StmtModifier.Static], + /* initializer */ definitionFunction)], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[])); + } else { + // Create back-patch definition. + const classReference = outputCtx.importExpr(directive.type.reference); + + // Create the back-patch statement + outputCtx.statements.push(new o.CommentStmt(BUILD_OPTIMIZER_COLOCATE)); + outputCtx.statements.push( + classReference.prop(definitionField).set(definitionFunction).toStmt()); + } } export function compileComponent( outputCtx: OutputContext, component: CompileDirectiveMetadata, pipes: CompilePipeSummary[], - template: TemplateAst[], reflector: CompileReflector, bindingParser: BindingParser) { + template: TemplateAst[], reflector: CompileReflector, bindingParser: BindingParser, + mode: OutputMode) { const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; const field = (key: string, value: o.Expression | null) => { @@ -150,22 +165,33 @@ export function compileComponent( field('features', o.literalArr(features)); } - const className = identifierName(component.type) !; - className || error(`Cannot resolver the name of ${component.type}`); + const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Component); + const definitionFunction = + o.importExpr(R3.defineComponent).callFn([o.literalMap(definitionMapValues)]); + if (mode === OutputMode.PartialClass) { + 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 */ outputCtx.constantPool.propertyNameOf(DefinitionKind.Component), - /* type */ o.INFERRED_TYPE, - /* modifiers */[o.StmtModifier.Static], - /* initializer */ o.importExpr(R3.defineComponent).callFn([o.literalMap( - definitionMapValues)]))], - /* getters */[], - /* constructorMethod */ new o.ClassMethod(null, [], []), - /* methods */[])); + // 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 */ definitionField, + /* type */ o.INFERRED_TYPE, + /* modifiers */[o.StmtModifier.Static], + /* initializer */ definitionFunction)], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[])); + } else { + const classReference = outputCtx.importExpr(component.type.reference); + + // Create the back-patch statement + outputCtx.statements.push( + new o.CommentStmt(BUILD_OPTIMIZER_COLOCATE), + classReference.prop(definitionField).set(definitionFunction).toStmt()); + } } // TODO: Remove these when the things are fully supported diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index 57d5019047..e48c7e2915 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -55,6 +55,7 @@ export const settings: ts.CompilerOptions = { export interface EmitterOptions { emitMetadata: boolean; mockData?: MockDirectory; + context?: Map; } function calcPathsOnDisc() { @@ -74,12 +75,16 @@ export class EmittingCompilerHost implements ts.CompilerHost { private scriptNames: string[]; private root = '/'; private collector = new MetadataCollector(); + private cachedAddedDirectories: Set|undefined; constructor(scriptNames: string[], private options: EmitterOptions) { // Rewrite references to scripts with '@angular' to its corresponding location in // the source tree. this.scriptNames = scriptNames.map(f => this.effectiveName(f)); this.root = rootPath; + if (options.context) { + this.addedFiles = mergeMaps(options.context); + } } public writtenAngularFiles(target = new Map()): Map { @@ -93,12 +98,14 @@ export class EmittingCompilerHost implements ts.CompilerHost { public addScript(fileName: string, content: string) { const scriptName = this.effectiveName(fileName); this.addedFiles.set(scriptName, content); + this.cachedAddedDirectories = undefined; this.scriptNames.push(scriptName); } public override(fileName: string, content: string) { const scriptName = this.effectiveName(fileName); this.addedFiles.set(scriptName, content); + this.cachedAddedDirectories = undefined; } public addWrittenFile(fileName: string, content: string) { @@ -140,6 +147,7 @@ export class EmittingCompilerHost implements ts.CompilerHost { directoryExists(directoryName: string): boolean { return directoryExists(directoryName, this.options.mockData) || + this.getAddedDirectories().has(directoryName) || (fs.existsSync(directoryName) && fs.statSync(directoryName).isDirectory()); } @@ -188,6 +196,23 @@ export class EmittingCompilerHost implements ts.CompilerHost { } useCaseSensitiveFileNames(): boolean { return false; } getNewLine(): string { return '\n'; } + + private getAddedDirectories(): Set { + let result = this.cachedAddedDirectories; + if (!result) { + const newCache = new Set(); + const addFile = (fileName: string) => { + const directory = fileName.substr(0, fileName.lastIndexOf('/')); + if (!newCache.has(directory)) { + newCache.add(directory); + addFile(directory); + } + }; + Array.from(this.addedFiles.keys()).forEach(addFile); + this.cachedAddedDirectories = result = newCache; + } + return result; + } } export class MockCompilerHost implements ts.CompilerHost { @@ -703,3 +728,43 @@ function stripNgResourceSuffix(fileName: string): string { function addNgResourceSuffix(fileName: string): string { return `${fileName}.$ngresource$`; } + +function extractFileNames(directory: MockDirectory): string[] { + const result: string[] = []; + const scan = (directory: MockDirectory, prefix: string) => { + for (let name of Object.getOwnPropertyNames(directory)) { + const entry = directory[name]; + const fileName = `${prefix}/${name}`; + if (typeof entry === 'string') { + result.push(fileName); + } else if (entry) { + scan(entry, fileName); + } + } + }; + scan(directory, ''); + return result; +} + +export function emitLibrary( + context: Map, mockData: MockDirectory, + scriptFiles?: string[]): Map { + const emittingHost = new EmittingCompilerHost( + scriptFiles || extractFileNames(mockData), {emitMetadata: true, mockData, context}); + const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost); + expectNoDiagnostics(emittingProgram); + emittingProgram.emit(); + return emittingHost.written; +} + +export function mergeMaps(...maps: Map[]): Map { + const result = new Map(); + + for (const map of maps) { + for (const [key, value] of Array.from(map.entries())) { + result.set(key, value); + } + } + + return result; +} diff --git a/packages/compiler/test/render3/mock_compile.ts b/packages/compiler/test/render3/mock_compile.ts index bfbdf796ed..9d84b9f7c0 100644 --- a/packages/compiler/test/render3/mock_compile.ts +++ b/packages/compiler/test/render3/mock_compile.ts @@ -6,14 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, ParseError, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler'; +import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileIdentifierMetadata, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompileTypeMetadata, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, ParseError, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver, templateSourceUrl} from '@angular/compiler'; import {ViewEncapsulation} from '@angular/core'; import * as ts from 'typescript'; +import {NgAnalyzedModules} from '../../src/aot/compiler'; import {ConstantPool} from '../../src/constant_pool'; import {ParserError} from '../../src/expression_parser/ast'; import * as o from '../../src/output/output_ast'; +import {ModuleKind, compileModuleBackPatch} from '../../src/render3/r3_back_patch_compiler'; +import {compileModuleFactory} from '../../src/render3/r3_module_factory_compiler'; import {compilePipe} from '../../src/render3/r3_pipe_compiler'; +import {OutputMode} from '../../src/render3/r3_types'; import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler'; import {BindingParser} from '../../src/template_parser/binding_parser'; import {OutputContext} from '../../src/util'; @@ -122,9 +126,13 @@ function r(...pieces: (string | RegExp)[]): RegExp { return new RegExp(results.join('')); } -export function compile( +function doCompile( data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {}, - errorCollector: (error: any, fileName?: string) => void = error => { throw error;}) { + errorCollector: (error: any, fileName?: string) => void = error => { throw error; }, + compileAction: ( + outputCtx: OutputContext, analyzedModules: NgAnalyzedModules, + resolver: CompileMetadataResolver, htmlParser: HtmlParser, templateParser: TemplateParser, + hostBindingParser: BindingParser, reflector: StaticReflector) => void) { const testFiles = toMockFileArray(data); const scripts = testFiles.map(entry => entry.fileName); const angularFilesArray = toMockFileArray(angularFiles); @@ -207,37 +215,9 @@ export function compile( resolver.loadNgModuleDirectiveAndPipeMetadata(module.type.reference, true); } - // Compile the directives. - for (const pipeOrDirective of pipesOrDirectives) { - const module = analyzedModules.ngModuleByPipeOrDirective.get(pipeOrDirective); - if (!module || !module.type.reference.filePath.startsWith('/app')) { - continue; - } - if (resolver.isDirective(pipeOrDirective)) { - const metadata = resolver.getDirectiveMetadata(pipeOrDirective); - 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( - fakeOutputContext, metadata, pipes, parsedTemplate.template, staticReflector, - hostBindingParser); - } else { - compileDirective(fakeOutputContext, metadata, staticReflector, hostBindingParser); - } - } else if (resolver.isPipe(pipeOrDirective)) { - const metadata = resolver.getPipeMetadata(pipeOrDirective); - if (metadata) { - compilePipe(fakeOutputContext, metadata, staticReflector); - } - } - } + compileAction( + fakeOutputContext, analyzedModules, resolver, htmlParser, templateParser, hostBindingParser, + staticReflector); fakeOutputContext.statements.unshift(...fakeOutputContext.constantPool.statements); @@ -257,3 +237,109 @@ export function compile( return {source: result.sourceText, outputContext: fakeOutputContext}; } + +export function compile( + data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {}, + errorCollector: (error: any, fileName?: string) => void = error => { throw error;}) { + return doCompile( + data, angularFiles, options, errorCollector, + (outputCtx: OutputContext, analyzedModules: NgAnalyzedModules, + resolver: CompileMetadataResolver, htmlParser: HtmlParser, templateParser: TemplateParser, + hostBindingParser: BindingParser, reflector: StaticReflector) => { + const pipesOrDirectives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys()); + for (const pipeOrDirective of pipesOrDirectives) { + const module = analyzedModules.ngModuleByPipeOrDirective.get(pipeOrDirective); + if (!module || !module.type.reference.filePath.startsWith('/app')) { + continue; + } + if (resolver.isDirective(pipeOrDirective)) { + const metadata = resolver.getDirectiveMetadata(pipeOrDirective); + 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( + outputCtx, metadata, pipes, parsedTemplate.template, reflector, hostBindingParser, + OutputMode.PartialClass); + } else { + compileDirective( + outputCtx, metadata, reflector, hostBindingParser, OutputMode.PartialClass); + } + } else if (resolver.isPipe(pipeOrDirective)) { + const metadata = resolver.getPipeMetadata(pipeOrDirective); + if (metadata) { + compilePipe(outputCtx, metadata, reflector, OutputMode.PartialClass); + } + } + } + + }); +} + +const DTS = /\.d\.ts$/; +const EXT = /(\.\w+)+$/; +const NONE_WORD = /\W/g; +const NODE_MODULES = /^.*\/node_modules\//; + +function getBackPatchFunctionName(type: CompileTypeMetadata) { + const filePath = (type.reference.filePath as string) + .replace(EXT, '') + .replace(NODE_MODULES, '') + .replace(NONE_WORD, '_'); + return `ngBackPatch_${filePath.split('/').filter(s => !!s).join('_')}_${type.reference.name}`; +} + +function getBackPatchReference(type: CompileTypeMetadata): o.Expression { + return o.variable(getBackPatchFunctionName(type)); +} + +export function backPatch( + data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {}, + errorCollector: (error: any, fileName?: string) => void = error => { throw error;}) { + return doCompile( + data, angularFiles, options, errorCollector, + (outputCtx: OutputContext, analyzedModules: NgAnalyzedModules, + resolver: CompileMetadataResolver, htmlParser: HtmlParser, templateParser: TemplateParser, + hostBindingParser: BindingParser, reflector: StaticReflector) => { + + const parseTemplate = + (compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, + directiveIdentifiers: CompileIdentifierMetadata[]) => { + const directives = + directiveIdentifiers.map(dir => resolver.getDirectiveSummary(dir.reference)); + const pipes = ngModule.transitiveModule.pipes.map( + pipe => resolver.getPipeSummary(pipe.reference)); + return templateParser.parse( + compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas, + templateSourceUrl(ngModule.type, compMeta, compMeta.template !), true); + }; + + for (const module of analyzedModules.ngModules) { + compileModuleBackPatch( + outputCtx, getBackPatchFunctionName(module.type), module, + DTS.test(module.type.reference.filePath) ? ModuleKind.Renderer2 : + ModuleKind.Renderer3, + getBackPatchReference, parseTemplate, reflector, resolver); + } + }); +} + +export function createFactories( + data: MockDirectory, context: MockData, options: AotCompilerOptions = {}, + errorCollector: (error: any, fileName?: string) => void = error => { throw error;}) { + return doCompile( + data, context, options, errorCollector, + (outputCtx: OutputContext, analyzedModules: NgAnalyzedModules, + resolver: CompileMetadataResolver, htmlParser: HtmlParser, templateParser: TemplateParser, + hostBindingParser: BindingParser, reflector: StaticReflector) => { + for (const module of analyzedModules.ngModules) { + compileModuleFactory(outputCtx, module, getBackPatchReference, resolver); + } + }); +} \ No newline at end of file diff --git a/packages/compiler/test/render3/r3_back_patch_compiler_spec.ts b/packages/compiler/test/render3/r3_back_patch_compiler_spec.ts new file mode 100644 index 0000000000..4c34089180 --- /dev/null +++ b/packages/compiler/test/render3/r3_back_patch_compiler_spec.ts @@ -0,0 +1,218 @@ + +/** + * @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 {MockDirectory, emitLibrary, mergeMaps, setup} from '../aot/test_util'; + +import {backPatch, expectEmit} from './mock_compile'; + +describe('r3_back_patch_compiler', () => { + const angularFiles = setup({ + compileAngular: true, + compileAnimations: false, + compileCommon: true, + }); + + it('should back-patch a component in a library', () => { + const libraries = { + lib1: { + src: { + 'component.ts': ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'lib1-cmp', + template: '

Hello, {{name}}!

' + }) + export class Lib1Component { + name: string; + } + `, + 'directive.ts': ` + import {Directive, HostBinding} from '@angular/core'; + + @Directive({selector: '[lib1-dir]'}) + export class Lib1Directive { + @HostBinding('id') dirId = 'some id'; + } + `, + 'service.ts': ` + import {Injectable} from '@angular/core'; + + @Injectable() + export class Lib1Service { + getSomeInfo() { return 'some info'; } + } + `, + 'pipe.ts': ` + import {Pipe} from '@angular/core'; + + @Pipe({name: 'lib1Pipe', pure: true}) + export class Lib1Pipe { + transform(v: any) { return v; } + } + `, + 'module.ts': ` + import {NgModule} from '@angular/core'; + + import {Lib1Component} from './component'; + import {Lib1Directive} from './directive'; + import {Lib1Service} from './service'; + import {Lib1Pipe} from './pipe'; + + @NgModule({ + exports: [Lib1Component, Lib1Directive, Lib1Pipe], + declarations: [Lib1Component, Lib1Directive, Lib1Pipe], + providers: [Lib1Service] + }) + export class Lib1Module {} + ` + } + }, + lib2: { + src: { + 'component.ts': ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'lib2-cmp', + template: '

Hello, {{name}}!

' + }) + export class Lib2Component { + name: string; + } + `, + 'directive.ts': ` + import {Directive, HostBinding} from '@angular/core'; + + @Directive({selector: '[lib2-dir]'}) + export class Lib2Directive { + @HostBinding('id') dirId = 'some id'; + } + `, + 'service.ts': ` + import {Injectable} from '@angular/core'; + + @Injectable() + export class Lib2Service { + getSomeInfo() { return 'some info'; } + } + `, + 'pipe.ts': ` + import {Pipe} from '@angular/core'; + + @Pipe({name: 'lib2Pipe', pure: true}) + export class Lib2Pipe { + transform(v: any) { return v; } + } + `, + 'module.ts': ` + import {NgModule} from '@angular/core'; + + import {Lib1Module} from '../../lib1/src/module'; + import {Lib2Component} from './component'; + import {Lib2Directive} from './directive'; + import {Lib2Service} from './service'; + import {Lib2Pipe} from './pipe'; + + @NgModule({ + imports: [Lib1Module], + exports: [Lib2Component, Lib2Directive, Lib2Pipe], + declarations: [Lib2Component, Lib2Directive, Lib2Pipe], + providers: [Lib2Service] + }) + export class Lib2Module {} + ` + } + }, + }; + const app = { + app: { + src: { + 'app.component.ts': ` + import {Component} from '@angular/core'; + import {Lib1Service} from '../../lib1/src/service'; + import {Lib2Service} from '../../lib2/src/service'; + + @Component({ + selector: 'app-cmp', + template: \` + {{'v' | lib1Pipe | lib2Pipe}} + {{'v' | lib2Pipe | lib2Pipe}} + \` + }) + export class AppComponent { + constructor(public lib1s: Lib1Service, public lib2s: Lib2Service) {} + } + `, + 'app.module.ts': ` + import {NgModule} from '@angular/core'; + import {Lib1Module} from '../../lib1/src/module'; + import {Lib2Module} from '../../lib2/src/module'; + + import {AppComponent} from './app.component'; + + @NgModule({ + imports: [Lib1Module, Lib2Module], + declarations: [AppComponent] + }) + export class AppModule { + } + ` + } + } + }; + + const lib1_module_back_patch = ` + export function ngBackPatch__lib1_src_module_Lib1Module() { + // @__BUILD_OPTIMIZER_COLOCATE__ + $lib1_c$.Lib1Component.ngComponentDef = $r3$.ɵdefineComponent(…); + + // @__BUILD_OPTIMIZER_COLOCATE__ + $lib1_d$.Lib1Directive.ngDirectiveDef = $r3$.ɵdefineDirective(…); + + // @__BUILD_OPTIMIZER_COLOCATE__ + $lib1_p$.Lib1Pipe.ngPipeDef = $r3$.ɵdefinePipe(…); + } + `; + + const lib2_module_back_patch = ` + export function ngBackPatch__lib2_src_module_Lib2Module() { + // @__BUILD_OPTIMIZER_REMOVE__ + ngBackPatch__lib1_src_module_Lib1Module(); + + // @__BUILD_OPTIMIZER_COLOCATE__ + $lib2_c$.Lib2Component.ngComponentDef = $r3$.ɵdefineComponent(…); + + // @__BUILD_OPTIMIZER_COLOCATE__ + $lib2_d$.Lib2Directive.ngDirectiveDef = $r3$.ɵdefineDirective(…); + + // @__BUILD_OPTIMIZER_COLOCATE__ + $lib1_p$.Lib2Pipe.ngPipeDef = $r3$.ɵdefinePipe(…); + } + `; + + const app_module_back_patch = ` + export function ngBackPatch__app_src_app_AppModule() { + // @__BUILD_OPTIMIZER_REMOVE__ + ngBackPatch__lib1_src_module_Lib1Module(); + // @__BUILD_OPTIMIZER_REMOVE__ + ngBackPatch__lib2_src_module_Lib2Module(); + } + `; + + const context = mergeMaps(emitLibrary(angularFiles, libraries), angularFiles); + + const result = backPatch(app, context); + + expectEmit(result.source, lib1_module_back_patch, 'Invalid lib1 back-patch'); + expectEmit(result.source, lib2_module_back_patch, 'Invalid lib2 back-patch'); + expectEmit(result.source, app_module_back_patch, 'Invalid app module back-patch'); + }); + +}); diff --git a/packages/compiler/test/render3/r3_module_factory_compiler_spec.ts b/packages/compiler/test/render3/r3_module_factory_compiler_spec.ts new file mode 100644 index 0000000000..b0fb78256b --- /dev/null +++ b/packages/compiler/test/render3/r3_module_factory_compiler_spec.ts @@ -0,0 +1,202 @@ +/** + * @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 {MockDirectory, emitLibrary, mergeMaps, setup} from '../aot/test_util'; +import {createFactories, expectEmit} from './mock_compile'; + +describe('r3_factory_compiler', () => { + const angularFiles = setup({ + compileAngular: true, + compileAnimations: false, + compileCommon: true, + }); + + it('should generate factories for all modules', () => { + const libraries = { + lib1: { + src: { + 'component.ts': ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'lib1-cmp', + template: '

Hello, {{name}}!

' + }) + export class Lib1Component { + name: string; + } + `, + 'directive.ts': ` + import {Directive, HostBinding} from '@angular/core'; + + @Directive({selector: '[lib1-dir]'}) + export class Lib1Directive { + @HostBinding('id') dirId = 'some id'; + } + `, + 'service.ts': ` + import {Injectable} from '@angular/core'; + + @Injectable() + export class Lib1Service { + getSomeInfo() { return 'some info'; } + } + `, + 'module.ts': ` + import {NgModule} from '@angular/core'; + + import {Lib1Component} from './component'; + import {Lib1Directive} from './directive'; + import {Lib1Service} from './service'; + + @NgModule({ + exports: [Lib1Component, Lib1Directive], + declarations: [Lib1Component, Lib1Directive], + providers: [Lib1Service] + }) + export class Lib1Module {} + ` + } + }, + lib2: { + src: { + 'component.ts': ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'lib2-cmp', + template: '

Hello, {{name}}!

' + }) + export class Lib2Component { + name: string; + } + `, + 'directive.ts': ` + import {Directive, HostBinding} from '@angular/core'; + + @Directive({selector: '[lib2-dir]'}) + export class Lib2Directive { + @HostBinding('id') dirId = 'some id'; + } + `, + 'service.ts': ` + import {Injectable} from '@angular/core'; + + @Injectable() + export class Lib2Service { + getSomeInfo() { return 'some info'; } + } + `, + 'module.ts': ` + import {NgModule} from '@angular/core'; + + import {Lib1Module} from '../../lib1/src/module'; + import {Lib2Component} from './component'; + import {Lib2Directive} from './directive'; + import {Lib2Service} from './service'; + + @NgModule({ + imports: [Lib1Module], + exports: [Lib2Component, Lib2Directive], + declarations: [Lib2Component, Lib2Directive], + providers: [Lib2Service] + }) + export class Lib2Module {} + ` + }, + } + }; + + const app = { + app: { + src: { + 'app.component.ts': ` + import {Component} from '@angular/core'; + import {Lib1Service} from '../../lib1/src/service'; + import {Lib2Service} from '../../lib2/src/service'; + + @Component({ + selector: 'app-cmp', + template: \` + + + \` + }) + export class AppComponent { + constructor(public lib1s: Lib1Service, public lib2s: Lib2Service) {} + } + `, + 'app.module.ts': ` + import {NgModule} from '@angular/core'; + import {Lib1Module} from '../../lib1/src/module'; + import {Lib2Module} from '../../lib2/src/module'; + + import {AppComponent} from './app.component'; + + @NgModule({ + imports: [Lib1Module, Lib2Module], + declarations: [AppComponent], + bootstrap: [AppComponent] + }) + export class AppModule { + } + ` + } + } + }; + + + const lib1_module_factory = ` + export const Lib1ModuleNgFactory: $any$ = { + moduleType: $i1$.Lib1Module, + create: function Lib1ModuleNgFactory_Create(parentInjector: $any$) { + if ((this.patchedDeps !== true)) { + this.patchedDeps = true; + ngBackPatch__lib1_src_module_Lib1Module(); + } + … + } + }; + `; + + const lib2_module_factory = ` + export const Lib2ModuleNgFactory: $any$ = { + moduleType: $i2$.Lib2Module, + create: function Lib2ModuleNgFactory_Create(parentInjector: $any$) { + if ((this.patchedDeps !== true)) { + this.patchedDeps = true; + ngBackPatch__lib2_src_module_Lib2Module(); + } + … + } + }; + `; + + // TODO(chuckj): What should we do with the bootstrap components? + const app_module_factory = ` + export const AppModuleNgFactory: $any$ = { + moduleType: AppModule, + create: function AppModuleNgFactory_Create(parentInjector: $any$) { + if ((this.patchedDeps !== true)) { + this.patchedDeps = true; + ngBackPatch__app_src_app_AppModule(); + } + … + } + }; + `; + + const context = mergeMaps(emitLibrary(angularFiles, libraries), angularFiles); + + const result = createFactories(app, context); + + expectEmit(result.source, lib1_module_factory, 'Invalid module factory for lib1'); + expectEmit(result.source, lib2_module_factory, 'Invalid module factory for lib2'); + expectEmit(result.source, app_module_factory, 'Invalid module factory for app'); + }); +}); \ No newline at end of file