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;
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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()) {}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in New Issue