refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
15
packages/platform-server/.babelrc
Normal file
15
packages/platform-server/.babelrc
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
'@angular/core': 'ng.core',
|
||||
'@angular/common': 'ng.common',
|
||||
'@angular/compiler': 'ng.compiler',
|
||||
'@angular/platform-browser': 'ng.platformBrowser',
|
||||
'@angular/platform-server': 'ng.platformServer'
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/platform-server"
|
||||
}
|
18
packages/platform-server/.babelrc-testing
Normal file
18
packages/platform-server/.babelrc-testing
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
"@angular/core": "ng.core",
|
||||
"@angular/common": "ng.common",
|
||||
"@angular/compiler": "ng.compiler",
|
||||
"@angular/compiler/testing": "ng.compiler.testing",
|
||||
"@angular/platform-browser": "ng.platformBrowser",
|
||||
"@angular/platform-server": "ng.platformServer",
|
||||
"@angular/platform-server/testing": "ng.platformServer.testing",
|
||||
"@angular/platform-browser-dynamic/testing": "ng.platformBrowserDynamic.testing"
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/platform-server/testing"
|
||||
}
|
14
packages/platform-server/index.ts
Normal file
14
packages/platform-server/index.ts
Normal file
@ -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
|
||||
*/
|
||||
|
||||
// This file is not used to build this module. It is only used during editing
|
||||
// by the TypeScript language service and during build for verification. `ngc`
|
||||
// replaces this file with production index.ts when it rewrites private symbol
|
||||
// names.
|
||||
|
||||
export * from './public_api';
|
25
packages/platform-server/package.json
Normal file
25
packages/platform-server/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@angular/platform-server",
|
||||
"version": "0.0.0-PLACEHOLDER",
|
||||
"description": "Angular - library for using Angular in Node.js",
|
||||
"main": "./bundles/platform-server.umd.js",
|
||||
"module": "./@angular/platform-server.es5.js",
|
||||
"es2015": "./@angular/platform-server.js",
|
||||
"typings": "./typings/platform-server.d.ts",
|
||||
"author": "angular",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@angular/core": "0.0.0-PLACEHOLDER",
|
||||
"@angular/common": "0.0.0-PLACEHOLDER",
|
||||
"@angular/compiler": "0.0.0-PLACEHOLDER",
|
||||
"@angular/platform-browser": "0.0.0-PLACEHOLDER"
|
||||
},
|
||||
"dependencies": {
|
||||
"parse5": "^3.0.1",
|
||||
"xhr2": "^0.1.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/angular/angular.git"
|
||||
}
|
||||
}
|
16
packages/platform-server/public_api.ts
Normal file
16
packages/platform-server/public_api.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the platform-server package.
|
||||
*/
|
||||
export * from './src/platform-server';
|
||||
|
||||
// This file only reexports content of the `src` folder. Keep it that way.
|
127
packages/platform-server/src/http.ts
Normal file
127
packages/platform-server/src/http.ts
Normal file
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @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 xhr2: any = require('xhr2');
|
||||
|
||||
import {Injectable, Provider} from '@angular/core';
|
||||
import {BrowserXhr, Connection, ConnectionBackend, Http, ReadyState, Request, RequestOptions, Response, XHRBackend, XSRFStrategy} from '@angular/http';
|
||||
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Observer} from 'rxjs/Observer';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
|
||||
@Injectable()
|
||||
export class ServerXhr implements BrowserXhr {
|
||||
build(): XMLHttpRequest { return new xhr2.XMLHttpRequest(); }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ServerXsrfStrategy implements XSRFStrategy {
|
||||
configureRequest(req: Request): void {}
|
||||
}
|
||||
|
||||
export class ZoneMacroTaskConnection implements Connection {
|
||||
response: Observable<Response>;
|
||||
lastConnection: Connection;
|
||||
|
||||
constructor(public request: Request, backend: XHRBackend) {
|
||||
this.response = new Observable((observer: Observer<Response>) => {
|
||||
let task: Task = null;
|
||||
let scheduled: boolean = false;
|
||||
let sub: Subscription = null;
|
||||
let savedResult: any = null;
|
||||
let savedError: any = null;
|
||||
|
||||
const scheduleTask = (_task: Task) => {
|
||||
task = _task;
|
||||
scheduled = true;
|
||||
|
||||
this.lastConnection = backend.createConnection(request);
|
||||
sub = (this.lastConnection.response as Observable<Response>)
|
||||
.subscribe(
|
||||
res => savedResult = res,
|
||||
err => {
|
||||
if (!scheduled) {
|
||||
throw new Error('invoke twice');
|
||||
}
|
||||
savedError = err;
|
||||
scheduled = false;
|
||||
task.invoke();
|
||||
},
|
||||
() => {
|
||||
if (!scheduled) {
|
||||
throw new Error('invoke twice');
|
||||
}
|
||||
scheduled = false;
|
||||
task.invoke();
|
||||
});
|
||||
};
|
||||
|
||||
const cancelTask = (_task: Task) => {
|
||||
if (!scheduled) {
|
||||
return;
|
||||
}
|
||||
scheduled = false;
|
||||
if (sub) {
|
||||
sub.unsubscribe();
|
||||
sub = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onComplete = () => {
|
||||
if (savedError !== null) {
|
||||
observer.error(savedError);
|
||||
} else {
|
||||
observer.next(savedResult);
|
||||
observer.complete();
|
||||
}
|
||||
};
|
||||
|
||||
// MockBackend is currently synchronous, which means that if scheduleTask is by
|
||||
// scheduleMacroTask, the request will hit MockBackend and the response will be
|
||||
// sent, causing task.invoke() to be called.
|
||||
const _task = Zone.current.scheduleMacroTask(
|
||||
'ZoneMacroTaskConnection.subscribe', onComplete, {}, () => null, cancelTask);
|
||||
scheduleTask(_task);
|
||||
|
||||
return () => {
|
||||
if (scheduled && task) {
|
||||
task.zone.cancelTask(task);
|
||||
scheduled = false;
|
||||
}
|
||||
if (sub) {
|
||||
sub.unsubscribe();
|
||||
sub = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get readyState(): ReadyState {
|
||||
return !!this.lastConnection ? this.lastConnection.readyState : ReadyState.Unsent;
|
||||
}
|
||||
}
|
||||
|
||||
export class ZoneMacroTaskBackend implements ConnectionBackend {
|
||||
constructor(private backend: XHRBackend) {}
|
||||
|
||||
createConnection(request: any): ZoneMacroTaskConnection {
|
||||
return new ZoneMacroTaskConnection(request, this.backend);
|
||||
}
|
||||
}
|
||||
|
||||
export function httpFactory(xhrBackend: XHRBackend, options: RequestOptions) {
|
||||
const macroBackend = new ZoneMacroTaskBackend(xhrBackend);
|
||||
return new Http(macroBackend, options);
|
||||
}
|
||||
|
||||
export const SERVER_HTTP_PROVIDERS: Provider[] = [
|
||||
{provide: Http, useFactory: httpFactory, deps: [XHRBackend, RequestOptions]},
|
||||
{provide: BrowserXhr, useClass: ServerXhr},
|
||||
{provide: XSRFStrategy, useClass: ServerXsrfStrategy},
|
||||
];
|
93
packages/platform-server/src/location.ts
Normal file
93
packages/platform-server/src/location.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @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 {LocationChangeEvent, LocationChangeListener, PlatformLocation} from '@angular/common';
|
||||
import {Inject, Injectable, Optional} from '@angular/core';
|
||||
import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/platform-browser';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import * as url from 'url';
|
||||
import {INITIAL_CONFIG, PlatformConfig} from './tokens';
|
||||
|
||||
|
||||
function parseUrl(urlStr: string): {pathname: string, search: string, hash: string} {
|
||||
const parsedUrl = url.parse(urlStr);
|
||||
return {
|
||||
pathname: parsedUrl.pathname || '',
|
||||
search: parsedUrl.search || '',
|
||||
hash: parsedUrl.hash || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side implementation of URL state. Implements `pathname`, `search`, and `hash`
|
||||
* but not the state stack.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ServerPlatformLocation implements PlatformLocation {
|
||||
private _path: string = '/';
|
||||
private _search: string = '';
|
||||
private _hash: string = '';
|
||||
private _hashUpdate = new Subject<LocationChangeEvent>();
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private _doc: any, @Optional() @Inject(INITIAL_CONFIG) _config: any) {
|
||||
const config = _config as PlatformConfig | null;
|
||||
if (!!config && !!config.url) {
|
||||
const parsedUrl = parseUrl(config.url);
|
||||
this._path = parsedUrl.pathname;
|
||||
this._search = parsedUrl.search;
|
||||
this._hash = parsedUrl.hash;
|
||||
}
|
||||
}
|
||||
|
||||
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc); }
|
||||
|
||||
onPopState(fn: LocationChangeListener): void {
|
||||
// No-op: a state stack is not implemented, so
|
||||
// no events will ever come.
|
||||
}
|
||||
|
||||
onHashChange(fn: LocationChangeListener): void { this._hashUpdate.subscribe(fn); }
|
||||
|
||||
get pathname(): string { return this._path; }
|
||||
get search(): string { return this._search; }
|
||||
get hash(): string { return this._hash; }
|
||||
|
||||
get url(): string { return `${this.pathname}${this.search}${this.hash}`; }
|
||||
|
||||
private setHash(value: string, oldUrl: string) {
|
||||
if (this._hash === value) {
|
||||
// Don't fire events if the hash has not changed.
|
||||
return;
|
||||
}
|
||||
this._hash = value;
|
||||
const newUrl = this.url;
|
||||
scheduleMicroTask(
|
||||
() => this._hashUpdate.next({ type: 'hashchange', oldUrl, newUrl } as LocationChangeEvent));
|
||||
}
|
||||
|
||||
replaceState(state: any, title: string, newUrl: string): void {
|
||||
const oldUrl = this.url;
|
||||
const parsedUrl = parseUrl(newUrl);
|
||||
this._path = parsedUrl.pathname;
|
||||
this._search = parsedUrl.search;
|
||||
this.setHash(parsedUrl.hash, oldUrl);
|
||||
}
|
||||
|
||||
pushState(state: any, title: string, newUrl: string): void {
|
||||
this.replaceState(state, title, newUrl);
|
||||
}
|
||||
|
||||
forward(): void { throw new Error('Not implemented'); }
|
||||
|
||||
back(): void { throw new Error('Not implemented'); }
|
||||
}
|
||||
|
||||
export function scheduleMicroTask(fn: Function) {
|
||||
Zone.current.scheduleMicroTask('scheduleMicrotask', fn);
|
||||
}
|
787
packages/platform-server/src/parse5_adapter.ts
Normal file
787
packages/platform-server/src/parse5_adapter.ts
Normal file
@ -0,0 +1,787 @@
|
||||
/**
|
||||
* @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 {ɵglobal as global} from '@angular/core';
|
||||
import {ɵDomAdapter as DomAdapter, ɵsetRootDomAdapter as setRootDomAdapter, ɵsetValueOnPath as setValueOnPath} from '@angular/platform-browser';
|
||||
import {SelectorMatcher, CssSelector} from '@angular/compiler';
|
||||
|
||||
let treeAdapter: any;
|
||||
|
||||
const _attrToPropMap: {[key: string]: string} = {
|
||||
'class': 'className',
|
||||
'innerHtml': 'innerHTML',
|
||||
'readonly': 'readOnly',
|
||||
'tabindex': 'tabIndex',
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
* @security Tread carefully! Interacting with the DOM directly is dangerous and
|
||||
* can introduce XSS risks.
|
||||
*/
|
||||
export class Parse5DomAdapter extends DomAdapter {
|
||||
static makeCurrent() {
|
||||
treeAdapter = parse5.treeAdapters.htmlparser2;
|
||||
setRootDomAdapter(new Parse5DomAdapter());
|
||||
}
|
||||
|
||||
hasProperty(element: any, name: string): boolean {
|
||||
return _HTMLElementPropertyList.indexOf(name) > -1;
|
||||
}
|
||||
// TODO(tbosch): don't even call this method when we run the tests on server side
|
||||
// by not using the DomRenderer in tests. Keeping this for now to make tests happy...
|
||||
setProperty(el: any, name: string, value: any) {
|
||||
if (name === 'innerHTML') {
|
||||
this.setInnerHTML(el, value);
|
||||
} else if (name === 'className') {
|
||||
el.attribs['class'] = el.className = value;
|
||||
} else {
|
||||
el[name] = value;
|
||||
}
|
||||
}
|
||||
// TODO(tbosch): don't even call this method when we run the tests on server side
|
||||
// by not using the DomRenderer in tests. Keeping this for now to make tests happy...
|
||||
getProperty(el: any, name: string): any { return el[name]; }
|
||||
|
||||
logError(error: string) { console.error(error); }
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
log(error: string) { console.log(error); }
|
||||
|
||||
logGroup(error: string) { console.error(error); }
|
||||
|
||||
logGroupEnd() {}
|
||||
|
||||
get attrToPropMap() { return _attrToPropMap; }
|
||||
|
||||
querySelector(el: any, selector: string): any {
|
||||
return this.querySelectorAll(el, selector)[0] || null;
|
||||
}
|
||||
|
||||
querySelectorAll(el: any, selector: string): any[] {
|
||||
const res: any[] = [];
|
||||
const _recursive = (result: any, node: any, selector: any, matcher: any) => {
|
||||
const cNodes = node.childNodes;
|
||||
if (cNodes && cNodes.length > 0) {
|
||||
for (let i = 0; i < cNodes.length; i++) {
|
||||
const childNode = cNodes[i];
|
||||
if (this.elementMatches(childNode, selector, matcher)) {
|
||||
result.push(childNode);
|
||||
}
|
||||
_recursive(result, childNode, selector, matcher);
|
||||
}
|
||||
}
|
||||
};
|
||||
const matcher = new SelectorMatcher();
|
||||
matcher.addSelectables(CssSelector.parse(selector));
|
||||
_recursive(res, el, selector, matcher);
|
||||
return res;
|
||||
}
|
||||
elementMatches(node: any, selector: string, matcher: any = null): boolean {
|
||||
if (this.isElementNode(node) && selector === '*') {
|
||||
return true;
|
||||
}
|
||||
let result = false;
|
||||
if (selector && selector.charAt(0) == '#') {
|
||||
result = this.getAttribute(node, 'id') == selector.substring(1);
|
||||
} else if (selector) {
|
||||
if (!matcher) {
|
||||
matcher = new SelectorMatcher();
|
||||
matcher.addSelectables(CssSelector.parse(selector));
|
||||
}
|
||||
|
||||
const cssSelector = new CssSelector();
|
||||
cssSelector.setElement(this.tagName(node));
|
||||
if (node.attribs) {
|
||||
for (const attrName in node.attribs) {
|
||||
cssSelector.addAttribute(attrName, node.attribs[attrName]);
|
||||
}
|
||||
}
|
||||
const classList = this.classList(node);
|
||||
for (let i = 0; i < classList.length; i++) {
|
||||
cssSelector.addClassName(classList[i]);
|
||||
}
|
||||
|
||||
matcher.match(cssSelector, function(selector: any, cb: any) { result = true; });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
on(el: any, evt: any, listener: any) {
|
||||
let listenersMap: {[k: string]: any} = el._eventListenersMap;
|
||||
if (!listenersMap) {
|
||||
listenersMap = {};
|
||||
el._eventListenersMap = listenersMap;
|
||||
}
|
||||
const listeners = listenersMap[evt] || [];
|
||||
listenersMap[evt] = [...listeners, listener];
|
||||
}
|
||||
onAndCancel(el: any, evt: any, listener: any): Function {
|
||||
this.on(el, evt, listener);
|
||||
return () => { remove(<any[]>(el._eventListenersMap[evt]), listener); };
|
||||
}
|
||||
dispatchEvent(el: any, evt: any) {
|
||||
if (!evt.target) {
|
||||
evt.target = el;
|
||||
}
|
||||
if (el._eventListenersMap) {
|
||||
const listeners: any = el._eventListenersMap[evt.type];
|
||||
if (listeners) {
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
listeners[i](evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (el.parent) {
|
||||
this.dispatchEvent(el.parent, evt);
|
||||
}
|
||||
if (el._window) {
|
||||
this.dispatchEvent(el._window, evt);
|
||||
}
|
||||
}
|
||||
createMouseEvent(eventType: any): Event { return this.createEvent(eventType); }
|
||||
createEvent(eventType: string): Event {
|
||||
const event = <Event>{
|
||||
type: eventType,
|
||||
defaultPrevented: false,
|
||||
preventDefault: () => { (<any>event).defaultPrevented = true; }
|
||||
};
|
||||
return event;
|
||||
}
|
||||
preventDefault(event: any) { event.returnValue = false; }
|
||||
isPrevented(event: any): boolean { return event.returnValue != null && !event.returnValue; }
|
||||
getInnerHTML(el: any): string {
|
||||
return parse5.serialize(this.templateAwareRoot(el), {treeAdapter});
|
||||
}
|
||||
getTemplateContent(el: any): Node { return null; }
|
||||
getOuterHTML(el: any): string {
|
||||
const fragment = treeAdapter.createDocumentFragment();
|
||||
this.appendChild(fragment, el);
|
||||
return parse5.serialize(fragment, {treeAdapter});
|
||||
}
|
||||
nodeName(node: any): string { return node.tagName; }
|
||||
nodeValue(node: any): string { return node.nodeValue; }
|
||||
type(node: any): string { throw _notImplemented('type'); }
|
||||
content(node: any): string { return node.childNodes[0]; }
|
||||
firstChild(el: any): Node { return el.firstChild; }
|
||||
nextSibling(el: any): Node { return el.nextSibling; }
|
||||
parentElement(el: any): Node { return el.parent; }
|
||||
childNodes(el: any): Node[] { return el.childNodes; }
|
||||
childNodesAsList(el: any): any[] {
|
||||
const childNodes = el.childNodes;
|
||||
const res = new Array(childNodes.length);
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
res[i] = childNodes[i];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
clearNodes(el: any) {
|
||||
while (el.childNodes.length > 0) {
|
||||
this.remove(el.childNodes[0]);
|
||||
}
|
||||
}
|
||||
appendChild(el: any, node: any) {
|
||||
this.remove(node);
|
||||
treeAdapter.appendChild(this.templateAwareRoot(el), node);
|
||||
}
|
||||
removeChild(el: any, node: any) {
|
||||
if (el.childNodes.indexOf(node) > -1) {
|
||||
this.remove(node);
|
||||
}
|
||||
}
|
||||
remove(el: any): HTMLElement {
|
||||
const parent = el.parent;
|
||||
if (parent) {
|
||||
const index = parent.childNodes.indexOf(el);
|
||||
parent.childNodes.splice(index, 1);
|
||||
}
|
||||
const prev = el.previousSibling;
|
||||
const next = el.nextSibling;
|
||||
if (prev) {
|
||||
prev.next = next;
|
||||
}
|
||||
if (next) {
|
||||
next.prev = prev;
|
||||
}
|
||||
el.prev = null;
|
||||
el.next = null;
|
||||
el.parent = null;
|
||||
return el;
|
||||
}
|
||||
insertBefore(parent: any, ref: any, newNode: any) {
|
||||
this.remove(newNode);
|
||||
if (ref) {
|
||||
treeAdapter.insertBefore(parent, newNode, ref);
|
||||
} else {
|
||||
this.appendChild(parent, newNode);
|
||||
}
|
||||
}
|
||||
insertAllBefore(parent: any, ref: any, nodes: any) {
|
||||
nodes.forEach((n: any) => this.insertBefore(parent, ref, n));
|
||||
}
|
||||
insertAfter(parent: any, ref: any, node: any) {
|
||||
if (ref.nextSibling) {
|
||||
this.insertBefore(parent, ref.nextSibling, node);
|
||||
} else {
|
||||
this.appendChild(parent, node);
|
||||
}
|
||||
}
|
||||
setInnerHTML(el: any, value: any) {
|
||||
this.clearNodes(el);
|
||||
const content = parse5.parseFragment(value, {treeAdapter});
|
||||
for (let i = 0; i < content.childNodes.length; i++) {
|
||||
treeAdapter.appendChild(el, content.childNodes[i]);
|
||||
}
|
||||
}
|
||||
getText(el: any, isRecursive?: boolean): string {
|
||||
if (this.isTextNode(el)) {
|
||||
return el.data;
|
||||
}
|
||||
|
||||
if (this.isCommentNode(el)) {
|
||||
// In the DOM, comments within an element return an empty string for textContent
|
||||
// However, comment node instances return the comment content for textContent getter
|
||||
return isRecursive ? '' : el.data;
|
||||
}
|
||||
|
||||
if (!el.childNodes || el.childNodes.length == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let textContent = '';
|
||||
for (let i = 0; i < el.childNodes.length; i++) {
|
||||
textContent += this.getText(el.childNodes[i], true);
|
||||
}
|
||||
return textContent;
|
||||
}
|
||||
|
||||
setText(el: any, value: string) {
|
||||
if (this.isTextNode(el) || this.isCommentNode(el)) {
|
||||
el.data = value;
|
||||
} else {
|
||||
this.clearNodes(el);
|
||||
if (value !== '') treeAdapter.insertText(el, value);
|
||||
}
|
||||
}
|
||||
getValue(el: any): string { return el.value; }
|
||||
setValue(el: any, value: string) { el.value = value; }
|
||||
getChecked(el: any): boolean { return el.checked; }
|
||||
setChecked(el: any, value: boolean) { el.checked = value; }
|
||||
createComment(text: string): Comment { return treeAdapter.createCommentNode(text); }
|
||||
createTemplate(html: any): HTMLElement {
|
||||
const template = treeAdapter.createElement('template', 'http://www.w3.org/1999/xhtml', []);
|
||||
const content = parse5.parseFragment(html, {treeAdapter});
|
||||
treeAdapter.setTemplateContent(template, content);
|
||||
return template;
|
||||
}
|
||||
createElement(tagName: any): HTMLElement {
|
||||
return treeAdapter.createElement(tagName, 'http://www.w3.org/1999/xhtml', []);
|
||||
}
|
||||
createElementNS(ns: any, tagName: any): HTMLElement {
|
||||
return treeAdapter.createElement(tagName, ns, []);
|
||||
}
|
||||
createTextNode(text: string): Text {
|
||||
const t = <any>this.createComment(text);
|
||||
t.type = 'text';
|
||||
return t;
|
||||
}
|
||||
createScriptTag(attrName: string, attrValue: string): HTMLElement {
|
||||
return treeAdapter.createElement(
|
||||
'script', 'http://www.w3.org/1999/xhtml', [{name: attrName, value: attrValue}]);
|
||||
}
|
||||
createStyleElement(css: string): HTMLStyleElement {
|
||||
const style = this.createElement('style');
|
||||
this.setText(style, css);
|
||||
return <HTMLStyleElement>style;
|
||||
}
|
||||
createShadowRoot(el: any): HTMLElement {
|
||||
el.shadowRoot = treeAdapter.createDocumentFragment();
|
||||
el.shadowRoot.parent = el;
|
||||
return el.shadowRoot;
|
||||
}
|
||||
getShadowRoot(el: any): Element { return el.shadowRoot; }
|
||||
getHost(el: any): string { return el.host; }
|
||||
getDistributedNodes(el: any): Node[] { throw _notImplemented('getDistributedNodes'); }
|
||||
clone(node: Node): Node {
|
||||
const _recursive = (node: any) => {
|
||||
const nodeClone = Object.create(Object.getPrototypeOf(node));
|
||||
for (const prop in node) {
|
||||
const desc = Object.getOwnPropertyDescriptor(node, prop);
|
||||
if (desc && 'value' in desc && typeof desc.value !== 'object') {
|
||||
nodeClone[prop] = node[prop];
|
||||
}
|
||||
}
|
||||
nodeClone.parent = null;
|
||||
nodeClone.prev = null;
|
||||
nodeClone.next = null;
|
||||
nodeClone.children = null;
|
||||
|
||||
mapProps.forEach(mapName => {
|
||||
if (node[mapName] != null) {
|
||||
nodeClone[mapName] = {};
|
||||
for (const prop in node[mapName]) {
|
||||
nodeClone[mapName][prop] = node[mapName][prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
const cNodes = node.children;
|
||||
if (cNodes) {
|
||||
const cNodesClone = new Array(cNodes.length);
|
||||
for (let i = 0; i < cNodes.length; i++) {
|
||||
const childNode = cNodes[i];
|
||||
const childNodeClone = _recursive(childNode);
|
||||
cNodesClone[i] = childNodeClone;
|
||||
if (i > 0) {
|
||||
childNodeClone.prev = cNodesClone[i - 1];
|
||||
cNodesClone[i - 1].next = childNodeClone;
|
||||
}
|
||||
childNodeClone.parent = nodeClone;
|
||||
}
|
||||
nodeClone.children = cNodesClone;
|
||||
}
|
||||
return nodeClone;
|
||||
};
|
||||
return _recursive(node);
|
||||
}
|
||||
getElementsByClassName(element: any, name: string): HTMLElement[] {
|
||||
return this.querySelectorAll(element, '.' + name);
|
||||
}
|
||||
getElementsByTagName(element: any, name: string): HTMLElement[] {
|
||||
return this.querySelectorAll(element, name);
|
||||
}
|
||||
classList(element: any): string[] {
|
||||
let classAttrValue: any = null;
|
||||
const attributes = element.attribs;
|
||||
|
||||
if (attributes && attributes['class'] != null) {
|
||||
classAttrValue = attributes['class'];
|
||||
}
|
||||
return classAttrValue ? classAttrValue.trim().split(/\s+/g) : [];
|
||||
}
|
||||
addClass(element: any, className: string) {
|
||||
const classList = this.classList(element);
|
||||
const index = classList.indexOf(className);
|
||||
if (index == -1) {
|
||||
classList.push(className);
|
||||
element.attribs['class'] = element.className = classList.join(' ');
|
||||
}
|
||||
}
|
||||
removeClass(element: any, className: string) {
|
||||
const classList = this.classList(element);
|
||||
const index = classList.indexOf(className);
|
||||
if (index > -1) {
|
||||
classList.splice(index, 1);
|
||||
element.attribs['class'] = element.className = classList.join(' ');
|
||||
}
|
||||
}
|
||||
hasClass(element: any, className: string): boolean {
|
||||
return this.classList(element).indexOf(className) > -1;
|
||||
}
|
||||
hasStyle(element: any, styleName: string, styleValue: string = null): boolean {
|
||||
const value = this.getStyle(element, styleName) || '';
|
||||
return styleValue ? value == styleValue : value.length > 0;
|
||||
}
|
||||
/** @internal */
|
||||
_readStyleAttribute(element: any) {
|
||||
const styleMap = {};
|
||||
const attributes = element.attribs;
|
||||
if (attributes && attributes['style'] != null) {
|
||||
const styleAttrValue = attributes['style'];
|
||||
const styleList = styleAttrValue.split(/;+/g);
|
||||
for (let i = 0; i < styleList.length; i++) {
|
||||
if (styleList[i].length > 0) {
|
||||
const elems = styleList[i].split(/:+/g);
|
||||
(styleMap as any)[elems[0].trim()] = elems[1].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return styleMap;
|
||||
}
|
||||
/** @internal */
|
||||
_writeStyleAttribute(element: any, styleMap: any) {
|
||||
let styleAttrValue = '';
|
||||
for (const key in styleMap) {
|
||||
const newValue = styleMap[key];
|
||||
if (newValue) {
|
||||
styleAttrValue += key + ':' + styleMap[key] + ';';
|
||||
}
|
||||
}
|
||||
element.attribs['style'] = styleAttrValue;
|
||||
}
|
||||
setStyle(element: any, styleName: string, styleValue: string) {
|
||||
const styleMap = this._readStyleAttribute(element);
|
||||
(styleMap as any)[styleName] = styleValue;
|
||||
this._writeStyleAttribute(element, styleMap);
|
||||
}
|
||||
removeStyle(element: any, styleName: string) { this.setStyle(element, styleName, null); }
|
||||
getStyle(element: any, styleName: string): string {
|
||||
const styleMap = this._readStyleAttribute(element);
|
||||
return styleMap.hasOwnProperty(styleName) ? (styleMap as any)[styleName] : '';
|
||||
}
|
||||
tagName(element: any): string { return element.tagName == 'style' ? 'STYLE' : element.tagName; }
|
||||
attributeMap(element: any): Map<string, string> {
|
||||
const res = new Map<string, string>();
|
||||
const elAttrs = treeAdapter.getAttrList(element);
|
||||
for (let i = 0; i < elAttrs.length; i++) {
|
||||
const attrib = elAttrs[i];
|
||||
res.set(attrib.name, attrib.value);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
hasAttribute(element: any, attribute: string): boolean {
|
||||
return element.attribs && element.attribs[attribute] != null;
|
||||
}
|
||||
hasAttributeNS(element: any, ns: string, attribute: string): boolean { throw 'not implemented'; }
|
||||
getAttribute(element: any, attribute: string): string {
|
||||
return this.hasAttribute(element, attribute) ? element.attribs[attribute] : null;
|
||||
}
|
||||
getAttributeNS(element: any, ns: string, attribute: string): string { throw 'not implemented'; }
|
||||
setAttribute(element: any, attribute: string, value: string) {
|
||||
if (attribute) {
|
||||
element.attribs[attribute] = value;
|
||||
if (attribute === 'class') {
|
||||
element.className = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
setAttributeNS(element: any, ns: string, attribute: string, value: string) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
removeAttribute(element: any, attribute: string) {
|
||||
if (attribute) {
|
||||
delete element.attribs[attribute];
|
||||
}
|
||||
}
|
||||
removeAttributeNS(element: any, ns: string, name: string) { throw 'not implemented'; }
|
||||
templateAwareRoot(el: any): any {
|
||||
return this.isTemplateElement(el) ? treeAdapter.getTemplateContent(el) : el;
|
||||
}
|
||||
createHtmlDocument(): Document {
|
||||
const newDoc = treeAdapter.createDocument();
|
||||
newDoc.title = 'fakeTitle';
|
||||
const head = treeAdapter.createElement('head', null, []);
|
||||
const body = treeAdapter.createElement('body', 'http://www.w3.org/1999/xhtml', []);
|
||||
this.appendChild(newDoc, head);
|
||||
this.appendChild(newDoc, body);
|
||||
newDoc['head'] = head;
|
||||
newDoc['body'] = body;
|
||||
newDoc['_window'] = {};
|
||||
return newDoc;
|
||||
}
|
||||
getBoundingClientRect(el: any): any { return {left: 0, top: 0, width: 0, height: 0}; }
|
||||
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';
|
||||
}
|
||||
isTextNode(node: any): boolean { return treeAdapter.isTextNode(node); }
|
||||
isCommentNode(node: any): boolean { return treeAdapter.isCommentNode(node); }
|
||||
isElementNode(node: any): boolean { return node ? treeAdapter.isElementNode(node) : false; }
|
||||
hasShadowRoot(node: any): boolean { return node.shadowRoot != null; }
|
||||
isShadowRoot(node: any): boolean { return this.getShadowRoot(node) == node; }
|
||||
importIntoDoc(node: any): any { return this.clone(node); }
|
||||
adoptNode(node: any): any { return node; }
|
||||
getHref(el: any): string { return el.href; }
|
||||
resolveAndSetHref(el: any, baseUrl: string, href: string) {
|
||||
if (href == null) {
|
||||
el.href = baseUrl;
|
||||
} else {
|
||||
el.href = baseUrl + '/../' + href;
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
_buildRules(parsedRules: any, css?: any) {
|
||||
const rules: any[] = [];
|
||||
for (let i = 0; i < parsedRules.length; i++) {
|
||||
const parsedRule = parsedRules[i];
|
||||
const rule: {[key: string]: any} = {};
|
||||
rule['cssText'] = css;
|
||||
rule['style'] = {content: '', cssText: ''};
|
||||
if (parsedRule.type == 'rule') {
|
||||
rule['type'] = 1;
|
||||
|
||||
rule['selectorText'] =
|
||||
parsedRule.selectors.join(', '.replace(/\s{2,}/g, ' ')
|
||||
.replace(/\s*~\s*/g, ' ~ ')
|
||||
.replace(/\s*\+\s*/g, ' + ')
|
||||
.replace(/\s*>\s*/g, ' > ')
|
||||
.replace(/\[(\w+)=(\w+)\]/g, '[$1="$2"]'));
|
||||
if (parsedRule.declarations == null) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < parsedRule.declarations.length; j++) {
|
||||
const declaration = parsedRule.declarations[j];
|
||||
rule['style'] = declaration.property[declaration.value];
|
||||
rule['style'].cssText += declaration.property + ': ' + declaration.value + ';';
|
||||
}
|
||||
} else if (parsedRule.type == 'media') {
|
||||
rule['type'] = 4;
|
||||
rule['media'] = {mediaText: parsedRule.media};
|
||||
if (parsedRule.rules) {
|
||||
rule['cssRules'] = this._buildRules(parsedRule.rules);
|
||||
}
|
||||
}
|
||||
rules.push(rule);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
supportsDOMEvents(): boolean { return false; }
|
||||
supportsNativeShadowDOM(): boolean { return false; }
|
||||
getGlobalEventTarget(doc: Document, target: string): any {
|
||||
if (target == 'window') {
|
||||
return (<any>doc)._window;
|
||||
} else if (target == 'document') {
|
||||
return doc;
|
||||
} else if (target == 'body') {
|
||||
return doc.body;
|
||||
}
|
||||
}
|
||||
getBaseHref(doc: Document): string {
|
||||
const base = this.querySelector(doc, 'base');
|
||||
let href = '';
|
||||
if (base) {
|
||||
href = this.getHref(base);
|
||||
}
|
||||
// TODO(alxhub): Need relative path logic from BrowserDomAdapter here?
|
||||
return href == null ? null : href;
|
||||
}
|
||||
resetBaseElement(): void { throw 'not implemented'; }
|
||||
getHistory(): History { throw 'not implemented'; }
|
||||
getLocation(): Location { throw 'not implemented'; }
|
||||
getUserAgent(): string { return 'Fake user agent'; }
|
||||
getData(el: any, name: string): string { return this.getAttribute(el, 'data-' + name); }
|
||||
getComputedStyle(el: any): any { throw 'not implemented'; }
|
||||
setData(el: any, name: string, value: string) { this.setAttribute(el, 'data-' + name, value); }
|
||||
// TODO(tbosch): move this into a separate environment class once we have it
|
||||
setGlobalVar(path: string, value: any) { setValueOnPath(global, path, value); }
|
||||
supportsWebAnimation(): boolean { return false; }
|
||||
performanceNow(): number { return Date.now(); }
|
||||
getAnimationPrefix(): string { return ''; }
|
||||
getTransitionEnd(): string { return 'transitionend'; }
|
||||
supportsAnimation(): boolean { return true; }
|
||||
|
||||
replaceChild(el: any, newNode: any, oldNode: any) { throw new Error('not implemented'); }
|
||||
parse(templateHtml: string) { throw new Error('not implemented'); }
|
||||
invoke(el: Element, methodName: string, args: any[]): any { throw new Error('not implemented'); }
|
||||
getEventKey(event: any): string { throw new Error('not implemented'); }
|
||||
|
||||
supportsCookies(): boolean { return false; }
|
||||
getCookie(name: string): string { throw new Error('not implemented'); }
|
||||
setCookie(name: string, value: string) { throw new Error('not implemented'); }
|
||||
animate(element: any, keyframes: any[], options: any): any { throw new Error('not implemented'); }
|
||||
}
|
||||
|
||||
// TODO: build a proper list, this one is all the keys of a HTMLInputElement
|
||||
const _HTMLElementPropertyList = [
|
||||
'webkitEntries',
|
||||
'incremental',
|
||||
'webkitdirectory',
|
||||
'selectionDirection',
|
||||
'selectionEnd',
|
||||
'selectionStart',
|
||||
'labels',
|
||||
'validationMessage',
|
||||
'validity',
|
||||
'willValidate',
|
||||
'width',
|
||||
'valueAsNumber',
|
||||
'valueAsDate',
|
||||
'value',
|
||||
'useMap',
|
||||
'defaultValue',
|
||||
'type',
|
||||
'step',
|
||||
'src',
|
||||
'size',
|
||||
'required',
|
||||
'readOnly',
|
||||
'placeholder',
|
||||
'pattern',
|
||||
'name',
|
||||
'multiple',
|
||||
'min',
|
||||
'minLength',
|
||||
'maxLength',
|
||||
'max',
|
||||
'list',
|
||||
'indeterminate',
|
||||
'height',
|
||||
'formTarget',
|
||||
'formNoValidate',
|
||||
'formMethod',
|
||||
'formEnctype',
|
||||
'formAction',
|
||||
'files',
|
||||
'form',
|
||||
'disabled',
|
||||
'dirName',
|
||||
'checked',
|
||||
'defaultChecked',
|
||||
'autofocus',
|
||||
'autocomplete',
|
||||
'alt',
|
||||
'align',
|
||||
'accept',
|
||||
'onautocompleteerror',
|
||||
'onautocomplete',
|
||||
'onwaiting',
|
||||
'onvolumechange',
|
||||
'ontoggle',
|
||||
'ontimeupdate',
|
||||
'onsuspend',
|
||||
'onsubmit',
|
||||
'onstalled',
|
||||
'onshow',
|
||||
'onselect',
|
||||
'onseeking',
|
||||
'onseeked',
|
||||
'onscroll',
|
||||
'onresize',
|
||||
'onreset',
|
||||
'onratechange',
|
||||
'onprogress',
|
||||
'onplaying',
|
||||
'onplay',
|
||||
'onpause',
|
||||
'onmousewheel',
|
||||
'onmouseup',
|
||||
'onmouseover',
|
||||
'onmouseout',
|
||||
'onmousemove',
|
||||
'onmouseleave',
|
||||
'onmouseenter',
|
||||
'onmousedown',
|
||||
'onloadstart',
|
||||
'onloadedmetadata',
|
||||
'onloadeddata',
|
||||
'onload',
|
||||
'onkeyup',
|
||||
'onkeypress',
|
||||
'onkeydown',
|
||||
'oninvalid',
|
||||
'oninput',
|
||||
'onfocus',
|
||||
'onerror',
|
||||
'onended',
|
||||
'onemptied',
|
||||
'ondurationchange',
|
||||
'ondrop',
|
||||
'ondragstart',
|
||||
'ondragover',
|
||||
'ondragleave',
|
||||
'ondragenter',
|
||||
'ondragend',
|
||||
'ondrag',
|
||||
'ondblclick',
|
||||
'oncuechange',
|
||||
'oncontextmenu',
|
||||
'onclose',
|
||||
'onclick',
|
||||
'onchange',
|
||||
'oncanplaythrough',
|
||||
'oncanplay',
|
||||
'oncancel',
|
||||
'onblur',
|
||||
'onabort',
|
||||
'spellcheck',
|
||||
'isContentEditable',
|
||||
'contentEditable',
|
||||
'outerText',
|
||||
'innerText',
|
||||
'accessKey',
|
||||
'hidden',
|
||||
'webkitdropzone',
|
||||
'draggable',
|
||||
'tabIndex',
|
||||
'dir',
|
||||
'translate',
|
||||
'lang',
|
||||
'title',
|
||||
'childElementCount',
|
||||
'lastElementChild',
|
||||
'firstElementChild',
|
||||
'children',
|
||||
'onwebkitfullscreenerror',
|
||||
'onwebkitfullscreenchange',
|
||||
'nextElementSibling',
|
||||
'previousElementSibling',
|
||||
'onwheel',
|
||||
'onselectstart',
|
||||
'onsearch',
|
||||
'onpaste',
|
||||
'oncut',
|
||||
'oncopy',
|
||||
'onbeforepaste',
|
||||
'onbeforecut',
|
||||
'onbeforecopy',
|
||||
'shadowRoot',
|
||||
'dataset',
|
||||
'classList',
|
||||
'className',
|
||||
'outerHTML',
|
||||
'innerHTML',
|
||||
'scrollHeight',
|
||||
'scrollWidth',
|
||||
'scrollTop',
|
||||
'scrollLeft',
|
||||
'clientHeight',
|
||||
'clientWidth',
|
||||
'clientTop',
|
||||
'clientLeft',
|
||||
'offsetParent',
|
||||
'offsetHeight',
|
||||
'offsetWidth',
|
||||
'offsetTop',
|
||||
'offsetLeft',
|
||||
'localName',
|
||||
'prefix',
|
||||
'namespaceURI',
|
||||
'id',
|
||||
'style',
|
||||
'attributes',
|
||||
'tagName',
|
||||
'parentElement',
|
||||
'textContent',
|
||||
'baseURI',
|
||||
'ownerDocument',
|
||||
'nextSibling',
|
||||
'previousSibling',
|
||||
'lastChild',
|
||||
'firstChild',
|
||||
'childNodes',
|
||||
'parentNode',
|
||||
'nodeType',
|
||||
'nodeValue',
|
||||
'nodeName',
|
||||
'closure_lm_714617',
|
||||
'__jsaction',
|
||||
];
|
||||
|
||||
function remove<T>(list: T[], el: T): void {
|
||||
const index = list.indexOf(el);
|
||||
if (index > -1) {
|
||||
list.splice(index, 1);
|
||||
}
|
||||
}
|
15
packages/platform-server/src/platform-server.ts
Normal file
15
packages/platform-server/src/platform-server.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export {PlatformState} from './platform_state';
|
||||
export {ServerModule, platformDynamicServer, platformServer} from './server';
|
||||
export {INITIAL_CONFIG, PlatformConfig} from './tokens';
|
||||
export {renderModule, renderModuleFactory} from './utils';
|
||||
|
||||
export * from './private_export';
|
||||
export {VERSION} from './version';
|
32
packages/platform-server/src/platform_state.ts
Normal file
32
packages/platform-server/src/platform_state.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @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, ɵgetDOM as getDOM} from '@angular/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; }
|
||||
}
|
11
packages/platform-server/src/private_export.ts
Normal file
11
packages/platform-server/src/private_export.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
||||
export {INTERNAL_SERVER_PLATFORM_PROVIDERS as ɵINTERNAL_SERVER_PLATFORM_PROVIDERS, SERVER_RENDER_PROVIDERS as ɵSERVER_RENDER_PROVIDERS} from './server';
|
||||
export {ServerRendererFactory2 as ɵServerRendererFactory2} from './server_renderer';
|
85
packages/platform-server/src/server.ts
Normal file
85
packages/platform-server/src/server.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @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 {PlatformLocation, ɵPLATFORM_SERVER_ID as PLATFORM_SERVER_ID} from '@angular/common';
|
||||
import {platformCoreDynamic} from '@angular/compiler';
|
||||
import {Injectable, InjectionToken, Injector, NgModule, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, Testability, createPlatformFactory, isDevMode, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS} from '@angular/core';
|
||||
import {HttpModule} from '@angular/http';
|
||||
import {BrowserModule, DOCUMENT, ɵSharedStylesHost as SharedStylesHost, ɵgetDOM as getDOM} from '@angular/platform-browser';
|
||||
|
||||
import {SERVER_HTTP_PROVIDERS} from './http';
|
||||
import {ServerPlatformLocation} from './location';
|
||||
import {Parse5DomAdapter, parseDocument} from './parse5_adapter';
|
||||
import {PlatformState} from './platform_state';
|
||||
import {ServerRendererFactory2} from './server_renderer';
|
||||
import {ServerStylesHost} from './styles_host';
|
||||
import {INITIAL_CONFIG, PlatformConfig} from './tokens';
|
||||
|
||||
function notSupported(feature: string): Error {
|
||||
throw new Error(`platform-server does not support '${feature}'.`);
|
||||
}
|
||||
|
||||
export const INTERNAL_SERVER_PLATFORM_PROVIDERS: Array<any /*Type | Provider | any[]*/> = [
|
||||
{provide: DOCUMENT, useFactory: _document, deps: [Injector]},
|
||||
{provide: PLATFORM_ID, useValue: PLATFORM_SERVER_ID},
|
||||
{provide: PLATFORM_INITIALIZER, useFactory: initParse5Adapter, multi: true, deps: [Injector]},
|
||||
{provide: PlatformLocation, useClass: ServerPlatformLocation}, PlatformState,
|
||||
// Add special provider that allows multiple instances of platformServer* to be created.
|
||||
{provide: ALLOW_MULTIPLE_PLATFORMS, useValue: true}
|
||||
];
|
||||
|
||||
function initParse5Adapter(injector: Injector) {
|
||||
return () => { Parse5DomAdapter.makeCurrent(); };
|
||||
}
|
||||
|
||||
export const SERVER_RENDER_PROVIDERS: Provider[] = [
|
||||
ServerRendererFactory2,
|
||||
{provide: RendererFactory2, useExisting: ServerRendererFactory2},
|
||||
ServerStylesHost,
|
||||
{provide: SharedStylesHost, useExisting: ServerStylesHost},
|
||||
];
|
||||
|
||||
/**
|
||||
* The ng module for the server.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
@NgModule({
|
||||
exports: [BrowserModule],
|
||||
imports: [HttpModule],
|
||||
providers: [
|
||||
SERVER_RENDER_PROVIDERS,
|
||||
SERVER_HTTP_PROVIDERS,
|
||||
{provide: Testability, useValue: null},
|
||||
],
|
||||
})
|
||||
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
|
||||
*/
|
||||
export const platformServer =
|
||||
createPlatformFactory(platformCore, 'server', INTERNAL_SERVER_PLATFORM_PROVIDERS);
|
||||
|
||||
/**
|
||||
* The server platform that supports the runtime compiler.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const platformDynamicServer =
|
||||
createPlatformFactory(platformCoreDynamic, 'serverDynamic', INTERNAL_SERVER_PLATFORM_PROVIDERS);
|
206
packages/platform-server/src/server_renderer.ts
Normal file
206
packages/platform-server/src/server_renderer.ts
Normal file
@ -0,0 +1,206 @@
|
||||
/**
|
||||
* @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 {DomElementSchemaRegistry} from '@angular/compiler';
|
||||
import {APP_ID, Inject, Injectable, NgZone, RenderComponentType, Renderer, Renderer2, RendererFactory2, RendererType2, RootRenderer, ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
|
||||
import {DOCUMENT, ɵNAMESPACE_URIS as NAMESPACE_URIS, ɵSharedStylesHost as SharedStylesHost, ɵflattenStyles as flattenStyles, ɵgetDOM as getDOM, ɵshimContentAttribute as shimContentAttribute, ɵshimHostAttribute as shimHostAttribute} from '@angular/platform-browser';
|
||||
|
||||
const EMPTY_ARRAY: any[] = [];
|
||||
|
||||
@Injectable()
|
||||
export class ServerRendererFactory2 implements RendererFactory2 {
|
||||
private rendererByCompId = new Map<string, Renderer2>();
|
||||
private defaultRenderer: Renderer2;
|
||||
private schema = new DomElementSchemaRegistry();
|
||||
|
||||
constructor(
|
||||
private ngZone: NgZone, @Inject(DOCUMENT) private document: any,
|
||||
private sharedStylesHost: SharedStylesHost) {
|
||||
this.defaultRenderer = new DefaultServerRenderer2(document, ngZone, this.schema);
|
||||
};
|
||||
|
||||
createRenderer(element: any, type: RendererType2): Renderer2 {
|
||||
if (!element || !type) {
|
||||
return this.defaultRenderer;
|
||||
}
|
||||
switch (type.encapsulation) {
|
||||
case ViewEncapsulation.Emulated: {
|
||||
let renderer = this.rendererByCompId.get(type.id);
|
||||
if (!renderer) {
|
||||
renderer = new EmulatedEncapsulationServerRenderer2(
|
||||
this.document, this.ngZone, this.sharedStylesHost, this.schema, type);
|
||||
this.rendererByCompId.set(type.id, renderer);
|
||||
}
|
||||
(<EmulatedEncapsulationServerRenderer2>renderer).applyToHost(element);
|
||||
return renderer;
|
||||
}
|
||||
case ViewEncapsulation.Native:
|
||||
throw new Error('Native encapsulation is not supported on the server!');
|
||||
default: {
|
||||
if (!this.rendererByCompId.has(type.id)) {
|
||||
const styles = flattenStyles(type.id, type.styles, []);
|
||||
this.sharedStylesHost.addStyles(styles);
|
||||
this.rendererByCompId.set(type.id, this.defaultRenderer);
|
||||
}
|
||||
return this.defaultRenderer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultServerRenderer2 implements Renderer2 {
|
||||
data: {[key: string]: any} = Object.create(null);
|
||||
|
||||
constructor(
|
||||
private document: any, private ngZone: NgZone, private schema: DomElementSchemaRegistry) {}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
destroyNode: null;
|
||||
|
||||
createElement(name: string, namespace?: string, debugInfo?: any): any {
|
||||
if (namespace) {
|
||||
return getDOM().createElementNS(NAMESPACE_URIS[namespace], name);
|
||||
}
|
||||
|
||||
return getDOM().createElement(name);
|
||||
}
|
||||
|
||||
createComment(value: string, debugInfo?: any): any { return getDOM().createComment(value); }
|
||||
|
||||
createText(value: string, debugInfo?: any): any { return getDOM().createTextNode(value); }
|
||||
|
||||
appendChild(parent: any, newChild: any): void { getDOM().appendChild(parent, newChild); }
|
||||
|
||||
insertBefore(parent: any, newChild: any, refChild: any): void {
|
||||
if (parent) {
|
||||
getDOM().insertBefore(parent, refChild, newChild);
|
||||
}
|
||||
}
|
||||
|
||||
removeChild(parent: any, oldChild: any): void {
|
||||
if (parent) {
|
||||
getDOM().removeChild(parent, oldChild);
|
||||
}
|
||||
}
|
||||
|
||||
selectRootElement(selectorOrNode: string|any, debugInfo?: any): any {
|
||||
let el: any;
|
||||
if (typeof selectorOrNode === 'string') {
|
||||
el = getDOM().querySelector(this.document, selectorOrNode);
|
||||
if (!el) {
|
||||
throw new Error(`The selector "${selectorOrNode}" did not match any elements`);
|
||||
}
|
||||
} else {
|
||||
el = selectorOrNode;
|
||||
}
|
||||
getDOM().clearNodes(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
parentNode(node: any): any { return getDOM().parentElement(node); }
|
||||
|
||||
nextSibling(node: any): any { return getDOM().nextSibling(node); }
|
||||
|
||||
setAttribute(el: any, name: string, value: string, namespace?: string): void {
|
||||
if (namespace) {
|
||||
getDOM().setAttributeNS(el, NAMESPACE_URIS[namespace], namespace + ':' + name, value);
|
||||
} else {
|
||||
getDOM().setAttribute(el, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
removeAttribute(el: any, name: string, namespace?: string): void {
|
||||
if (namespace) {
|
||||
getDOM().removeAttributeNS(el, NAMESPACE_URIS[namespace], name);
|
||||
} else {
|
||||
getDOM().removeAttribute(el, name);
|
||||
}
|
||||
}
|
||||
|
||||
addClass(el: any, name: string): void { getDOM().addClass(el, name); }
|
||||
|
||||
removeClass(el: any, name: string): void { getDOM().removeClass(el, name); }
|
||||
|
||||
setStyle(el: any, style: string, value: any, hasVendorPrefix: boolean, hasImportant: boolean):
|
||||
void {
|
||||
getDOM().setStyle(el, style, value);
|
||||
}
|
||||
|
||||
removeStyle(el: any, style: string, hasVendorPrefix: boolean): void {
|
||||
getDOM().removeStyle(el, style);
|
||||
}
|
||||
|
||||
// The value was validated already as a property binding, against the property name.
|
||||
// To know this value is safe to use as an attribute, the security context of the
|
||||
// attribute with the given name is checked against that security context of the
|
||||
// property.
|
||||
private _isSafeToReflectProperty(tagName: string, propertyName: string): boolean {
|
||||
return this.schema.securityContext(tagName, propertyName, true) ===
|
||||
this.schema.securityContext(tagName, propertyName, false);
|
||||
}
|
||||
|
||||
setProperty(el: any, name: string, value: any): void {
|
||||
checkNoSyntheticProp(name, 'property');
|
||||
getDOM().setProperty(el, name, value);
|
||||
// Mirror property values for known HTML element properties in the attributes.
|
||||
const tagName = (el.tagName as string).toLowerCase();
|
||||
if (value != null && (typeof value === 'number' || typeof value == 'string') &&
|
||||
this.schema.hasElement(tagName, EMPTY_ARRAY) &&
|
||||
this.schema.hasProperty(tagName, name, EMPTY_ARRAY) &&
|
||||
this._isSafeToReflectProperty(tagName, name)) {
|
||||
this.setAttribute(el, name, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
setValue(node: any, value: string): void { getDOM().setText(node, value); }
|
||||
|
||||
listen(
|
||||
target: 'document'|'window'|'body'|any, eventName: string,
|
||||
callback: (event: any) => boolean): () => void {
|
||||
// Note: We are not using the EventsPlugin here as this is not needed
|
||||
// to run our tests.
|
||||
checkNoSyntheticProp(eventName, 'listener');
|
||||
const el =
|
||||
typeof target === 'string' ? getDOM().getGlobalEventTarget(this.document, target) : target;
|
||||
const outsideHandler = (event: any) => this.ngZone.runGuarded(() => callback(event));
|
||||
return this.ngZone.runOutsideAngular(() => getDOM().onAndCancel(el, eventName, outsideHandler));
|
||||
}
|
||||
}
|
||||
|
||||
const AT_CHARCODE = '@'.charCodeAt(0);
|
||||
function checkNoSyntheticProp(name: string, nameKind: string) {
|
||||
if (name.charCodeAt(0) === AT_CHARCODE) {
|
||||
throw new Error(
|
||||
`Found the synthetic ${nameKind} ${name}. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.`);
|
||||
}
|
||||
}
|
||||
|
||||
class EmulatedEncapsulationServerRenderer2 extends DefaultServerRenderer2 {
|
||||
private contentAttr: string;
|
||||
private hostAttr: string;
|
||||
|
||||
constructor(
|
||||
document: any, ngZone: NgZone, sharedStylesHost: SharedStylesHost,
|
||||
schema: DomElementSchemaRegistry, private component: RendererType2) {
|
||||
super(document, ngZone, schema);
|
||||
const styles = flattenStyles(component.id, component.styles, []);
|
||||
sharedStylesHost.addStyles(styles);
|
||||
|
||||
this.contentAttr = shimContentAttribute(component.id);
|
||||
this.hostAttr = shimHostAttribute(component.id);
|
||||
}
|
||||
|
||||
applyToHost(element: any) { super.setAttribute(element, this.hostAttr, ''); }
|
||||
|
||||
createElement(parent: any, name: string): Element {
|
||||
const el = super.createElement(parent, name);
|
||||
super.setAttribute(el, this.contentAttr, '');
|
||||
return el;
|
||||
}
|
||||
}
|
36
packages/platform-server/src/styles_host.ts
Normal file
36
packages/platform-server/src/styles_host.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @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, Inject, Injectable, Optional} from '@angular/core';
|
||||
import {DOCUMENT, ɵSharedStylesHost as SharedStylesHost, ɵTRANSITION_ID, ɵgetDOM as getDOM} from '@angular/platform-browser';
|
||||
|
||||
import {Parse5DomAdapter} from './parse5_adapter';
|
||||
|
||||
@Injectable()
|
||||
export class ServerStylesHost extends SharedStylesHost {
|
||||
private head: any = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private doc: any,
|
||||
@Optional() @Inject(ɵTRANSITION_ID) private transitionId: string) {
|
||||
super();
|
||||
this.head = getDOM().getElementsByTagName(doc, 'head')[0];
|
||||
}
|
||||
|
||||
private _addStyle(style: string): void {
|
||||
let adapter: Parse5DomAdapter = getDOM() as Parse5DomAdapter;
|
||||
const el = adapter.createElement('style');
|
||||
adapter.setText(el, style);
|
||||
if (!!this.transitionId) {
|
||||
adapter.setAttribute(el, 'ng-transition', this.transitionId);
|
||||
}
|
||||
adapter.appendChild(this.head, el);
|
||||
}
|
||||
|
||||
onStylesAdded(additions: Set<string>) { additions.forEach(style => this._addStyle(style)); }
|
||||
}
|
26
packages/platform-server/src/tokens.ts
Normal file
26
packages/platform-server/src/tokens.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @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 {InjectionToken} from '@angular/core';
|
||||
|
||||
/**
|
||||
* 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<PlatformConfig>('Server.INITIAL_CONFIG');
|
79
packages/platform-server/src/utils.ts
Normal file
79
packages/platform-server/src/utils.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @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} from '@angular/core';
|
||||
import {ɵTRANSITION_ID} from '@angular/platform-browser';
|
||||
import {filter} from 'rxjs/operator/filter';
|
||||
import {first} from 'rxjs/operator/first';
|
||||
import {toPromise} from 'rxjs/operator/toPromise';
|
||||
|
||||
import {PlatformState} from './platform_state';
|
||||
import {platformDynamicServer, platformServer} from './server';
|
||||
import {INITIAL_CONFIG} from './tokens';
|
||||
|
||||
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<T>(
|
||||
platform: PlatformRef, moduleRefPromise: Promise<NgModuleRef<T>>): Promise<string> {
|
||||
return moduleRefPromise.then((moduleRef) => {
|
||||
const transitionId = moduleRef.injector.get(ɵTRANSITION_ID, null);
|
||||
if (!transitionId) {
|
||||
throw new Error(
|
||||
`renderModule[Factory]() requires the use of BrowserModule.withServerTransition() to ensure
|
||||
the server-rendered app can be properly bootstrapped into a client app.`);
|
||||
}
|
||||
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();
|
||||
platform.destroy();
|
||||
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<T>(module: Type<T>, options: PlatformOptions): Promise<string> {
|
||||
const platform = _getPlatform(platformDynamicServer, options);
|
||||
return _render(platform, platform.bootstrapModule(module));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a {@link NgModuleFactory} to string.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function renderModuleFactory<T>(
|
||||
moduleFactory: NgModuleFactory<T>, options: PlatformOptions): Promise<string> {
|
||||
const platform = _getPlatform(platformServer, options);
|
||||
return _render(platform, platform.bootstrapModuleFactory(moduleFactory));
|
||||
}
|
19
packages/platform-server/src/version.ts
Normal file
19
packages/platform-server/src/version.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the common package.
|
||||
*/
|
||||
|
||||
import {Version} from '@angular/core';
|
||||
/**
|
||||
* @stable
|
||||
*/
|
||||
export const VERSION = new Version('0.0.0-PLACEHOLDER');
|
370
packages/platform-server/test/integration_spec.ts
Normal file
370
packages/platform-server/test/integration_spec.ts
Normal file
@ -0,0 +1,370 @@
|
||||
/**
|
||||
* @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 {PlatformLocation, isPlatformServer} from '@angular/common';
|
||||
import {ApplicationRef, CompilerFactory, Component, NgModule, NgModuleRef, NgZone, PLATFORM_ID, PlatformRef, destroyPlatform, getPlatform} from '@angular/core';
|
||||
import {TestBed, async, inject} from '@angular/core/testing';
|
||||
import {Http, HttpModule, Response, ResponseOptions, XHRBackend} from '@angular/http';
|
||||
import {MockBackend, MockConnection} from '@angular/http/testing';
|
||||
import {BrowserModule, DOCUMENT} from '@angular/platform-browser';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
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 {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
bootstrap: [MyServerApp],
|
||||
declarations: [MyServerApp],
|
||||
imports: [ServerModule],
|
||||
providers: [
|
||||
MockBackend,
|
||||
{provide: XHRBackend, useExisting: MockBackend},
|
||||
]
|
||||
})
|
||||
class ExampleModule {
|
||||
}
|
||||
|
||||
@Component({selector: 'app', template: `Works too!`})
|
||||
class MyServerApp2 {
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyServerApp2], imports: [ServerModule], bootstrap: [MyServerApp2]})
|
||||
class ExampleModule2 {
|
||||
}
|
||||
|
||||
@Component({selector: 'app', template: '{{text}}'})
|
||||
class MyAsyncServerApp {
|
||||
text = '';
|
||||
|
||||
ngOnInit() {
|
||||
Promise.resolve(null).then(() => setTimeout(() => { this.text = 'Works!'; }, 10));
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyAsyncServerApp],
|
||||
imports: [BrowserModule.withServerTransition({appId: 'async-server'}), ServerModule],
|
||||
bootstrap: [MyAsyncServerApp]
|
||||
})
|
||||
class AsyncServerModule {
|
||||
}
|
||||
|
||||
@Component({selector: 'app', template: `Works!`, styles: [':host { color: red; }']})
|
||||
class MyStylesApp {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MyStylesApp],
|
||||
imports: [BrowserModule.withServerTransition({appId: 'example-styles'}), ServerModule],
|
||||
bootstrap: [MyStylesApp]
|
||||
})
|
||||
class ExampleStylesModule {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
bootstrap: [MyServerApp],
|
||||
declarations: [MyServerApp],
|
||||
imports: [HttpModule, ServerModule],
|
||||
providers: [
|
||||
MockBackend,
|
||||
{provide: XHRBackend, useExisting: MockBackend},
|
||||
]
|
||||
})
|
||||
export class HttpBeforeExampleModule {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
bootstrap: [MyServerApp],
|
||||
declarations: [MyServerApp],
|
||||
imports: [ServerModule, HttpModule],
|
||||
providers: [
|
||||
MockBackend,
|
||||
{provide: XHRBackend, useExisting: MockBackend},
|
||||
]
|
||||
})
|
||||
export class HttpAfterExampleModule {
|
||||
}
|
||||
|
||||
@Component({selector: 'app', template: `<img [src]="'link'">`})
|
||||
class ImageApp {
|
||||
}
|
||||
|
||||
@NgModule({declarations: [ImageApp], imports: [ServerModule], bootstrap: [ImageApp]})
|
||||
class ImageExampleModule {
|
||||
}
|
||||
|
||||
export function main() {
|
||||
if (getDOM().supportsDOMEvents()) return; // NODE only
|
||||
|
||||
describe('platform-server integration', () => {
|
||||
beforeEach(() => {
|
||||
if (getPlatform()) destroyPlatform();
|
||||
});
|
||||
|
||||
it('should bootstrap', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
|
||||
platform.bootstrapModule(ExampleModule).then((moduleRef) => {
|
||||
expect(isPlatformServer(moduleRef.injector.get(PLATFORM_ID))).toBe(true);
|
||||
const doc = moduleRef.injector.get(DOCUMENT);
|
||||
expect(getDOM().getText(doc)).toEqual('Works!');
|
||||
platform.destroy();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should allow multiple platform instances', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
|
||||
const platform2 = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
|
||||
|
||||
platform.bootstrapModule(ExampleModule).then((moduleRef) => {
|
||||
const doc = moduleRef.injector.get(DOCUMENT);
|
||||
expect(getDOM().getText(doc)).toEqual('Works!');
|
||||
platform.destroy();
|
||||
});
|
||||
|
||||
platform2.bootstrapModule(ExampleModule2).then((moduleRef) => {
|
||||
const doc = moduleRef.injector.get(DOCUMENT);
|
||||
expect(getDOM().getText(doc)).toEqual('Works too!');
|
||||
platform2.destroy();
|
||||
});
|
||||
}));
|
||||
|
||||
it('adds styles with ng-transition attribute', async(() => {
|
||||
const platform = platformDynamicServer([{
|
||||
provide: INITIAL_CONFIG,
|
||||
useValue: {document: '<html><head></head><body><app></app></body></html>'}
|
||||
}]);
|
||||
platform.bootstrapModule(ExampleStylesModule).then(ref => {
|
||||
const doc = ref.injector.get(DOCUMENT);
|
||||
const head = getDOM().getElementsByTagName(doc, 'head')[0];
|
||||
const styles: any[] = head.children as any;
|
||||
expect(styles.length).toBe(1);
|
||||
expect(getDOM().getText(styles[0])).toContain('color: red');
|
||||
expect(getDOM().getAttribute(styles[0], 'ng-transition')).toBe('example-styles');
|
||||
});
|
||||
}));
|
||||
|
||||
it('copies known properties to attributes', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.bootstrapModule(ImageExampleModule).then(ref => {
|
||||
const appRef: ApplicationRef = ref.injector.get(ApplicationRef);
|
||||
const app = appRef.components[0].location.nativeElement;
|
||||
const img = getDOM().getElementsByTagName(app, 'img')[0] as any;
|
||||
expect(img.attribs['src']).toEqual('link');
|
||||
});
|
||||
}));
|
||||
|
||||
describe('PlatformLocation', () => {
|
||||
it('is injectable', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.bootstrapModule(ExampleModule).then(appRef => {
|
||||
const location: PlatformLocation = appRef.injector.get(PlatformLocation);
|
||||
expect(location.pathname).toBe('/');
|
||||
platform.destroy();
|
||||
});
|
||||
}));
|
||||
it('is configurable via INITIAL_CONFIG', () => {
|
||||
platformDynamicServer([{
|
||||
provide: INITIAL_CONFIG,
|
||||
useValue: {document: '<app></app>', url: 'http://test.com/deep/path?query#hash'}
|
||||
}])
|
||||
.bootstrapModule(ExampleModule)
|
||||
.then(appRef => {
|
||||
const location: PlatformLocation = appRef.injector.get(PlatformLocation);
|
||||
expect(location.pathname).toBe('/deep/path');
|
||||
expect(location.search).toBe('?query');
|
||||
expect(location.hash).toBe('#hash');
|
||||
});
|
||||
});
|
||||
it('handles empty search and hash portions of the url', () => {
|
||||
platformDynamicServer([{
|
||||
provide: INITIAL_CONFIG,
|
||||
useValue: {document: '<app></app>', url: 'http://test.com/deep/path'}
|
||||
}])
|
||||
.bootstrapModule(ExampleModule)
|
||||
.then(appRef => {
|
||||
const location: PlatformLocation = appRef.injector.get(PlatformLocation);
|
||||
expect(location.pathname).toBe('/deep/path');
|
||||
expect(location.search).toBe('');
|
||||
expect(location.hash).toBe('');
|
||||
});
|
||||
});
|
||||
it('pushState causes the URL to update', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.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');
|
||||
platform.destroy();
|
||||
});
|
||||
}));
|
||||
it('allows subscription to the hash state', done => {
|
||||
const platform =
|
||||
platformDynamicServer([{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.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');
|
||||
platform.destroy();
|
||||
done();
|
||||
});
|
||||
location.pushState(null, 'Test', '/foo#bar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
let doc: string;
|
||||
let called: boolean;
|
||||
let expectedOutput =
|
||||
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>';
|
||||
|
||||
beforeEach(() => {
|
||||
// PlatformConfig takes in a parsed document so that it can be cached across requests.
|
||||
doc = '<html><head></head><body><app></app></body></html>';
|
||||
called = false;
|
||||
});
|
||||
afterEach(() => { expect(called).toBe(true); });
|
||||
|
||||
it('using long from should work', 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);
|
||||
platform.destroy();
|
||||
called = true;
|
||||
});
|
||||
}));
|
||||
|
||||
it('using renderModule should work', async(() => {
|
||||
renderModule(AsyncServerModule, {document: doc}).then(output => {
|
||||
expect(output).toBe(expectedOutput);
|
||||
called = true;
|
||||
});
|
||||
}));
|
||||
|
||||
it('using renderModuleFactory should work',
|
||||
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;
|
||||
});
|
||||
})));
|
||||
});
|
||||
|
||||
describe('http', () => {
|
||||
it('can inject Http', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.bootstrapModule(ExampleModule).then(ref => {
|
||||
expect(ref.injector.get(Http) instanceof Http).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
it('can make Http requests', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.bootstrapModule(ExampleModule).then(ref => {
|
||||
const mock = ref.injector.get(MockBackend);
|
||||
const http = ref.injector.get(Http);
|
||||
ref.injector.get(NgZone).run(() => {
|
||||
NgZone.assertInAngularZone();
|
||||
mock.connections.subscribe((mc: MockConnection) => {
|
||||
NgZone.assertInAngularZone();
|
||||
expect(mc.request.url).toBe('/testing');
|
||||
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
|
||||
});
|
||||
http.get('/testing').subscribe(resp => {
|
||||
NgZone.assertInAngularZone();
|
||||
expect(resp.text()).toBe('success!');
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
it('requests are macrotasks', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.bootstrapModule(ExampleModule).then(ref => {
|
||||
const mock = ref.injector.get(MockBackend);
|
||||
const http = ref.injector.get(Http);
|
||||
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
|
||||
ref.injector.get(NgZone).run(() => {
|
||||
NgZone.assertInAngularZone();
|
||||
mock.connections.subscribe((mc: MockConnection) => {
|
||||
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
|
||||
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
|
||||
});
|
||||
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
|
||||
});
|
||||
});
|
||||
}));
|
||||
it('works when HttpModule is included before ServerModule', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.bootstrapModule(HttpBeforeExampleModule).then(ref => {
|
||||
const mock = ref.injector.get(MockBackend);
|
||||
const http = ref.injector.get(Http);
|
||||
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
|
||||
ref.injector.get(NgZone).run(() => {
|
||||
NgZone.assertInAngularZone();
|
||||
mock.connections.subscribe((mc: MockConnection) => {
|
||||
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
|
||||
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
|
||||
});
|
||||
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
|
||||
});
|
||||
});
|
||||
}));
|
||||
it('works when HttpModule is included after ServerModule', async(() => {
|
||||
const platform = platformDynamicServer(
|
||||
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
|
||||
platform.bootstrapModule(HttpAfterExampleModule).then(ref => {
|
||||
const mock = ref.injector.get(MockBackend);
|
||||
const http = ref.injector.get(Http);
|
||||
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
|
||||
ref.injector.get(NgZone).run(() => {
|
||||
NgZone.assertInAngularZone();
|
||||
mock.connections.subscribe((mc: MockConnection) => {
|
||||
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
|
||||
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
|
||||
});
|
||||
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
14
packages/platform-server/testing/src/index.ts
Normal file
14
packages/platform-server/testing/src/index.ts
Normal file
@ -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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the platform-browser-dynamic/testing package.
|
||||
*/
|
||||
export * from './server';
|
30
packages/platform-server/testing/src/server.ts
Normal file
30
packages/platform-server/testing/src/server.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @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 {platformCoreDynamicTesting} from '@angular/compiler/testing';
|
||||
import {NgModule, PlatformRef, Provider, createPlatformFactory} from '@angular/core';
|
||||
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
|
||||
import {ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as INTERNAL_SERVER_PLATFORM_PROVIDERS, ɵSERVER_RENDER_PROVIDERS as SERVER_RENDER_PROVIDERS} from '@angular/platform-server';
|
||||
|
||||
|
||||
/**
|
||||
* Platform for testing
|
||||
*
|
||||
* @experimental API related to bootstrapping are still under review.
|
||||
*/
|
||||
export const platformServerTesting = createPlatformFactory(
|
||||
platformCoreDynamicTesting, 'serverTesting', INTERNAL_SERVER_PLATFORM_PROVIDERS);
|
||||
|
||||
/**
|
||||
* NgModule for testing.
|
||||
*
|
||||
* @experimental API related to bootstrapping are still under review.
|
||||
*/
|
||||
@NgModule({exports: [BrowserDynamicTestingModule], providers: SERVER_RENDER_PROVIDERS})
|
||||
export class ServerTestingModule {
|
||||
}
|
27
packages/platform-server/testing/tsconfig-build.json
Normal file
27
packages/platform-server/testing/tsconfig-build.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig-build",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages-dist/core"],
|
||||
"@angular/core/testing": ["../../../dist/packages-dist/core/testing"],
|
||||
"@angular/common": ["../../../dist/packages-dist/common"],
|
||||
"@angular/common/testing": ["../../../dist/packages-dist/common/testing"],
|
||||
"@angular/compiler": ["../../../dist/packages-dist/compiler"],
|
||||
"@angular/compiler/testing": ["../../../dist/packages-dist/compiler/testing"],
|
||||
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
|
||||
"@angular/platform-browser/testing": ["../../../dist/packages-dist/platform-browser/testing"],
|
||||
"@angular/platform-browser-dynamic": ["../../../dist/packages-dist/platform-browser-dynamic"],
|
||||
"@angular/platform-browser-dynamic/testing": ["../../../dist/packages-dist/platform-browser-dynamic/testing"],
|
||||
"@angular/platform-server": ["../../../dist/packages-dist/platform-server"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"testing/index.ts",
|
||||
"../../../node_modules/@types/jasmine/index.d.ts",
|
||||
"../../../node_modules/@types/node/index.d.ts",
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
38
packages/platform-server/tsconfig-build.json
Normal file
38
packages/platform-server/tsconfig-build.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"stripInternal": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "../../../dist/packages-dist/platform-server",
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages-dist/core"],
|
||||
"@angular/common": ["../../../dist/packages-dist/common"],
|
||||
"@angular/compiler": ["../../../dist/packages-dist/compiler"],
|
||||
"@angular/http": ["../../../dist/packages-dist/http"],
|
||||
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
|
||||
"@angular/platform-browser-dynamic": ["../../../dist/packages-dist/platform-browser-dynamic"]
|
||||
},
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"target": "es2015",
|
||||
"skipLibCheck": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
// don't auto-discover @types/node, it results in a ///<reference in the .d.ts output
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"public_api.ts",
|
||||
"../../../node_modules/@types/node/index.d.ts",
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true,
|
||||
"flatModuleOutFile": "index.js",
|
||||
"flatModuleId": "@angular/platform-server"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user