fix(ivy): ensure that the correct `document` is available (#33712)

Most of the use of `document` in the framework is within
the DI so they just inject the `DOCUMENT` token and are done.

Ivy is special because it does not rely upon the DI and must
get hold of the document some other way. There are a limited
number of places relevant to ivy that currently consume a global
document object.

The solution is modelled on the `LOCALE_ID` approach, which has
`getLocaleId()` and `setLocaleId()` top-level functions for ivy (see
`core/src/render3/i18n.ts`).  In the rest of Angular (i.e. using DI) the
`LOCALE_ID` token has a provider that also calls setLocaleId() to
ensure that ivy has the same value.

This commit defines `getDocument()` and `setDocument() `top-level
functions for ivy. Wherever ivy needs the global `document`, it calls
`getDocument()` instead.  Each of the platforms (e.g. Browser, Server,
WebWorker) have providers for `DOCUMENT`. In each of those providers
they also call `setDocument()` accordingly.

Fixes #33651

PR Close #33712
This commit is contained in:
Pete Bacon Darwin 2019-11-09 19:00:53 +00:00 committed by Kara Erickson
parent c303371b26
commit e511bfcab5
12 changed files with 99 additions and 15 deletions

View File

@ -243,6 +243,10 @@ export {
LContext as ɵLContext,
} from './render3/interfaces/context';
export {
setDocument as ɵsetDocument
} from './render3/interfaces/document';
// we reexport these symbols just so that they are retained during the dead code elimination
// performed by rollup while it's creating fesm files.
//

View File

@ -20,6 +20,7 @@ import {setDelayProjection} from './instructions/all';
import {attachI18nOpCodesDebug} from './instructions/lview_debug';
import {TsickleIssue1009, allocExpando, elementAttributeInternal, elementPropertyInternal, getOrCreateTNode, setInputsForProperty, setNgReflectProperties, textBindingInternal} from './instructions/shared';
import {LContainer, NATIVE} from './interfaces/container';
import {getDocument} from './interfaces/document';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n';
import {TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from './interfaces/node';
import {RComment, RElement, RText} from './interfaces/renderer';
@ -1180,7 +1181,7 @@ function icuStart(
function parseIcuCase(
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
expandoStartIndex: number): IcuCase {
const inertBodyHelper = new InertBodyHelper(document);
const inertBodyHelper = new InertBodyHelper(getDocument());
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
if (!inertBodyElement) {
throw new Error('Unable to generate inert body element');

View File

@ -0,0 +1,56 @@
/**
* @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
*/
/**
* Most of the use of `document` in Angular is from within the DI system so it is possible to simply
* inject the `DOCUMENT` token and are done.
*
* Ivy is special because it does not rely upon the DI and must get hold of the document some other
* way.
*
* The solution is to define `getDocument()` and `setDocument()` top-level functions for ivy.
* Wherever ivy needs the global document, it calls `getDocument()` instead.
*
* When running ivy outside of a browser environment, it is necessary to call `setDocument()` to
* tell ivy what the global `document` is.
*
* Angular does this for us in each of the standard platforms (`Browser`, `Server`, and `WebWorker`)
* by calling `setDocument()` when providing the `DOCUMENT` token.
*/
let DOCUMENT: Document|undefined = undefined;
/**
* Tell ivy what the `document` is for this platform.
*
* It is only necessary to call this if the current platform is not a browser.
*
* @param document The object representing the global `document` in this environment.
*/
export function setDocument(document: Document | undefined): void {
DOCUMENT = document;
}
/**
* Access the object that represents the `document` for this platform.
*
* Ivy calls this whenever it needs to access the `document` object.
* For example to create the renderer or to do sanitization.
*/
export function getDocument(): Document {
if (DOCUMENT !== undefined) {
return DOCUMENT;
} else if (typeof document !== 'undefined') {
return document;
}
// No "document" can be found. This should only happen if we are running ivy outside Angular and
// the current platform is not a browser. Since this is not a supported scenario at the moment
// this should not happen in Angular apps.
// Once we support running ivy outside of Angular we will need to publish `setDocument()` as a
// public API. Meanwhile we just return `undefined` and let the application fail.
return undefined !;
}

View File

@ -8,15 +8,15 @@
/**
* The goal here is to make sure that the browser DOM API is the Renderer.
* We do this by defining a subset of DOM API to be the renderer and than
* use that time for rendering.
* We do this by defining a subset of DOM API to be the renderer and then
* use that at runtime for rendering.
*
* At runtime we can then use the DOM api directly, in server or web-worker
* it will be easy to implement such API.
*/
import {RendererStyleFlags2, RendererType2} from '../../render/api';
import {getDocument} from './document';
// TODO: cleanup once the code is merged in angular/angular
export enum RendererStyleFlags3 {
@ -105,7 +105,7 @@ export interface RendererFactory3 {
export const domRendererFactory3: RendererFactory3 = {
createRenderer: (hostElement: RElement | null, rendererType: RendererType2 | null):
Renderer3 => { return document;}
Renderer3 => { return getDocument();}
};
/** Subset of API needed for appending elements and text nodes. */

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getDocument} from '../render3/interfaces/document';
import {SANITIZER} from '../render3/interfaces/view';
import {getLView} from '../render3/state';
import {renderStringify} from '../render3/util/misc_utils';
@ -42,7 +43,7 @@ export function ɵɵsanitizeHtml(unsafeHtml: any): string {
if (allowSanitizationBypassAndThrow(unsafeHtml, BypassType.Html)) {
return unwrapSafeValue(unsafeHtml);
}
return _sanitizeHtml(document, renderStringify(unsafeHtml));
return _sanitizeHtml(getDocument(), renderStringify(unsafeHtml));
}
/**

View File

@ -26,6 +26,9 @@
{
"name": "DECLARATION_VIEW"
},
{
"name": "DOCUMENT"
},
{
"name": "DepComponent"
},
@ -308,6 +311,9 @@
{
"name": "getDirectiveDef"
},
{
"name": "getDocument"
},
{
"name": "getElementDepthCount"
},

View File

@ -26,6 +26,9 @@
{
"name": "DECLARATION_VIEW"
},
{
"name": "DOCUMENT"
},
{
"name": "EMPTY_ARRAY"
},
@ -245,6 +248,9 @@
{
"name": "getDirectiveDef"
},
{
"name": "getDocument"
},
{
"name": "getFactoryDef"
},

View File

@ -47,6 +47,9 @@
{
"name": "DEFAULT_TOTAL_SOURCES"
},
{
"name": "DOCUMENT"
},
{
"name": "DefaultIterableDiffer"
},
@ -656,6 +659,9 @@
{
"name": "getDirectiveDef"
},
{
"name": "getDocument"
},
{
"name": "getElementDepthCount"
},

View File

@ -7,7 +7,7 @@
*/
import {CommonModule, DOCUMENT, PlatformLocation, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
import {APP_ID, ApplicationModule, ErrorHandler, Inject, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore, ɵConsole as Console, ɵINJECTOR_SCOPE as INJECTOR_SCOPE} from '@angular/core';
import {APP_ID, ApplicationModule, ErrorHandler, Inject, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore, ɵConsole as Console, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵsetDocument} from '@angular/core';
import {BrowserDomAdapter} from './browser/browser_adapter';
import {SERVER_TRANSITION_PROVIDERS, TRANSITION_ID} from './browser/server-transition';
import {BrowserGetTestability} from './browser/testability';
@ -57,6 +57,8 @@ export function errorHandler(): ErrorHandler {
}
export function _document(): any {
// Tell ivy about the global document
ɵsetDocument(document);
return document;
}

View File

@ -9,7 +9,7 @@
import {ɵAnimationEngine} from '@angular/animations/browser';
import {DOCUMENT, PlatformLocation, ViewportScroller, ɵNullViewportScroller as NullViewportScroller, ɵPLATFORM_SERVER_ID as PLATFORM_SERVER_ID, ɵgetDOM as getDOM} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {Injector, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, StaticProvider, Testability, createPlatformFactory, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS} from '@angular/core';
import {Injector, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, StaticProvider, Testability, createPlatformFactory, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS, ɵsetDocument} from '@angular/core';
import {BrowserModule, EVENT_MANAGER_PLUGINS, ɵSharedStylesHost as SharedStylesHost} from '@angular/platform-browser';
import {ɵplatformCoreDynamic as platformCoreDynamic} from '@angular/platform-browser-dynamic';
import {NoopAnimationsModule, ɵAnimationRendererFactory} from '@angular/platform-browser/animations';
@ -81,11 +81,11 @@ export class ServerModule {
function _document(injector: Injector) {
let config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
if (config && config.document) {
return parseDocument(config.document, config.url);
} else {
return getDOM().createHtmlDocument();
}
const document = config && config.document ? parseDocument(config.document, config.url) :
getDOM().createHtmlDocument();
// Tell ivy about the global document
ɵsetDocument(document);
return document;
}
/**

View File

@ -7,7 +7,7 @@
*/
import {DOCUMENT, ɵPLATFORM_WORKER_UI_ID as PLATFORM_WORKER_UI_ID} from '@angular/common';
import {ErrorHandler, Injectable, InjectionToken, Injector, NgZone, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, StaticProvider, Testability, createPlatformFactory, isDevMode, platformCore, ɵAPP_ID_RANDOM_PROVIDER as APP_ID_RANDOM_PROVIDER} from '@angular/core';
import {ErrorHandler, Injectable, InjectionToken, Injector, NgZone, PLATFORM_ID, PLATFORM_INITIALIZER, RendererFactory2, StaticProvider, Testability, createPlatformFactory, isDevMode, platformCore, ɵAPP_ID_RANDOM_PROVIDER as APP_ID_RANDOM_PROVIDER, ɵsetDocument} from '@angular/core';
import {EVENT_MANAGER_PLUGINS, EventManager, HAMMER_GESTURE_CONFIG, HammerGestureConfig, ɵBROWSER_SANITIZATION_PROVIDERS as BROWSER_SANITIZATION_PROVIDERS, ɵBrowserDomAdapter as BrowserDomAdapter, ɵBrowserGetTestability as BrowserGetTestability, ɵDomEventsPlugin as DomEventsPlugin, ɵDomRendererFactory2 as DomRendererFactory2, ɵDomSharedStylesHost as DomSharedStylesHost, ɵHammerGesturesPlugin as HammerGesturesPlugin, ɵKeyEventsPlugin as KeyEventsPlugin, ɵSharedStylesHost as SharedStylesHost} from '@angular/platform-browser';
import {ON_WEB_WORKER} from './web_workers/shared/api';
@ -159,6 +159,8 @@ function _exceptionHandler(): ErrorHandler {
}
function _document(): any {
// Tell ivy about the global document
ɵsetDocument(document);
return document;
}

View File

@ -40,7 +40,7 @@ export interface MessageBusSource {
/** @deprecated */
export declare const platformWorkerApp: (extraProviders?: StaticProvider[] | undefined) => PlatformRef;
export declare const platformWorkerUi: (extraProviders?: StaticProvider[] | undefined) => PlatformRef;
export declare const platformWorkerUi: (extraProviders?: StaticProvider[] | undefined) => import("@angular/core").PlatformRef;
/** @deprecated */
export interface ReceivedMessage {