diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index 24f173fdf65cf..694f5763b3c50 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -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__( injector: Injector, options: CompilerOptions, moduleType: Type): Promise> { 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(list: T[], el: T): void { } } +function _lastDefined(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)); diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 3e4a0cda93dd0..24fdccfd2a182 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -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, diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 91801507e0f78..e77baf8d46d11 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -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, 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, }; diff --git a/packages/core/src/render3/jit/jit_options.ts b/packages/core/src/render3/jit/jit_options.ts new file mode 100644 index 0000000000000..2c291ff345eb3 --- /dev/null +++ b/packages/core/src/render3/jit/jit_options.ts @@ -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; +} diff --git a/packages/core/src/util/ng_jit_mode.ts b/packages/core/src/util/ng_jit_mode.ts new file mode 100644 index 0000000000000..87c4feeacdf47 --- /dev/null +++ b/packages/core/src/util/ng_jit_mode.ts @@ -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 {}; diff --git a/packages/core/test/acceptance/bootstrap_spec.ts b/packages/core/test/acceptance/bootstrap_spec.ts index bcd355159faea..91dd57d46b1fa 100644 --- a/packages/core/test/acceptance/bootstrap_spec.ts +++ b/packages/core/test/acceptance/bootstrap_spec.ts @@ -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('
before|
', 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: 'a b', + 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('', async() => { + const TestModule = createComponentAndModule(); + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule); + expect(document.body.innerHTML).toContain('', async() => { + const TestModule = createComponentAndModule(); + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule( + TestModule, {defaultEncapsulation: ViewEncapsulation.None}); + expect(document.body.innerHTML).toContain(''); + expect(document.body.innerHTML).not.toContain('_ngcontent-'); + ngModuleRef.destroy(); + })); + + it('should allow setting defaultEncapsulation using compiler option', + withBody('', async() => { + const TestModule = createComponentAndModule(); + + const ngModuleRef = await platformBrowserDynamic([{ + provide: COMPILER_OPTIONS, + useValue: {defaultEncapsulation: ViewEncapsulation.None}, + multi: true + }]).bootstrapModule(TestModule); + expect(document.body.innerHTML).toContain(''); + expect(document.body.innerHTML).not.toContain('_ngcontent-'); + ngModuleRef.destroy(); + })); + + it('should prefer encapsulation on component over bootstrap option', + withBody('', async() => { + const TestModule = createComponentAndModule({encapsulation: ViewEncapsulation.Emulated}); + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule( + TestModule, {defaultEncapsulation: ViewEncapsulation.None}); + expect(document.body.innerHTML).toContain('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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 { -} \ No newline at end of file +} diff --git a/packages/private/testing/src/render3.ts b/packages/private/testing/src/render3.ts index c01c5875b5241..8a536f8f767c4 100644 --- a/packages/private/testing/src/render3.ts +++ b/packages/private/testing/src/render3.ts @@ -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);