From b4d444a0a7ee2c2b31b691d03b93368d1edd1572 Mon Sep 17 00:00:00 2001 From: vikerman Date: Tue, 14 Feb 2017 16:14:40 -0800 Subject: [PATCH] feat(platform-server): add API to render Module and ModuleFactory to string (#14381) - PlatformState provides an interface to serialize the current Platform State as a string or Document. - renderModule and renderModuleFactory are convenience methods to wait for Angular Application to stabilize and then render the state to a string. - refactor code to remove defaultDoc from DomAdapter and inject DOCUMENT where it's needed. --- .../core/src/di/reflective_provider.ts | 4 +- .../core/test/dom/dom_adapter_spec.ts | 15 +- .../core/test/linker/integration_spec.ts | 15 +- .../@angular/platform-browser/src/browser.ts | 12 +- .../src/browser/browser_adapter.ts | 14 +- .../src/browser/generic_browser_adapter.ts | 4 +- .../location/browser_platform_location.ts | 11 +- .../platform-browser/src/browser/meta.ts | 17 +- .../platform-browser/src/browser/title.ts | 11 +- .../platform-browser/src/dom/dom_adapter.ts | 12 +- .../src/dom/events/dom_events.ts | 7 +- .../src/dom/events/event_manager.ts | 5 +- .../src/dom/events/hammer_gestures.ts | 9 +- .../src/dom/events/key_events.ts | 7 +- .../src/security/dom_sanitization_service.ts | 9 +- .../src/security/html_sanitizer.ts | 5 +- .../test/browser/meta_spec.ts | 5 +- .../test/browser/title_spec.ts | 11 +- .../test/dom/events/event_manager_spec.ts | 20 ++- .../test/dom/events/hammer_gestures_spec.ts | 2 +- .../test/dom/events/key_events_spec.ts | 2 +- .../security/dom_sanitization_service_spec.ts | 2 +- .../test/security/html_sanitizer_spec.ts | 48 +++--- .../@angular/platform-server/src/location.ts | 7 +- .../platform-server/src/parse5_adapter.ts | 30 ++-- .../platform-server/src/platform-server.ts | 5 +- .../platform-server/src/platform_state.ts | 34 ++++ .../@angular/platform-server/src/server.ts | 51 ++++-- .../platform-server/src/server_renderer.ts | 2 +- modules/@angular/platform-server/src/utils.ts | 71 +++++++++ .../platform-server/test/integration_spec.ts | 150 +++++++++++++----- .../src/web_workers/worker/worker_adapter.ts | 10 +- .../platform-webworker/src/worker_app.ts | 3 +- .../platform-webworker/src/worker_render.ts | 2 +- .../src/tree/ng2_ftl/app.ngfactory.ts | 8 +- modules/benchmarks/src/tree/ng2_next/tree.ts | 2 +- .../src/tree/ng2_static_ftl/app.ngfactory.ts | 8 +- .../platform-browser/index.d.ts | 3 +- .../platform-server/index.d.ts | 16 ++ 39 files changed, 462 insertions(+), 187 deletions(-) create mode 100644 modules/@angular/platform-server/src/platform_state.ts create mode 100644 modules/@angular/platform-server/src/utils.ts diff --git a/modules/@angular/core/src/di/reflective_provider.ts b/modules/@angular/core/src/di/reflective_provider.ts index 2946b06588..58128b5329 100644 --- a/modules/@angular/core/src/di/reflective_provider.ts +++ b/modules/@angular/core/src/di/reflective_provider.ts @@ -223,7 +223,7 @@ function _extractToken( if (!Array.isArray(metadata)) { if (metadata instanceof Inject) { - return _createDependency(metadata.token, optional, null); + return _createDependency(metadata['token'], optional, null); } else { return _createDependency(metadata, optional, null); } @@ -238,7 +238,7 @@ function _extractToken( token = paramMetadata; } else if (paramMetadata instanceof Inject) { - token = paramMetadata.token; + token = paramMetadata['token']; } else if (paramMetadata instanceof Optional) { optional = true; diff --git a/modules/@angular/core/test/dom/dom_adapter_spec.ts b/modules/@angular/core/test/dom/dom_adapter_spec.ts index a11ca02e5d..b892a9b34f 100644 --- a/modules/@angular/core/test/dom/dom_adapter_spec.ts +++ b/modules/@angular/core/test/dom/dom_adapter_spec.ts @@ -12,6 +12,11 @@ import {el, stringifyElement} from '@angular/platform-browser/testing/browser_ut export function main() { describe('dom adapter', () => { + let defaultDoc: any; + beforeEach(() => { + defaultDoc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument(); + }); + it('should not coalesque text nodes', () => { const el1 = el('
a
'); const el2 = el('
b
'); @@ -68,15 +73,15 @@ export function main() { beforeEach(() => getDOM().resetBaseElement()); it('should return null if base element is absent', - () => { expect(getDOM().getBaseHref()).toBeNull(); }); + () => { expect(getDOM().getBaseHref(defaultDoc)).toBeNull(); }); it('should return the value of the base element', () => { const baseEl = getDOM().createElement('base'); getDOM().setAttribute(baseEl, 'href', '/drop/bass/connon/'); - const headEl = getDOM().defaultDoc().head; + const headEl = defaultDoc.head; getDOM().appendChild(headEl, baseEl); - const baseHref = getDOM().getBaseHref(); + const baseHref = getDOM().getBaseHref(defaultDoc); getDOM().removeChild(headEl, baseEl); getDOM().resetBaseElement(); @@ -86,10 +91,10 @@ export function main() { it('should return a relative url', () => { const baseEl = getDOM().createElement('base'); getDOM().setAttribute(baseEl, 'href', 'base'); - const headEl = getDOM().defaultDoc().head; + const headEl = defaultDoc.head; getDOM().appendChild(headEl, baseEl); - const baseHref = getDOM().getBaseHref(); + const baseHref = getDOM().getBaseHref(defaultDoc); getDOM().removeChild(headEl, baseEl); getDOM().resetBaseElement(); diff --git a/modules/@angular/core/test/linker/integration_spec.ts b/modules/@angular/core/test/linker/integration_spec.ts index 1963e4154a..916bfe6ba5 100644 --- a/modules/@angular/core/test/linker/integration_spec.ts +++ b/modules/@angular/core/test/linker/integration_spec.ts @@ -21,6 +21,7 @@ import {Attribute, Component, ContentChildren, Directive, HostBinding, HostListe import {Renderer} from '@angular/core/src/render'; import {TestBed, async, fakeAsync, getTestBed, tick} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; +import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens'; import {dispatchEvent, el} from '@angular/platform-browser/testing/browser_util'; import {expect} from '@angular/platform-browser/testing/matchers'; @@ -838,19 +839,20 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea const template = '
'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); + const doc = TestBed.get(DOCUMENT); const tc = fixture.debugElement.children[0]; const listener = tc.injector.get(DirectiveListeningDomEvent); - dispatchEvent(getDOM().getGlobalEventTarget('window'), 'domEvent'); + dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent'); expect(listener.eventTypes).toEqual(['window_domEvent']); listener.eventTypes = []; - dispatchEvent(getDOM().getGlobalEventTarget('document'), 'domEvent'); + dispatchEvent(getDOM().getGlobalEventTarget(doc, 'document'), 'domEvent'); expect(listener.eventTypes).toEqual(['document_domEvent', 'window_domEvent']); fixture.destroy(); listener.eventTypes = []; - dispatchEvent(getDOM().getGlobalEventTarget('body'), 'domEvent'); + dispatchEvent(getDOM().getGlobalEventTarget(doc, 'body'), 'domEvent'); expect(listener.eventTypes).toEqual([]); }); @@ -990,6 +992,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea const template = '
'; TestBed.overrideComponent(MyComp, {set: {template}}); const fixture = TestBed.createComponent(MyComp); + const doc = TestBed.get(DOCUMENT); globalCounter = 0; fixture.componentInstance.ctxBoolProp = true; @@ -999,7 +1002,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea const listener = tc.injector.get(DirectiveListeningDomEvent); const listenerother = tc.injector.get(DirectiveListeningDomEventOther); - dispatchEvent(getDOM().getGlobalEventTarget('window'), 'domEvent'); + dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent'); expect(listener.eventTypes).toEqual(['window_domEvent']); expect(listenerother.eventType).toEqual('other_domEvent'); expect(globalCounter).toEqual(1); @@ -1007,12 +1010,12 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea fixture.componentInstance.ctxBoolProp = false; fixture.detectChanges(); - dispatchEvent(getDOM().getGlobalEventTarget('window'), 'domEvent'); + dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent'); expect(globalCounter).toEqual(1); fixture.componentInstance.ctxBoolProp = true; fixture.detectChanges(); - dispatchEvent(getDOM().getGlobalEventTarget('window'), 'domEvent'); + dispatchEvent(getDOM().getGlobalEventTarget(doc, 'window'), 'domEvent'); expect(globalCounter).toEqual(2); // need to destroy to release all remaining global event listeners diff --git a/modules/@angular/platform-browser/src/browser.ts b/modules/@angular/platform-browser/src/browser.ts index 73a6e9ae75..24eb0b129c 100644 --- a/modules/@angular/platform-browser/src/browser.ts +++ b/modules/@angular/platform-browser/src/browser.ts @@ -30,7 +30,8 @@ import {DomSanitizer, DomSanitizerImpl} from './security/dom_sanitization_servic export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: Provider[] = [ {provide: PLATFORM_INITIALIZER, useValue: initDomAdapter, multi: true}, - {provide: PlatformLocation, useClass: BrowserPlatformLocation} + {provide: PlatformLocation, useClass: BrowserPlatformLocation}, + {provide: DOCUMENT, useFactory: _document, deps: []}, ]; /** @@ -59,12 +60,8 @@ export function errorHandler(): ErrorHandler { return new ErrorHandler(); } -export function meta(): Meta { - return new Meta(getDOM()); -} - export function _document(): any { - return getDOM().defaultDoc(); + return document; } export function _resolveDefaultAnimationDriver(): AnimationDriver { @@ -83,7 +80,6 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver { providers: [ BROWSER_SANITIZATION_PROVIDERS, {provide: ErrorHandler, useFactory: errorHandler, deps: []}, - {provide: DOCUMENT, useFactory: _document, deps: []}, {provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true}, {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true}, {provide: EVENT_MANAGER_PLUGINS, useClass: HammerGesturesPlugin, multi: true}, @@ -92,11 +88,11 @@ export function _resolveDefaultAnimationDriver(): AnimationDriver { {provide: RootRenderer, useExisting: DomRootRenderer}, {provide: SharedStylesHost, useExisting: DomSharedStylesHost}, {provide: AnimationDriver, useFactory: _resolveDefaultAnimationDriver}, - {provide: Meta, useFactory: meta}, DomSharedStylesHost, Testability, EventManager, ELEMENT_PROBE_PROVIDERS, + Meta, Title, ], exports: [CommonModule, ApplicationModule] diff --git a/modules/@angular/platform-browser/src/browser/browser_adapter.ts b/modules/@angular/platform-browser/src/browser/browser_adapter.ts index 4f0bfbbeeb..2beae22f63 100644 --- a/modules/@angular/platform-browser/src/browser/browser_adapter.ts +++ b/modules/@angular/platform-browser/src/browser/browser_adapter.ts @@ -107,10 +107,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { get attrToPropMap(): any { return _attrToPropMap; } - query(selector: string): any { return document.querySelector(selector); } - querySelector(el: Element, selector: string): HTMLElement { - return el.querySelector(selector) as HTMLElement; - } + querySelector(el: Element, selector: string): any { return el.querySelector(selector); } querySelectorAll(el: any, selector: string): any[] { return el.querySelectorAll(selector); } on(el: Node, evt: any, listener: any) { el.addEventListener(evt, listener, false); } onAndCancel(el: Node, evt: any, listener: any): Function { @@ -274,7 +271,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { createHtmlDocument(): HTMLDocument { return document.implementation.createHTMLDocument('fakeTitle'); } - defaultDoc(): HTMLDocument { return document; } getBoundingClientRect(el: Element): any { try { return el.getBoundingClientRect(); @@ -282,8 +278,8 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { return {top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0}; } } - getTitle(): string { return document.title; } - setTitle(newTitle: string) { document.title = newTitle || ''; } + getTitle(doc: Document): string { return document.title; } + setTitle(doc: Document, newTitle: string) { document.title = newTitle || ''; } elementMatches(n: any, selector: string): boolean { if (n instanceof HTMLElement) { return n.matches && n.matches(selector) || @@ -330,7 +326,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { return _keyMap[key] || key; } - getGlobalEventTarget(target: string): EventTarget { + getGlobalEventTarget(doc: Document, target: string): EventTarget { if (target === 'window') { return window; } @@ -343,7 +339,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { } getHistory(): History { return window.history; } getLocation(): Location { return window.location; } - getBaseHref(): string { + getBaseHref(doc: Document): string { const href = getBaseElementHref(); return isBlank(href) ? null : relativePath(href); } diff --git a/modules/@angular/platform-browser/src/browser/generic_browser_adapter.ts b/modules/@angular/platform-browser/src/browser/generic_browser_adapter.ts index 60b4230851..f2b5776e30 100644 --- a/modules/@angular/platform-browser/src/browser/generic_browser_adapter.ts +++ b/modules/@angular/platform-browser/src/browser/generic_browser_adapter.ts @@ -23,7 +23,7 @@ export abstract class GenericBrowserDomAdapter extends DomAdapter { constructor() { super(); try { - const element = this.createElement('div', this.defaultDoc()); + const element = this.createElement('div', document); if (isPresent(this.getStyle(element, 'animationName'))) { this._animationPrefix = ''; } else { @@ -61,7 +61,7 @@ export abstract class GenericBrowserDomAdapter extends DomAdapter { } supportsDOMEvents(): boolean { return true; } supportsNativeShadowDOM(): boolean { - return typeof(this.defaultDoc().body).createShadowRoot === 'function'; + return typeof(document.body).createShadowRoot === 'function'; } getAnimationPrefix(): string { return this._animationPrefix ? this._animationPrefix : ''; } getTransitionEnd(): string { return this._transitionEnd ? this._transitionEnd : ''; } diff --git a/modules/@angular/platform-browser/src/browser/location/browser_platform_location.ts b/modules/@angular/platform-browser/src/browser/location/browser_platform_location.ts index 33c395e722..9cf47389c2 100644 --- a/modules/@angular/platform-browser/src/browser/location/browser_platform_location.ts +++ b/modules/@angular/platform-browser/src/browser/location/browser_platform_location.ts @@ -7,9 +7,10 @@ */ import {LocationChangeListener, PlatformLocation} from '@angular/common'; -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; import {getDOM} from '../../dom/dom_adapter'; +import {DOCUMENT} from '../../dom/dom_tokens'; import {supportsState} from './history'; @@ -25,7 +26,7 @@ export class BrowserPlatformLocation extends PlatformLocation { private _location: Location; private _history: History; - constructor() { + constructor(@Inject(DOCUMENT) private _doc: any) { super(); this._init(); } @@ -39,14 +40,14 @@ export class BrowserPlatformLocation extends PlatformLocation { get location(): Location { return this._location; } - getBaseHrefFromDOM(): string { return getDOM().getBaseHref(); } + getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc); } onPopState(fn: LocationChangeListener): void { - getDOM().getGlobalEventTarget('window').addEventListener('popstate', fn, false); + getDOM().getGlobalEventTarget(this._doc, 'window').addEventListener('popstate', fn, false); } onHashChange(fn: LocationChangeListener): void { - getDOM().getGlobalEventTarget('window').addEventListener('hashchange', fn, false); + getDOM().getGlobalEventTarget(this._doc, 'window').addEventListener('hashchange', fn, false); } get pathname(): string { return this._location.pathname; } diff --git a/modules/@angular/platform-browser/src/browser/meta.ts b/modules/@angular/platform-browser/src/browser/meta.ts index b9ee0770f2..35717ff9dd 100644 --- a/modules/@angular/platform-browser/src/browser/meta.ts +++ b/modules/@angular/platform-browser/src/browser/meta.ts @@ -6,8 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable} from '@angular/core'; -import {DomAdapter} from '../dom/dom_adapter'; +import {Inject, Injectable} from '@angular/core'; + +import {DomAdapter, getDOM} from '../dom/dom_adapter'; +import {DOCUMENT} from '../dom/dom_tokens'; + /** * Represents a meta element. @@ -33,7 +36,8 @@ export type MetaDefinition = { */ @Injectable() export class Meta { - constructor(private _dom: DomAdapter) {} + private _dom: DomAdapter; + constructor(@Inject(DOCUMENT) private _doc: any) { this._dom = getDOM(); } addTag(tag: MetaDefinition, forceCreation: boolean = false): HTMLMetaElement { if (!tag) return null; @@ -52,13 +56,12 @@ export class Meta { getTag(attrSelector: string): HTMLMetaElement { if (!attrSelector) return null; - return this._dom.query(`meta[${attrSelector}]`); + return this._dom.querySelector(this._doc, `meta[${attrSelector}]`); } getTags(attrSelector: string): HTMLMetaElement[] { if (!attrSelector) return []; - const list /*NodeList*/ = - this._dom.querySelectorAll(this._dom.defaultDoc(), `meta[${attrSelector}]`); + const list /*NodeList*/ = this._dom.querySelectorAll(this._doc, `meta[${attrSelector}]`); return list ? [].slice.call(list) : []; } @@ -92,7 +95,7 @@ export class Meta { } const element: HTMLMetaElement = this._dom.createElement('meta') as HTMLMetaElement; this._setMetaElementAttributes(meta, element); - const head = this._dom.getElementsByTagName(this._dom.defaultDoc(), 'head')[0]; + const head = this._dom.getElementsByTagName(this._doc, 'head')[0]; this._dom.appendChild(head, element); return element; } diff --git a/modules/@angular/platform-browser/src/browser/title.ts b/modules/@angular/platform-browser/src/browser/title.ts index 4878f38665..588f323f45 100644 --- a/modules/@angular/platform-browser/src/browser/title.ts +++ b/modules/@angular/platform-browser/src/browser/title.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {Inject, Injectable} from '@angular/core'; + import {getDOM} from '../dom/dom_adapter'; +import {DOCUMENT} from '../dom/dom_tokens'; + + /** * A service that can be used to get and set the title of a current HTML document. * @@ -17,16 +22,18 @@ import {getDOM} from '../dom/dom_adapter'; * * @experimental */ +@Injectable() export class Title { + constructor(@Inject(DOCUMENT) private _doc: any) {} /** * Get the title of the current HTML document. * @returns {string} */ - getTitle(): string { return getDOM().getTitle(); } + getTitle(): string { return getDOM().getTitle(this._doc); } /** * Set the title of the current HTML document. * @param newTitle */ - setTitle(newTitle: string) { getDOM().setTitle(newTitle); } + setTitle(newTitle: string) { getDOM().setTitle(this._doc, newTitle); } } diff --git a/modules/@angular/platform-browser/src/dom/dom_adapter.ts b/modules/@angular/platform-browser/src/dom/dom_adapter.ts index 9682584b28..84303f8cb1 100644 --- a/modules/@angular/platform-browser/src/dom/dom_adapter.ts +++ b/modules/@angular/platform-browser/src/dom/dom_adapter.ts @@ -53,8 +53,7 @@ export abstract class DomAdapter { _attrToPropMap: {[key: string]: string}; abstract parse(templateHtml: string): any /** TODO #9100 */; - abstract query(selector: string): any; - abstract querySelector(el: any /** TODO #9100 */, selector: string): HTMLElement; + abstract querySelector(el: any /** TODO #9100 */, selector: string): any; abstract querySelectorAll(el: any /** TODO #9100 */, selector: string): any[]; abstract on( el: any /** TODO #9100 */, evt: any /** TODO #9100 */, listener: any /** TODO #9100 */): any @@ -145,10 +144,9 @@ export abstract class DomAdapter { /** TODO #9100 */; abstract templateAwareRoot(el: any /** TODO #9100 */): any /** TODO #9100 */; abstract createHtmlDocument(): HTMLDocument; - abstract defaultDoc(): HTMLDocument; abstract getBoundingClientRect(el: any /** TODO #9100 */): any /** TODO #9100 */; - abstract getTitle(): string; - abstract setTitle(newTitle: string): any /** TODO #9100 */; + abstract getTitle(doc: Document): string; + abstract setTitle(doc: Document, newTitle: string): any /** TODO #9100 */; abstract elementMatches(n: any /** TODO #9100 */, selector: string): boolean; abstract isTemplateElement(el: any): boolean; abstract isTextNode(node: any /** TODO #9100 */): boolean; @@ -164,10 +162,10 @@ export abstract class DomAdapter { /** TODO #9100 */; abstract supportsDOMEvents(): boolean; abstract supportsNativeShadowDOM(): boolean; - abstract getGlobalEventTarget(target: string): any; + abstract getGlobalEventTarget(doc: Document, target: string): any; abstract getHistory(): History; abstract getLocation(): Location; - abstract getBaseHref(): string; + abstract getBaseHref(doc: Document): string; abstract resetBaseElement(): void; abstract getUserAgent(): string; abstract setData(element: any /** TODO #9100 */, name: string, value: string): any diff --git a/modules/@angular/platform-browser/src/dom/events/dom_events.ts b/modules/@angular/platform-browser/src/dom/events/dom_events.ts index e1d48049ec..324603e035 100644 --- a/modules/@angular/platform-browser/src/dom/events/dom_events.ts +++ b/modules/@angular/platform-browser/src/dom/events/dom_events.ts @@ -6,11 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; + +import {DOCUMENT} from '../dom_tokens'; + import {EventManagerPlugin} from './event_manager'; @Injectable() export class DomEventsPlugin extends EventManagerPlugin { + constructor(@Inject(DOCUMENT) doc: any) { super(doc); } + // This plugin should come last in the list of plugins, because it accepts all // events. supports(eventName: string): boolean { return true; } diff --git a/modules/@angular/platform-browser/src/dom/events/event_manager.ts b/modules/@angular/platform-browser/src/dom/events/event_manager.ts index be89369801..6254e1d9d1 100644 --- a/modules/@angular/platform-browser/src/dom/events/event_manager.ts +++ b/modules/@angular/platform-browser/src/dom/events/event_manager.ts @@ -10,7 +10,6 @@ import {Inject, Injectable, InjectionToken, NgZone} from '@angular/core'; import {getDOM} from '../dom_adapter'; - /** * @stable */ @@ -62,6 +61,8 @@ export class EventManager { } export abstract class EventManagerPlugin { + constructor(private _doc: any) {} + manager: EventManager; abstract supports(eventName: string): boolean; @@ -69,7 +70,7 @@ export abstract class EventManagerPlugin { abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function; addGlobalEventListener(element: string, eventName: string, handler: Function): Function { - const target: HTMLElement = getDOM().getGlobalEventTarget(element); + const target: HTMLElement = getDOM().getGlobalEventTarget(this._doc, element); if (!target) { throw new Error(`Unsupported event target ${target} for event ${eventName}`); } diff --git a/modules/@angular/platform-browser/src/dom/events/hammer_gestures.ts b/modules/@angular/platform-browser/src/dom/events/hammer_gestures.ts index e3bc720c3d..fc695c3f57 100644 --- a/modules/@angular/platform-browser/src/dom/events/hammer_gestures.ts +++ b/modules/@angular/platform-browser/src/dom/events/hammer_gestures.ts @@ -7,6 +7,9 @@ */ import {Inject, Injectable, InjectionToken} from '@angular/core'; + +import {DOCUMENT} from '../dom_tokens'; + import {EventManagerPlugin} from './event_manager'; const EVENT_NAMES = { @@ -85,7 +88,11 @@ export class HammerGestureConfig { @Injectable() export class HammerGesturesPlugin extends EventManagerPlugin { - constructor(@Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig) { super(); } + constructor( + @Inject(DOCUMENT) doc: any, + @Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig) { + super(doc); + } supports(eventName: string): boolean { if (!EVENT_NAMES.hasOwnProperty(eventName.toLowerCase()) && !this.isCustomEvent(eventName)) { diff --git a/modules/@angular/platform-browser/src/dom/events/key_events.ts b/modules/@angular/platform-browser/src/dom/events/key_events.ts index e085fc96e7..0be06d9f17 100644 --- a/modules/@angular/platform-browser/src/dom/events/key_events.ts +++ b/modules/@angular/platform-browser/src/dom/events/key_events.ts @@ -6,8 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, NgZone} from '@angular/core'; +import {Inject, Injectable, NgZone} from '@angular/core'; + import {getDOM} from '../dom_adapter'; +import {DOCUMENT} from '../dom_tokens'; + import {EventManagerPlugin} from './event_manager'; const MODIFIER_KEYS = ['alt', 'control', 'meta', 'shift']; @@ -23,7 +26,7 @@ const MODIFIER_KEY_GETTERS: {[key: string]: (event: KeyboardEvent) => boolean} = */ @Injectable() export class KeyEventsPlugin extends EventManagerPlugin { - constructor() { super(); } + constructor(@Inject(DOCUMENT) doc: any) { super(doc); } supports(eventName: string): boolean { return KeyEventsPlugin.parseEventName(eventName) != null; } diff --git a/modules/@angular/platform-browser/src/security/dom_sanitization_service.ts b/modules/@angular/platform-browser/src/security/dom_sanitization_service.ts index 8fa0aa6d35..c7c5801b4f 100644 --- a/modules/@angular/platform-browser/src/security/dom_sanitization_service.ts +++ b/modules/@angular/platform-browser/src/security/dom_sanitization_service.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, Sanitizer, SecurityContext} from '@angular/core'; +import {Inject, Injectable, Sanitizer, SecurityContext} from '@angular/core'; + +import {DOCUMENT} from '../dom/dom_tokens'; import {sanitizeHtml} from './html_sanitizer'; import {sanitizeStyle} from './style_sanitizer'; @@ -15,6 +17,7 @@ import {sanitizeUrl} from './url_sanitizer'; export {SecurityContext}; + /** * Marker interface for a value that's safe to use in a particular context. * @@ -147,6 +150,8 @@ export abstract class DomSanitizer implements Sanitizer { @Injectable() export class DomSanitizerImpl extends DomSanitizer { + constructor(@Inject(DOCUMENT) private _doc: any) { super(); } + sanitize(ctx: SecurityContext, value: any): string { if (value == null) return null; switch (ctx) { @@ -155,7 +160,7 @@ export class DomSanitizerImpl extends DomSanitizer { case SecurityContext.HTML: if (value instanceof SafeHtmlImpl) return value.changingThisBreaksApplicationSecurity; this.checkNotSafeValue(value, 'HTML'); - return sanitizeHtml(String(value)); + return sanitizeHtml(this._doc, String(value)); case SecurityContext.STYLE: if (value instanceof SafeStyleImpl) return value.changingThisBreaksApplicationSecurity; this.checkNotSafeValue(value, 'Style'); diff --git a/modules/@angular/platform-browser/src/security/html_sanitizer.ts b/modules/@angular/platform-browser/src/security/html_sanitizer.ts index b657f27124..1e4db7db28 100644 --- a/modules/@angular/platform-browser/src/security/html_sanitizer.ts +++ b/modules/@angular/platform-browser/src/security/html_sanitizer.ts @@ -9,6 +9,7 @@ import {isDevMode} from '@angular/core'; import {DomAdapter, getDOM} from '../dom/dom_adapter'; +import {DOCUMENT} from '../dom/dom_tokens'; import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer'; @@ -243,7 +244,7 @@ function stripCustomNsAttrs(el: Element) { * Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to * the DOM in a browser environment. */ -export function sanitizeHtml(unsafeHtmlInput: string): string { +export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string { try { const containerEl = getInertElement(); // Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime). @@ -262,7 +263,7 @@ export function sanitizeHtml(unsafeHtmlInput: string): string { unsafeHtml = parsedHtml; DOM.setInnerHTML(containerEl, unsafeHtml); - if ((DOM.defaultDoc() as any).documentMode) { + if (defaultDoc.documentMode) { // strip custom-namespaced attributes on IE<=11 stripCustomNsAttrs(containerEl); } diff --git a/modules/@angular/platform-browser/test/browser/meta_spec.ts b/modules/@angular/platform-browser/test/browser/meta_spec.ts index 46858ccb01..ce2dcdfba2 100644 --- a/modules/@angular/platform-browser/test/browser/meta_spec.ts +++ b/modules/@angular/platform-browser/test/browser/meta_spec.ts @@ -14,9 +14,8 @@ import {expect} from '@angular/platform-browser/testing/matchers'; export function main() { describe('Meta service', () => { - - const metaService: Meta = new Meta(getDOM()); - const doc: HTMLDocument = getDOM().defaultDoc(); + const doc: HTMLDocument = getDOM().createHtmlDocument(); + const metaService: Meta = new Meta(doc); let defaultMeta: HTMLMetaElement; beforeEach(() => { diff --git a/modules/@angular/platform-browser/test/browser/title_spec.ts b/modules/@angular/platform-browser/test/browser/title_spec.ts index 5a843eecff..4bf89ff636 100644 --- a/modules/@angular/platform-browser/test/browser/title_spec.ts +++ b/modules/@angular/platform-browser/test/browser/title_spec.ts @@ -14,23 +14,24 @@ import {expect} from '@angular/platform-browser/testing/matchers'; export function main() { describe('title service', () => { - const initialTitle = getDOM().getTitle(); - const titleService = new Title(); + const doc = getDOM().createHtmlDocument(); + const initialTitle = getDOM().getTitle(doc); + const titleService = new Title(doc); - afterEach(() => { getDOM().setTitle(initialTitle); }); + afterEach(() => { getDOM().setTitle(doc, initialTitle); }); it('should allow reading initial title', () => { expect(titleService.getTitle()).toEqual(initialTitle); }); it('should set a title on the injected document', () => { titleService.setTitle('test title'); - expect(getDOM().getTitle()).toEqual('test title'); + expect(getDOM().getTitle(doc)).toEqual('test title'); expect(titleService.getTitle()).toEqual('test title'); }); it('should reset title to empty string if title not provided', () => { titleService.setTitle(null); - expect(getDOM().getTitle()).toEqual(''); + expect(getDOM().getTitle(doc)).toEqual(''); }); }); diff --git a/modules/@angular/platform-browser/test/dom/events/event_manager_spec.ts b/modules/@angular/platform-browser/test/dom/events/event_manager_spec.ts index 4f6bf70649..8266961d6b 100644 --- a/modules/@angular/platform-browser/test/dom/events/event_manager_spec.ts +++ b/modules/@angular/platform-browser/test/dom/events/event_manager_spec.ts @@ -15,16 +15,20 @@ import {el} from '../../../testing/browser_util'; export function main() { let domEventPlugin: DomEventsPlugin; + let doc: any; describe('EventManager', () => { - beforeEach(() => { domEventPlugin = new DomEventsPlugin(); }); + beforeEach(() => { + doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument(); + domEventPlugin = new DomEventsPlugin(doc); + }); it('should delegate event bindings to plugins that are passed in from the most generic one to the most specific one', () => { const element = el('
'); const handler = (e: any /** TODO #9100 */) => e; - const plugin = new FakeEventManagerPlugin(['click']); + const plugin = new FakeEventManagerPlugin(doc, ['click']); const manager = new EventManager([domEventPlugin, plugin], new FakeNgZone()); manager.addEventListener(element, 'click', handler); expect(plugin.eventHandler['click']).toBe(handler); @@ -34,8 +38,8 @@ export function main() { const element = el('
'); const clickHandler = (e: any /** TODO #9100 */) => e; const dblClickHandler = (e: any /** TODO #9100 */) => e; - const plugin1 = new FakeEventManagerPlugin(['dblclick']); - const plugin2 = new FakeEventManagerPlugin(['click', 'dblclick']); + const plugin1 = new FakeEventManagerPlugin(doc, ['dblclick']); + const plugin2 = new FakeEventManagerPlugin(doc, ['click', 'dblclick']); const manager = new EventManager([plugin2, plugin1], new FakeNgZone()); manager.addEventListener(element, 'click', clickHandler); manager.addEventListener(element, 'dblclick', dblClickHandler); @@ -45,7 +49,7 @@ export function main() { it('should throw when no plugin can handle the event', () => { const element = el('
'); - const plugin = new FakeEventManagerPlugin(['dblclick']); + const plugin = new FakeEventManagerPlugin(doc, ['dblclick']); const manager = new EventManager([plugin], new FakeNgZone()); expect(() => manager.addEventListener(element, 'click', null)) .toThrowError('No event manager plugin found for event click'); @@ -54,7 +58,7 @@ export function main() { it('events are caught when fired from a child', () => { const element = el('
'); // Workaround for https://bugs.webkit.org/show_bug.cgi?id=122755 - getDOM().appendChild(getDOM().defaultDoc().body, element); + getDOM().appendChild(doc.body, element); const child = getDOM().firstChild(element); const dispatchedEvent = getDOM().createMouseEvent('click'); @@ -69,7 +73,7 @@ export function main() { it('should add and remove global event listeners', () => { const element = el('
'); - getDOM().appendChild(getDOM().defaultDoc().body, element); + getDOM().appendChild(doc.body, element); const dispatchedEvent = getDOM().createMouseEvent('click'); let receivedEvent: any /** TODO #9100 */ = null; const handler = (e: any /** TODO #9100 */) => { receivedEvent = e; }; @@ -91,7 +95,7 @@ export function main() { class FakeEventManagerPlugin extends EventManagerPlugin { eventHandler: {[event: string]: Function} = {}; - constructor(public supportedEvents: string[]) { super(); } + constructor(doc: any, public supportedEvents: string[]) { super(doc); } supports(eventName: string): boolean { return this.supportedEvents.indexOf(eventName) > -1; } diff --git a/modules/@angular/platform-browser/test/dom/events/hammer_gestures_spec.ts b/modules/@angular/platform-browser/test/dom/events/hammer_gestures_spec.ts index 84d2a1adfb..f1db6434b0 100644 --- a/modules/@angular/platform-browser/test/dom/events/hammer_gestures_spec.ts +++ b/modules/@angular/platform-browser/test/dom/events/hammer_gestures_spec.ts @@ -12,7 +12,7 @@ export function main() { describe('HammerGesturesPlugin', () => { it('should implement addGlobalEventListener', () => { - const plugin = new HammerGesturesPlugin(new HammerGestureConfig()); + const plugin = new HammerGesturesPlugin(document, new HammerGestureConfig()); spyOn(plugin, 'addEventListener').and.callFake(() => {}); diff --git a/modules/@angular/platform-browser/test/dom/events/key_events_spec.ts b/modules/@angular/platform-browser/test/dom/events/key_events_spec.ts index d8b4de8888..3cf2b1d24d 100644 --- a/modules/@angular/platform-browser/test/dom/events/key_events_spec.ts +++ b/modules/@angular/platform-browser/test/dom/events/key_events_spec.ts @@ -59,7 +59,7 @@ export function main() { }); it('should implement addGlobalEventListener', () => { - const plugin = new KeyEventsPlugin(); + const plugin = new KeyEventsPlugin(document); spyOn(plugin, 'addEventListener').and.callFake(() => {}); diff --git a/modules/@angular/platform-browser/test/security/dom_sanitization_service_spec.ts b/modules/@angular/platform-browser/test/security/dom_sanitization_service_spec.ts index c3b83d7ddb..e51d688e4a 100644 --- a/modules/@angular/platform-browser/test/security/dom_sanitization_service_spec.ts +++ b/modules/@angular/platform-browser/test/security/dom_sanitization_service_spec.ts @@ -14,7 +14,7 @@ import {DomSanitizerImpl} from '../../src/security/dom_sanitization_service'; export function main() { t.describe('DOM Sanitization Service', () => { t.it('accepts resource URL values for resource contexts', () => { - const svc = new DomSanitizerImpl(); + const svc = new DomSanitizerImpl(null); const resourceUrl = svc.bypassSecurityTrustResourceUrl('http://hello/world'); t.expect(svc.sanitize(SecurityContext.URL, resourceUrl)).toBe('http://hello/world'); }); diff --git a/modules/@angular/platform-browser/test/security/html_sanitizer_spec.ts b/modules/@angular/platform-browser/test/security/html_sanitizer_spec.ts index 309a213b65..04064a1c7b 100644 --- a/modules/@angular/platform-browser/test/security/html_sanitizer_spec.ts +++ b/modules/@angular/platform-browser/test/security/html_sanitizer_spec.ts @@ -14,10 +14,12 @@ import {sanitizeHtml} from '../../src/security/html_sanitizer'; export function main() { t.describe('HTML sanitizer', () => { + let defaultDoc: any; let originalLog: (msg: any) => any = null; let logMsgs: string[]; t.beforeEach(() => { + defaultDoc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument(); logMsgs = []; originalLog = getDOM().log; // Monkey patch DOM.log. getDOM().log = (msg) => logMsgs.push(msg); @@ -25,52 +27,55 @@ export function main() { t.afterEach(() => { getDOM().log = originalLog; }); t.it('serializes nested structures', () => { - t.expect(sanitizeHtml('

a

bcde
')) + t.expect(sanitizeHtml(defaultDoc, '

a

bcde
')) .toEqual('

a

bcde
'); t.expect(logMsgs).toEqual([]); }); t.it('serializes self closing elements', () => { - t.expect(sanitizeHtml('

Hello
World

')).toEqual('

Hello
World

'); + t.expect(sanitizeHtml(defaultDoc, '

Hello
World

')) + .toEqual('

Hello
World

'); }); t.it('supports namespaced elements', () => { - t.expect(sanitizeHtml('abc')).toEqual('abc'); + t.expect(sanitizeHtml(defaultDoc, 'abc')).toEqual('abc'); }); t.it('supports namespaced attributes', () => { - t.expect(sanitizeHtml('t')) + t.expect(sanitizeHtml(defaultDoc, 't')) .toEqual('t'); - t.expect(sanitizeHtml('t')).toEqual('t'); - t.expect(sanitizeHtml('t')) + t.expect(sanitizeHtml(defaultDoc, 't')).toEqual('t'); + t.expect(sanitizeHtml(defaultDoc, 't')) .toEqual('t'); }); t.it('supports HTML5 elements', () => { - t.expect(sanitizeHtml('
Works
')) + t.expect(sanitizeHtml(defaultDoc, '
Works
')) .toEqual('
Works
'); }); t.it('sanitizes srcset attributes', () => { - t.expect(sanitizeHtml('')) + t.expect(sanitizeHtml(defaultDoc, '')) .toEqual(''); }); t.it('supports sanitizing plain text', () => { - t.expect(sanitizeHtml('Hello, World')).toEqual('Hello, World'); + t.expect(sanitizeHtml(defaultDoc, 'Hello, World')).toEqual('Hello, World'); }); t.it('ignores non-element, non-attribute nodes', () => { - t.expect(sanitizeHtml('no.')).toEqual('no.'); - t.expect(sanitizeHtml('no.')).toEqual('no.'); + t.expect(sanitizeHtml(defaultDoc, 'no.')).toEqual('no.'); + t.expect(sanitizeHtml(defaultDoc, 'no.')).toEqual('no.'); t.expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/); }); t.it('supports sanitizing escaped entities', () => { - t.expect(sanitizeHtml('🚀')).toEqual('🚀'); + t.expect(sanitizeHtml(defaultDoc, '🚀')).toEqual('🚀'); t.expect(logMsgs).toEqual([]); }); t.it('does not warn when just re-encoding text', () => { - t.expect(sanitizeHtml('

Hellö Wörld

')).toEqual('

Hellö Wörld

'); + t.expect(sanitizeHtml(defaultDoc, '

Hellö Wörld

')) + .toEqual('

Hellö Wörld

'); t.expect(logMsgs).toEqual([]); }); t.it('escapes entities', () => { - t.expect(sanitizeHtml('

Hello < World

')).toEqual('

Hello < World

'); - t.expect(sanitizeHtml('

Hello < World

')).toEqual('

Hello < World

'); - t.expect(sanitizeHtml('

Hello

')) + t.expect(sanitizeHtml(defaultDoc, '

Hello < World

')) + .toEqual('

Hello < World

'); + t.expect(sanitizeHtml(defaultDoc, '

Hello < World

')).toEqual('

Hello < World

'); + t.expect(sanitizeHtml(defaultDoc, '

Hello

')) .toEqual('

Hello

'); // NB: quote encoded as ASCII ". }); t.describe('should strip dangerous elements', () => { @@ -80,11 +85,12 @@ export function main() { ]; for (const tag of dangerousTags) { - t.it( - `${tag}`, () => { t.expect(sanitizeHtml(`<${tag}>evil!`)).toEqual('evil!'); }); + t.it(`${tag}`, () => { + t.expect(sanitizeHtml(defaultDoc, `<${tag}>evil!`)).toEqual('evil!'); + }); } t.it(`swallows frame entirely`, () => { - t.expect(sanitizeHtml(`evil!`)).not.toContain(''); + t.expect(sanitizeHtml(defaultDoc, `evil!`)).not.toContain(''); }); }); t.describe('should strip dangerous attributes', () => { @@ -92,14 +98,14 @@ export function main() { for (const attr of dangerousAttrs) { t.it(`${attr}`, () => { - t.expect(sanitizeHtml(`evil!`)).toEqual('evil!'); + t.expect(sanitizeHtml(defaultDoc, `evil!`)).toEqual('evil!'); }); } }); if (browserDetection.isWebkit) { t.it('should prevent mXSS attacks', function() { - t.expect(sanitizeHtml('CLICKME')) + t.expect(sanitizeHtml(defaultDoc, 'CLICKME')) .toEqual('CLICKME'); }); } diff --git a/modules/@angular/platform-server/src/location.ts b/modules/@angular/platform-server/src/location.ts index 56dafb10a6..b91a0c3f8c 100644 --- a/modules/@angular/platform-server/src/location.ts +++ b/modules/@angular/platform-server/src/location.ts @@ -7,7 +7,8 @@ */ import {LocationChangeEvent, LocationChangeListener, PlatformLocation} from '@angular/common'; -import {Injectable} from '@angular/core'; +import {Inject, Injectable} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; import {Subject} from 'rxjs/Subject'; import * as url from 'url'; @@ -27,7 +28,9 @@ export class ServerPlatformLocation implements PlatformLocation { private _hash: string = ''; private _hashUpdate = new Subject(); - getBaseHrefFromDOM(): string { return getDOM().getBaseHref(); } + constructor(@Inject(DOCUMENT) private _doc: any) {} + + getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc); } onPopState(fn: LocationChangeListener): void { // No-op: a state stack is not implemented, so diff --git a/modules/@angular/platform-server/src/parse5_adapter.ts b/modules/@angular/platform-server/src/parse5_adapter.ts index 473a180c49..4542e6e9cc 100644 --- a/modules/@angular/platform-server/src/parse5_adapter.ts +++ b/modules/@angular/platform-server/src/parse5_adapter.ts @@ -22,14 +22,20 @@ const _attrToPropMap: {[key: string]: string} = { 'tabindex': 'tabIndex', }; -let defDoc: any = null; - const mapProps = ['attribs', 'x-attribsNamespace', 'x-attribsPrefix']; function _notImplemented(methodName: string) { return new Error('This method is not implemented in Parse5DomAdapter: ' + methodName); } +/** + * Parses a document string to a Document object. + */ +export function parseDocument(html: string) { + return parse5.parse(html, {treeAdapter: parse5.treeAdapters.htmlparser2}); +} + + /* tslint:disable:requireParameterType */ /** * A `DomAdapter` powered by the `parse5` NodeJS module. @@ -72,7 +78,6 @@ export class Parse5DomAdapter extends DomAdapter { get attrToPropMap() { return _attrToPropMap; } - query(selector: any) { throw _notImplemented('query'); } querySelector(el: any, selector: string): any { return this.querySelectorAll(el, selector)[0]; } querySelectorAll(el: any, selector: string): any[] { const res: any[] = []; @@ -468,7 +473,7 @@ export class Parse5DomAdapter extends DomAdapter { } createHtmlDocument(): Document { const newDoc = treeAdapter.createDocument(); - newDoc.title = 'fake title'; + newDoc.title = 'fakeTitle'; const head = treeAdapter.createElement('head', null, []); const body = treeAdapter.createElement('body', 'http://www.w3.org/1999/xhtml', []); this.appendChild(newDoc, head); @@ -478,10 +483,9 @@ export class Parse5DomAdapter extends DomAdapter { newDoc['_window'] = {}; return newDoc; } - defaultDoc(): Document { return defDoc = defDoc || this.createHtmlDocument(); } getBoundingClientRect(el: any): any { return {left: 0, top: 0, width: 0, height: 0}; } - getTitle(): string { return this.defaultDoc().title || ''; } - setTitle(newTitle: string) { this.defaultDoc().title = newTitle; } + getTitle(doc: Document): string { return doc.title || ''; } + setTitle(doc: Document, newTitle: string) { doc.title = newTitle; } isTemplateElement(el: any): boolean { return this.isElementNode(el) && this.tagName(el) === 'template'; } @@ -538,17 +542,17 @@ export class Parse5DomAdapter extends DomAdapter { } supportsDOMEvents(): boolean { return false; } supportsNativeShadowDOM(): boolean { return false; } - getGlobalEventTarget(target: string): any { + getGlobalEventTarget(doc: Document, target: string): any { if (target == 'window') { - return (this.defaultDoc())._window; + return (doc)._window; } else if (target == 'document') { - return this.defaultDoc(); + return doc; } else if (target == 'body') { - return this.defaultDoc().body; + return doc.body; } } - getBaseHref(): string { - const base = this.querySelector(this.defaultDoc(), 'base'); + getBaseHref(doc: Document): string { + const base = this.querySelector(doc, 'base'); let href = ''; if (base) { href = this.getHref(base); diff --git a/modules/@angular/platform-server/src/platform-server.ts b/modules/@angular/platform-server/src/platform-server.ts index 31c1d80ec1..a054afa842 100644 --- a/modules/@angular/platform-server/src/platform-server.ts +++ b/modules/@angular/platform-server/src/platform-server.ts @@ -6,6 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -export {ServerModule, platformDynamicServer, platformServer} from './server'; +export {PlatformState} from './platform_state'; +export {INITIAL_CONFIG, ServerModule, platformDynamicServer, platformServer} from './server'; +export {renderModule, renderModuleFactory} from './utils'; + export * from './private_export'; export {VERSION} from './version'; diff --git a/modules/@angular/platform-server/src/platform_state.ts b/modules/@angular/platform-server/src/platform_state.ts new file mode 100644 index 0000000000..aaaaff5b30 --- /dev/null +++ b/modules/@angular/platform-server/src/platform_state.ts @@ -0,0 +1,34 @@ +/** + * @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 + */ + +const parse5 = require('parse5'); + +import {Injectable, Inject} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; + +import {getDOM} from './private_import_platform-browser'; + +/** + * Representation of the current platform state. + * + * @experimental + */ +@Injectable() +export class PlatformState { + constructor(@Inject(DOCUMENT) private _doc: any) {} + + /** + * Renders the current state of the platform to string. + */ + renderToString(): string { return getDOM().getInnerHTML(this._doc); } + + /** + * Returns the current DOM state. + */ + getDocument(): any { return this._doc; } +} \ No newline at end of file diff --git a/modules/@angular/platform-server/src/server.ts b/modules/@angular/platform-server/src/server.ts index db302546b6..da9f68574f 100644 --- a/modules/@angular/platform-server/src/server.ts +++ b/modules/@angular/platform-server/src/server.ts @@ -8,13 +8,14 @@ import {PlatformLocation} from '@angular/common'; import {platformCoreDynamic} from '@angular/compiler'; -import {Injectable, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; -import {BrowserModule} from '@angular/platform-browser'; +import {Injectable, InjectionToken, Injector, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; +import {BrowserModule, DOCUMENT} from '@angular/platform-browser'; import {ServerPlatformLocation} from './location'; -import {Parse5DomAdapter} from './parse5_adapter'; +import {Parse5DomAdapter, parseDocument} from './parse5_adapter'; +import {PlatformState} from './platform_state'; import {DebugDomRootRenderer} from './private_import_core'; -import {DomAdapter, SharedStylesHost} from './private_import_platform-browser'; +import {SharedStylesHost, getDOM} from './private_import_platform-browser'; import {ServerRootRenderer} from './server_renderer'; @@ -23,15 +24,16 @@ function notSupported(feature: string): Error { } export const INTERNAL_SERVER_PLATFORM_PROVIDERS: Array = [ - {provide: PLATFORM_INITIALIZER, useValue: initParse5Adapter, multi: true}, + {provide: DOCUMENT, useFactory: _document, deps: [Injector]}, + {provide: PLATFORM_INITIALIZER, useFactory: initParse5Adapter, multi: true, deps: [Injector]}, {provide: PlatformLocation, useClass: ServerPlatformLocation}, + PlatformState, ]; -function initParse5Adapter() { - Parse5DomAdapter.makeCurrent(); +function initParse5Adapter(injector: Injector) { + return () => { Parse5DomAdapter.makeCurrent(); }; } - export function _createConditionalRootRenderer(rootRenderer: any) { if (isDevMode()) { return new DebugDomRootRenderer(rootRenderer); @@ -46,15 +48,46 @@ export const SERVER_RENDER_PROVIDERS: Provider[] = [ SharedStylesHost ]; +/** + * Config object passed to initialize the platform. + * + * @experimental + */ +export interface PlatformConfig { + document?: string; + url?: string; +} + +/** + * The DI token for setting the initial config for the platform. + * + * @experimental + */ +export const INITIAL_CONFIG = new InjectionToken('Server.INITIAL_CONFIG'); + /** * The ng module for the server. * * @experimental */ -@NgModule({exports: [BrowserModule], providers: SERVER_RENDER_PROVIDERS}) +@NgModule({ + exports: [BrowserModule], + providers: [ + SERVER_RENDER_PROVIDERS, + ] +}) export class ServerModule { } +function _document(injector: Injector) { + let config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null); + if (config && config.document) { + return parseDocument(config.document); + } else { + return getDOM().createHtmlDocument(); + } +} + /** * @experimental */ diff --git a/modules/@angular/platform-server/src/server_renderer.ts b/modules/@angular/platform-server/src/server_renderer.ts index 5a09ff65c4..eb26230be7 100644 --- a/modules/@angular/platform-server/src/server_renderer.ts +++ b/modules/@angular/platform-server/src/server_renderer.ts @@ -136,7 +136,7 @@ export class ServerRenderer implements Renderer { } listenGlobal(target: string, name: string, callback: Function): Function { - const renderElement = getDOM().getGlobalEventTarget(target); + const renderElement = getDOM().getGlobalEventTarget(this._rootRenderer.document, target); return this.listen(renderElement, name, callback); } diff --git a/modules/@angular/platform-server/src/utils.ts b/modules/@angular/platform-server/src/utils.ts new file mode 100644 index 0000000000..c86ad3203b --- /dev/null +++ b/modules/@angular/platform-server/src/utils.ts @@ -0,0 +1,71 @@ +/** + * @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 {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Type, destroyPlatform} from '@angular/core'; +import {filter} from 'rxjs/operator/filter'; +import {first} from 'rxjs/operator/first'; +import {toPromise} from 'rxjs/operator/toPromise'; + +import {PlatformState} from './platform_state'; +import {INITIAL_CONFIG, platformDynamicServer, platformServer} from './server'; + +const parse5 = require('parse5'); + +export interface PlatformOptions { + document?: string; + url?: string; + extraProviders?: Provider[]; +} + +function _getPlatform( + platformFactory: (extraProviders: Provider[]) => PlatformRef, + options: PlatformOptions): PlatformRef { + const extraProviders = options.extraProviders ? options.extraProviders : []; + return platformFactory([ + {provide: INITIAL_CONFIG, useValue: {document: options.document, url: options.url}}, + extraProviders + ]); +} + +function _render( + platform: PlatformRef, moduleRefPromise: Promise>): Promise { + return moduleRefPromise.then((moduleRef) => { + const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); + return toPromise + .call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable))) + .then(() => { + const output = platform.injector.get(PlatformState).renderToString(); + destroyPlatform(); + return output; + }); + }); +} + +/** + * Renders a Module to string. + * + * Do not use this in a production server environment. Use pre-compiled {@link NgModuleFactory} with + * {link renderModuleFactory} instead. + * + * @experimental + */ +export function renderModule(module: Type, options: PlatformOptions): Promise { + const platform = _getPlatform(platformDynamicServer, options); + return _render(platform, platform.bootstrapModule(module)); +} + +/** + * Renders a {@link NgModuleFactory} to string. + * + * @experimental + */ +export function renderModuleFactory( + moduleFactory: NgModuleFactory, options: PlatformOptions): Promise { + const platform = _getPlatform(platformServer, options); + return _render(platform, platform.bootstrapModuleFactory(moduleFactory)); +} diff --git a/modules/@angular/platform-server/test/integration_spec.ts b/modules/@angular/platform-server/test/integration_spec.ts index e7938515b7..bc4b80427b 100644 --- a/modules/@angular/platform-server/test/integration_spec.ts +++ b/modules/@angular/platform-server/test/integration_spec.ts @@ -7,19 +7,15 @@ */ import {PlatformLocation} from '@angular/common'; -import {Component, NgModule, destroyPlatform} from '@angular/core'; -import {async} from '@angular/core/testing'; +import {ApplicationRef, CompilerFactory, Component, NgModule, NgModuleRef, PlatformRef, destroyPlatform, getPlatform} from '@angular/core'; +import {async, inject} from '@angular/core/testing'; +import {DOCUMENT} from '@angular/platform-browser'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {ServerModule, platformDynamicServer} from '@angular/platform-server'; - -function writeBody(html: string): any { - const dom = getDOM(); - const doc = dom.defaultDoc(); - const body = dom.querySelector(doc, 'body'); - dom.setInnerHTML(body, html); - return body; -} - +import {INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server'; +import {Subscription} from 'rxjs/Subscription'; +import {filter} from 'rxjs/operator/filter'; +import {first} from 'rxjs/operator/first'; +import {toPromise} from 'rxjs/operator/toPromise'; @Component({selector: 'app', template: `Works!`}) class MyServerApp { @@ -38,43 +34,117 @@ export function main() { afterEach(() => destroyPlatform()); it('should bootstrap', async(() => { - const body = writeBody(''); - platformDynamicServer().bootstrapModule(ExampleModule).then(() => { - expect(getDOM().getText(body)).toEqual('Works!'); - }); + platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]) + .bootstrapModule(ExampleModule) + .then((moduleRef) => { + const doc = moduleRef.injector.get(DOCUMENT); + expect(getDOM().getText(doc)).toEqual('Works!'); + }); })); describe('PlatformLocation', () => { it('is injectable', () => { - const body = writeBody(''); - platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => { - const location: PlatformLocation = appRef.injector.get(PlatformLocation); - expect(location.pathname).toBe('/'); - }); + platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]) + .bootstrapModule(ExampleModule) + .then(appRef => { + const location: PlatformLocation = appRef.injector.get(PlatformLocation); + expect(location.pathname).toBe('/'); + }); }); it('pushState causes the URL to update', () => { - const body = writeBody(''); - platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => { - const location: PlatformLocation = appRef.injector.get(PlatformLocation); - location.pushState(null, 'Test', '/foo#bar'); - expect(location.pathname).toBe('/foo'); - expect(location.hash).toBe('#bar'); - }); + platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]) + .bootstrapModule(ExampleModule) + .then(appRef => { + const location: PlatformLocation = appRef.injector.get(PlatformLocation); + location.pushState(null, 'Test', '/foo#bar'); + expect(location.pathname).toBe('/foo'); + expect(location.hash).toBe('#bar'); + }); }); it('allows subscription to the hash state', done => { - const body = writeBody(''); - platformDynamicServer().bootstrapModule(ExampleModule).then(appRef => { - const location: PlatformLocation = appRef.injector.get(PlatformLocation); - expect(location.pathname).toBe('/'); - location.onHashChange((e: any) => { - expect(e.type).toBe('hashchange'); - expect(e.oldUrl).toBe('/'); - expect(e.newUrl).toBe('/foo#bar'); - done(); - }); - location.pushState(null, 'Test', '/foo#bar'); - }); + platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: ''}}]) + .bootstrapModule(ExampleModule) + .then(appRef => { + const location: PlatformLocation = appRef.injector.get(PlatformLocation); + expect(location.pathname).toBe('/'); + location.onHashChange((e: any) => { + expect(e.type).toBe('hashchange'); + expect(e.oldUrl).toBe('/'); + expect(e.newUrl).toBe('/foo#bar'); + done(); + }); + location.pushState(null, 'Test', '/foo#bar'); + }); }); }); }); + + describe('Platform Server', () => { + @Component({selector: 'app', template: '{{text}}'}) + class MyAsyncServerApp { + text = ''; + + ngOnInit() { + Promise.resolve(null).then(() => setTimeout(() => { this.text = 'Works!'; }, 10)); + } + } + + @NgModule( + {declarations: [MyAsyncServerApp], imports: [ServerModule], bootstrap: [MyAsyncServerApp]}) + class AsyncServerModule { + } + + let doc: string; + let called: boolean; + let expectedOutput = + 'Works!'; + + beforeEach(() => { + destroyPlatform(); + // PlatformConfig takes in a parsed document so that it can be cached across requests. + doc = ''; + called = false; + }); + afterEach(() => { + expect(called).toBe(true); + // Platform should have been destroyed at the end of rendering. + expect(getPlatform()).toBeNull(); + }); + + it('PlatformState should render to string (Long form rendering)', async(() => { + const platform = + platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: doc}}]); + + platform.bootstrapModule(AsyncServerModule) + .then((moduleRef) => { + const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); + return toPromise.call(first.call( + filter.call(applicationRef.isStable, (isStable: boolean) => isStable))); + }) + .then((b) => { + expect(platform.injector.get(PlatformState).renderToString()).toBe(expectedOutput); + destroyPlatform(); + called = true; + }); + })); + + it('renderModule should render to string (short form rendering)', async(() => { + renderModule(AsyncServerModule, {document: doc}).then(output => { + expect(output).toBe(expectedOutput); + called = true; + }); + })); + + it('renderModuleFactory should render to string (short form rendering)', + async(inject([PlatformRef], (defaultPlatform: PlatformRef) => { + const compilerFactory: CompilerFactory = + defaultPlatform.injector.get(CompilerFactory, null); + const moduleFactory = + compilerFactory.createCompiler().compileModuleSync(AsyncServerModule); + renderModuleFactory(moduleFactory, {document: doc}).then(output => { + expect(output).toBe(expectedOutput); + called = true; + }); + }))); + }); } diff --git a/modules/@angular/platform-webworker/src/web_workers/worker/worker_adapter.ts b/modules/@angular/platform-webworker/src/web_workers/worker/worker_adapter.ts index 3278c195e6..3834af9296 100644 --- a/modules/@angular/platform-webworker/src/web_workers/worker/worker_adapter.ts +++ b/modules/@angular/platform-webworker/src/web_workers/worker/worker_adapter.ts @@ -55,7 +55,6 @@ export class WorkerDomAdapter extends DomAdapter { set attrToPropMap(value: {[key: string]: string}) { throw 'not implemented'; } parse(templateHtml: string) { throw 'not implemented'; } - query(selector: string): any { throw 'not implemented'; } querySelector(el: any /** TODO #9100 */, selector: string): HTMLElement { throw 'not implemented'; } @@ -169,10 +168,9 @@ export class WorkerDomAdapter extends DomAdapter { } templateAwareRoot(el: any /** TODO #9100 */) { throw 'not implemented'; } createHtmlDocument(): HTMLDocument { throw 'not implemented'; } - defaultDoc(): HTMLDocument { throw 'not implemented'; } getBoundingClientRect(el: any /** TODO #9100 */) { throw 'not implemented'; } - getTitle(): string { throw 'not implemented'; } - setTitle(newTitle: string) { throw 'not implemented'; } + getTitle(doc: Document): string { throw 'not implemented'; } + setTitle(doc: Document, newTitle: string) { throw 'not implemented'; } elementMatches(n: any /** TODO #9100 */, selector: string): boolean { throw 'not implemented'; } isTemplateElement(el: any): boolean { throw 'not implemented'; } isTextNode(node: any /** TODO #9100 */): boolean { throw 'not implemented'; } @@ -189,10 +187,10 @@ export class WorkerDomAdapter extends DomAdapter { } supportsDOMEvents(): boolean { throw 'not implemented'; } supportsNativeShadowDOM(): boolean { throw 'not implemented'; } - getGlobalEventTarget(target: string): any { throw 'not implemented'; } + getGlobalEventTarget(doc: Document, target: string): any { throw 'not implemented'; } getHistory(): History { throw 'not implemented'; } getLocation(): Location { throw 'not implemented'; } - getBaseHref(): string { throw 'not implemented'; } + getBaseHref(doc: Document): string { throw 'not implemented'; } resetBaseElement(): void { throw 'not implemented'; } getUserAgent(): string { throw 'not implemented'; } setData(element: any /** TODO #9100 */, name: string, value: string) { throw 'not implemented'; } diff --git a/modules/@angular/platform-webworker/src/worker_app.ts b/modules/@angular/platform-webworker/src/worker_app.ts index 72ad03736f..27e3e7405e 100644 --- a/modules/@angular/platform-webworker/src/worker_app.ts +++ b/modules/@angular/platform-webworker/src/worker_app.ts @@ -8,6 +8,7 @@ import {CommonModule} from '@angular/common'; import {APP_INITIALIZER, ApplicationModule, ErrorHandler, NgModule, NgZone, PlatformRef, Provider, RootRenderer, createPlatformFactory, platformCore} from '@angular/core'; +import {DOCUMENT} from '@angular/platform-browser'; import {BROWSER_SANITIZATION_PROVIDERS} from './private_import_platform-browser'; import {ON_WEB_WORKER} from './web_workers/shared/api'; @@ -60,7 +61,7 @@ export function setupWebWorker(): void { */ @NgModule({ providers: [ - BROWSER_SANITIZATION_PROVIDERS, Serializer, + BROWSER_SANITIZATION_PROVIDERS, Serializer, {provide: DOCUMENT, useValue: null}, {provide: ClientMessageBrokerFactory, useClass: ClientMessageBrokerFactory_}, {provide: ServiceMessageBrokerFactory, useClass: ServiceMessageBrokerFactory_}, WebWorkerRootRenderer, {provide: RootRenderer, useExisting: WebWorkerRootRenderer}, diff --git a/modules/@angular/platform-webworker/src/worker_render.ts b/modules/@angular/platform-webworker/src/worker_render.ts index 3c8d103d70..c210a606df 100644 --- a/modules/@angular/platform-webworker/src/worker_render.ts +++ b/modules/@angular/platform-webworker/src/worker_render.ts @@ -135,7 +135,7 @@ function _exceptionHandler(): ErrorHandler { } function _document(): any { - return getDOM().defaultDoc(); + return document; } function createNgZone(): NgZone { diff --git a/modules/benchmarks/src/tree/ng2_ftl/app.ngfactory.ts b/modules/benchmarks/src/tree/ng2_ftl/app.ngfactory.ts index 4c36699c61..b079f02fa4 100644 --- a/modules/benchmarks/src/tree/ng2_ftl/app.ngfactory.ts +++ b/modules/benchmarks/src/tree/ng2_ftl/app.ngfactory.ts @@ -120,8 +120,8 @@ class AppModuleInjector extends import0.NgModuleInjector { get _EVENT_MANAGER_PLUGINS_15(): any[] { if ((this.__EVENT_MANAGER_PLUGINS_15 == (null as any))) { (this.__EVENT_MANAGER_PLUGINS_15 = [ - new import20.DomEventsPlugin(), new import21.KeyEventsPlugin(), - new import10.HammerGesturesPlugin(this._HAMMER_GESTURE_CONFIG_14) + new import20.DomEventsPlugin(document), new import21.KeyEventsPlugin(document), + new import10.HammerGesturesPlugin(document, this._HAMMER_GESTURE_CONFIG_14) ]); } return this.__EVENT_MANAGER_PLUGINS_15; @@ -163,7 +163,7 @@ class AppModuleInjector extends import0.NgModuleInjector { } get _DomSanitizer_21(): import14.DomSanitizerImpl { if ((this.__DomSanitizer_21 == (null as any))) { - (this.__DomSanitizer_21 = new import14.DomSanitizerImpl()); + (this.__DomSanitizer_21 = new import14.DomSanitizerImpl(document)); } return this.__DomSanitizer_21; } @@ -206,7 +206,7 @@ class AppModuleInjector extends import0.NgModuleInjector { } get _Title_27(): import16.Title { if ((this.__Title_27 == (null as any))) { - (this.__Title_27 = new import16.Title()); + (this.__Title_27 = new import16.Title(document)); } return this.__Title_27; } diff --git a/modules/benchmarks/src/tree/ng2_next/tree.ts b/modules/benchmarks/src/tree/ng2_next/tree.ts index cde503adb3..a68acea014 100644 --- a/modules/benchmarks/src/tree/ng2_next/tree.ts +++ b/modules/benchmarks/src/tree/ng2_next/tree.ts @@ -92,7 +92,7 @@ export class AppModule implements Injector { constructor() { initServicesIfNeeded(); - this.sanitizer = new DomSanitizerImpl(); + this.sanitizer = new DomSanitizerImpl(document); trustedEmptyColor = this.sanitizer.bypassSecurityTrustStyle(''); trustedGreyColor = this.sanitizer.bypassSecurityTrustStyle('grey'); this.componentFactory = createComponentFactory('#root', TreeComponent, TreeComponent_Host); diff --git a/modules/benchmarks/src/tree/ng2_static_ftl/app.ngfactory.ts b/modules/benchmarks/src/tree/ng2_static_ftl/app.ngfactory.ts index 9107df9387..356a8cd1f2 100644 --- a/modules/benchmarks/src/tree/ng2_static_ftl/app.ngfactory.ts +++ b/modules/benchmarks/src/tree/ng2_static_ftl/app.ngfactory.ts @@ -120,8 +120,8 @@ class AppModuleInjector extends import0.NgModuleInjector { get _EVENT_MANAGER_PLUGINS_15(): any[] { if ((this.__EVENT_MANAGER_PLUGINS_15 == (null as any))) { (this.__EVENT_MANAGER_PLUGINS_15 = [ - new import20.DomEventsPlugin(), new import21.KeyEventsPlugin(), - new import10.HammerGesturesPlugin(this._HAMMER_GESTURE_CONFIG_14) + new import20.DomEventsPlugin(document), new import21.KeyEventsPlugin(document), + new import10.HammerGesturesPlugin(document, this._HAMMER_GESTURE_CONFIG_14) ]); } return this.__EVENT_MANAGER_PLUGINS_15; @@ -163,7 +163,7 @@ class AppModuleInjector extends import0.NgModuleInjector { } get _DomSanitizer_21(): import14.DomSanitizerImpl { if ((this.__DomSanitizer_21 == (null as any))) { - (this.__DomSanitizer_21 = new import14.DomSanitizerImpl()); + (this.__DomSanitizer_21 = new import14.DomSanitizerImpl(document)); } return this.__DomSanitizer_21; } @@ -206,7 +206,7 @@ class AppModuleInjector extends import0.NgModuleInjector { } get _Title_27(): import16.Title { if ((this.__Title_27 == (null as any))) { - (this.__Title_27 = new import16.Title()); + (this.__Title_27 = new import16.Title(document)); } return this.__Title_27; } diff --git a/tools/public_api_guard/platform-browser/index.d.ts b/tools/public_api_guard/platform-browser/index.d.ts index 4fb87b358f..680cdcbd6b 100644 --- a/tools/public_api_guard/platform-browser/index.d.ts +++ b/tools/public_api_guard/platform-browser/index.d.ts @@ -60,7 +60,7 @@ export declare class HammerGestureConfig { /** @experimental */ export declare class Meta { - constructor(_dom: DomAdapter); + constructor(_doc: any); addTag(tag: MetaDefinition, forceCreation?: boolean): HTMLMetaElement; addTags(tags: MetaDefinition[], forceCreation?: boolean): HTMLMetaElement[]; getTag(attrSelector: string): HTMLMetaElement; @@ -117,6 +117,7 @@ export interface SafeUrl extends SafeValue { /** @experimental */ export declare class Title { + constructor(_doc: any); getTitle(): string; setTitle(newTitle: string): void; } diff --git a/tools/public_api_guard/platform-server/index.d.ts b/tools/public_api_guard/platform-server/index.d.ts index 525f763bef..de8e4ac8a8 100644 --- a/tools/public_api_guard/platform-server/index.d.ts +++ b/tools/public_api_guard/platform-server/index.d.ts @@ -1,9 +1,25 @@ +/** @experimental */ +export declare const INITIAL_CONFIG: InjectionToken; + /** @experimental */ export declare const platformDynamicServer: (extraProviders?: Provider[]) => PlatformRef; /** @experimental */ export declare const platformServer: (extraProviders?: Provider[]) => PlatformRef; +/** @experimental */ +export declare class PlatformState { + constructor(_doc: any); + getDocument(): any; + renderToString(): string; +} + +/** @experimental */ +export declare function renderModule(module: Type, options: PlatformOptions): Promise; + +/** @experimental */ +export declare function renderModuleFactory(moduleFactory: NgModuleFactory, options: PlatformOptions): Promise; + /** @experimental */ export declare class ServerModule { }