@skatejs/ssr
Version:
Server-side render your web components.
255 lines (228 loc) • 6.9 kB
JavaScript
require('undom/register');
const { parseFragment } = require('parse5');
const ElementProto = Element.prototype;
const NodeProto = Node.prototype;
const { insertBefore, removeChild } = NodeProto;
const { dispatchEvent } = ElementProto;
const { defineProperty: prop } = Object;
const isConnected = Symbol('isConnected');
function expose (name, value) {
global[name] = window[name] = value;
return value;
}
function connectNode (node) {
if (node.connectedCallback && !node[isConnected]) {
node.connectedCallback();
}
node[isConnected] = true;
}
function disconnectNode (node) {
if (node.disconnectedCallback && node[isConnected]) {
node.disconnectedCallback();
}
node[isConnected] = false;
}
function each (node, call) {
if (node instanceof DocumentFragment) {
Array.from(node.childNodes).forEach(call);
} else {
call(node);
}
return node;
}
function translateParsed (parsed) {
let node;
const { attrs, childNodes, nodeName, value } = parsed;
if (nodeName === '#document-fragment') {
node = document.createDocumentFragment();
} else if (nodeName === '#text') {
node = document.createTextNode(value);
} else {
node = document.createElement(nodeName);
attrs.forEach(({ name, value }) => node.setAttribute(name, value));
}
if (childNodes) {
childNodes.forEach(c => node.appendChild(translateParsed(c)));
}
return node;
}
function patchCustomElements () {
const customElementRegistry = {};
expose('customElements', {
define (name, func) {
prop(func.prototype, 'nodeName', { value: name.toUpperCase() });
customElementRegistry[name] = func;
},
get (name) {
return customElementRegistry[name];
}
});
const createElement = document.createElement.bind(document);
document.createElement = function (name) {
const Ctor = window.customElements.get(name);
const elem = Ctor ? new Ctor() : createElement(name);
prop(elem, 'localName', { value: name });
return elem;
};
}
function patchDocumentFragment () {
expose('DocumentFragment', class extends Node {
get nodeName () {
return '#document-fragment';
}
});
document.createDocumentFragment = () => new DocumentFragment();
}
function patchElement () {
const { getAttribute, removeAttribute, setAttribute } = ElementProto;
ElementProto.dispatchEvent = function (evnt) {
evnt.target = this;
return dispatchEvent.call(this, evnt);
};
ElementProto.getAttribute = function (name) {
const value = getAttribute.call(this, name);
return value == null ? null : value;
};
ElementProto.hasAttribute = function (name) {
return this.getAttribute(name) !== null;
};
ElementProto.removeAttribute = function (name) {
const oldValue = this.getAttribute(name);
removeAttribute.call(this, name);
if (this.attributeChangedCallback) {
this.attributeChangedCallback(name, oldValue, null);
}
};
ElementProto.setAttribute = function (name, newValue) {
const oldValue = this.getAttribute(name);
setAttribute.call(this, name, newValue);
if (this.attributeChangedCallback) {
this.attributeChangedCallback(name, oldValue, newValue);
}
};
ElementProto.assignedNodes = function () {
if (this.nodeName !== 'SLOT') {
throw new Error('Non-standard: assignedNodes() called on non-slot element.');
}
const name = this.getAttribute('name') || this.name;
let node = this, host;
while (node = node.parentNode) {
if (node.host) {
host = node.host;
break;
}
}
return host
? host.childNodes.filter(n => {
return name
? n.getAttribute && n.getAttribute('slot') === name
: !n.getAttribute || !n.getAttribute('slot')
})
: [];
};
prop(ElementProto, 'innerHTML', {
get () {
return this.childNodes.map(c => c.outerHTML || c.textContent).join('');
},
set (val) {
while (this.hasChildNodes()) {
this.removeChild(this.firstChild);
}
this.appendChild(translateParsed(parseFragment(val)));
}
});
prop(ElementProto, 'outerHTML', {
get () {
const { attributes, nodeName } = this;
const name = nodeName.toLowerCase();
return `<${name}${attributes.reduce((prev, { name, value }) => prev + ` ${name}="${value}"`, '')}>${this.innerHTML}</${name}>`;
},
set (val) {
throw new Error('Not implemented: set outerHTML');
}
});
}
function patchEvents () {
expose('CustomEvent', expose('Event', class extends Event {
constructor (evnt, opts = {}) {
super(evnt, opts);
}
initEvent (type, bubbles, cancelable) {
this.bubbles = bubbles;
this.cancelable = cancelable;
this.type = type;
}
initCustomEvent (type, bubbles, cancelable, detail) {
this.initEvent(type, bubbles, cancelable);
this.detail = detail;
}
}));
document.createEvent = function (name) {
return new window[name]();
};
}
function patchHTMLElement () {
function HTMLElement() {
let newTarget = this.constructor;
return Reflect.construct(Element, [], newTarget);
}
HTMLElement.prototype = Object.create(Element.prototype, {
constructor: { value: HTMLElement, configurable: true, writable: true }
});
HTMLElement.prototype.attachShadow = function({ mode }) {
const shadowRoot = document.createElement('shadow-root');
prop(this, 'shadowRoot', { value: shadowRoot });
prop(shadowRoot, 'host', { value: this });
prop(shadowRoot, 'mode', { value: mode });
prop(shadowRoot, 'parentNode', { value: this });
return shadowRoot;
};
expose('HTMLElement', HTMLElement);
}
function patchNode () {
Node.DOCUMENT_FRAGMENT_NODE = 11;
Node.ELEMENT_NODE = 1;
Node.TEXT_NODE = 3;
prop(NodeProto, 'textContent', {
get () {
return this.childNodes.map(c => c.nodeValue).join('');
},
set (val) {
this.appendChild(document.createTextNode(val));
}
});
NodeProto.contains = function (node) {
if (this === node) {
return true;
}
for (let childNode of this.childNodes) {
if (childNode.contains(node)) {
return true;
}
}
return false;
};
NodeProto.hasChildNodes = function () {
return this.childNodes.length;
};
// Undom internally calls insertBefore in appendChild.
NodeProto.insertBefore = function (newNode, refNode) {
return each(newNode, newNode => {
insertBefore.call(this, newNode, refNode);
connectNode(newNode);
});
};
// Undom internally calls removeChild in replaceChild.
NodeProto.removeChild = function (refNode) {
return each(refNode, refNode => {
removeChild.call(this, refNode);
disconnectNode(refNode);
});
};
}
patchCustomElements();
patchDocumentFragment();
patchElement();
patchEvents();
patchHTMLElement();
patchNode();