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:
parent
71100e6d72
commit
0ede987ced
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
}));
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue