feat(ivy): Support resource resolution in JIT mode. (#24637)

Used to resolve resource URLs on `@Component` when used with JIT compilation.

```
@Component({
  selector: 'my-comp',
  templateUrl: 'my-comp.html', // This requires asynchronous resolution
})
class MyComponnent{
}

// Calling `renderComponent` will fail because `MyComponent`'s `@Compenent.templateUrl`
// needs to be resolved because `renderComponent` is synchronous process.
// renderComponent(MyComponent);

// Calling `resolveComponentResources` will resolve `@Compenent.templateUrl` into
// `@Compenent.template`, which would allow `renderComponent` to proceed in synchronous manner.
// Use browser's `fetch` function as the default resource resolution strategy.
resolveComponentResources(fetch).then(() => {
  // After resolution all URLs have been converted into strings.
  renderComponent(MyComponent);
});

```

PR Close #24637
This commit is contained in:
Miško Hevery 2018-06-22 19:05:31 -07:00
parent 71100e6d72
commit 0ede987ced
5 changed files with 267 additions and 43 deletions

View File

@ -18,6 +18,7 @@ export {APP_ROOT as ɵAPP_ROOT} from './di/scope';
export {ivyEnabled as ɵivyEnabled} from './ivy_switch';
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';
export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver';
export {resolveComponentResources as ɵresolveComponentResources} from './metadata/resource_loading';
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types';
export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo} from './render/api';

View File

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

View File

@ -0,0 +1,103 @@
/**
* @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 {Component} from './directives';
/**
* Used to resolve resource URLs on `@Component` when used with JIT compilation.
*
* Example:
* ```
* @Component({
* selector: 'my-comp',
* templateUrl: 'my-comp.html', // This requires asynchronous resolution
* })
* class MyComponnent{
* }
*
* // Calling `renderComponent` will fail because `MyComponent`'s `@Compenent.templateUrl`
* // needs to be resolved because `renderComponent` is synchronous process.
* // renderComponent(MyComponent);
*
* // Calling `resolveComponentResources` will resolve `@Compenent.templateUrl` into
* // `@Compenent.template`, which would allow `renderComponent` to proceed in synchronous manner.
* // Use browser's `fetch` function as the default resource resolution strategy.
* resolveComponentResources(fetch).then(() => {
* // After resolution all URLs have been converted into strings.
* renderComponent(MyComponent);
* });
*
* ```
*
* NOTE: In AOT the resolution happens during compilation, and so there should be no need
* to call this method outside JIT mode.
*
* @param resourceResolver a function which is responsible to returning a `Promise` of the resolved
* URL. Browser's `fetch` method is a good default implementation.
*/
export function resolveComponentResources(
resourceResolver: (url: string) => (Promise<string|{text(): Promise<string>}>)): Promise<null> {
// Store all promises which are fetching the resources.
const urlFetches: Promise<string>[] = [];
// Cache so that we don't fetch the same resource more than once.
const urlMap = new Map<string, Promise<string>>();
function cachedResourceResolve(url: string): Promise<string> {
let promise = urlMap.get(url);
if (!promise) {
const resp = resourceResolver(url);
urlMap.set(url, promise = resp.then(unwrapResponse));
urlFetches.push(promise);
}
return promise;
}
componentResourceResolutionQueue.forEach((component: Component) => {
if (component.templateUrl) {
cachedResourceResolve(component.templateUrl).then((template) => {
component.template = template;
component.templateUrl = undefined;
});
}
const styleUrls = component.styleUrls;
const styles = component.styles || (component.styles = []);
const styleOffset = component.styles.length;
styleUrls && styleUrls.forEach((styleUrl, index) => {
styles.push(''); // pre-allocate array.
cachedResourceResolve(styleUrl).then((style) => {
styles[styleOffset + index] = style;
styleUrls.splice(styleUrls.indexOf(styleUrl), 1);
if (styleUrls.length == 0) {
component.styleUrls = undefined;
}
});
});
});
componentResourceResolutionQueue.clear();
return Promise.all(urlFetches).then(() => null);
}
const componentResourceResolutionQueue: Set<Component> = new Set();
export function maybeQueueResolutionOfComponentResources(metadata: Component) {
if (componentNeedsResolution(metadata)) {
componentResourceResolutionQueue.add(metadata);
}
}
export function componentNeedsResolution(component: Component) {
return component.templateUrl || component.styleUrls && component.styleUrls.length;
}
export function clearResolutionOfComponentResourcesQueue() {
componentResourceResolutionQueue.clear();
}
function unwrapResponse(response: string | {text(): Promise<string>}): string|Promise<string> {
return typeof response == 'string' ? response : response.text();
}

View File

@ -9,16 +9,16 @@
import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseHostBindings, parseTemplate} from '@angular/compiler';
import {Component, Directive, HostBinding, HostListener, Input, Output} from '../../metadata/directives';
import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
import {ReflectionCapabilities} from '../../reflection/reflection_capabilities';
import {Type} from '../../type';
import {stringify} from '../../util';
import {angularCoreEnv} from './environment';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from './fields';
import {patchComponentDefWithScope} from './module';
import {getReflect, reflectDependencies} from './util';
let _pendingPromises: Promise<void>[] = [];
type StringMap = {
[key: string]: string
};
@ -29,30 +29,39 @@ type StringMap = {
*
* 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.
* will enqueue resource resolution into a global queue and will fail to return the `ngComponentDef`
* until the global queue has been resolved with a call to `resolveComponentResources`.
*/
export function compileComponent(type: Type<any>, metadata: Component): Promise<void>|null {
// TODO(alxhub): implement ResourceLoader support for template compilation.
if (!metadata.template) {
throw new Error('templateUrl not yet supported');
}
const templateStr = metadata.template;
export function compileComponent(type: Type<any>, metadata: Component): void {
let def: any = null;
// Metadata may have resources which need to be resolved.
maybeQueueResolutionOfComponentResources(metadata);
Object.defineProperty(type, NG_COMPONENT_DEF, {
get: () => {
if (def === null) {
if (componentNeedsResolution(metadata)) {
const error = [`Component '${stringify(type)}' is not resolved:`];
if (metadata.templateUrl) {
error.push(` - templateUrl: ${stringify(metadata.templateUrl)}`);
}
if (metadata.styleUrls && metadata.styleUrls.length) {
error.push(` - styleUrls: ${JSON.stringify(metadata.styleUrls)}`);
}
error.push(`Did you run and wait for 'resolveComponentResources()'?`);
throw new Error(error.join('\n'));
}
// 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`, {
preserveWhitespaces: metadata.preserveWhitespaces || false,
});
const template =
parseTemplate(metadata.template !, `ng://${stringify(type)}/template.html`, {
preserveWhitespaces: metadata.preserveWhitespaces || false,
});
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}`);
throw new Error(
`Errors during JIT compilation of template for ${stringify(type)}: ${errors}`);
}
// Compile the component metadata, including template, into an expression.
@ -81,8 +90,6 @@ export function compileComponent(type: Type<any>, metadata: Component): Promise<
return def;
},
});
return null;
}
function hasSelectorScope<T>(component: Type<T>): component is Type<T>&
@ -97,7 +104,7 @@ function hasSelectorScope<T>(component: Type<T>): component is Type<T>&
* 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 {
export function compileDirective(type: Type<any>, directive: Directive): void {
let def: any = null;
Object.defineProperty(type, NG_DIRECTIVE_DEF, {
get: () => {
@ -111,31 +118,8 @@ export function compileDirective(type: Type<any>, directive: Directive): Promise
return def;
},
});
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<any>, 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<void> {
const res = Promise.all(_pendingPromises).then(() => undefined);
_pendingPromises = [];
return res;
}
/**
* Extract the `R3DirectiveMetadata` for a particular directive (either a `Directive` or a

View File

@ -0,0 +1,136 @@
/**
* @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 {jasmineAwait} from '@angular/core/testing';
import {Component} from '../../src/core';
import {clearResolutionOfComponentResourcesQueue, resolveComponentResources} from '../../src/metadata/resource_loading';
import {ComponentType} from '../../src/render3/interfaces/definition';
import {compileComponent} from '../../src/render3/jit/directive';
describe('resource_loading', () => {
describe('error handling', () => {
afterEach(clearResolutionOfComponentResourcesQueue);
it('should throw an error when compiling component that has unresolved templateUrl', () => {
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
compileComponent(MyComponent, {templateUrl: 'someUrl'});
expect(() => MyComponent.ngComponentDef).toThrowError(`
Component 'MyComponent' is not resolved:
- templateUrl: someUrl
Did you run and wait for 'resolveComponentResources()'?`.trim());
});
it('should throw an error when compiling component that has unresolved styleUrls', () => {
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
compileComponent(MyComponent, {styleUrls: ['someUrl1', 'someUrl2']});
expect(() => MyComponent.ngComponentDef).toThrowError(`
Component 'MyComponent' is not resolved:
- styleUrls: ["someUrl1","someUrl2"]
Did you run and wait for 'resolveComponentResources()'?`.trim());
});
it('should throw an error when compiling component that has unresolved templateUrl and styleUrls',
() => {
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
compileComponent(
MyComponent, {templateUrl: 'someUrl', styleUrls: ['someUrl1', 'someUrl2']});
expect(() => MyComponent.ngComponentDef).toThrowError(`
Component 'MyComponent' is not resolved:
- templateUrl: someUrl
- styleUrls: ["someUrl1","someUrl2"]
Did you run and wait for 'resolveComponentResources()'?`.trim());
});
});
describe('resolution', () => {
const URLS: {[url: string]: Promise<string>} = {
'test://content': Promise.resolve('content'),
'test://style1': Promise.resolve('style1'),
'test://style2': Promise.resolve('style2'),
};
let resourceFetchCount: number;
function testResolver(url: string): Promise<string> {
resourceFetchCount++;
return URLS[url] || Promise.reject('NOT_FOUND: ' + url);
}
beforeEach(() => resourceFetchCount = 0);
it('should resolve template', jasmineAwait(async() => {
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
const metadata: Component = {templateUrl: 'test://content'};
compileComponent(MyComponent, metadata);
await resolveComponentResources(testResolver);
expect(MyComponent.ngComponentDef).toBeDefined();
expect(metadata.templateUrl).toBe(undefined);
expect(metadata.template).toBe('content');
expect(resourceFetchCount).toBe(1);
}));
it('should resolve styleUrls', jasmineAwait(async() => {
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
const metadata: Component = {template: '', styleUrls: ['test://style1', 'test://style2']};
compileComponent(MyComponent, metadata);
await resolveComponentResources(testResolver);
expect(MyComponent.ngComponentDef).toBeDefined();
expect(metadata.styleUrls).toBe(undefined);
expect(metadata.styles).toEqual(['style1', 'style2']);
expect(resourceFetchCount).toBe(2);
}));
it('should cache multiple resolution to same URL', jasmineAwait(async() => {
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
const metadata: Component = {template: '', styleUrls: ['test://style1', 'test://style1']};
compileComponent(MyComponent, metadata);
await resolveComponentResources(testResolver);
expect(MyComponent.ngComponentDef).toBeDefined();
expect(metadata.styleUrls).toBe(undefined);
expect(metadata.styles).toEqual(['style1', 'style1']);
expect(resourceFetchCount).toBe(1);
}));
it('should keep order even if the resolution is out of order', jasmineAwait(async() => {
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
const metadata: Component = {
template: '',
styles: ['existing'],
styleUrls: ['test://style1', 'test://style2']
};
compileComponent(MyComponent, metadata);
const resolvers: any[] = [];
const resolved = resolveComponentResources(
(url) => new Promise((resolve, response) => resolvers.push(url, resolve)));
// Out of order resolution
expect(resolvers[0]).toEqual('test://style1');
expect(resolvers[2]).toEqual('test://style2');
resolvers[3]('second');
resolvers[1]('first');
await resolved;
expect(metadata.styleUrls).toBe(undefined);
expect(metadata.styles).toEqual(['existing', 'first', 'second']);
}));
});
describe('fetch', () => {
function fetch(url: string): Promise<Response> {
return Promise.resolve({
text() { return 'response for ' + url; }
} as any as Response);
}
it('should work with fetch', jasmineAwait(async() => {
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
const metadata: Component = {templateUrl: 'test://content'};
compileComponent(MyComponent, metadata);
await resolveComponentResources(fetch);
expect(MyComponent.ngComponentDef).toBeDefined();
expect(metadata.templateUrl).toBe(undefined);
expect(metadata.template).toBe('response for test://content');
}));
});
});