fix(core): adhere to bootstrap options for JIT compiled components (#35534)
When using `platformBrowserDynamic().bootstrapModule()`, it is possible to set `defaultEncapsulation` and `preserveWhitespaces` as default configuration to influence how components are compiled. When compiling components in JIT with Ivy, these options were not taken into account. This commit publishes the options to be globally available, so that the lazy compilation of JIT components has access to the configured bootstrap options. Note that this approach does not allow changing the options once they have been set, as Ivy's compilation model does not allow for multiple compilations to exist at the same time. For applications that bootstrap multiple modules, it is now required to provide the exact same bootstrap options. An error is logged if incompatible bootstrap options are provided, in which case the updated options will be ignored. Fixes #35230 Resolved FW-1838 PR Close #35534
This commit is contained in:
parent
c0143cb2ab
commit
e342ffd855
|
@ -6,6 +6,8 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import './util/ng_jit_mode';
|
||||
|
||||
import {Observable, Observer, Subscription, merge} from 'rxjs';
|
||||
import {share} from 'rxjs/operators';
|
||||
|
||||
|
@ -29,6 +31,7 @@ import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from
|
|||
import {assertNgModuleType} from './render3/assert';
|
||||
import {ComponentFactory as R3ComponentFactory} from './render3/component_ref';
|
||||
import {setLocaleId} from './render3/i18n';
|
||||
import {setJitOptions} from './render3/jit/jit_options';
|
||||
import {NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
|
||||
import {publishDefaultGlobalUtils as _publishDefaultGlobalUtils} from './render3/util/global_utils';
|
||||
import {Testability, TestabilityRegistry} from './testability/testability';
|
||||
|
@ -56,13 +59,27 @@ export function compileNgModuleFactory__POST_R3__<M>(
|
|||
injector: Injector, options: CompilerOptions,
|
||||
moduleType: Type<M>): Promise<NgModuleFactory<M>> {
|
||||
ngDevMode && assertNgModuleType(moduleType);
|
||||
|
||||
const compilerOptions = injector.get(COMPILER_OPTIONS, []).concat(options);
|
||||
|
||||
if (typeof ngJitMode === 'undefined' || ngJitMode) {
|
||||
// Configure the compiler to use the provided options. This call may fail when multiple modules
|
||||
// are bootstrapped with incompatible options, as a component can only be compiled according to
|
||||
// a single set of options.
|
||||
setJitOptions({
|
||||
defaultEncapsulation:
|
||||
_lastDefined(compilerOptions.map(options => options.defaultEncapsulation)),
|
||||
preserveWhitespaces:
|
||||
_lastDefined(compilerOptions.map(options => options.preserveWhitespaces)),
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -748,6 +765,15 @@ function remove<T>(list: T[], el: T): void {
|
|||
}
|
||||
}
|
||||
|
||||
function _lastDefined<T>(args: T[]): T|undefined {
|
||||
for (let i = args.length - 1; i >= 0; i--) {
|
||||
if (args[i] !== undefined) {
|
||||
return args[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function _mergeArrays(parts: any[][]): any[] {
|
||||
const result: any[] = [];
|
||||
parts.forEach((part) => part && result.push(...part));
|
||||
|
|
|
@ -197,6 +197,9 @@ export {
|
|||
export {
|
||||
compilePipe as ɵcompilePipe,
|
||||
} from './render3/jit/pipe';
|
||||
export {
|
||||
resetJitOptions as ɵresetJitOptions,
|
||||
} from './render3/jit/jit_options';
|
||||
|
||||
export {
|
||||
NgModuleDef as ɵNgModuleDef,
|
||||
|
|
|
@ -23,6 +23,7 @@ import {ComponentType} from '../interfaces/definition';
|
|||
import {stringifyForError} from '../util/misc_utils';
|
||||
|
||||
import {angularCoreEnv} from './environment';
|
||||
import {getJitOptions} from './jit_options';
|
||||
import {flushModuleScopingQueueAsMuchAsPossible, patchComponentDefWithScope, transitiveScopesFor} from './module';
|
||||
|
||||
|
||||
|
@ -68,18 +69,34 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
|
|||
throw new Error(error.join('\n'));
|
||||
}
|
||||
|
||||
const jitOptions = getJitOptions();
|
||||
let preserveWhitespaces = metadata.preserveWhitespaces;
|
||||
if (preserveWhitespaces === undefined) {
|
||||
if (jitOptions !== null && jitOptions.preserveWhitespaces !== undefined) {
|
||||
preserveWhitespaces = jitOptions.preserveWhitespaces;
|
||||
} else {
|
||||
preserveWhitespaces = false;
|
||||
}
|
||||
}
|
||||
let encapsulation = metadata.encapsulation;
|
||||
if (encapsulation === undefined) {
|
||||
if (jitOptions !== null && jitOptions.defaultEncapsulation !== undefined) {
|
||||
encapsulation = jitOptions.defaultEncapsulation;
|
||||
} else {
|
||||
encapsulation = ViewEncapsulation.Emulated;
|
||||
}
|
||||
}
|
||||
|
||||
const templateUrl = metadata.templateUrl || `ng:///${type.name}/template.html`;
|
||||
const meta: R3ComponentMetadataFacade = {
|
||||
...directiveMetadata(type, metadata),
|
||||
typeSourceSpan: compiler.createParseSourceSpan('Component', type.name, templateUrl),
|
||||
template: metadata.template || '',
|
||||
preserveWhitespaces: metadata.preserveWhitespaces || false,
|
||||
template: metadata.template || '', preserveWhitespaces,
|
||||
styles: metadata.styles || EMPTY_ARRAY,
|
||||
animations: metadata.animations,
|
||||
directives: [],
|
||||
changeDetection: metadata.changeDetection,
|
||||
pipes: new Map(),
|
||||
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated,
|
||||
pipes: new Map(), encapsulation,
|
||||
interpolation: metadata.interpolation,
|
||||
viewProviders: metadata.viewProviders || null,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @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 {ViewEncapsulation} from '../../metadata/view';
|
||||
|
||||
export interface JitCompilerOptions {
|
||||
defaultEncapsulation?: ViewEncapsulation;
|
||||
preserveWhitespaces?: boolean;
|
||||
}
|
||||
|
||||
let jitOptions: JitCompilerOptions|null = null;
|
||||
|
||||
export function setJitOptions(options: JitCompilerOptions): void {
|
||||
if (jitOptions !== null) {
|
||||
if (options.defaultEncapsulation !== jitOptions.defaultEncapsulation) {
|
||||
ngDevMode &&
|
||||
console.error(
|
||||
'Provided value for `defaultEncapsulation` can not be changed once it has been set.');
|
||||
return;
|
||||
}
|
||||
if (options.preserveWhitespaces !== jitOptions.preserveWhitespaces) {
|
||||
ngDevMode &&
|
||||
console.error(
|
||||
'Provided value for `preserveWhitespaces` can not be changed once it has been set.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
jitOptions = options;
|
||||
}
|
||||
|
||||
export function getJitOptions(): JitCompilerOptions|null {
|
||||
return jitOptions;
|
||||
}
|
||||
|
||||
export function resetJitOptions(): void {
|
||||
jitOptions = null;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
declare global {
|
||||
const ngJitMode: boolean;
|
||||
}
|
||||
|
||||
// Make this an ES module to be able to augment the global scope
|
||||
export {};
|
|
@ -6,12 +6,16 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
import {COMPILER_OPTIONS, Component, NgModule, ViewEncapsulation, destroyPlatform} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
import {withBody} from '@angular/private/testing';
|
||||
import {onlyInIvy, withBody} from '@angular/private/testing';
|
||||
|
||||
describe('bootstrap', () => {
|
||||
|
||||
beforeEach(destroyPlatform);
|
||||
afterEach(destroyPlatform);
|
||||
|
||||
it('should bootstrap using #id selector',
|
||||
withBody('<div>before|</div><button id="my-app"></button>', async() => {
|
||||
try {
|
||||
|
@ -34,6 +38,219 @@ describe('bootstrap', () => {
|
|||
console.error(err);
|
||||
}
|
||||
}));
|
||||
|
||||
describe('options', () => {
|
||||
function createComponentAndModule(
|
||||
options: {encapsulation?: ViewEncapsulation; preserveWhitespaces?: boolean} = {}) {
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
styles: [''],
|
||||
template: '<span>a b</span>',
|
||||
encapsulation: options.encapsulation,
|
||||
preserveWhitespaces: options.preserveWhitespaces,
|
||||
jit: true,
|
||||
})
|
||||
class TestComponent {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule],
|
||||
declarations: [TestComponent],
|
||||
bootstrap: [TestComponent],
|
||||
jit: true,
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
|
||||
return TestModule;
|
||||
}
|
||||
|
||||
it('should use ViewEncapsulation.Emulated as default',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
|
||||
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
|
||||
expect(document.body.innerHTML).toContain('<span _ngcontent-');
|
||||
ngModuleRef.destroy();
|
||||
}));
|
||||
|
||||
it('should allow setting defaultEncapsulation using bootstrap option',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
|
||||
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(
|
||||
TestModule, {defaultEncapsulation: ViewEncapsulation.None});
|
||||
expect(document.body.innerHTML).toContain('<span>');
|
||||
expect(document.body.innerHTML).not.toContain('_ngcontent-');
|
||||
ngModuleRef.destroy();
|
||||
}));
|
||||
|
||||
it('should allow setting defaultEncapsulation using compiler option',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
|
||||
const ngModuleRef = await platformBrowserDynamic([{
|
||||
provide: COMPILER_OPTIONS,
|
||||
useValue: {defaultEncapsulation: ViewEncapsulation.None},
|
||||
multi: true
|
||||
}]).bootstrapModule(TestModule);
|
||||
expect(document.body.innerHTML).toContain('<span>');
|
||||
expect(document.body.innerHTML).not.toContain('_ngcontent-');
|
||||
ngModuleRef.destroy();
|
||||
}));
|
||||
|
||||
it('should prefer encapsulation on component over bootstrap option',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule({encapsulation: ViewEncapsulation.Emulated});
|
||||
|
||||
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(
|
||||
TestModule, {defaultEncapsulation: ViewEncapsulation.None});
|
||||
expect(document.body.innerHTML).toContain('<span _ngcontent-');
|
||||
ngModuleRef.destroy();
|
||||
}));
|
||||
|
||||
it('should use preserveWhitespaces: false as default',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
|
||||
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
|
||||
expect(document.body.innerHTML).toContain('a b');
|
||||
ngModuleRef.destroy();
|
||||
}));
|
||||
|
||||
it('should allow setting preserveWhitespaces using bootstrap option',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
|
||||
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(
|
||||
TestModule, {preserveWhitespaces: true});
|
||||
expect(document.body.innerHTML).toContain('a b');
|
||||
ngModuleRef.destroy();
|
||||
}));
|
||||
|
||||
it('should allow setting preserveWhitespaces using compiler option',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
|
||||
const ngModuleRef =
|
||||
await platformBrowserDynamic([
|
||||
{provide: COMPILER_OPTIONS, useValue: {preserveWhitespaces: true}, multi: true}
|
||||
]).bootstrapModule(TestModule);
|
||||
expect(document.body.innerHTML).toContain('a b');
|
||||
ngModuleRef.destroy();
|
||||
}));
|
||||
|
||||
it('should prefer preserveWhitespaces on component over bootstrap option',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule({preserveWhitespaces: false});
|
||||
|
||||
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(
|
||||
TestModule, {preserveWhitespaces: true});
|
||||
expect(document.body.innerHTML).toContain('a b');
|
||||
ngModuleRef.destroy();
|
||||
}));
|
||||
|
||||
onlyInIvy('options cannot be changed in Ivy').describe('changing bootstrap options', () => {
|
||||
beforeEach(() => { spyOn(console, 'error'); });
|
||||
|
||||
it('should log an error when changing defaultEncapsulation bootstrap options',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
const platformRef = platformBrowserDynamic();
|
||||
|
||||
const ngModuleRef = await platformRef.bootstrapModule(
|
||||
TestModule, {defaultEncapsulation: ViewEncapsulation.None});
|
||||
ngModuleRef.destroy();
|
||||
|
||||
const ngModuleRef2 = await platformRef.bootstrapModule(
|
||||
TestModule, {defaultEncapsulation: ViewEncapsulation.ShadowDom});
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith(
|
||||
'Provided value for `defaultEncapsulation` can not be changed once it has been set.');
|
||||
|
||||
// The options should not have been changed
|
||||
expect(document.body.innerHTML).not.toContain('_ngcontent-');
|
||||
|
||||
ngModuleRef2.destroy();
|
||||
}));
|
||||
|
||||
it('should log an error when changing preserveWhitespaces bootstrap options',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
const platformRef = platformBrowserDynamic();
|
||||
|
||||
const ngModuleRef =
|
||||
await platformRef.bootstrapModule(TestModule, {preserveWhitespaces: true});
|
||||
ngModuleRef.destroy();
|
||||
|
||||
const ngModuleRef2 =
|
||||
await platformRef.bootstrapModule(TestModule, {preserveWhitespaces: false});
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith(
|
||||
'Provided value for `preserveWhitespaces` can not be changed once it has been set.');
|
||||
|
||||
// The options should not have been changed
|
||||
expect(document.body.innerHTML).toContain('a b');
|
||||
|
||||
ngModuleRef2.destroy();
|
||||
}));
|
||||
|
||||
it('should log an error when changing defaultEncapsulation to its default',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
const platformRef = platformBrowserDynamic();
|
||||
|
||||
const ngModuleRef = await platformRef.bootstrapModule(TestModule);
|
||||
ngModuleRef.destroy();
|
||||
|
||||
const ngModuleRef2 = await platformRef.bootstrapModule(
|
||||
TestModule, {defaultEncapsulation: ViewEncapsulation.Emulated});
|
||||
// Although the configured value may be identical to the default, the provided set of
|
||||
// options has still been changed compared to the previously provided options.
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith(
|
||||
'Provided value for `defaultEncapsulation` can not be changed once it has been set.');
|
||||
|
||||
ngModuleRef2.destroy();
|
||||
}));
|
||||
|
||||
it('should log an error when changing preserveWhitespaces to its default',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
const platformRef = platformBrowserDynamic();
|
||||
|
||||
const ngModuleRef = await platformRef.bootstrapModule(TestModule);
|
||||
ngModuleRef.destroy();
|
||||
|
||||
const ngModuleRef2 =
|
||||
await platformRef.bootstrapModule(TestModule, {preserveWhitespaces: false});
|
||||
// Although the configured value may be identical to the default, the provided set of
|
||||
// options has still been changed compared to the previously provided options.
|
||||
expect(console.error)
|
||||
.toHaveBeenCalledWith(
|
||||
'Provided value for `preserveWhitespaces` can not be changed once it has been set.');
|
||||
|
||||
ngModuleRef2.destroy();
|
||||
}));
|
||||
|
||||
it('should not log an error when passing identical bootstrap options',
|
||||
withBody('<my-app></my-app>', async() => {
|
||||
const TestModule = createComponentAndModule();
|
||||
const platformRef = platformBrowserDynamic();
|
||||
|
||||
const ngModuleRef1 = await platformRef.bootstrapModule(
|
||||
TestModule,
|
||||
{defaultEncapsulation: ViewEncapsulation.None, preserveWhitespaces: true});
|
||||
ngModuleRef1.destroy();
|
||||
|
||||
// Bootstrapping multiple modules using the exact same options should be allowed.
|
||||
const ngModuleRef2 = await platformRef.bootstrapModule(
|
||||
TestModule,
|
||||
{defaultEncapsulation: ViewEncapsulation.None, preserveWhitespaces: true});
|
||||
ngModuleRef2.destroy();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
|
@ -64,4 +281,4 @@ export class MultipleSelectorsAppComponent {
|
|||
bootstrap: [MultipleSelectorsAppComponent],
|
||||
})
|
||||
export class MultipleSelectorsAppModule {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 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 {ɵresetJitOptions as resetJitOptions} from '@angular/core';
|
||||
|
||||
/**
|
||||
* Wraps a function in a new function which sets up document and HTML for running a test.
|
||||
|
@ -120,3 +121,5 @@ export function cleanupDocument(): void {
|
|||
|
||||
if (typeof beforeEach == 'function') beforeEach(ensureDocument);
|
||||
if (typeof afterEach == 'function') afterEach(cleanupDocument);
|
||||
|
||||
if (typeof afterEach === 'function') afterEach(resetJitOptions);
|
||||
|
|
Loading…
Reference in New Issue