feat(ivy): JIT renders the TODO app (#24138)

This commit builds out enough of the JIT compiler to render
//packages/core/test/bundling/todo, and allows the tests to run in
JIT mode.

To play with the app, run:

bazel run --define=compile=jit //packages/core/test/bundling/todo:prodserver

PR Close #24138
This commit is contained in:
Alex Rickabaugh
2018-05-21 08:15:19 -07:00
committed by Victor Berchet
parent 24e5c5b425
commit 646b42a113
22 changed files with 348 additions and 194 deletions

View File

@ -8,5 +8,6 @@
export const ivyEnabled = false;
export const R3_COMPILE_COMPONENT: ((type: any, meta: any) => void)|null = null;
export const R3_COMPILE_DIRECTIVE: ((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;

View File

@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileComponentDecorator} from './render3/jit/directive';
import {compileComponentDecorator, compileDirective} 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_DIRECTIVE = compileDirective;
export const R3_COMPILE_INJECTABLE = compileInjectable;
export const R3_COMPILE_NGMODULE = compileNgModule;

View File

@ -8,7 +8,7 @@
import {ChangeDetectionStrategy} from '../change_detection/constants';
import {Provider} from '../di';
import {R3_COMPILE_COMPONENT} from '../ivy_switch';
import {R3_COMPILE_COMPONENT, R3_COMPILE_DIRECTIVE} from '../ivy_switch';
import {Type} from '../type';
import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators';
import {ViewEncapsulation} from './view';
@ -400,8 +400,9 @@ export interface Directive {
*
* @Annotation
*/
export const Directive: DirectiveDecorator =
makeDecorator('Directive', (dir: Directive = {}) => dir);
export const Directive: DirectiveDecorator = makeDecorator(
'Directive', (dir: Directive = {}) => dir, undefined, undefined,
(type: Type<any>, meta: Directive) => (R3_COMPILE_DIRECTIVE || (() => {}))(type, meta));
/**
* Type of the Component decorator / constructor function.

View File

@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileComponent as compileIvyComponent, parseTemplate, ConstantPool, makeBindingParser, WrappedNodeExpr, jitPatchDefinition,} from '@angular/compiler';
import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileIvyComponent, compileDirectiveFromMetadata as compileIvyDirective, jitExpression, makeBindingParser, parseTemplate} from '@angular/compiler';
import {Component} from '../../metadata/directives';
import {Component, Directive, HostBinding, Input, Output} from '../../metadata/directives';
import {ReflectionCapabilities} from '../../reflection/reflection_capabilities';
import {Type} from '../../type';
import {angularCoreEnv} from './environment';
import {reflectDependencies} from './util';
import {getReflect, reflectDependencies} from './util';
let _pendingPromises: Promise<void>[] = [];
@ -31,45 +31,65 @@ export function compileComponent(type: Type<any>, metadata: Component): Promise<
if (!metadata.template) {
throw new Error('templateUrl not yet supported');
}
const templateStr = metadata.template;
// 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}`);
}
let def: any = null;
Object.defineProperty(type, 'ngComponentDef', {
get: () => {
if (def === null) {
// The ConstantPool is a requirement of the JIT'er.
const constantPool = new ConstantPool();
// The ConstantPool is a requirement of the JIT'er.
const constantPool = new ConstantPool();
// Parse the template and check for errors.
const template = parseTemplate(templateStr, `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}`);
}
// 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());
// Compile the component metadata, including template, into an expression.
// TODO(alxhub): implement inputs, outputs, queries, etc.
const res = compileIvyComponent(
{
...directiveMetadata(type, metadata),
template,
directives: new Map(),
pipes: new Map(),
viewQueries: [],
},
constantPool, makeBindingParser());
// Patch the generated expression as ngComponentDef on the type.
jitPatchDefinition(type, 'ngComponentDef', res.expression, angularCoreEnv, constantPool);
def = jitExpression(
res.expression, angularCoreEnv, `ng://${type.name}/ngComponentDef.js`, constantPool);
}
return def;
},
});
return null;
}
/**
* Compile an Angular directive according to its decorator metadata, and patch the resulting
* ngDirectiveDef onto the component type.
*
* In the event that compilation is not immediate, `compileDirective` will return a `Promise` which
* will resolve when compilation completes and the directive becomes usable.
*/
export function compileDirective(type: Type<any>, directive: Directive): Promise<void>|null {
let def: any = null;
Object.defineProperty(type, 'ngDirectiveDef', {
get: () => {
if (def === null) {
const constantPool = new ConstantPool();
const sourceMapUrl = `ng://${type && type.name}/ngDirectiveDef.js`;
const res = compileIvyDirective(
directiveMetadata(type, directive), constantPool, makeBindingParser());
def = jitExpression(res.expression, angularCoreEnv, sourceMapUrl, constantPool);
}
return def;
},
});
return null;
}
@ -95,3 +115,51 @@ export function awaitCurrentlyCompilingComponents(): Promise<void> {
_pendingPromises = [];
return res;
}
/**
* Extract the `R3DirectiveMetadata` for a particular directive (either a `Directive` or a
* `Component`).
*/
function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMetadata {
// Reflect inputs and outputs.
const props = getReflect().propMetadata(type);
const inputs: {[key: string]: string} = {};
const outputs: {[key: string]: string} = {};
for (let field in props) {
props[field].forEach(ann => {
if (isInput(ann)) {
inputs[field] = ann.bindingPropertyName || field;
} else if (isOutput(ann)) {
outputs[field] = ann.bindingPropertyName || field;
}
});
}
return {
name: type.name,
type: new WrappedNodeExpr(type),
selector: metadata.selector !,
deps: reflectDependencies(type),
host: {
attributes: {},
listeners: {},
properties: {},
},
inputs,
outputs,
queries: [],
lifecycle: {
usesOnChanges: type.prototype.ngOnChanges !== undefined,
},
typeSourceSpan: null !,
};
}
function isInput(value: any): value is Input {
return value.ngMetadataName === 'Input';
}
function isOutput(value: any): value is Output {
return value.ngMetadataName === 'Output';
}

View File

@ -17,15 +17,37 @@ import * as r3 from '../index';
*
* This should be kept up to date with the public exports of @angular/core.
*/
export const angularCoreEnv = {
export const angularCoreEnv: {[name: string]: Function} = {
'ɵdefineComponent': r3.defineComponent,
'ɵdefineDirective': r3.defineDirective,
'defineInjectable': defineInjectable,
'ɵdefineNgModule': defineNgModule,
'ɵdirectiveInject': r3.directiveInject,
'inject': inject,
'ɵinjectAttribute': r3.injectAttribute,
'ɵinjectChangeDetectorRef': r3.injectChangeDetectorRef,
'ɵinjectElementRef': r3.injectElementRef,
'ɵinjectTemplateRef': r3.injectTemplateRef,
'ɵinjectViewContainerRef': r3.injectViewContainerRef,
'ɵNgOnChangesFeature': r3.NgOnChangesFeature,
'ɵa': r3.a,
'ɵb': r3.b,
'ɵC': r3.C,
'ɵcR': r3.cR,
'ɵcr': r3.cr,
'ɵd': r3.d,
'ɵE': r3.E,
'ɵe': r3.e,
'ɵf0': r3.f0,
'ɵf1': r3.f1,
'ɵf2': r3.f2,
'ɵf3': r3.f3,
'ɵf4': r3.f4,
'ɵf5': r3.f5,
'ɵf6': r3.f6,
'ɵf7': r3.f7,
'ɵf8': r3.f8,
'ɵfV': r3.fV,
'ɵi1': r3.i1,
'ɵi2': r3.i2,
'ɵi3': r3.i3,
@ -34,6 +56,23 @@ export const angularCoreEnv = {
'ɵi6': r3.i6,
'ɵi7': r3.i7,
'ɵi8': r3.i8,
'ɵk': r3.k,
'ɵkn': r3.kn,
'ɵL': r3.L,
'ɵld': r3.ld,
'ɵp': r3.p,
'ɵpb1': r3.pb1,
'ɵpb2': r3.pb2,
'ɵpb3': r3.pb3,
'ɵpb4': r3.pb4,
'ɵpbV': r3.pbV,
'ɵQ': r3.Q,
'ɵqR': r3.qR,
'ɵs': r3.s,
'ɵsn': r3.sn,
'ɵst': r3.st,
'ɵT': r3.T,
'ɵt': r3.t,
'ɵV': r3.V,
'ɵv': r3.v,
};

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Expression, LiteralExpr, R3DependencyMetadata, WrappedNodeExpr, compileInjectable as compileIvyInjectable, jitPatchDefinition} from '@angular/compiler';
import {Expression, LiteralExpr, R3DependencyMetadata, WrappedNodeExpr, compileInjectable as compileIvyInjectable, jitExpression} from '@angular/compiler';
import {Injectable} from '../../di/injectable';
import {ClassSansProvider, ExistingSansProvider, FactorySansProvider, StaticClassSansProvider, ValueProvider, ValueSansProvider} from '../../di/provider';
@ -27,60 +27,69 @@ export function compileInjectable(type: Type<any>, meta?: Injectable): void {
return;
}
// Check whether the injectable metadata includes a provider specification.
const hasAProvider = isUseClassProvider(meta) || isUseFactoryProvider(meta) ||
isUseValueProvider(meta) || isUseExistingProvider(meta);
let def: any = null;
Object.defineProperty(type, 'ngInjectableDef', {
get: () => {
if (def === null) {
// 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) || [];
}
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;
// 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.`);
}
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,
const {expression} = compileIvyInjectable({
name: type.name,
type: new WrappedNodeExpr(type),
providedIn: computeProvidedIn(meta.providedIn),
useClass,
useFactory,
useValue,
useExisting,
deps,
});
def = jitExpression(expression, angularCoreEnv, `ng://${type.name}/ngInjectableDef.js`);
}
return def;
},
});
jitPatchDefinition(type, 'ngInjectableDef', expression, angularCoreEnv);
}
function computeProvidedIn(providedIn: Type<any>| string | null | undefined): Expression {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Expression, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule as compileIvyNgModule, jitPatchDefinition} from '@angular/compiler';
import {Expression, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule as compileIvyNgModule, jitExpression} from '@angular/compiler';
import {ModuleWithProviders, NgModule, NgModuleDef} from '../../metadata/ng_module';
import {Type} from '../../type';
@ -33,17 +33,6 @@ export function compileNgModule(type: Type<any>, ngModule: NgModule): void {
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<any>& {ngModuleDef: NgModuleDef<any>}): void {
module.ngModuleDef.exports.forEach((exp: any) => {
@ -60,16 +49,49 @@ export function compileNgModule(type: Type<any>, ngModule: NgModule): void {
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<any>).transitiveCompileScope = transitiveCompileScope;
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;
}
});
let def: any = null;
Object.defineProperty(type, 'ngModuleDef', {
get: () => {
if (def === null) {
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);
def = jitExpression(res.expression, angularCoreEnv, `ng://${type.name}/ngModuleDef.js`);
def.transitiveCompileScope = transitiveCompileScope;
}
return def;
},
});
}
export function patchComponentWithScope<C, M>(
component: Type<C>& {ngComponentDef: ComponentDef<C>},
module: Type<M>& {ngModuleDef: NgModuleDef<M>}) {
component.ngComponentDef.directiveDefs = () =>
module.ngModuleDef.transitiveCompileScope !.directives.map(
dir => dir.ngDirectiveDef || dir.ngComponentDef);
module.ngModuleDef.transitiveCompileScope !.directives
.map(dir => dir.ngDirectiveDef || dir.ngComponentDef)
.filter(def => !!def);
component.ngComponentDef.pipeDefs = () =>
module.ngModuleDef.transitiveCompileScope !.pipes.map(pipe => pipe.ngPipeDef);
}

View File

@ -19,9 +19,12 @@ import {Type} from '../../type';
let _reflect: ReflectionCapabilities|null = null;
export function getReflect(): ReflectionCapabilities {
return (_reflect = _reflect || new ReflectionCapabilities());
}
export function reflectDependencies(type: Type<any>): R3DependencyMetadata[] {
_reflect = _reflect || new ReflectionCapabilities();
return convertDependencies(_reflect.parameters(type));
return convertDependencies(getReflect().parameters(type));
}
export function convertDependencies(deps: any[]): R3DependencyMetadata[] {

View File

@ -20,7 +20,10 @@ const __window = typeof window !== 'undefined' && window;
const __self = typeof self !== 'undefined' && typeof WorkerGlobalScope !== 'undefined' &&
self instanceof WorkerGlobalScope && self;
const __global = typeof global !== 'undefined' && global;
const _global: {[name: string]: any} = __window || __global || __self;
// Check __global first, because in Node tests both __global and __window may be defined and _global
// should be __global in that case.
const _global: {[name: string]: any} = __global || __window || __self;
const promise: Promise<any> = Promise.resolve(0);
/**