@skatejs/ssr
Version:
Server-side render your web components.
342 lines (295 loc) • 7.53 kB
JavaScript
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const { parseFragment } = require('parse5');
const ClassList = require('./ClassList');
const Node = require('./Node');
const MutationObserver = require('./MutationObserver');
const CSSStyleSheet = require('./StyleSheet');
const { execFile, nodeName, prop } = require('../util');
const ElementProto = Element.prototype;
const {
dispatchEvent,
getAttribute,
removeAttribute,
setAttribute
} = ElementProto;
let settingProp = false;
const attrToPropMap = {
class: 'className',
id: 'id',
src: 'src',
type: 'type'
};
function translateParsed(parsed) {
let node;
const { attrs, childNodes, data, nodeName, value } = parsed;
// Parse5 doesn't give nodeType, so we have to use the node name.
if (nodeName === '#comment') {
node = document.createComment(data);
} else if (nodeName === '#document-fragment') {
node = document.createDocumentFragment();
} else if (nodeName === '#text') {
node = document.createTextNode(value);
} else {
node = document.createElement(nodeName);
if (attrs) {
attrs.forEach(({ name, value }) => node.setAttribute(name, value));
}
}
if (childNodes) {
childNodes.forEach(c => node.appendChild(translateParsed(c)));
}
return node;
}
// Copy inherited stuff from Node statics.
for (const key in Node) {
Element[key] = Node[key];
}
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.hasAttributes = function() {
return !!this.attributes.length;
};
ElementProto.removeAttribute = function(name) {
const oldValue = this.getAttribute(name);
removeAttribute.call(this, name);
MutationObserver.trigger('attribute', this, name, oldValue);
if (this.attributeChangedCallback) {
this.attributeChangedCallback(name, oldValue, null);
}
};
ElementProto.setAttribute = function(name, newValue) {
if (settingProp) return;
const observedAttributes = this.constructor.observedAttributes || [];
const oldValue = this.getAttribute(name);
const propName = attrToPropMap[name];
setAttribute.call(this, name, newValue);
if (propName) {
settingProp = true;
this[propName] = newValue;
settingProp = false;
}
MutationObserver.trigger('attribute', this, name, oldValue);
if (this.attributeChangedCallback && observedAttributes.indexOf(name) > -1) {
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');
})
: [];
};
// This really should go on HTMLElement, however, all nodes created by Undom
// derive from Element.
ElementProto.attachShadow = function({ mode, test }) {
// We use an element to denote this. This is proposed to be the element that
// will be used as the declarative form for Shadow DOM. This is subject to
// change.
const host = this;
const shadowRoot = document.createElement('shadowroot');
if (this.__hasShadowRoot) {
throw new Error(
'Failed to execute "attachShadow" on "Element": Shadow root cannot be created on a host which already hosts a shadow tree.'
);
}
// This is so we can check for multiple invocations.
this.__hasShadowRoot = true;
prop(shadowRoot, 'host', {
get() {
return host;
}
});
prop(shadowRoot, 'mode', {
get() {
return mode;
}
});
if (mode === 'open') {
prop(this, 'shadowRoot', {
get() {
return shadowRoot;
}
});
}
return shadowRoot;
};
prop(ElementProto, 'classList', {
get() {
return new ClassList(this);
}
});
prop(ElementProto, 'className', {
get() {
return this.getAttribute('class');
},
set(val) {
this.setAttribute('class', val);
}
});
prop(ElementProto, 'innerHTML', {
get() {
return this.childNodes
.map(c => {
return c.nodeType === Node.COMMENT_NODE
? `<!--${c.textContent}-->`
: c.outerHTML || c.textContent;
})
.join('');
},
set(val) {
if (this.nodeName === 'SCRIPT') {
return (this.textContent = val);
}
while (this.hasChildNodes()) {
this.removeChild(this.firstChild);
}
if (val) {
this.appendChild(translateParsed(parseFragment(val)));
}
}
});
prop(ElementProto, 'localName', {
get() {
return (this.nodeName || '').toLowerCase();
}
});
prop(ElementProto, 'nextElementSibling', {
get() {
let sib = this;
while ((sib = sib.nextSibling)) {
if (sib.nodeType === 1) {
return sib;
}
}
return null;
}
});
prop(ElementProto, 'nodeName', {
get() {
return this._nodeName || customElements.__fixLostNodeNameForElement(this);
},
// TODO check to see if this is necessary anymore. This shouldn't be settable.
set(val) {
this._nodeName = val;
}
});
prop(ElementProto, 'nodeType', {
get() {
return Node.ELEMENT_NODE;
}
});
prop(ElementProto, 'nodeValue', {
get() {
return null;
}
});
prop(ElementProto, 'onload', {
get() {
return this._onload;
},
set(val) {
this._onload = val;
if (!this._loaded && this._onload && this._src) {
this._loaded = true;
this._onload();
}
}
});
prop(ElementProto, 'outerHTML', {
get() {
const { localName } = this;
if (localName === '#comment') {
return `<!--${this.innerHTML}-->`;
}
return `<${localName}${this.attributes.reduce(
(prev, { name, value }) => prev + ` ${name}="${value}"`,
''
)}>${this.innerHTML}</${localName}>`;
},
set(val) {
throw new Error('Not implemented: set outerHTML');
}
});
prop(ElementProto, 'previousElementSibling', {
get() {
let sib = this;
while ((sib = sib.previousSibling)) {
if (sib.nodeType === 1) {
return sib;
}
}
return null;
}
});
prop(ElementProto, 'sheet', {
get() {
if (this.nodeName === 'STYLE') {
return this._sheet || (this._sheet = new CSSStyleSheet(this));
}
}
});
prop(ElementProto, 'src', {
get() {
return this._src;
},
set(val) {
this.setAttribute('src', val);
this._loaded = false;
this._src = val;
if (
this.nodeName === 'SCRIPT' &&
(!this.type || this.type === 'text/javascript')
) {
execFile(val);
}
if (!this._loaded && this._onload) {
this._loaded = true;
this._onload();
}
}
});
prop(ElementProto, 'tagName', {
get() {
return this.nodeName;
}
});
prop(ElementProto, 'textContent', {
get() {
return this.childNodes.map(c => c.textContent).join('');
},
set(textContent) {
while (this.hasChildNodes()) {
this.removeChild(this.firstChild);
}
this.appendChild(document.createTextNode(textContent));
}
});
module.exports = Element;