diff --git a/packages/compiler-cli/src/ngtsc/transform/src/injectable.ts b/packages/compiler-cli/src/ngtsc/transform/src/injectable.ts index a97ffe38a1..d6444bc584 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/injectable.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Expression, IvyInjectableDep, IvyInjectableMetadata, LiteralExpr, WrappedNodeExpr, compileIvyInjectable} from '@angular/compiler'; +import {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler'; import * as ts from 'typescript'; import {Decorator} from '../../metadata'; @@ -18,20 +18,20 @@ import {AddStaticFieldInstruction, AnalysisOutput, CompilerAdapter} from './api' /** * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. */ -export class InjectableCompilerAdapter implements CompilerAdapter { +export class InjectableCompilerAdapter implements CompilerAdapter { constructor(private checker: ts.TypeChecker) {} detect(decorator: Decorator[]): Decorator|undefined { return decorator.find(dec => dec.name === 'Injectable' && dec.from === '@angular/core'); } - analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { + analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { return { analysis: extractInjectableMetadata(node, decorator, this.checker), }; } - compile(node: ts.ClassDeclaration, analysis: IvyInjectableMetadata): AddStaticFieldInstruction { + compile(node: ts.ClassDeclaration, analysis: R3InjectableMetadata): AddStaticFieldInstruction { const res = compileIvyInjectable(analysis); return { field: 'ngInjectableDef', @@ -47,7 +47,7 @@ export class InjectableCompilerAdapter implements CompilerAdapter getDep(dep, checker))); } - return {name, type, providedIn, useFactory: {factory, deps}}; + return {name, type, providedIn, useFactory: factory, deps}; } else { - const useType = getUseType(clazz, checker); - return {name, type, providedIn, useType}; + const deps = getConstructorDependencies(clazz, checker); + return {name, type, providedIn, deps}; } } else { throw new Error(`Too many arguments to @Injectable`); } } -function getUseType(clazz: ts.ClassDeclaration, checker: ts.TypeChecker): IvyInjectableDep[] { - const useType: IvyInjectableDep[] = []; +function getConstructorDependencies( + clazz: ts.ClassDeclaration, checker: ts.TypeChecker): R3DependencyMetadata[] { + const useType: R3DependencyMetadata[] = []; const ctorParams = (reflectConstructorParameters(clazz, checker) || []); ctorParams.forEach(param => { let tokenExpr = param.typeValueExpr; @@ -131,18 +132,20 @@ function getUseType(clazz: ts.ClassDeclaration, checker: ts.TypeChecker): IvyInj } }); const token = new WrappedNodeExpr(tokenExpr); - useType.push({token, optional, self, skipSelf, attribute: false}); + useType.push( + {token, optional, self, skipSelf, host: false, resolved: R3ResolvedDependencyType.Token}); }); return useType; } -function getDep(dep: ts.Expression, checker: ts.TypeChecker): IvyInjectableDep { - const depObj = { +function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetadata { + const meta: R3DependencyMetadata = { token: new WrappedNodeExpr(dep), + host: false, + resolved: R3ResolvedDependencyType.Token, optional: false, self: false, skipSelf: false, - attribute: false, }; function maybeUpdateDecorator(dec: ts.Identifier, token?: ts.Expression): void { @@ -153,17 +156,17 @@ function getDep(dep: ts.Expression, checker: ts.TypeChecker): IvyInjectableDep { switch (source.name) { case 'Inject': if (token !== undefined) { - depObj.token = new WrappedNodeExpr(token); + meta.token = new WrappedNodeExpr(token); } break; case 'Optional': - depObj.optional = true; + meta.optional = true; break; case 'SkipSelf': - depObj.skipSelf = true; + meta.skipSelf = true; break; case 'Self': - depObj.self = true; + meta.self = true; break; } } @@ -178,5 +181,5 @@ function getDep(dep: ts.Expression, checker: ts.TypeChecker): IvyInjectableDep { } }); } - return depObj; + return meta; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 161369cc4b..85d6fcb9c1 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {WrappedNodeExpr, compileIvyInjectable} from '@angular/compiler'; +import {WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; import {IvyCompilation} from './compilation'; diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index af87fc8db7..45d80a6206 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -22,10 +22,10 @@ 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 {compileNgModule as compileIvyModule} from '../render3/r3_module_compiler'; -import {compilePipe as compileIvyPipe} from '../render3/r3_pipe_compiler'; +import {compileNgModuleFromRender2 as compileR3Module} from '../render3/r3_module_compiler'; +import {compilePipe as compileR3Pipe} from '../render3/r3_pipe_compiler'; import {htmlAstToRender3Ast} from '../render3/r3_template_transform'; -import {compileComponentFromRender2 as compileIvyComponent, compileDirectiveFromRender2 as compileIvyDirective} from '../render3/view/compiler'; +import {compileComponentFromRender2 as compileR3Component, compileDirectiveFromRender2 as compileR3Directive} from '../render3/view/compiler'; import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; import {SummaryResolver} from '../summary_resolver'; @@ -362,7 +362,7 @@ export class AotCompiler { private _compileShallowModules( fileName: string, shallowModules: CompileShallowModuleMetadata[], context: OutputContext): void { - shallowModules.forEach(module => compileIvyModule(context, module, this._injectableCompiler)); + shallowModules.forEach(module => compileR3Module(context, module, this._injectableCompiler)); } private _compilePartialModule( @@ -413,18 +413,18 @@ export class AotCompiler { pipes.forEach(pipe => { pipeTypeByName.set(pipe.name, pipe.type.reference); }); - compileIvyComponent( + compileR3Component( context, directiveMetadata, render3Ast, this.reflector, hostBindingParser, directiveTypeBySel, pipeTypeByName); } else { - compileIvyDirective(context, directiveMetadata, this.reflector, hostBindingParser); + compileR3Directive(context, directiveMetadata, this.reflector, hostBindingParser); } }); pipes.forEach(pipeType => { const pipeMetadata = this._metadataResolver.getPipeMetadata(pipeType); if (pipeMetadata) { - compileIvyPipe(context, pipeMetadata, this.reflector); + compileR3Pipe(context, pipeMetadata, this.reflector); } }); diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index a0f1c7cbe3..88303c3d46 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -50,6 +50,7 @@ export {JitCompiler} from './jit/compiler'; export * from './compile_reflector'; export * from './url_resolver'; export * from './resource_loader'; +export {ConstantPool} from './constant_pool'; export {DirectiveResolver} from './directive_resolver'; export {PipeResolver} from './pipe_resolver'; export {NgModuleResolver} from './ng_module_resolver'; @@ -79,4 +80,10 @@ export {ViewCompiler} from './view_compiler/view_compiler'; export {getParseErrors, isSyntaxError, syntaxError, Version} from './util'; export {SourceMap} from './output/source_map'; export * from './injectable_compiler_2'; +export * from './render3/view/api'; +export {jitPatchDefinition} from './render3/r3_jit'; +export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory'; +export {compileNgModule, R3NgModuleMetadata} from './render3/r3_module_compiler'; +export {makeBindingParser, parseTemplate} from './render3/view/template'; +export {compileComponent, compileDirective} from './render3/view/compiler'; // This file only reexports content of the `src` folder. Keep it that way. \ No newline at end of file diff --git a/packages/compiler/src/identifiers.ts b/packages/compiler/src/identifiers.ts index 9c85cae1e9..dcfc98598e 100644 --- a/packages/compiler/src/identifiers.ts +++ b/packages/compiler/src/identifiers.ts @@ -65,6 +65,7 @@ export class Identifiers { static INJECTOR: o.ExternalReference = {name: 'INJECTOR', moduleName: CORE}; static Injector: o.ExternalReference = {name: 'Injector', moduleName: CORE}; static defineInjectable: o.ExternalReference = {name: 'defineInjectable', moduleName: CORE}; + static InjectableDef: o.ExternalReference = {name: 'InjectableDef', moduleName: CORE}; static ViewEncapsulation: o.ExternalReference = { name: 'ViewEncapsulation', moduleName: CORE, diff --git a/packages/compiler/src/injectable_compiler_2.ts b/packages/compiler/src/injectable_compiler_2.ts index 0f01f881b0..083d2217f4 100644 --- a/packages/compiler/src/injectable_compiler_2.ts +++ b/packages/compiler/src/injectable_compiler_2.ts @@ -7,96 +7,107 @@ */ import {InjectFlags} from './core'; +import {Identifiers} from './identifiers'; import * as o from './output/output_ast'; -import {Identifiers} from './render3/r3_identifiers'; - - -type MapEntry = { - key: string; quoted: boolean; value: o.Expression; -}; - -function mapToMapExpression(map: {[key: string]: o.Expression}): o.LiteralMapExpr { - const result = Object.keys(map).map(key => ({key, value: map[key], quoted: false})); - return o.literalMap(result); -} +import {R3DependencyMetadata, compileFactoryFunction} from './render3/r3_factory'; +import {mapToMapExpression} from './render3/util'; export interface InjectableDef { expression: o.Expression; type: o.Type; } -export interface IvyInjectableDep { - token: o.Expression; - optional: boolean; - self: boolean; - skipSelf: boolean; - attribute: boolean; -} - -export interface IvyInjectableMetadata { +export interface R3InjectableMetadata { name: string; type: o.Expression; providedIn: o.Expression; - useType?: IvyInjectableDep[]; useClass?: o.Expression; - useFactory?: {factory: o.Expression; deps: IvyInjectableDep[];}; + useFactory?: o.Expression; useExisting?: o.Expression; useValue?: o.Expression; + deps?: R3DependencyMetadata[]; } -export function compileIvyInjectable(meta: IvyInjectableMetadata): InjectableDef { - let ret: o.Expression = o.NULL_EXPR; - if (meta.useType !== undefined) { - const args = meta.useType.map(dep => injectDep(dep)); - ret = new o.InstantiateExpr(meta.type, args); - } else if (meta.useClass !== undefined) { - const factory = - new o.ReadPropExpr(new o.ReadPropExpr(meta.useClass, 'ngInjectableDef'), 'factory'); - ret = new o.InvokeFunctionExpr(factory, []); +export function compileInjectable(meta: R3InjectableMetadata): InjectableDef { + let factory: o.Expression = o.NULL_EXPR; + + function makeFn(ret: o.Expression): o.Expression { + return o.fn([], [new o.ReturnStatement(ret)], undefined, undefined, `${meta.name}_Factory`); + } + + if (meta.useClass !== undefined || meta.useFactory !== undefined) { + // First, handle useClass and useFactory together, since both involve a similar call to + // `compileFactoryFunction`. Either dependencies are explicitly specified, in which case + // a factory function call is generated, or they're not specified and the calls are special- + // cased. + if (meta.deps !== undefined) { + // Either call `new meta.useClass(...)` or `meta.useFactory(...)`. + const fnOrClass: o.Expression = meta.useClass || meta.useFactory !; + + // useNew: true if meta.useClass, false for meta.useFactory. + const useNew = meta.useClass !== undefined; + + factory = compileFactoryFunction({ + name: meta.name, + fnOrClass, + useNew, + injectFn: Identifiers.inject, + useOptionalParam: true, + deps: meta.deps, + }); + } else if (meta.useClass !== undefined) { + // Special case for useClass where the factory from the class's ngInjectableDef is used. + if (meta.useClass.isEquivalent(meta.type)) { + // For the injectable compiler, useClass represents a foreign type that should be + // instantiated to satisfy construction of the given type. It's not valid to specify + // useClass === type, since the useClass type is expected to already be compiled. + throw new Error( + `useClass is the same as the type, but no deps specified, which is invalid.`); + } + factory = + makeFn(new o.ReadPropExpr(new o.ReadPropExpr(meta.useClass, 'ngInjectableDef'), 'factory') + .callFn([])); + } else if (meta.useFactory !== undefined) { + // Special case for useFactory where no arguments are passed. + factory = meta.useFactory.callFn([]); + } else { + // Can't happen - outer conditional guards against both useClass and useFactory being + // undefined. + throw new Error('Reached unreachable block in injectable compiler.'); + } } else if (meta.useValue !== undefined) { - ret = meta.useValue; + // Note: it's safe to use `meta.useValue` instead of the `USE_VALUE in meta` check used for + // client code because meta.useValue is an Expression which will be defined even if the actual + // value is undefined. + factory = makeFn(meta.useValue); } else if (meta.useExisting !== undefined) { - ret = o.importExpr(Identifiers.inject).callFn([meta.useExisting]); - } else if (meta.useFactory !== undefined) { - const args = meta.useFactory.deps.map(dep => injectDep(dep)); - ret = new o.InvokeFunctionExpr(meta.useFactory.factory, args); + // useExisting is an `inject` call on the existing token. + factory = makeFn(o.importExpr(Identifiers.inject).callFn([meta.useExisting])); } else { - throw new Error('No instructions for injectable compiler!'); + // A strict type is compiled according to useClass semantics, except the dependencies are + // required. + if (meta.deps === undefined) { + throw new Error(`Type compilation of an injectable requires dependencies.`); + } + factory = compileFactoryFunction({ + name: meta.name, + fnOrClass: meta.type, + useNew: true, + injectFn: Identifiers.inject, + useOptionalParam: true, + deps: meta.deps, + }); } const token = meta.type; const providedIn = meta.providedIn; - const factory = - o.fn([], [new o.ReturnStatement(ret)], undefined, undefined, `${meta.name}_Factory`); - const expression = o.importExpr({ - moduleName: '@angular/core', - name: 'defineInjectable', - }).callFn([mapToMapExpression({token, factory, providedIn})]); - const type = new o.ExpressionType(o.importExpr( - { - moduleName: '@angular/core', - name: 'InjectableDef', - }, - [new o.ExpressionType(meta.type)])); + const expression = o.importExpr(Identifiers.defineInjectable).callFn([mapToMapExpression( + {token, factory, providedIn})]); + const type = new o.ExpressionType( + o.importExpr(Identifiers.InjectableDef, [new o.ExpressionType(meta.type)])); return { expression, type, }; } - -function injectDep(dep: IvyInjectableDep): o.Expression { - const defaultValue = dep.optional ? o.NULL_EXPR : o.literal(undefined); - const flags = o.literal( - InjectFlags.Default | (dep.self && InjectFlags.Self || 0) | - (dep.skipSelf && InjectFlags.SkipSelf || 0)); - if (!dep.optional && !dep.skipSelf && !dep.self) { - return o.importExpr(Identifiers.inject).callFn([dep.token]); - } else { - return o.importExpr(Identifiers.inject).callFn([ - dep.token, - defaultValue, - flags, - ]); - } -} diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts index e168a5a28f..9780ded14f 100644 --- a/packages/compiler/src/output/abstract_emitter.ts +++ b/packages/compiler/src/output/abstract_emitter.ts @@ -312,7 +312,7 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex ctx.print(expr, `)`); return null; } - visitWrappedNodeExpr(ast: o.WrappedNodeExpr, ctx: EmitterVisitorContext): never { + visitWrappedNodeExpr(ast: o.WrappedNodeExpr, ctx: EmitterVisitorContext): any { throw new Error('Abstract emitter cannot visit WrappedNodeExpr.'); } visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): any { diff --git a/packages/compiler/src/output/abstract_js_emitter.ts b/packages/compiler/src/output/abstract_js_emitter.ts index 943fab81e1..621a67af40 100644 --- a/packages/compiler/src/output/abstract_js_emitter.ts +++ b/packages/compiler/src/output/abstract_js_emitter.ts @@ -70,9 +70,10 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor { ctx.println(stmt, `};`); } - visitWrappedNodeExpr(ast: o.WrappedNodeExpr, ctx: EmitterVisitorContext): never { + visitWrappedNodeExpr(ast: o.WrappedNodeExpr, ctx: EmitterVisitorContext): any { throw new Error('Cannot emit a WrappedNodeExpr in Javascript.'); } + visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): string|null { if (ast.builtin === o.BuiltinVar.This) { ctx.print(ast, 'self'); diff --git a/packages/compiler/src/output/output_jit.ts b/packages/compiler/src/output/output_jit.ts index 7fb32a64cc..3c077aa911 100644 --- a/packages/compiler/src/output/output_jit.ts +++ b/packages/compiler/src/output/output_jit.ts @@ -68,15 +68,12 @@ export class JitEmitterVisitor extends AbstractJsEmitterVisitor { } visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any { - const value = this.reflector.resolveExternalReference(ast.value); - let id = this._evalArgValues.indexOf(value); - if (id === -1) { - id = this._evalArgValues.length; - this._evalArgValues.push(value); - const name = identifierName({reference: value}) || 'val'; - this._evalArgNames.push(`jit_${name}_${id}`); - } - ctx.print(ast, this._evalArgNames[id]); + this._emitReferenceToExternal(ast, this.reflector.resolveExternalReference(ast.value), ctx); + return null; + } + + visitWrappedNodeExpr(ast: o.WrappedNodeExpr, ctx: EmitterVisitorContext): any { + this._emitReferenceToExternal(ast, ast.node, ctx); return null; } @@ -100,4 +97,16 @@ export class JitEmitterVisitor extends AbstractJsEmitterVisitor { } return super.visitDeclareClassStmt(stmt, ctx); } + + private _emitReferenceToExternal(ast: o.Expression, value: any, ctx: EmitterVisitorContext): + void { + let id = this._evalArgValues.indexOf(value); + if (id === -1) { + id = this._evalArgValues.length; + this._evalArgValues.push(value); + const name = identifierName({reference: value}) || 'val'; + this._evalArgNames.push(`jit_${name}_${id}`); + } + ctx.print(ast, this._evalArgNames[id]); + } } diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 030a2b9b94..bbde7f2f02 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -110,6 +110,8 @@ export class Identifiers { moduleName: CORE, }; + static defineNgModule: o.ExternalReference = {name: 'ɵdefineNgModule', moduleName: CORE}; + static definePipe: o.ExternalReference = {name: 'ɵdefinePipe', moduleName: CORE}; static query: o.ExternalReference = {name: 'ɵQ', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_jit.ts b/packages/compiler/src/render3/r3_jit.ts new file mode 100644 index 0000000000..5d1a93e3d9 --- /dev/null +++ b/packages/compiler/src/render3/r3_jit.ts @@ -0,0 +1,78 @@ +/** + * @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 {CompileReflector} from '../compile_reflector'; +import {ConstantPool} from '../constant_pool'; +import * as o from '../output/output_ast'; +import {jitStatements} from '../output/output_jit'; + +/** + * Implementation of `CompileReflector` which resolves references to @angular/core + * symbols at runtime, according to a consumer-provided mapping. + * + * Only supports `resolveExternalReference`, all other methods throw. + */ +class R3JitReflector implements CompileReflector { + constructor(private context: {[key: string]: any}) {} + + resolveExternalReference(ref: o.ExternalReference): any { + // This reflector only handles @angular/core imports. + if (ref.moduleName !== '@angular/core') { + throw new Error( + `Cannot resolve external reference to ${ref.moduleName}, only references to @angular/core are supported.`); + } + if (!this.context.hasOwnProperty(ref.name !)) { + throw new Error(`No value provided for @angular/core symbol '${ref.name!}'.`); + } + return this.context[ref.name !]; + } + + parameters(typeOrFunc: any): any[][] { throw new Error('Not implemented.'); } + + annotations(typeOrFunc: any): any[] { throw new Error('Not implemented.'); } + + shallowAnnotations(typeOrFunc: any): any[] { throw new Error('Not implemented.'); } + + tryAnnotations(typeOrFunc: any): any[] { throw new Error('Not implemented.'); } + + propMetadata(typeOrFunc: any): {[key: string]: any[];} { throw new Error('Not implemented.'); } + + hasLifecycleHook(type: any, lcProperty: string): boolean { throw new Error('Not implemented.'); } + + guards(typeOrFunc: any): {[key: string]: any;} { throw new Error('Not implemented.'); } + + componentModuleUrl(type: any, cmpMetadata: any): string { throw new Error('Not implemented.'); } +} + +/** + * JIT compiles an expression and monkey-patches the result of executing the expression onto a given + * type. + * + * @param type the type which will receive the monkey-patched result + * @param field name of the field on the type to monkey-patch + * @param def the definition which will be compiled and executed to get the value to patch + * @param context an object map of @angular/core symbol names to symbols which will be available in + * the context of the compiled expression + * @param constantPool an optional `ConstantPool` which contains constants used in the expression + */ +export function jitPatchDefinition( + type: any, field: string, def: o.Expression, context: {[key: string]: any}, + constantPool?: ConstantPool): void { + // The ConstantPool may contain Statements which declare variables used in the final expression. + // Therefore, its statements need to precede the actual JIT operation. The final statement is a + // declaration of $def which is set to the expression being compiled. + const statements: o.Statement[] = [ + ...(constantPool !== undefined ? constantPool.statements : []), + new o.DeclareVarStmt('$def', def, undefined, [o.StmtModifier.Exported]), + ]; + + // Monkey patch the field on the given type with the result of compilation. + // TODO(alxhub): consider a better source url. + type[field] = jitStatements( + `ng://${type && type.name}/${field}`, statements, new R3JitReflector(context), false)['$def']; +} diff --git a/packages/compiler/src/render3/r3_module_compiler.ts b/packages/compiler/src/render3/r3_module_compiler.ts index f0d2c93a83..91a365a3dc 100644 --- a/packages/compiler/src/render3/r3_module_compiler.ts +++ b/packages/compiler/src/render3/r3_module_compiler.ts @@ -14,22 +14,72 @@ import * as o from '../output/output_ast'; import {OutputContext} from '../util'; import {Identifiers as R3} from './r3_identifiers'; +import {convertMetaToOutput, mapToMapExpression} from './util'; -function convertMetaToOutput(meta: any, ctx: OutputContext): o.Expression { - if (Array.isArray(meta)) { - return o.literalArr(meta.map(entry => convertMetaToOutput(entry, ctx))); - } - if (meta instanceof StaticSymbol) { - return ctx.importExpr(meta); - } - if (meta == null) { - return o.literal(meta); - } - - throw new Error(`Internal error: Unsupported or unknown metadata: ${meta}`); +export interface R3NgModuleDef { + expression: o.Expression; + type: o.Type; + additionalStatements: o.Statement[]; } -export function compileNgModule( +/** + * Metadata required by the module compiler to generate a `ngModuleDef` for a type. + */ +export interface R3NgModuleMetadata { + /** + * An expression representing the module type being compiled. + */ + type: o.Expression; + + /** + * An array of expressions representing the bootstrap components specified by the module. + */ + bootstrap: o.Expression[]; + + /** + * An array of expressions representing the directives and pipes declared by the module. + */ + declarations: o.Expression[]; + + /** + * An array of expressions representing the imports of the module. + */ + imports: o.Expression[]; + + /** + * An array of expressions representing the exports of the module. + */ + exports: o.Expression[]; + + /** + * Whether to emit the selector scope values (declarations, imports, exports) inline into the + * module definition, or to generate additional statements which patch them on. Inline emission + * does not allow components to be tree-shaken, but is useful for JIT mode. + */ + emitInline: boolean; +} + +/** + * Construct an `R3NgModuleDef` for the given `R3NgModuleMetadata`. + */ +export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { + const {type: moduleType, bootstrap, declarations, imports, exports} = meta; + const expression = o.importExpr(R3.defineNgModule).callFn([mapToMapExpression({ + type: moduleType, + bootstrap: o.literalArr(bootstrap), + declarations: o.literalArr(declarations), + imports: o.literalArr(imports), + exports: o.literalArr(exports), + })]); + + // TODO(alxhub): write a proper type reference when AOT compilation of @NgModule is implemented. + const type = new o.ExpressionType(o.NULL_EXPR); + const additionalStatements: o.Statement[] = []; + return {expression, type, additionalStatements}; +} + +// TODO(alxhub): integrate this with `compileNgModule`. Currently the two are separate operations. +export function compileNgModuleFromRender2( ctx: OutputContext, ngModule: CompileShallowModuleMetadata, injectableCompiler: InjectableCompiler): void { const className = identifierName(ngModule.type) !; @@ -57,4 +107,9 @@ export function compileNgModule( /* getters */[], /* constructorMethod */ new o.ClassMethod(null, [], []), /* methods */[])); -} \ No newline at end of file +} + +function accessExportScope(module: o.Expression): o.Expression { + const selectorScope = new o.ReadPropExpr(module, 'ngModuleDef'); + return new o.ReadPropExpr(selectorScope, 'exported'); +} diff --git a/packages/compiler/src/render3/util.ts b/packages/compiler/src/render3/util.ts new file mode 100644 index 0000000000..dd5fe5214e --- /dev/null +++ b/packages/compiler/src/render3/util.ts @@ -0,0 +1,38 @@ +/** + * @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 {StaticSymbol} from '../aot/static_symbol'; +import * as o from '../output/output_ast'; +import {OutputContext} from '../util'; + +/** + * Convert an object map with `Expression` values into a `LiteralMapExpr`. + */ +export function mapToMapExpression(map: {[key: string]: o.Expression}): o.LiteralMapExpr { + const result = Object.keys(map).map(key => ({key, value: map[key], quoted: false})); + return o.literalMap(result); +} + +/** + * Convert metadata into an `Expression` in the given `OutputContext`. + * + * This operation will handle arrays, references to symbols, or literal `null` or `undefined`. + */ +export function convertMetaToOutput(meta: any, ctx: OutputContext): o.Expression { + if (Array.isArray(meta)) { + return o.literalArr(meta.map(entry => convertMetaToOutput(entry, ctx))); + } + if (meta instanceof StaticSymbol) { + return ctx.importExpr(meta); + } + if (meta == null) { + return o.literal(meta); + } + + throw new Error(`Internal error: Unsupported or unknown metadata: ${meta}`); +} diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 8faa85963f..e79e802377 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -12,12 +12,20 @@ import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, c import {ConstantPool} from '../../constant_pool'; import * as core from '../../core'; import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast'; +import {Lexer} from '../../expression_parser/lexer'; +import {Parser} from '../../expression_parser/parser'; +import * as html from '../../ml_parser/ast'; +import {HtmlParser} from '../../ml_parser/html_parser'; +import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; import * as o from '../../output/output_ast'; -import {ParseSourceSpan} from '../../parse_util'; +import {ParseError, ParseSourceSpan} from '../../parse_util'; +import {DomElementSchemaRegistry} from '../../schema/dom_element_schema_registry'; import {CssSelector, SelectorMatcher} from '../../selector'; +import {BindingParser} from '../../template_parser/binding_parser'; import {OutputContext, error} from '../../util'; import * as t from '../r3_ast'; import {Identifiers as R3} from '../r3_identifiers'; +import {htmlAstToRender3Ast} from '../r3_template_transform'; import {R3QueryMetadata} from './api'; import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, REFERENCE_PREFIX, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, getQueryPredicate, invalid, mapToExpression, noop, temporaryAllocator, trimTrailingNulls, unsupported} from './util'; @@ -713,3 +721,35 @@ function interpolate(args: o.Expression[]): o.Expression { error(`Invalid interpolation argument length ${args.length}`); return o.importExpr(R3.interpolationV).callFn([o.literalArr(args)]); } + +/** + * Parse a template into render3 `Node`s and additional metadata, with no other dependencies. + * + * @param template text of the template to parse + * @param templateUrl URL to use for source mapping of the parsed template + */ +export function parseTemplate(template: string, templateUrl: string): + {errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} { + const bindingParser = makeBindingParser(); + const htmlParser = new HtmlParser(); + const parseResult = htmlParser.parse(template, templateUrl); + if (parseResult.errors && parseResult.errors.length > 0) { + return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []}; + } + const {nodes, hasNgContent, ngContentSelectors, errors} = + htmlAstToRender3Ast(parseResult.rootNodes, bindingParser); + if (errors && errors.length > 0) { + return {errors, nodes: [], hasNgContent: false, ngContentSelectors: []}; + } + + return {nodes, hasNgContent, ngContentSelectors}; +} + +/** + * Construct a `BindingParser` with a default configuration. + */ +export function makeBindingParser(): BindingParser { + return new BindingParser( + new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), [], + []); +} diff --git a/packages/core/BUILD.bazel b/packages/core/BUILD.bazel index 7bbfa1911e..68a4cea600 100644 --- a/packages/core/BUILD.bazel +++ b/packages/core/BUILD.bazel @@ -16,6 +16,7 @@ ng_module( module_name = "@angular/core", deps = [ "//packages:types", + "//packages/compiler", "@rxjs", ], ) diff --git a/packages/core/rollup.config.js b/packages/core/rollup.config.js index 35c4334b89..93e50a2578 100644 --- a/packages/core/rollup.config.js +++ b/packages/core/rollup.config.js @@ -10,8 +10,9 @@ const resolve = require('rollup-plugin-node-resolve'); const sourcemaps = require('rollup-plugin-sourcemaps'); const globals = { + '@angular/compiler': 'ng.compiler', 'rxjs': 'rxjs', - 'rxjs/operators': 'rxjs.operators' + 'rxjs/operators': 'rxjs.operators', }; module.exports = { diff --git a/packages/core/src/application_module.ts b/packages/core/src/application_module.ts index affd64c99c..55caa5d9b7 100644 --- a/packages/core/src/application_module.ts +++ b/packages/core/src/application_module.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationInitStatus} from './application_init'; +import {APP_INITIALIZER, ApplicationInitStatus} from './application_init'; import {ApplicationRef} from './application_ref'; import {APP_ID_RANDOM_PROVIDER} from './application_tokens'; import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection'; +import {forwardRef} from './di/forward_ref'; import {Inject, Optional, SkipSelf} from './di/metadata'; import {LOCALE_ID} from './i18n/tokens'; import {Compiler} from './linker/compiler'; @@ -46,7 +47,7 @@ export function _localeFactory(locale?: string): string { useFactory: _localeFactory, deps: [[new Inject(LOCALE_ID), new Optional(), new SkipSelf()]] }, - ] + ], }) export class ApplicationModule { // Inject ApplicationRef to make it eager... diff --git a/packages/core/src/di/injectable.ts b/packages/core/src/di/injectable.ts index 507623051a..740878d3bf 100644 --- a/packages/core/src/di/injectable.ts +++ b/packages/core/src/di/injectable.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {R3_COMPILE_INJECTABLE} from '../ivy_switch'; import {ReflectionCapabilities} from '../reflection/reflection_capabilities'; import {Type} from '../type'; import {makeDecorator, makeParamDecorator} from '../util/decorators'; @@ -67,10 +68,7 @@ export interface InjectableDecorator { * * @experimental */ -export interface Injectable { - providedIn?: Type|'root'|null; - factory: () => any; -} +export interface Injectable { providedIn?: Type|'root'|null; } const EMPTY_ARRAY: any[] = []; @@ -110,6 +108,20 @@ export function convertInjectableProviderToFactory( } } +/** + * Supports @Injectable() in JIT mode for Render2. + */ +function preR3InjectableCompile( + injectableType: InjectableType, + options: {providedIn?: Type| 'root' | null} & InjectableProvider): void { + if (options && options.providedIn !== undefined && injectableType.ngInjectableDef === undefined) { + injectableType.ngInjectableDef = defineInjectable({ + providedIn: options.providedIn, + factory: convertInjectableProviderToFactory(injectableType, options), + }); + } +} + /** * Injectable decorator and metadata. * @@ -118,16 +130,8 @@ export function convertInjectableProviderToFactory( */ export const Injectable: InjectableDecorator = makeDecorator( 'Injectable', undefined, undefined, undefined, - (injectableType: InjectableType, - options: {providedIn?: Type| 'root' | null} & InjectableProvider) => { - if (options && options.providedIn !== undefined && - injectableType.ngInjectableDef === undefined) { - injectableType.ngInjectableDef = defineInjectable({ - providedIn: options.providedIn, - factory: convertInjectableProviderToFactory(injectableType, options) - }); - } - }); + (type: Type, meta: Injectable) => + (R3_COMPILE_INJECTABLE || preR3InjectableCompile)(type, meta)); /** * Type representing injectable service. diff --git a/packages/core/src/ivy_switch.ts b/packages/core/src/ivy_switch.ts index a584e99587..e461fc900a 100644 --- a/packages/core/src/ivy_switch.ts +++ b/packages/core/src/ivy_switch.ts @@ -32,3 +32,5 @@ * symbol in `./ivy_switch_false` and `./ivy_switch_false` depending on the compilation mode. */ export * from './ivy_switch_false'; + +// TODO(alxhub): debug why metadata doesn't properly propagate through this file. diff --git a/packages/core/src/ivy_switch_false.ts b/packages/core/src/ivy_switch_false.ts index 599a11d63e..b3d9427670 100644 --- a/packages/core/src/ivy_switch_false.ts +++ b/packages/core/src/ivy_switch_false.ts @@ -6,4 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -export const ivyEnabled = false; \ No newline at end of file +export const ivyEnabled = false; +export const R3_COMPILE_COMPONENT: ((type: any, meta: any) => void)|null = null; +export const R3_COMPILE_INJECTABLE: ((type: any, meta: any) => void)|null = null; +export const R3_COMPILE_NGMODULE: ((type: any, meta: any) => void)|null = null; diff --git a/packages/core/src/ivy_switch_true.ts b/packages/core/src/ivy_switch_true.ts index fc5c97d002..0dfe7be6ee 100644 --- a/packages/core/src/ivy_switch_true.ts +++ b/packages/core/src/ivy_switch_true.ts @@ -6,4 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -export const ivyEnabled = true; \ No newline at end of file +import {compileComponentDecorator} from './render3/jit/directive'; +import {compileInjectable} from './render3/jit/injectable'; +import {compileNgModule} from './render3/jit/module'; + +export const ivyEnabled = true; +export const R3_COMPILE_COMPONENT = compileComponentDecorator; +export const R3_COMPILE_INJECTABLE = compileInjectable; +export const R3_COMPILE_NGMODULE = compileNgModule; diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index e606c12fa4..2866829128 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -13,7 +13,7 @@ import {Attribute, ContentChild, ContentChildren, Query, ViewChild, ViewChildren} from './metadata/di'; import {Component, Directive, HostBinding, HostListener, Input, Output, Pipe} from './metadata/directives'; -import {ModuleWithProviders, NgModule, SchemaMetadata} from './metadata/ng_module'; +import {ModuleWithProviders, NgModule, NgModuleDef, SchemaMetadata, defineNgModule} from './metadata/ng_module'; import {ViewEncapsulation} from './metadata/view'; export {ANALYZE_FOR_ENTRY_COMPONENTS, Attribute, ContentChild, ContentChildDecorator, ContentChildren, ContentChildrenDecorator, Query, ViewChild, ViewChildDecorator, ViewChildren, ViewChildrenDecorator} from './metadata/di'; diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index 145f5ed18f..34bb977191 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -8,9 +8,9 @@ import {ChangeDetectionStrategy} from '../change_detection/constants'; import {Provider} from '../di'; +import {R3_COMPILE_COMPONENT} from '../ivy_switch'; import {Type} from '../type'; import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators'; - import {ViewEncapsulation} from './view'; @@ -754,7 +754,8 @@ export interface Component extends Directive { */ export const Component: ComponentDecorator = makeDecorator( 'Component', (c: Component = {}) => ({changeDetection: ChangeDetectionStrategy.Default, ...c}), - Directive); + Directive, undefined, + (type: Type, meta: Component) => (R3_COMPILE_COMPONENT || (() => {}))(type, meta)); /** * Type of the Pipe decorator / constructor function. diff --git a/packages/core/src/metadata/ng_module.ts b/packages/core/src/metadata/ng_module.ts index 60e9986efb..4ab8d8744a 100644 --- a/packages/core/src/metadata/ng_module.ts +++ b/packages/core/src/metadata/ng_module.ts @@ -9,9 +9,31 @@ import {InjectorDef, InjectorType, defineInjector} from '../di/defs'; import {convertInjectableProviderToFactory} from '../di/injectable'; import {Provider} from '../di/provider'; +import {R3_COMPILE_NGMODULE} from '../ivy_switch'; import {Type} from '../type'; import {TypeDecorator, makeDecorator} from '../util/decorators'; +export interface NgModuleDef { + type: T; + bootstrap: Type[]; + declarations: Type[]; + imports: Type[]; + exports: Type[]; + + transitiveCompileScope: {directives: any[]; pipes: any[];}|undefined; +} + +export function defineNgModule(def: {type: T} & Partial>): never { + const res: NgModuleDef = { + type: def.type, + bootstrap: def.bootstrap || [], + declarations: def.declarations || [], + imports: def.imports || [], + exports: def.exports || [], + transitiveCompileScope: undefined, + }; + return res as never; +} /** * A wrapper around a module that also includes the providers. @@ -187,6 +209,19 @@ export interface NgModule { id?: string; } +function preR3NgModuleCompile(moduleType: InjectorType, metadata: NgModule): void { + let imports = (metadata && metadata.imports) || []; + if (metadata && metadata.exports) { + imports = [...imports, metadata.exports]; + } + + moduleType.ngInjectorDef = defineInjector({ + factory: convertInjectableProviderToFactory(moduleType, {useClass: moduleType}), + providers: metadata && metadata.providers, + imports: imports, + }); +} + /** * NgModule decorator and metadata. * @@ -195,15 +230,4 @@ export interface NgModule { */ export const NgModule: NgModuleDecorator = makeDecorator( 'NgModule', (ngModule: NgModule) => ngModule, undefined, undefined, - (moduleType: InjectorType, metadata: NgModule) => { - let imports = (metadata && metadata.imports) || []; - if (metadata && metadata.exports) { - imports = [...imports, metadata.exports]; - } - - moduleType.ngInjectorDef = defineInjector({ - factory: convertInjectableProviderToFactory(moduleType, {useClass: moduleType}), - providers: metadata && metadata.providers, - imports: imports, - }); - }); + (type: Type, meta: NgModule) => (R3_COMPILE_NGMODULE || preR3NgModuleCompile)(type, meta)); diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts new file mode 100644 index 0000000000..7cabe4f363 --- /dev/null +++ b/packages/core/src/render3/jit/directive.ts @@ -0,0 +1,97 @@ +/** + * @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 {compileComponent as compileIvyComponent, parseTemplate, ConstantPool, makeBindingParser, WrappedNodeExpr, jitPatchDefinition,} from '@angular/compiler'; + +import {Component} from '../../metadata/directives'; +import {ReflectionCapabilities} from '../../reflection/reflection_capabilities'; +import {Type} from '../../type'; + +import {angularCoreEnv} from './environment'; +import {reflectDependencies} from './util'; + +let _pendingPromises: Promise[] = []; + +/** + * Compile an Angular component according to its decorator metadata, and patch the resulting + * ngComponentDef onto the component type. + * + * Compilation may be asynchronous (due to the need to resolve URLs for the component template or + * other resources, for example). In the event that compilation is not immediate, `compileComponent` + * will return a `Promise` which will resolve when compilation completes and the component becomes + * usable. + */ +export function compileComponent(type: Type, metadata: Component): Promise|null { + // TODO(alxhub): implement ResourceLoader support for template compilation. + if (!metadata.template) { + throw new Error('templateUrl not yet supported'); + } + + // Parse the template and check for errors. + const template = parseTemplate(metadata.template !, `ng://${type.name}/template.html`); + if (template.errors !== undefined) { + const errors = template.errors.map(err => err.toString()).join(', '); + throw new Error(`Errors during JIT compilation of template for ${type.name}: ${errors}`); + } + + // The ConstantPool is a requirement of the JIT'er. + const constantPool = new ConstantPool(); + + // Compile the component metadata, including template, into an expression. + // TODO(alxhub): implement inputs, outputs, queries, etc. + const res = compileIvyComponent( + { + name: type.name, + type: new WrappedNodeExpr(type), + selector: metadata.selector !, template, + deps: reflectDependencies(type), + directives: new Map(), + pipes: new Map(), + host: { + attributes: {}, + listeners: {}, + properties: {}, + }, + inputs: {}, + outputs: {}, + lifecycle: { + usesOnChanges: false, + }, + queries: [], + typeSourceSpan: null !, + viewQueries: [], + }, + constantPool, makeBindingParser()); + + // Patch the generated expression as ngComponentDef on the type. + jitPatchDefinition(type, 'ngComponentDef', res.expression, angularCoreEnv, constantPool); + return null; +} + +/** + * A wrapper around `compileComponent` which is intended to be used for the `@Component` decorator. + * + * This wrapper keeps track of the `Promise` returned by `compileComponent` and will cause + * `awaitCurrentlyCompilingComponents` to wait on the compilation to be finished. + */ +export function compileComponentDecorator(type: Type, metadata: Component): void { + const res = compileComponent(type, metadata); + if (res !== null) { + _pendingPromises.push(res); + } +} + +/** + * Returns a promise which will await the compilation of any `@Component`s which have been defined + * since the last time `awaitCurrentlyCompilingComponents` was called. + */ +export function awaitCurrentlyCompilingComponents(): Promise { + const res = Promise.all(_pendingPromises).then(() => undefined); + _pendingPromises = []; + return res; +} diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts new file mode 100644 index 0000000000..a5068eee8c --- /dev/null +++ b/packages/core/src/render3/jit/environment.ts @@ -0,0 +1,39 @@ +/** + * @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 {defineInjectable} from '../../di/defs'; +import {inject} from '../../di/injector'; +import {defineNgModule} from '../../metadata/ng_module'; +import * as r3 from '../index'; + + +/** + * A mapping of the @angular/core API surface used in generated expressions to the actual symbols. + * + * This should be kept up to date with the public exports of @angular/core. + */ +export const angularCoreEnv = { + 'ɵdefineComponent': r3.defineComponent, + 'defineInjectable': defineInjectable, + 'ɵdefineNgModule': defineNgModule, + 'ɵdirectiveInject': r3.directiveInject, + 'inject': inject, + 'ɵC': r3.C, + 'ɵE': r3.E, + 'ɵe': r3.e, + 'ɵi1': r3.i1, + 'ɵi2': r3.i2, + 'ɵi3': r3.i3, + 'ɵi4': r3.i4, + 'ɵi5': r3.i5, + 'ɵi6': r3.i6, + 'ɵi7': r3.i7, + 'ɵi8': r3.i8, + 'ɵT': r3.T, + 'ɵt': r3.t, +}; diff --git a/packages/core/src/render3/jit/injectable.ts b/packages/core/src/render3/jit/injectable.ts new file mode 100644 index 0000000000..e0b4bc1a26 --- /dev/null +++ b/packages/core/src/render3/jit/injectable.ts @@ -0,0 +1,114 @@ +/** + * @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 {Expression, LiteralExpr, R3DependencyMetadata, WrappedNodeExpr, compileInjectable as compileIvyInjectable, jitPatchDefinition} from '@angular/compiler'; + +import {Injectable} from '../../di/injectable'; +import {ClassSansProvider, ExistingSansProvider, FactorySansProvider, StaticClassSansProvider, ValueProvider, ValueSansProvider} from '../../di/provider'; +import {Type} from '../../type'; +import {getClosureSafeProperty} from '../../util/property'; + +import {angularCoreEnv} from './environment'; +import {convertDependencies, reflectDependencies} from './util'; + + +/** + * Compile an Angular injectable according to its `Injectable` metadata, and patch the resulting + * `ngInjectableDef` onto the injectable type. + */ +export function compileInjectable(type: Type, meta?: Injectable): void { + // TODO(alxhub): handle JIT of bare @Injectable(). + if (!meta) { + return; + } + + // Check whether the injectable metadata includes a provider specification. + const hasAProvider = isUseClassProvider(meta) || isUseFactoryProvider(meta) || + isUseValueProvider(meta) || isUseExistingProvider(meta); + + let deps: R3DependencyMetadata[]|undefined = undefined; + if (!hasAProvider || (isUseClassProvider(meta) && type === meta.useClass)) { + deps = reflectDependencies(type); + } else if (isUseClassProvider(meta)) { + deps = meta.deps && convertDependencies(meta.deps); + } else if (isUseFactoryProvider(meta)) { + deps = meta.deps && convertDependencies(meta.deps) || []; + } + + // Decide which flavor of factory to generate, based on the provider specified. + // Only one of the use* fields should be set. + let useClass: Expression|undefined = undefined; + let useFactory: Expression|undefined = undefined; + let useValue: Expression|undefined = undefined; + let useExisting: Expression|undefined = undefined; + + if (!hasAProvider) { + // In the case the user specifies a type provider, treat it as {provide: X, useClass: X}. + // The deps will have been reflected above, causing the factory to create the class by calling + // its constructor with injected deps. + useClass = new WrappedNodeExpr(type); + } else if (isUseClassProvider(meta)) { + // The user explicitly specified useClass, and may or may not have provided deps. + useClass = new WrappedNodeExpr(meta.useClass); + } else if (isUseValueProvider(meta)) { + // The user explicitly specified useValue. + useValue = new WrappedNodeExpr(meta.useValue); + } else if (isUseFactoryProvider(meta)) { + // The user explicitly specified useFactory. + useFactory = new WrappedNodeExpr(meta.useFactory); + } else if (isUseExistingProvider(meta)) { + // The user explicitly specified useExisting. + useExisting = new WrappedNodeExpr(meta.useExisting); + } else { + // Can't happen - either hasAProvider will be false, or one of the providers will be set. + throw new Error(`Unreachable state.`); + } + + const {expression} = compileIvyInjectable({ + name: type.name, + type: new WrappedNodeExpr(type), + providedIn: computeProvidedIn(meta.providedIn), + useClass, + useFactory, + useValue, + useExisting, + deps, + }); + + jitPatchDefinition(type, 'ngInjectableDef', expression, angularCoreEnv); +} + +function computeProvidedIn(providedIn: Type| string | null | undefined): Expression { + if (providedIn == null || typeof providedIn === 'string') { + return new LiteralExpr(providedIn); + } else { + return new WrappedNodeExpr(providedIn); + } +} + +type UseClassProvider = Injectable & ClassSansProvider & {deps?: any[]}; + +function isUseClassProvider(meta: Injectable): meta is UseClassProvider { + return (meta as UseClassProvider).useClass !== undefined; +} + +const GET_PROPERTY_NAME = {} as any; +const USE_VALUE = getClosureSafeProperty( + {provide: String, useValue: GET_PROPERTY_NAME}, GET_PROPERTY_NAME); + +function isUseValueProvider(meta: Injectable): meta is Injectable&ValueSansProvider { + return USE_VALUE in meta; +} + +function isUseFactoryProvider(meta: Injectable): meta is Injectable&FactorySansProvider { + return (meta as FactorySansProvider).useFactory !== undefined; +} + +function isUseExistingProvider(meta: Injectable): meta is Injectable&ExistingSansProvider { + return (meta as ExistingSansProvider).useExisting !== undefined; +} diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts new file mode 100644 index 0000000000..991faf2913 --- /dev/null +++ b/packages/core/src/render3/jit/module.ts @@ -0,0 +1,94 @@ +/** + * @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 {Expression, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule as compileIvyNgModule, jitPatchDefinition} from '@angular/compiler'; + +import {ModuleWithProviders, NgModule, NgModuleDef} from '../../metadata/ng_module'; +import {Type} from '../../type'; +import {ComponentDef} from '../interfaces/definition'; +import {flatten} from '../util'; + +import {angularCoreEnv} from './environment'; + +const EMPTY_ARRAY: Type[] = []; + +export function compileNgModule(type: Type, ngModule: NgModule): void { + const meta: R3NgModuleMetadata = { + type: wrap(type), + bootstrap: flatten(ngModule.bootstrap || EMPTY_ARRAY).map(wrap), + declarations: flatten(ngModule.declarations || EMPTY_ARRAY).map(wrap), + imports: flatten(ngModule.imports || EMPTY_ARRAY).map(expandModuleWithProviders).map(wrap), + exports: flatten(ngModule.exports || EMPTY_ARRAY).map(expandModuleWithProviders).map(wrap), + emitInline: true, + }; + const res = compileIvyNgModule(meta); + + // Compute transitiveCompileScope + const transitiveCompileScope = { + directives: [] as any[], + pipes: [] as any[], + }; + flatten(ngModule.declarations || EMPTY_ARRAY).forEach(decl => { + if (decl.ngPipeDef) { + transitiveCompileScope.pipes.push(decl); + } else if (decl.ngComponentDef) { + transitiveCompileScope.directives.push(decl); + patchComponentWithScope(decl, type as any); + } else { + transitiveCompileScope.directives.push(decl); + decl.ngSelectorScope = type; + } + }); + + function addExportsFrom(module: Type& {ngModuleDef: NgModuleDef}): void { + module.ngModuleDef.exports.forEach((exp: any) => { + if (isNgModule(exp)) { + addExportsFrom(exp); + } else if (exp.ngPipeDef) { + transitiveCompileScope.pipes.push(exp); + } else { + transitiveCompileScope.directives.push(exp); + } + }); + } + + flatten([(ngModule.imports || EMPTY_ARRAY), (ngModule.exports || EMPTY_ARRAY)]) + .filter(importExport => isNgModule(importExport)) + .forEach(mod => addExportsFrom(mod)); + jitPatchDefinition(type, 'ngModuleDef', res.expression, angularCoreEnv); + ((type as any).ngModuleDef as NgModuleDef).transitiveCompileScope = transitiveCompileScope; +} + +export function patchComponentWithScope( + component: Type& {ngComponentDef: ComponentDef}, + module: Type& {ngModuleDef: NgModuleDef}) { + component.ngComponentDef.directiveDefs = () => + module.ngModuleDef.transitiveCompileScope !.directives.map( + dir => dir.ngDirectiveDef || dir.ngComponentDef); + component.ngComponentDef.pipeDefs = () => + module.ngModuleDef.transitiveCompileScope !.pipes.map(pipe => pipe.ngPipeDef); +} + +function expandModuleWithProviders(value: Type| ModuleWithProviders): Type { + if (isModuleWithProviders(value)) { + return value.ngModule; + } + return value; +} + +function wrap(value: Type): Expression { + return new WrappedNodeExpr(value); +} + +function isModuleWithProviders(value: any): value is ModuleWithProviders { + return value.ngModule !== undefined; +} + +function isNgModule(value: any): value is Type&{ngModuleDef: NgModuleDef} { + return value.ngModuleDef !== undefined; +} diff --git a/packages/core/src/render3/jit/util.ts b/packages/core/src/render3/jit/util.ts new file mode 100644 index 0000000000..3c07880c82 --- /dev/null +++ b/packages/core/src/render3/jit/util.ts @@ -0,0 +1,86 @@ +/** + * @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 {LiteralExpr, R3DependencyMetadata, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler'; + +import {Injector} from '../../di/injector'; +import {Host, Inject, Optional, Self, SkipSelf} from '../../di/metadata'; +import {ElementRef} from '../../linker/element_ref'; +import {TemplateRef} from '../../linker/template_ref'; +import {ViewContainerRef} from '../../linker/view_container_ref'; +import {Attribute} from '../../metadata/di'; +import {ReflectionCapabilities} from '../../reflection/reflection_capabilities'; +import {Type} from '../../type'; + +let _reflect: ReflectionCapabilities|null = null; + +export function reflectDependencies(type: Type): R3DependencyMetadata[] { + _reflect = _reflect || new ReflectionCapabilities(); + return convertDependencies(_reflect.parameters(type)); +} + +export function convertDependencies(deps: any[]): R3DependencyMetadata[] { + return deps.map(dep => reflectDependency(dep)); +} + +function reflectDependency(dep: any | any[]): R3DependencyMetadata { + const meta: R3DependencyMetadata = { + token: new LiteralExpr(null), + host: false, + optional: false, + resolved: R3ResolvedDependencyType.Token, + self: false, + skipSelf: false, + }; + + function setTokenAndResolvedType(token: any): void { + if (token === ElementRef) { + meta.resolved = R3ResolvedDependencyType.ElementRef; + } else if (token === Injector) { + meta.resolved = R3ResolvedDependencyType.Injector; + } else if (token === TemplateRef) { + meta.resolved = R3ResolvedDependencyType.TemplateRef; + } else if (token === ViewContainerRef) { + meta.resolved = R3ResolvedDependencyType.ViewContainerRef; + } else { + meta.resolved = R3ResolvedDependencyType.Token; + } + meta.token = new WrappedNodeExpr(token); + } + + if (Array.isArray(dep)) { + if (dep.length === 0) { + throw new Error('Dependency array must have arguments.'); + } + for (let j = 0; j < dep.length; j++) { + const param = dep[j]; + if (param instanceof Optional || param.__proto__.ngMetadataName === 'Optional') { + meta.optional = true; + } else if (param instanceof SkipSelf || param.__proto__.ngMetadataName === 'SkipSelf') { + meta.skipSelf = true; + } else if (param instanceof Self || param.__proto__.ngMetadataName === 'Self') { + meta.self = true; + } else if (param instanceof Host || param.__proto__.ngMetadataName === 'Host') { + meta.host = true; + } else if (param instanceof Inject) { + meta.token = new WrappedNodeExpr(param.token); + } else if (param instanceof Attribute) { + if (param.attributeName === undefined) { + throw new Error(`Attribute name must be defined.`); + } + meta.token = new LiteralExpr(param.attributeName); + meta.resolved = R3ResolvedDependencyType.Attribute; + } else { + setTokenAndResolvedType(param); + } + } + } else { + setTokenAndResolvedType(dep); + } + return meta; +} diff --git a/packages/core/test/bundling/hello_world_jit/BUILD.bazel b/packages/core/test/bundling/hello_world_jit/BUILD.bazel new file mode 100644 index 0000000000..bd6acab2b3 --- /dev/null +++ b/packages/core/test/bundling/hello_world_jit/BUILD.bazel @@ -0,0 +1,58 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library", "ivy_ng_module") +load("//tools/symbol-extractor:index.bzl", "js_expected_symbol_test") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test", "rollup_bundle") +load("//tools/http-server:http_server.bzl", "http_server") + +ts_library( + name = "hello_world_jit", + srcs = ["index.ts"], + deps = [ + "//packages/core", + ], +) + +rollup_bundle( + name = "bundle", + # TODO(alexeagle): This is inconsistent. + # We try to teach users to always have their workspace at the start of a + # path, to disambiguate from other workspaces. + # Here, the rule implementation is looking in an execroot where the layout + # has an "external" directory for external dependencies. + # This should probably start with "angular/" and let the rule deal with it. + entry_point = "packages/core/test/bundling/hello_world_jit/index.js", + deps = [ + ":hello_world_jit", + "//packages/core", + ], +) + +ts_library( + name = "test_lib", + testonly = 1, + srcs = glob(["*_spec.ts"]), + deps = [ + "//packages:types", + "//packages/core", + "//packages/core/testing", + ], +) + +jasmine_node_test( + name = "test", + data = [ + ":bundle", + ":bundle.js", + ], + deps = [":test_lib"], +) + +http_server( + name = "devserver", + data = [ + "index.html", + ":bundle.min.js", + ":bundle.min_debug.js", + ], +) diff --git a/packages/core/test/bundling/hello_world_jit/index.html b/packages/core/test/bundling/hello_world_jit/index.html new file mode 100644 index 0000000000..c5c7bb3e0b --- /dev/null +++ b/packages/core/test/bundling/hello_world_jit/index.html @@ -0,0 +1,31 @@ + + + + + Angular Hello World Example + + + + + + + + + \ No newline at end of file diff --git a/packages/core/test/bundling/hello_world_jit/index.ts b/packages/core/test/bundling/hello_world_jit/index.ts new file mode 100644 index 0000000000..af6817a05e --- /dev/null +++ b/packages/core/test/bundling/hello_world_jit/index.ts @@ -0,0 +1,38 @@ +/** + * @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 'reflect-metadata'; + +import {Component, NgModule, ɵrenderComponent as renderComponent} from '@angular/core'; + +@Component({ + selector: 'greeting-cmp', + template: 'Hello World!', +}) +export class Greeting { +} + +@NgModule({ + declarations: [Greeting], + exports: [Greeting], +}) +export class GreetingModule { +} + +@Component({selector: 'hello-world', template: ''}) +export class HelloWorld { +} + +@NgModule({ + declarations: [HelloWorld], + imports: [GreetingModule], +}) +export class HelloWorldModule { +} + +renderComponent(HelloWorld); diff --git a/packages/core/test/bundling/hello_world_jit/integration_spec.ts b/packages/core/test/bundling/hello_world_jit/integration_spec.ts new file mode 100644 index 0000000000..ee349da0a6 --- /dev/null +++ b/packages/core/test/bundling/hello_world_jit/integration_spec.ts @@ -0,0 +1,23 @@ +/** + * @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 {ɵivyEnabled as ivyEnabled} from '@angular/core'; +import {withBody} from '@angular/core/testing'; +import * as fs from 'fs'; +import * as path from 'path'; + +const PACKAGE = 'angular/packages/core/test/bundling/hello_world_jit'; + +ivyEnabled && describe('Ivy JIT hello world', () => { + it('should render hello world', withBody('', () => { + require(path.join(PACKAGE, 'bundle.js')); + expect(document.body.textContent).toEqual('Hello World!'); + })); +}); + +xit('ensure at least one spec exists', () => {}); diff --git a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json index 1c925c3984..c792f20640 100644 --- a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json @@ -1325,6 +1325,9 @@ { "name": "Quote" }, + { + "name": "R3_COMPILE_INJECTABLE" + }, { "name": "REMOVE_EVENT_LISTENER" }, @@ -3620,6 +3623,9 @@ { "name": "platformCoreDynamic" }, + { + "name": "preR3InjectableCompile" + }, { "name": "preparseElement" }, diff --git a/packages/core/test/render3/BUILD.bazel b/packages/core/test/render3/BUILD.bazel index b3c3334c71..a3f89ee72f 100644 --- a/packages/core/test/render3/BUILD.bazel +++ b/packages/core/test/render3/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( "**/*_perf.ts", "domino.d.ts", "load_domino.ts", + "jit_spec.ts", ], ), deps = [ @@ -29,19 +30,28 @@ ts_library( ) ts_library( - name = "render3_node_lib", + name = "domino", testonly = 1, srcs = [ "domino.d.ts", "load_domino.ts", ], deps = [ - ":render3_lib", "//packages/platform-browser", "//packages/platform-server", ], ) +ts_library( + name = "render3_node_lib", + testonly = 1, + srcs = [], + deps = [ + ":domino", + ":render3_lib", + ], +) + jasmine_node_test( name = "render3", bootstrap = [ diff --git a/packages/core/test/render3/ivy/BUILD.bazel b/packages/core/test/render3/ivy/BUILD.bazel new file mode 100644 index 0000000000..0a14e7d6d7 --- /dev/null +++ b/packages/core/test/render3/ivy/BUILD.bazel @@ -0,0 +1,41 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library", "ts_web_test") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") + +ts_library( + name = "ivy_lib", + testonly = 1, + srcs = glob(["**/*.ts"]), + deps = [ + "//packages:types", + "//packages/core", + ], +) + +ts_library( + name = "ivy_node_lib", + testonly = 1, + srcs = [], + deps = [ + ":ivy_lib", + "//packages/core/test/render3:domino", + ], +) + +jasmine_node_test( + name = "ivy", + bootstrap = [ + "angular/packages/core/test/render3/load_domino", + ], + deps = [ + ":ivy_node_lib", + ], +) + +ts_web_test( + name = "ivy_web", + deps = [ + ":ivy_lib", + ], +) diff --git a/packages/core/test/render3/ivy/jit_spec.ts b/packages/core/test/render3/ivy/jit_spec.ts new file mode 100644 index 0000000000..f09edbe042 --- /dev/null +++ b/packages/core/test/render3/ivy/jit_spec.ts @@ -0,0 +1,166 @@ +/** + * @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 {Injectable} from '@angular/core/src/di/injectable'; +import {inject, setCurrentInjector} from '@angular/core/src/di/injector'; +import {ivyEnabled} from '@angular/core/src/ivy_switch'; +import {Component} from '@angular/core/src/metadata/directives'; +import {NgModule, NgModuleDef} from '@angular/core/src/metadata/ng_module'; +import {ComponentDef} from '@angular/core/src/render3/interfaces/definition'; + +ivyEnabled && describe('render3 jit', () => { + let injector: any; + beforeAll(() => { injector = setCurrentInjector(null); }); + + afterAll(() => { setCurrentInjector(injector); }); + + it('compiles a component', () => { + @Component({ + template: 'test', + selector: 'test-cmp', + }) + class SomeCmp { + } + const SomeCmpAny = SomeCmp as any; + + expect(SomeCmpAny.ngComponentDef).toBeDefined(); + expect(SomeCmpAny.ngComponentDef.factory() instanceof SomeCmp).toBe(true); + }); + + it('compiles an injectable with a type provider', () => { + @Injectable({providedIn: 'root'}) + class Service { + } + const ServiceAny = Service as any; + + expect(ServiceAny.ngInjectableDef).toBeDefined(); + expect(ServiceAny.ngInjectableDef.providedIn).toBe('root'); + expect(inject(Service) instanceof Service).toBe(true); + }); + + it('compiles an injectable with a useValue provider', () => { + @Injectable({providedIn: 'root', useValue: 'test'}) + class Service { + } + + expect(inject(Service)).toBe('test'); + }); + + it('compiles an injectable with a useExisting provider', () => { + @Injectable({providedIn: 'root', useValue: 'test'}) + class Existing { + } + + @Injectable({providedIn: 'root', useExisting: Existing}) + class Service { + } + + expect(inject(Service)).toBe('test'); + }); + + it('compiles an injectable with a useFactory provider, without deps', () => { + + @Injectable({providedIn: 'root', useFactory: () => 'test'}) + class Service { + } + + expect(inject(Service)).toBe('test'); + }); + + it('compiles an injectable with a useFactory provider, with deps', () => { + @Injectable({providedIn: 'root', useValue: 'test'}) + class Existing { + } + + @Injectable({providedIn: 'root', useFactory: (existing: any) => existing, deps: [Existing]}) + class Service { + } + + expect(inject(Service)).toBe('test'); + }); + + it('compiles an injectable with a useClass provider, with deps', () => { + @Injectable({providedIn: 'root', useValue: 'test'}) + class Existing { + } + + class Other { + constructor(public value: any) {} + } + + @Injectable({providedIn: 'root', useClass: Other, deps: [Existing]}) + class Service { + get value(): any { return null; } + } + const ServiceAny = Service as any; + + expect(inject(Service).value).toBe('test'); + }); + + it('compiles an injectable with a useClass provider, without deps', () => { + let _value = 1; + @Injectable({providedIn: 'root'}) + class Existing { + readonly value = _value++; + } + + @Injectable({providedIn: 'root', useClass: Existing}) + class Service { + get value(): number { return 0; } + } + + expect(inject(Existing).value).toBe(1); + const injected = inject(Service); + expect(injected instanceof Existing).toBe(true); + expect(injected.value).toBe(2); + }); + + it('compiles a module to a definition', () => { + @Component({ + template: 'foo', + selector: 'foo', + }) + class Cmp { + } + + @NgModule({ + declarations: [Cmp], + }) + class Module { + } + + const moduleDef: NgModuleDef = (Module as any).ngModuleDef; + expect(moduleDef).toBeDefined(); + expect(moduleDef.declarations.length).toBe(1); + expect(moduleDef.declarations[0]).toBe(Cmp); + }); + + it('patches a module onto the component', () => { + @Component({ + template: 'foo', + selector: 'foo', + }) + class Cmp { + } + const cmpDef: ComponentDef = (Cmp as any).ngComponentDef; + + expect(cmpDef.directiveDefs).toBeNull(); + + @NgModule({ + declarations: [Cmp], + }) + class Module { + } + + const moduleDef: NgModuleDef = (Module as any).ngModuleDef; + expect(cmpDef.directiveDefs instanceof Function).toBe(true); + expect((cmpDef.directiveDefs as Function)()).toEqual([cmpDef]); + }); +}); + +it('ensure at least one spec exists', () => {});