fix(ivy): platform module bootstrap does not resolve resources (#29083)
Currently with ViewEngine, if someone runs the platform's `bootstrapModule` method in order to boostrap a module in JIT mode, external component resources are properly resolved *automatically*. Currently with Ivy, the developer would need to manually call `resolveComponentResources` in order to asynchronously fetch the determined external component resources. In order to make this backwards compatible with ViewEngine, and also since platforms can already specify a `ResourceLoader` compiler provider, we need to automatically resolve all external component resources on module bootstrap. -- Since the `ResourceLoader` is part of the `@angular/compiler`, because ViewEngine performed the factory creation in the compiler, we can't access the `ResourceLoader` token from within core. In order to workaround this without introducing a breaking change, we just proxy the `ResourceLoader` token to `core` through the compiler facade. In the future, we should be able to move the `ResourceLoader` to core when ViewEngine code no longer exists in the `@angular/compiler`. PR Close #29083
This commit is contained in:
parent
7315a68ac6
commit
6085f335e8
@ -41,10 +41,15 @@ export interface CompilerFacade {
|
|||||||
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
|
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
|
||||||
|
|
||||||
R3ResolvedDependencyType: typeof R3ResolvedDependencyType;
|
R3ResolvedDependencyType: typeof R3ResolvedDependencyType;
|
||||||
|
ResourceLoader: {new (): ResourceLoader};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoreEnvironment { [name: string]: Function; }
|
export interface CoreEnvironment { [name: string]: Function; }
|
||||||
|
|
||||||
|
export type ResourceLoader = {
|
||||||
|
get(url: string): Promise<string>| string;
|
||||||
|
};
|
||||||
|
|
||||||
export type StringMap = {
|
export type StringMap = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
@ -23,10 +23,12 @@ import {R3Reference} from './render3/util';
|
|||||||
import {R3DirectiveMetadata, R3QueryMetadata} from './render3/view/api';
|
import {R3DirectiveMetadata, R3QueryMetadata} from './render3/view/api';
|
||||||
import {ParsedHostBindings, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
|
import {ParsedHostBindings, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
|
||||||
import {makeBindingParser, parseTemplate} from './render3/view/template';
|
import {makeBindingParser, parseTemplate} from './render3/view/template';
|
||||||
|
import {ResourceLoader} from './resource_loader';
|
||||||
import {DomElementSchemaRegistry} from './schema/dom_element_schema_registry';
|
import {DomElementSchemaRegistry} from './schema/dom_element_schema_registry';
|
||||||
|
|
||||||
export class CompilerFacadeImpl implements CompilerFacade {
|
export class CompilerFacadeImpl implements CompilerFacade {
|
||||||
R3ResolvedDependencyType = R3ResolvedDependencyType as any;
|
R3ResolvedDependencyType = R3ResolvedDependencyType as any;
|
||||||
|
ResourceLoader = ResourceLoader;
|
||||||
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
||||||
|
|
||||||
constructor(private jitEvaluator = new JitEvaluator()) {}
|
constructor(private jitEvaluator = new JitEvaluator()) {}
|
||||||
|
@ -36,6 +36,9 @@ const compilerCompilerFacade: compiler.CompilerFacade = null !as core.CompilerFa
|
|||||||
const coreCoreEnvironment: core.CoreEnvironment = null !as compiler.CoreEnvironment;
|
const coreCoreEnvironment: core.CoreEnvironment = null !as compiler.CoreEnvironment;
|
||||||
const compilerCoreEnvironment: compiler.CoreEnvironment = null !as core.CoreEnvironment;
|
const compilerCoreEnvironment: compiler.CoreEnvironment = null !as core.CoreEnvironment;
|
||||||
|
|
||||||
|
const coreResourceLoader: core.ResourceLoader = null !as compiler.ResourceLoader;
|
||||||
|
const compilerResourceLoader: compiler.ResourceLoader = null !as core.ResourceLoader;
|
||||||
|
|
||||||
const coreStringMap: core.StringMap = null !as compiler.StringMap;
|
const coreStringMap: core.StringMap = null !as compiler.StringMap;
|
||||||
const compilerStringMap: compiler.StringMap = null !as core.StringMap;
|
const compilerStringMap: compiler.StringMap = null !as core.StringMap;
|
||||||
|
|
||||||
|
@ -11,15 +11,17 @@ import {share} from 'rxjs/operators';
|
|||||||
|
|
||||||
import {ApplicationInitStatus} from './application_init';
|
import {ApplicationInitStatus} from './application_init';
|
||||||
import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens';
|
import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens';
|
||||||
|
import {getCompilerFacade} from './compiler/compiler_facade';
|
||||||
import {Console} from './console';
|
import {Console} from './console';
|
||||||
import {Injectable, InjectionToken, Injector, StaticProvider} from './di';
|
import {Injectable, InjectionToken, Injector, StaticProvider} from './di';
|
||||||
import {ErrorHandler} from './error_handler';
|
import {ErrorHandler} from './error_handler';
|
||||||
import {Type} from './interface/type';
|
import {Type} from './interface/type';
|
||||||
import {CompilerFactory, CompilerOptions} from './linker/compiler';
|
import {COMPILER_OPTIONS, CompilerFactory, CompilerOptions} from './linker/compiler';
|
||||||
import {ComponentFactory, ComponentRef} from './linker/component_factory';
|
import {ComponentFactory, ComponentRef} from './linker/component_factory';
|
||||||
import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from './linker/component_factory_resolver';
|
import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from './linker/component_factory_resolver';
|
||||||
import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from './linker/ng_module_factory';
|
import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from './linker/ng_module_factory';
|
||||||
import {InternalViewRef, ViewRef} from './linker/view_ref';
|
import {InternalViewRef, ViewRef} from './linker/view_ref';
|
||||||
|
import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from './metadata/resource_loading';
|
||||||
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
|
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
|
||||||
import {assertNgModuleType} from './render3/assert';
|
import {assertNgModuleType} from './render3/assert';
|
||||||
import {ComponentFactory as R3ComponentFactory} from './render3/component_ref';
|
import {ComponentFactory as R3ComponentFactory} from './render3/component_ref';
|
||||||
@ -49,7 +51,30 @@ export function compileNgModuleFactory__POST_R3__<M>(
|
|||||||
injector: Injector, options: CompilerOptions,
|
injector: Injector, options: CompilerOptions,
|
||||||
moduleType: Type<M>): Promise<NgModuleFactory<M>> {
|
moduleType: Type<M>): Promise<NgModuleFactory<M>> {
|
||||||
ngDevMode && assertNgModuleType(moduleType);
|
ngDevMode && assertNgModuleType(moduleType);
|
||||||
return Promise.resolve(new R3NgModuleFactory(moduleType));
|
const moduleFactory = new R3NgModuleFactory(moduleType);
|
||||||
|
|
||||||
|
if (isComponentResourceResolutionQueueEmpty()) {
|
||||||
|
return Promise.resolve(moduleFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compilerOptions = injector.get(COMPILER_OPTIONS, []).concat(options);
|
||||||
|
const compilerProviders = _mergeArrays(compilerOptions.map(o => o.providers !));
|
||||||
|
|
||||||
|
// In case there are no compiler providers, we just return the module factory as
|
||||||
|
// there won't be any resource loader. This can happen with Ivy, because AOT compiled
|
||||||
|
// modules can be still passed through "bootstrapModule". In that case we shouldn't
|
||||||
|
// unnecessarily require the JIT compiler.
|
||||||
|
if (compilerProviders.length === 0) {
|
||||||
|
return Promise.resolve(moduleFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compiler = getCompilerFacade();
|
||||||
|
const compilerInjector = Injector.create({providers: compilerProviders});
|
||||||
|
const resourceLoader = compilerInjector.get(compiler.ResourceLoader);
|
||||||
|
// The resource loader can also return a string while the "resolveComponentResources"
|
||||||
|
// always expects a promise. Therefore we need to wrap the returned value in a promise.
|
||||||
|
return resolveComponentResources(url => Promise.resolve(resourceLoader.get(url)))
|
||||||
|
.then(() => moduleFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
let isBoundToModule: <C>(cf: ComponentFactory<C>) => boolean = isBoundToModule__PRE_R3__;
|
let isBoundToModule: <C>(cf: ComponentFactory<C>) => boolean = isBoundToModule__PRE_R3__;
|
||||||
@ -671,3 +696,9 @@ function remove<T>(list: T[], el: T): void {
|
|||||||
list.splice(index, 1);
|
list.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _mergeArrays(parts: any[][]): any[] {
|
||||||
|
const result: any[] = [];
|
||||||
|
parts.forEach((part) => part && result.push(...part));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@ -41,10 +41,15 @@ export interface CompilerFacade {
|
|||||||
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
|
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
|
||||||
|
|
||||||
R3ResolvedDependencyType: typeof R3ResolvedDependencyType;
|
R3ResolvedDependencyType: typeof R3ResolvedDependencyType;
|
||||||
|
ResourceLoader: {new (): ResourceLoader};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoreEnvironment { [name: string]: Function; }
|
export interface CoreEnvironment { [name: string]: Function; }
|
||||||
|
|
||||||
|
export type ResourceLoader = {
|
||||||
|
get(url: string): Promise<string>| string;
|
||||||
|
};
|
||||||
|
|
||||||
export type StringMap = {
|
export type StringMap = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
@ -99,6 +99,10 @@ export function clearResolutionOfComponentResourcesQueue() {
|
|||||||
componentResourceResolutionQueue.clear();
|
componentResourceResolutionQueue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isComponentResourceResolutionQueueEmpty() {
|
||||||
|
return componentResourceResolutionQueue.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
function unwrapResponse(response: string | {text(): Promise<string>}): string|Promise<string> {
|
function unwrapResponse(response: string | {text(): Promise<string>}): string|Promise<string> {
|
||||||
return typeof response == 'string' ? response : response.text();
|
return typeof response == 'string' ? response : response.text();
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {ResourceLoader} from '@angular/compiler';
|
||||||
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, InjectionToken, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, InjectionToken, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
||||||
import {ApplicationRef} from '@angular/core/src/application_ref';
|
import {ApplicationRef} from '@angular/core/src/application_ref';
|
||||||
import {ErrorHandler} from '@angular/core/src/error_handler';
|
import {ErrorHandler} from '@angular/core/src/error_handler';
|
||||||
@ -41,7 +42,8 @@ class SomeComponent {
|
|||||||
getDOM().appendChild(doc.body, rootEl);
|
getDOM().appendChild(doc.body, rootEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateModuleOptions = {providers?: any[], ngDoBootstrap?: any, bootstrap?: any[]};
|
type CreateModuleOptions =
|
||||||
|
{providers?: any[], ngDoBootstrap?: any, bootstrap?: any[], component?: Type<any>};
|
||||||
|
|
||||||
function createModule(providers?: any[]): Type<any>;
|
function createModule(providers?: any[]): Type<any>;
|
||||||
function createModule(options: CreateModuleOptions): Type<any>;
|
function createModule(options: CreateModuleOptions): Type<any>;
|
||||||
@ -62,8 +64,8 @@ class SomeComponent {
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
providers: [{provide: ErrorHandler, useValue: errorHandler}, options.providers || []],
|
providers: [{provide: ErrorHandler, useValue: errorHandler}, options.providers || []],
|
||||||
imports: [platformModule],
|
imports: [platformModule],
|
||||||
declarations: [SomeComponent],
|
declarations: [options.component || SomeComponent],
|
||||||
entryComponents: [SomeComponent],
|
entryComponents: [options.component || SomeComponent],
|
||||||
bootstrap: options.bootstrap || []
|
bootstrap: options.bootstrap || []
|
||||||
})
|
})
|
||||||
class MyModule {
|
class MyModule {
|
||||||
@ -303,6 +305,27 @@ class SomeComponent {
|
|||||||
expect(ngZone instanceof NoopNgZone).toBe(true);
|
expect(ngZone instanceof NoopNgZone).toBe(true);
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should resolve component resources when creating module factory', async() => {
|
||||||
|
@Component({
|
||||||
|
selector: 'with-templates-app',
|
||||||
|
templateUrl: '/test-template.html',
|
||||||
|
})
|
||||||
|
class WithTemplateUrlComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadResourceSpy = jasmine.createSpy('load resource').and.returnValue('fakeContent');
|
||||||
|
const testModule = createModule({component: WithTemplateUrlComponent});
|
||||||
|
|
||||||
|
await defaultPlatform.bootstrapModule(testModule, {
|
||||||
|
providers: [
|
||||||
|
{provide: ResourceLoader, useValue: {get: loadResourceSpy}},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(loadResourceSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(loadResourceSpy).toHaveBeenCalledWith('/test-template.html');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('bootstrapModuleFactory', () => {
|
describe('bootstrapModuleFactory', () => {
|
||||||
|
@ -7,13 +7,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Component} from '../../src/core';
|
import {Component} from '../../src/core';
|
||||||
import {clearResolutionOfComponentResourcesQueue, resolveComponentResources} from '../../src/metadata/resource_loading';
|
import {clearResolutionOfComponentResourcesQueue, isComponentResourceResolutionQueueEmpty, resolveComponentResources} from '../../src/metadata/resource_loading';
|
||||||
import {ComponentType} from '../../src/render3/interfaces/definition';
|
import {ComponentType} from '../../src/render3/interfaces/definition';
|
||||||
import {compileComponent} from '../../src/render3/jit/directive';
|
import {compileComponent} from '../../src/render3/jit/directive';
|
||||||
|
|
||||||
describe('resource_loading', () => {
|
describe('resource_loading', () => {
|
||||||
|
afterEach(clearResolutionOfComponentResourcesQueue);
|
||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
afterEach(clearResolutionOfComponentResourcesQueue);
|
|
||||||
it('should throw an error when compiling component that has unresolved templateUrl', () => {
|
it('should throw an error when compiling component that has unresolved templateUrl', () => {
|
||||||
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
|
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
|
||||||
compileComponent(MyComponent, {templateUrl: 'someUrl'});
|
compileComponent(MyComponent, {templateUrl: 'someUrl'});
|
||||||
@ -111,6 +112,16 @@ Did you run and wait for 'resolveComponentResources()'?`.trim());
|
|||||||
expect(metadata.styles).toEqual(['existing', 'first', 'second']);
|
expect(metadata.styles).toEqual(['existing', 'first', 'second']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not add components without external resources to resolution queue', () => {
|
||||||
|
const MyComponent: ComponentType<any> = (class MyComponent{}) as any;
|
||||||
|
const MyComponent2: ComponentType<any> = (class MyComponent{}) as any;
|
||||||
|
|
||||||
|
compileComponent(MyComponent, {template: ''});
|
||||||
|
expect(isComponentResourceResolutionQueueEmpty()).toBe(true);
|
||||||
|
|
||||||
|
compileComponent(MyComponent2, {templateUrl: 'test://template'});
|
||||||
|
expect(isComponentResourceResolutionQueueEmpty()).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetch', () => {
|
describe('fetch', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user