feat(compiler): support sync runtime compile
Adds new abstraction `Compiler` with methods `compileComponentAsync` and `compileComponentSync`. This is in preparation of deprecating `ComponentResolver`. `compileComponentSync` is able to compile components synchronously given all components either have an inline template or they have been compiled before. Also changes `TestComponentBuilder.createSync` to take a `Type` and use the new `compileComponentSync` method. Also supports overriding the component metadata even if the component has already been compiled. Also fixes #7084 in a better way. BREAKING CHANGE: `TestComponentBuilder.createSync` now takes a component type and throws if not all templates are either inlined are compiled before via `createAsync`. Closes #9594
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
// Public API for compiler
|
||||
export {Compiler} from './linker/compiler';
|
||||
export {ComponentFactory, ComponentRef} from './linker/component_factory';
|
||||
export {ComponentFactoryResolver, NoComponentFactoryError} from './linker/component_factory_resolver';
|
||||
export {ComponentResolver} from './linker/component_resolver';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @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 {BaseException} from '../facade/exceptions';
|
||||
import {ConcreteType, Type, stringify} from '../facade/lang';
|
||||
|
||||
import {ComponentFactory} from './component_factory';
|
||||
|
||||
|
||||
/**
|
||||
* Low-level service for running the angular compiler duirng runtime
|
||||
* to create {@link ComponentFactory}s, which
|
||||
* can later be used to create and render a Component instance.
|
||||
* @stable
|
||||
*/
|
||||
export class Compiler {
|
||||
/**
|
||||
* Loads the template and styles of a component and returns the associated `ComponentFactory`.
|
||||
*/
|
||||
compileComponentAsync<T>(component: ConcreteType<T>): Promise<ComponentFactory<T>> {
|
||||
throw new BaseException(
|
||||
`Runtime compiler is not loaded. Tried to compile ${stringify(component)}`);
|
||||
}
|
||||
/**
|
||||
* Compiles the given component. All templates have to be either inline or compiled via
|
||||
* `compileComponentAsync` before.
|
||||
*/
|
||||
compileComponentSync<T>(component: ConcreteType<T>): ComponentFactory<T> {
|
||||
throw new BaseException(
|
||||
`Runtime compiler is not loaded. Tried to compile ${stringify(component)}`);
|
||||
}
|
||||
/**
|
||||
* Clears all caches
|
||||
*/
|
||||
clearCache(): void {}
|
||||
/**
|
||||
* Clears the cache for the given component.
|
||||
*/
|
||||
clearCacheFor(compType: Type) {}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {Inject, OpaqueToken, Optional, SkipSelf} from '../di';
|
||||
import {BaseException} from '../facade/exceptions';
|
||||
import {ClassWithConstructor, stringify} from '../facade/lang';
|
||||
import {ConcreteType, stringify} from '../facade/lang';
|
||||
|
||||
import {ComponentFactory} from './component_factory';
|
||||
|
||||
@@ -33,7 +33,7 @@ class _NullComponentFactoryResolver implements ComponentFactoryResolver {
|
||||
*/
|
||||
export abstract class ComponentFactoryResolver {
|
||||
static NULL: ComponentFactoryResolver = new _NullComponentFactoryResolver();
|
||||
abstract resolveComponentFactory<T>(component: ClassWithConstructor<T>): ComponentFactory<T>;
|
||||
abstract resolveComponentFactory<T>(component: ConcreteType<T>): ComponentFactory<T>;
|
||||
}
|
||||
|
||||
export class CodegenComponentFactoryResolver implements ComponentFactoryResolver {
|
||||
|
||||
@@ -19,7 +19,7 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities {
|
||||
|
||||
isReflectionEnabled(): boolean { return true; }
|
||||
|
||||
factory(t: ConcreteType): Function {
|
||||
factory(t: ConcreteType<any>): Function {
|
||||
switch (t.length) {
|
||||
case 0:
|
||||
return () => new t();
|
||||
|
||||
@@ -84,7 +84,7 @@ export interface TypeDecorator {
|
||||
/**
|
||||
* Generate a class from the definition and annotate it with {@link TypeDecorator#annotations}.
|
||||
*/
|
||||
Class(obj: ClassDefinition): ConcreteType;
|
||||
Class(obj: ClassDefinition): ConcreteType<any>;
|
||||
}
|
||||
|
||||
function extractAnnotation(annotation: any): any {
|
||||
@@ -219,7 +219,7 @@ function applyParams(fnOrArray: (Function | any[]), key: string): Function {
|
||||
* ```
|
||||
* @stable
|
||||
*/
|
||||
export function Class(clsDef: ClassDefinition): ConcreteType {
|
||||
export function Class(clsDef: ClassDefinition): ConcreteType<any> {
|
||||
var constructor = applyParams(
|
||||
clsDef.hasOwnProperty('constructor') ? clsDef.constructor : undefined, 'constructor');
|
||||
var proto = constructor.prototype;
|
||||
@@ -246,7 +246,7 @@ export function Class(clsDef: ClassDefinition): ConcreteType {
|
||||
(constructor as any /** TODO #9100 */)['overriddenName'] = `class${_nextClassId++}`;
|
||||
}
|
||||
|
||||
return <ConcreteType>constructor;
|
||||
return <ConcreteType<any>>constructor;
|
||||
}
|
||||
|
||||
var Reflect = global.Reflect;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {TestComponentBuilder} from '@angular/compiler/testing';
|
||||
import {ComponentFixture, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
|
||||
import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal';
|
||||
|
||||
import {isBlank, NumberWrapper,} from '../../src/facade/lang';
|
||||
import {isBlank, NumberWrapper, ConcreteType,} from '../../src/facade/lang';
|
||||
import {BaseException} from '../../src/facade/exceptions';
|
||||
import {StringMapWrapper} from '../../src/facade/collection';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
@@ -40,9 +40,9 @@ export function main() {
|
||||
var renderLog: RenderLog;
|
||||
var directiveLog: DirectiveLog;
|
||||
|
||||
function createCompFixture(
|
||||
template: string, compType: Type = TestComponent,
|
||||
_tcb: TestComponentBuilder = null): ComponentFixture<any> {
|
||||
function createCompFixture<T>(
|
||||
template: string, compType: ConcreteType<T> = <any>TestComponent,
|
||||
_tcb: TestComponentBuilder = null): ComponentFixture<T> {
|
||||
if (isBlank(_tcb)) {
|
||||
_tcb = tcb;
|
||||
}
|
||||
@@ -58,18 +58,19 @@ export function main() {
|
||||
return nodes.map(node => node.injector.get(dirType));
|
||||
}
|
||||
|
||||
function _bindSimpleProp(
|
||||
bindAttr: string, compType: Type = TestComponent): ComponentFixture<any> {
|
||||
function _bindSimpleProp<T>(
|
||||
bindAttr: string, compType: ConcreteType<T> = <any>TestComponent): ComponentFixture<T> {
|
||||
var template = `<div ${bindAttr}></div>`;
|
||||
return createCompFixture(template, compType);
|
||||
}
|
||||
|
||||
function _bindSimpleValue(
|
||||
expression: any, compType: Type = TestComponent): ComponentFixture<any> {
|
||||
function _bindSimpleValue<T>(
|
||||
expression: any, compType: ConcreteType<T> = <any>TestComponent): ComponentFixture<T> {
|
||||
return _bindSimpleProp(`[someProp]='${expression}'`, compType);
|
||||
}
|
||||
|
||||
function _bindAndCheckSimpleValue(expression: any, compType: Type = TestComponent): string[] {
|
||||
function _bindAndCheckSimpleValue<T>(
|
||||
expression: any, compType: ConcreteType<T> = <any>TestComponent): string[] {
|
||||
var ctx = _bindSimpleValue(expression, compType);
|
||||
ctx.detectChanges(false);
|
||||
return renderLog.log;
|
||||
|
||||
@@ -1504,20 +1504,15 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should report a meaningful error when a directive is missing annotation',
|
||||
inject(
|
||||
[TestComponentBuilder, AsyncTestCompleter],
|
||||
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||
tcb = tcb.overrideView(
|
||||
MyComp,
|
||||
new ViewMetadata({template: '', directives: [SomeDirectiveMissingAnnotation]}));
|
||||
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
tcb = tcb.overrideView(
|
||||
MyComp,
|
||||
new ViewMetadata({template: '', directives: [SomeDirectiveMissingAnnotation]}));
|
||||
|
||||
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
|
||||
expect(e.message).toEqual(
|
||||
`No Directive annotation found on ${stringify(SomeDirectiveMissingAnnotation)}`);
|
||||
async.done();
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
expect(() => tcb.createAsync(MyComp))
|
||||
.toThrowError(
|
||||
`No Directive annotation found on ${stringify(SomeDirectiveMissingAnnotation)}`);
|
||||
}));
|
||||
|
||||
it('should report a meaningful error when a component is missing view annotation',
|
||||
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
@@ -1530,19 +1525,13 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
}));
|
||||
|
||||
it('should report a meaningful error when a directive is null',
|
||||
inject(
|
||||
[TestComponentBuilder, AsyncTestCompleter],
|
||||
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||
tcb =
|
||||
tcb.overrideView(MyComp, new ViewMetadata({directives: [[null]], template: ''}));
|
||||
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
tcb = tcb.overrideView(MyComp, new ViewMetadata({directives: [[null]], template: ''}));
|
||||
|
||||
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
|
||||
expect(e.message).toEqual(
|
||||
`Unexpected directive value 'null' on the View of component '${stringify(MyComp)}'`);
|
||||
async.done();
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
expect(() => tcb.createAsync(MyComp))
|
||||
.toThrowError(
|
||||
`Unexpected directive value 'null' on the View of component '${stringify(MyComp)}'`);
|
||||
}));
|
||||
|
||||
it('should provide an error context when an error happens in DI',
|
||||
inject(
|
||||
@@ -1642,22 +1631,17 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
|
||||
if (!IS_DART) {
|
||||
it('should report a meaningful error when a directive is undefined',
|
||||
inject(
|
||||
[TestComponentBuilder, AsyncTestCompleter],
|
||||
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
|
||||
var undefinedValue: any = void(0);
|
||||
var undefinedValue: any = void(0);
|
||||
|
||||
tcb = tcb.overrideView(
|
||||
MyComp, new ViewMetadata({directives: [undefinedValue], template: ''}));
|
||||
tcb = tcb.overrideView(
|
||||
MyComp, new ViewMetadata({directives: [undefinedValue], template: ''}));
|
||||
|
||||
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
|
||||
expect(e.message).toEqual(
|
||||
`Unexpected directive value 'undefined' on the View of component '${stringify(MyComp)}'`);
|
||||
async.done();
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
expect(() => tcb.createAsync(MyComp))
|
||||
.toThrowError(
|
||||
`Unexpected directive value 'undefined' on the View of component '${stringify(MyComp)}'`);
|
||||
}));
|
||||
}
|
||||
|
||||
it('should specify a location of an error that happened during change detection (text)',
|
||||
|
||||
@@ -261,18 +261,14 @@ export function main() {
|
||||
}));
|
||||
|
||||
it('should throw with descriptive error when query selectors are not present',
|
||||
inject(
|
||||
[TestComponentBuilder, AsyncTestCompleter],
|
||||
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||
tcb.overrideTemplate(
|
||||
MyCompBroken0, '<has-null-query-condition></has-null-query-condition>')
|
||||
.createAsync(MyCompBroken0)
|
||||
.catch((e) => {
|
||||
expect(e.message).toEqual(
|
||||
`Can't construct a query for the property "errorTrigger" of "${stringify(HasNullQueryCondition)}" since the query selector wasn't defined.`);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
expect(
|
||||
() => tcb.overrideTemplate(
|
||||
MyCompBroken0, '<has-null-query-condition></has-null-query-condition>')
|
||||
.createAsync(MyCompBroken0))
|
||||
.toThrowError(
|
||||
`Can't construct a query for the property "errorTrigger" of "${stringify(HasNullQueryCondition)}" since the query selector wasn't defined.`);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('query for TemplateRef', () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {AsyncTestCompleter} from '@angular/core/testing/testing_internal';
|
||||
|
||||
import {IS_DART} from '../../src/facade/lang';
|
||||
|
||||
import {Component, Pipe, PipeTransform, provide, ViewMetadata, OpaqueToken, Injector} from '@angular/core';
|
||||
import {Component, Pipe, PipeTransform, provide, ViewMetadata, PLATFORM_PIPES, OpaqueToken, Injector, forwardRef} from '@angular/core';
|
||||
import {NgIf, NgClass} from '@angular/common';
|
||||
import {CompilerConfig} from '@angular/compiler';
|
||||
|
||||
@@ -170,8 +170,7 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
|
||||
it('should support ngClass before a component and content projection inside of an ngIf',
|
||||
inject(
|
||||
[TestComponentBuilder, AsyncTestCompleter],
|
||||
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||
[TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: any) => {
|
||||
tcb.overrideView(
|
||||
MyComp1, new ViewMetadata({
|
||||
template: `A<cmp-content *ngIf="true" [ngClass]="'red'">B</cmp-content>C`,
|
||||
@@ -185,6 +184,15 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should handle mutual recursion entered from multiple sides - #7084',
|
||||
inject(
|
||||
[TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: any) => {
|
||||
tcb.createAsync(FakeRecursiveComp).then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement).toHaveText('[]');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
}
|
||||
@@ -207,3 +215,37 @@ class CustomPipe implements PipeTransform {
|
||||
@Component({selector: 'cmp-content', template: `<ng-content></ng-content>`})
|
||||
class CmpWithNgContent {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'left',
|
||||
template: `L<right *ngIf="false"></right>`,
|
||||
directives: [
|
||||
NgIf,
|
||||
forwardRef(() => RightComp),
|
||||
]
|
||||
})
|
||||
class LeftComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'right',
|
||||
template: `R<left *ngIf="false"></left>`,
|
||||
directives: [
|
||||
NgIf,
|
||||
forwardRef(() => LeftComp),
|
||||
]
|
||||
})
|
||||
class RightComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'fakeRecursiveComp',
|
||||
template: `[<left *ngIf="false"></left><right *ngIf="false"></right>]`,
|
||||
directives: [
|
||||
NgIf,
|
||||
forwardRef(() => LeftComp),
|
||||
forwardRef(() => RightComp),
|
||||
]
|
||||
})
|
||||
export class FakeRecursiveComp {
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, beforeEachProviders, inject,} from '@angular/core/testing/testing_internal';
|
||||
import {fakeAsync, flushMicrotasks, tick, ComponentFixture} from '@angular/core/testing';
|
||||
import {TestComponentBuilder} from '@angular/compiler/testing';
|
||||
import {isBlank} from '../../src/facade/lang';
|
||||
import {isBlank, ConcreteType} from '../../src/facade/lang';
|
||||
import {Type, ViewContainerRef, TemplateRef, ElementRef, ChangeDetectorRef, ChangeDetectionStrategy, Directive, Component, DebugElement, forwardRef, Input, PipeTransform, Attribute, ViewMetadata, provide, Optional, Inject, Self, InjectMetadata, Pipe, Host, SkipSelfMetadata} from '@angular/core';
|
||||
import {NgIf, NgFor} from '@angular/common';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
@@ -242,10 +242,11 @@ class TestComp {
|
||||
export function main() {
|
||||
var tcb: TestComponentBuilder;
|
||||
|
||||
function createCompFixture(
|
||||
template: string, tcb: TestComponentBuilder, comp: Type = null): ComponentFixture<any> {
|
||||
function createCompFixture<T>(
|
||||
template: string, tcb: TestComponentBuilder,
|
||||
comp: ConcreteType<T> = null): ComponentFixture<T> {
|
||||
if (isBlank(comp)) {
|
||||
comp = TestComp;
|
||||
comp = <any>TestComp;
|
||||
}
|
||||
return tcb
|
||||
.overrideView(
|
||||
@@ -255,7 +256,7 @@ export function main() {
|
||||
}
|
||||
|
||||
function createComp(
|
||||
template: string, tcb: TestComponentBuilder, comp: Type = null): DebugElement {
|
||||
template: string, tcb: TestComponentBuilder, comp: ConcreteType<any> = null): DebugElement {
|
||||
var fixture = createCompFixture(template, tcb, comp);
|
||||
fixture.detectChanges();
|
||||
return fixture.debugElement;
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AnimationEntryMetadata, ComponentFactory, ComponentResolver, Injectable, Injector, NgZone, OpaqueToken, ViewMetadata} from '../index';
|
||||
import {AnimationEntryMetadata, Compiler, ComponentFactory, Injectable, Injector, NgZone, OpaqueToken, ViewMetadata} from '../index';
|
||||
import {PromiseWrapper} from '../src/facade/async';
|
||||
import {IS_DART, Type, isPresent} from '../src/facade/lang';
|
||||
import {ConcreteType, IS_DART, Type, isPresent} from '../src/facade/lang';
|
||||
|
||||
import {ComponentFixture} from './component_fixture';
|
||||
import {tick} from './fake_async';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An abstract class for inserting the root test component element in a platform independent way.
|
||||
*
|
||||
@@ -118,20 +119,21 @@ export class TestComponentBuilder {
|
||||
/**
|
||||
* Builds and returns a ComponentFixture.
|
||||
*/
|
||||
createAsync(rootComponentType: Type): Promise<ComponentFixture<any>> {
|
||||
createAsync<T>(rootComponentType: ConcreteType<T>): Promise<ComponentFixture<T>> {
|
||||
let noNgZone = IS_DART || this._injector.get(ComponentFixtureNoNgZone, false);
|
||||
let ngZone: NgZone = noNgZone ? null : this._injector.get(NgZone, null);
|
||||
let compiler: Compiler = this._injector.get(Compiler);
|
||||
|
||||
let initComponent = () => {
|
||||
let promise: Promise<ComponentFactory<any>> =
|
||||
this._injector.get(ComponentResolver).resolveComponent(rootComponentType);
|
||||
compiler.compileComponentAsync(rootComponentType);
|
||||
return promise.then(componentFactory => this.createFromFactory(ngZone, componentFactory));
|
||||
};
|
||||
|
||||
return ngZone == null ? initComponent() : ngZone.run(initComponent);
|
||||
}
|
||||
|
||||
createFakeAsync(rootComponentType: Type): ComponentFixture<any> {
|
||||
createFakeAsync<T>(rootComponentType: ConcreteType<T>): ComponentFixture<T> {
|
||||
let result: any /** TODO #9100 */;
|
||||
let error: any /** TODO #9100 */;
|
||||
PromiseWrapper.then(
|
||||
@@ -144,15 +146,16 @@ export class TestComponentBuilder {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated createSync will be replaced with the ability to precompile components from within
|
||||
* the test.
|
||||
*/
|
||||
createSync<C>(componentFactory: ComponentFactory<C>): ComponentFixture<C> {
|
||||
createSync<T>(rootComponentType: ConcreteType<T>): ComponentFixture<T> {
|
||||
let noNgZone = IS_DART || this._injector.get(ComponentFixtureNoNgZone, false);
|
||||
let ngZone: NgZone = noNgZone ? null : this._injector.get(NgZone, null);
|
||||
let compiler: Compiler = this._injector.get(Compiler);
|
||||
|
||||
let initComponent = () => {
|
||||
return this.createFromFactory(
|
||||
ngZone, this._injector.get(Compiler).compileComponentSync(rootComponentType));
|
||||
};
|
||||
|
||||
let initComponent = () => this.createFromFactory(ngZone, componentFactory);
|
||||
return ngZone == null ? initComponent() : ngZone.run(initComponent);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user