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:
Paul Gschwendtner 2019-03-03 18:19:27 +01:00 committed by Kara Erickson
parent 7315a68ac6
commit 6085f335e8
8 changed files with 91 additions and 7 deletions

View File

@ -41,10 +41,15 @@ export interface CompilerFacade {
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
R3ResolvedDependencyType: typeof R3ResolvedDependencyType;
ResourceLoader: {new (): ResourceLoader};
}
export interface CoreEnvironment { [name: string]: Function; }
export type ResourceLoader = {
get(url: string): Promise<string>| string;
};
export type StringMap = {
[key: string]: string;
};

View File

@ -23,10 +23,12 @@ import {R3Reference} from './render3/util';
import {R3DirectiveMetadata, R3QueryMetadata} from './render3/view/api';
import {ParsedHostBindings, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
import {makeBindingParser, parseTemplate} from './render3/view/template';
import {ResourceLoader} from './resource_loader';
import {DomElementSchemaRegistry} from './schema/dom_element_schema_registry';
export class CompilerFacadeImpl implements CompilerFacade {
R3ResolvedDependencyType = R3ResolvedDependencyType as any;
ResourceLoader = ResourceLoader;
private elementSchemaRegistry = new DomElementSchemaRegistry();
constructor(private jitEvaluator = new JitEvaluator()) {}

View File

@ -36,6 +36,9 @@ const compilerCompilerFacade: compiler.CompilerFacade = null !as core.CompilerFa
const coreCoreEnvironment: core.CoreEnvironment = null !as compiler.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 compilerStringMap: compiler.StringMap = null !as core.StringMap;

View File

@ -11,15 +11,17 @@ import {share} from 'rxjs/operators';
import {ApplicationInitStatus} from './application_init';
import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens';
import {getCompilerFacade} from './compiler/compiler_facade';
import {Console} from './console';
import {Injectable, InjectionToken, Injector, StaticProvider} from './di';
import {ErrorHandler} from './error_handler';
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 {ComponentFactoryBoundToModule, ComponentFactoryResolver} from './linker/component_factory_resolver';
import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from './linker/ng_module_factory';
import {InternalViewRef, ViewRef} from './linker/view_ref';
import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from './metadata/resource_loading';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
import {assertNgModuleType} from './render3/assert';
import {ComponentFactory as R3ComponentFactory} from './render3/component_ref';
@ -49,7 +51,30 @@ export function compileNgModuleFactory__POST_R3__<M>(
injector: Injector, options: CompilerOptions,
moduleType: Type<M>): Promise<NgModuleFactory<M>> {
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__;
@ -671,3 +696,9 @@ function remove<T>(list: T[], el: T): void {
list.splice(index, 1);
}
}
function _mergeArrays(parts: any[][]): any[] {
const result: any[] = [];
parts.forEach((part) => part && result.push(...part));
return result;
}

View File

@ -41,10 +41,15 @@ export interface CompilerFacade {
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
R3ResolvedDependencyType: typeof R3ResolvedDependencyType;
ResourceLoader: {new (): ResourceLoader};
}
export interface CoreEnvironment { [name: string]: Function; }
export type ResourceLoader = {
get(url: string): Promise<string>| string;
};
export type StringMap = {
[key: string]: string;
};

View File

@ -99,6 +99,10 @@ export function clearResolutionOfComponentResourcesQueue() {
componentResourceResolutionQueue.clear();
}
export function isComponentResourceResolutionQueueEmpty() {
return componentResourceResolutionQueue.size === 0;
}
function unwrapResponse(response: string | {text(): Promise<string>}): string|Promise<string> {
return typeof response == 'string' ? response : response.text();
}

View File

@ -6,6 +6,7 @@
* 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 {ApplicationRef} from '@angular/core/src/application_ref';
import {ErrorHandler} from '@angular/core/src/error_handler';
@ -41,7 +42,8 @@ class SomeComponent {
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(options: CreateModuleOptions): Type<any>;
@ -62,8 +64,8 @@ class SomeComponent {
@NgModule({
providers: [{provide: ErrorHandler, useValue: errorHandler}, options.providers || []],
imports: [platformModule],
declarations: [SomeComponent],
entryComponents: [SomeComponent],
declarations: [options.component || SomeComponent],
entryComponents: [options.component || SomeComponent],
bootstrap: options.bootstrap || []
})
class MyModule {
@ -303,6 +305,27 @@ class SomeComponent {
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', () => {

View File

@ -7,13 +7,14 @@
*/
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 {compileComponent} from '../../src/render3/jit/directive';
describe('resource_loading', () => {
afterEach(clearResolutionOfComponentResourcesQueue);
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'});
@ -111,6 +112,16 @@ Did you run and wait for 'resolveComponentResources()'?`.trim());
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', () => {