823 lines
26 KiB
JavaScript
823 lines
26 KiB
JavaScript
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('node:stream'), require('preact')) :
|
|
typeof define === 'function' && define.amd ? define(['exports', 'node:stream', 'preact'], factory) :
|
|
(global = global || self, factory(global.preactRenderToString = {}, global.node_stream, global.preact));
|
|
}(this, (function (exports, node_stream, preact) {
|
|
const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
|
|
const NAMESPACE_REPLACE_REGEX = /^(xlink|xmlns|xml)([A-Z])/;
|
|
const HTML_LOWER_CASE = /^(?:accessK|auto[A-Z]|cell|ch|col|cont|cross|dateT|encT|form[A-Z]|frame|hrefL|inputM|maxL|minL|noV|playsI|popoverT|readO|rowS|src[A-Z]|tabI|useM|item[A-Z])/;
|
|
const SVG_CAMEL_CASE = /^ac|^ali|arabic|basel|cap|clipPath$|clipRule$|color|dominant|enable|fill|flood|font|glyph[^R]|horiz|image|letter|lighting|marker[^WUH]|overline|panose|pointe|paint|rendering|shape|stop|strikethrough|stroke|text[^L]|transform|underline|unicode|units|^v[^i]|^w|^xH/; // Boolean DOM properties that translate to enumerated ('true'/'false') attributes
|
|
|
|
const HTML_ENUMERATED = new Set(['draggable', 'spellcheck']); // DOM properties that should NOT have "px" added when numeric
|
|
|
|
const ENCODED_ENTITIES = /["&<]/;
|
|
/** @param {string} str */
|
|
|
|
function encodeEntities(str) {
|
|
// Skip all work for strings with no entities needing encoding:
|
|
if (str.length === 0 || ENCODED_ENTITIES.test(str) === false) return str;
|
|
let last = 0,
|
|
i = 0,
|
|
out = '',
|
|
ch = ''; // Seek forward in str until the next entity char:
|
|
|
|
for (; i < str.length; i++) {
|
|
switch (str.charCodeAt(i)) {
|
|
case 34:
|
|
ch = '"';
|
|
break;
|
|
|
|
case 38:
|
|
ch = '&';
|
|
break;
|
|
|
|
case 60:
|
|
ch = '<';
|
|
break;
|
|
|
|
default:
|
|
continue;
|
|
} // Append skipped/buffered characters and the encoded entity:
|
|
|
|
|
|
if (i !== last) out = out + str.slice(last, i);
|
|
out = out + ch; // Start the next seek/buffer after the entity's offset:
|
|
|
|
last = i + 1;
|
|
}
|
|
|
|
if (i !== last) out = out + str.slice(last, i);
|
|
return out;
|
|
}
|
|
const JS_TO_CSS = {};
|
|
const IS_NON_DIMENSIONAL = new Set(['animation-iteration-count', 'border-image-outset', 'border-image-slice', 'border-image-width', 'box-flex', 'box-flex-group', 'box-ordinal-group', 'column-count', 'fill-opacity', 'flex', 'flex-grow', 'flex-negative', 'flex-order', 'flex-positive', 'flex-shrink', 'flood-opacity', 'font-weight', 'grid-column', 'grid-row', 'line-clamp', 'line-height', 'opacity', 'order', 'orphans', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'tab-size', 'widows', 'z-index', 'zoom']);
|
|
const CSS_REGEX = /[A-Z]/g; // Convert an Object style to a CSSText string
|
|
|
|
function styleObjToCss(s) {
|
|
let str = '';
|
|
|
|
for (let prop in s) {
|
|
let val = s[prop];
|
|
|
|
if (val != null && val !== '') {
|
|
const name = prop[0] == '-' ? prop : JS_TO_CSS[prop] || (JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$&').toLowerCase());
|
|
let suffix = ';';
|
|
|
|
if (typeof val === 'number' && // Exclude custom-attributes
|
|
!name.startsWith('--') && !IS_NON_DIMENSIONAL.has(name)) {
|
|
suffix = 'px;';
|
|
}
|
|
|
|
str = str + name + ':' + val + suffix;
|
|
}
|
|
}
|
|
|
|
return str || undefined;
|
|
}
|
|
|
|
function markAsDirty() {
|
|
this.__d = true;
|
|
}
|
|
|
|
function createComponent(vnode, context) {
|
|
return {
|
|
__v: vnode,
|
|
context,
|
|
props: vnode.props,
|
|
// silently drop state updates
|
|
setState: markAsDirty,
|
|
forceUpdate: markAsDirty,
|
|
__d: true,
|
|
// hooks
|
|
__h: new Array(0)
|
|
};
|
|
} // Necessary for createContext api. Setting this property will pass
|
|
/**
|
|
* @template T
|
|
*/
|
|
|
|
class Deferred {
|
|
constructor() {
|
|
// eslint-disable-next-line lines-around-comment
|
|
|
|
/** @type {Promise<T>} */
|
|
this.promise = new Promise((resolve, reject) => {
|
|
this.resolve = resolve;
|
|
this.reject = reject;
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
// Options hooks
|
|
const DIFF = '__b';
|
|
const RENDER = '__r';
|
|
const DIFFED = 'diffed';
|
|
const COMMIT = '__c';
|
|
const SKIP_EFFECTS = '__s';
|
|
const CATCH_ERROR = '__e'; // VNode properties
|
|
|
|
const COMPONENT = '__c';
|
|
const CHILDREN = '__k';
|
|
const PARENT = '__';
|
|
|
|
const VNODE = '__v';
|
|
const DIRTY = '__d';
|
|
const NEXT_STATE = '__s';
|
|
const CHILD_DID_SUSPEND = '__c';
|
|
|
|
const EMPTY_OBJ = {};
|
|
const EMPTY_ARR = [];
|
|
const isArray = Array.isArray;
|
|
const assign = Object.assign;
|
|
const EMPTY_STR = ''; // Global state for the current render pass
|
|
|
|
let beforeDiff, afterDiff, renderHook, ummountHook;
|
|
/**
|
|
* Render Preact JSX + Components to an HTML string.
|
|
* @param {VNode} vnode JSX Element / VNode to render
|
|
* @param {Object} [context={}] Initial root context object
|
|
* @param {RendererState} [_rendererState] for internal use
|
|
* @returns {string} serialized HTML
|
|
*/
|
|
|
|
function renderToString(vnode, context, _rendererState) {
|
|
// Performance optimization: `renderToString` is synchronous and we
|
|
// therefore don't execute any effects. To do that we pass an empty
|
|
// array to `options._commit` (`__c`). But we can go one step further
|
|
// and avoid a lot of dirty checks and allocations by setting
|
|
// `options._skipEffects` (`__s`) too.
|
|
const previousSkipEffects = preact.options[SKIP_EFFECTS];
|
|
preact.options[SKIP_EFFECTS] = true; // store options hooks once before each synchronous render call
|
|
|
|
beforeDiff = preact.options[DIFF];
|
|
afterDiff = preact.options[DIFFED];
|
|
renderHook = preact.options[RENDER];
|
|
ummountHook = preact.options.unmount;
|
|
const parent = preact.h(preact.Fragment, null);
|
|
parent[CHILDREN] = [vnode];
|
|
|
|
try {
|
|
const rendered = _renderToString(vnode, context || EMPTY_OBJ, false, undefined, parent, false, _rendererState);
|
|
|
|
if (isArray(rendered)) {
|
|
return rendered.join(EMPTY_STR);
|
|
}
|
|
|
|
return rendered;
|
|
} catch (e) {
|
|
if (e.then) {
|
|
throw new Error('Use "renderToStringAsync" for suspenseful rendering.');
|
|
}
|
|
|
|
throw e;
|
|
} finally {
|
|
// options._commit, we don't schedule any effects in this library right now,
|
|
// so we can pass an empty queue to this hook.
|
|
if (preact.options[COMMIT]) preact.options[COMMIT](vnode, EMPTY_ARR);
|
|
preact.options[SKIP_EFFECTS] = previousSkipEffects;
|
|
EMPTY_ARR.length = 0;
|
|
}
|
|
}
|
|
/**
|
|
* @param {VNode} vnode
|
|
* @param {Record<string, unknown>} context
|
|
*/
|
|
|
|
function renderClassComponent(vnode, context) {
|
|
let type =
|
|
/** @type {import("preact").ComponentClass<typeof vnode.props>} */
|
|
vnode.type;
|
|
let isMounting = true;
|
|
let c;
|
|
|
|
if (vnode[COMPONENT]) {
|
|
isMounting = false;
|
|
c = vnode[COMPONENT];
|
|
c.state = c[NEXT_STATE];
|
|
} else {
|
|
c = new type(vnode.props, context);
|
|
}
|
|
|
|
vnode[COMPONENT] = c;
|
|
c[VNODE] = vnode;
|
|
c.props = vnode.props;
|
|
c.context = context; // turn off stateful re-rendering:
|
|
|
|
c[DIRTY] = true;
|
|
if (c.state == null) c.state = EMPTY_OBJ;
|
|
|
|
if (c[NEXT_STATE] == null) {
|
|
c[NEXT_STATE] = c.state;
|
|
}
|
|
|
|
if (type.getDerivedStateFromProps) {
|
|
c.state = assign({}, c.state, type.getDerivedStateFromProps(c.props, c.state));
|
|
} else if (isMounting && c.componentWillMount) {
|
|
c.componentWillMount(); // If the user called setState in cWM we need to flush pending,
|
|
// state updates. This is the same behaviour in React.
|
|
|
|
c.state = c[NEXT_STATE] !== c.state ? c[NEXT_STATE] : c.state;
|
|
} else if (!isMounting && c.componentWillUpdate) {
|
|
c.componentWillUpdate();
|
|
}
|
|
|
|
if (renderHook) renderHook(vnode);
|
|
return c.render(c.props, c.state, context);
|
|
}
|
|
/**
|
|
* Recursively render VNodes to HTML.
|
|
* @param {VNode|any} vnode
|
|
* @param {any} context
|
|
* @param {boolean} isSvgMode
|
|
* @param {any} selectValue
|
|
* @param {VNode} parent
|
|
* @param {boolean} asyncMode
|
|
* @param {RendererState | undefined} [renderer]
|
|
* @returns {string | Promise<string> | (string | Promise<string>)[]}
|
|
*/
|
|
|
|
|
|
function _renderToString(vnode, context, isSvgMode, selectValue, parent, asyncMode, renderer) {
|
|
// Ignore non-rendered VNodes/values
|
|
if (vnode == null || vnode === true || vnode === false || vnode === EMPTY_STR) {
|
|
return EMPTY_STR;
|
|
}
|
|
|
|
let vnodeType = typeof vnode; // Text VNodes: escape as HTML
|
|
|
|
if (vnodeType != 'object') {
|
|
if (vnodeType == 'function') return EMPTY_STR;
|
|
return vnodeType == 'string' ? encodeEntities(vnode) : vnode + EMPTY_STR;
|
|
} // Recurse into children / Arrays
|
|
|
|
|
|
if (isArray(vnode)) {
|
|
let rendered = EMPTY_STR,
|
|
renderArray;
|
|
parent[CHILDREN] = vnode;
|
|
const vnodeLength = vnode.length;
|
|
|
|
for (let i = 0; i < vnodeLength; i++) {
|
|
let child = vnode[i];
|
|
if (child == null || typeof child == 'boolean') continue;
|
|
|
|
const childRender = _renderToString(child, context, isSvgMode, selectValue, parent, asyncMode, renderer);
|
|
|
|
if (typeof childRender == 'string') {
|
|
rendered = rendered + childRender;
|
|
} else {
|
|
if (!renderArray) {
|
|
renderArray = new Array(vnodeLength);
|
|
}
|
|
|
|
if (rendered) renderArray.push(rendered);
|
|
rendered = EMPTY_STR;
|
|
|
|
if (isArray(childRender)) {
|
|
renderArray.push(...childRender);
|
|
} else {
|
|
renderArray.push(childRender);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (renderArray) {
|
|
if (rendered) renderArray.push(rendered);
|
|
return renderArray;
|
|
}
|
|
|
|
return rendered;
|
|
} // VNodes have {constructor:undefined} to prevent JSON injection:
|
|
|
|
|
|
if (vnode.constructor !== undefined) return EMPTY_STR;
|
|
vnode[PARENT] = parent;
|
|
if (beforeDiff) beforeDiff(vnode);
|
|
let type = vnode.type,
|
|
props = vnode.props; // Invoke rendering on Components
|
|
|
|
if (typeof type == 'function') {
|
|
let cctx = context,
|
|
contextType,
|
|
rendered,
|
|
component;
|
|
|
|
if (type === preact.Fragment) {
|
|
// Serialized precompiled JSX.
|
|
if ('tpl' in props) {
|
|
let out = EMPTY_STR;
|
|
|
|
for (let i = 0; i < props.tpl.length; i++) {
|
|
out = out + props.tpl[i];
|
|
|
|
if (props.exprs && i < props.exprs.length) {
|
|
const value = props.exprs[i];
|
|
if (value == null) continue; // Check if we're dealing with a vnode or an array of nodes
|
|
|
|
if (typeof value == 'object' && (value.constructor === undefined || isArray(value))) {
|
|
out = out + _renderToString(value, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
} else {
|
|
// Values are pre-escaped by the JSX transform
|
|
out = out + value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return out;
|
|
} else if ('UNSTABLE_comment' in props) {
|
|
// Fragments are the least used components of core that's why
|
|
// branching here for comments has the least effect on perf.
|
|
return '<!--' + encodeEntities(props.UNSTABLE_comment) + '-->';
|
|
}
|
|
|
|
rendered = props.children;
|
|
} else {
|
|
contextType = type.contextType;
|
|
|
|
if (contextType != null) {
|
|
let provider = context[contextType.__c];
|
|
cctx = provider ? provider.props.value : contextType.__;
|
|
}
|
|
|
|
let isClassComponent = type.prototype && typeof type.prototype.render == 'function';
|
|
|
|
if (isClassComponent) {
|
|
rendered =
|
|
/**#__NOINLINE__**/
|
|
renderClassComponent(vnode, cctx);
|
|
component = vnode[COMPONENT];
|
|
} else {
|
|
vnode[COMPONENT] = component =
|
|
/**#__NOINLINE__**/
|
|
createComponent(vnode, cctx); // If a hook invokes setState() to invalidate the component during rendering,
|
|
// re-render it up to 25 times to allow "settling" of memoized states.
|
|
// Note:
|
|
// This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
|
|
// https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
|
|
|
|
let count = 0;
|
|
|
|
while (component[DIRTY] && count++ < 25) {
|
|
component[DIRTY] = false;
|
|
if (renderHook) renderHook(vnode);
|
|
rendered = type.call(component, props, cctx);
|
|
}
|
|
|
|
component[DIRTY] = true;
|
|
}
|
|
|
|
if (component.getChildContext != null) {
|
|
context = assign({}, context, component.getChildContext());
|
|
}
|
|
|
|
if (isClassComponent && preact.options.errorBoundaries && (type.getDerivedStateFromError || component.componentDidCatch)) {
|
|
// When a component returns a Fragment node we flatten it in core, so we
|
|
// need to mirror that logic here too
|
|
let isTopLevelFragment = rendered != null && rendered.type === preact.Fragment && rendered.key == null && rendered.props.tpl == null;
|
|
rendered = isTopLevelFragment ? rendered.props.children : rendered;
|
|
|
|
try {
|
|
return _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
} catch (err) {
|
|
if (type.getDerivedStateFromError) {
|
|
component[NEXT_STATE] = type.getDerivedStateFromError(err);
|
|
}
|
|
|
|
if (component.componentDidCatch) {
|
|
component.componentDidCatch(err, EMPTY_OBJ);
|
|
}
|
|
|
|
if (component[DIRTY]) {
|
|
rendered = renderClassComponent(vnode, context);
|
|
component = vnode[COMPONENT];
|
|
|
|
if (component.getChildContext != null) {
|
|
context = assign({}, context, component.getChildContext());
|
|
}
|
|
|
|
let isTopLevelFragment = rendered != null && rendered.type === preact.Fragment && rendered.key == null && rendered.props.tpl == null;
|
|
rendered = isTopLevelFragment ? rendered.props.children : rendered;
|
|
return _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
}
|
|
|
|
return EMPTY_STR;
|
|
} finally {
|
|
if (afterDiff) afterDiff(vnode);
|
|
if (ummountHook) ummountHook(vnode);
|
|
}
|
|
}
|
|
} // When a component returns a Fragment node we flatten it in core, so we
|
|
// need to mirror that logic here too
|
|
|
|
|
|
let isTopLevelFragment = rendered != null && rendered.type === preact.Fragment && rendered.key == null && rendered.props.tpl == null;
|
|
rendered = isTopLevelFragment ? rendered.props.children : rendered;
|
|
|
|
try {
|
|
// Recurse into children before invoking the after-diff hook
|
|
const str = _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
|
|
if (afterDiff) afterDiff(vnode); // when we are dealing with suspense we can't do this...
|
|
|
|
if (preact.options.unmount) preact.options.unmount(vnode);
|
|
return str;
|
|
} catch (error) {
|
|
if (!asyncMode && renderer && renderer.onError) {
|
|
let res = renderer.onError(error, vnode, (child, parent) => _renderToString(child, context, isSvgMode, selectValue, parent, asyncMode, renderer));
|
|
if (res !== undefined) return res;
|
|
let errorHook = preact.options[CATCH_ERROR];
|
|
if (errorHook) errorHook(error, vnode);
|
|
return EMPTY_STR;
|
|
}
|
|
|
|
if (!asyncMode) throw error;
|
|
if (!error || typeof error.then != 'function') throw error;
|
|
|
|
const renderNestedChildren = () => {
|
|
try {
|
|
return _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
} catch (e) {
|
|
if (!e || typeof e.then != 'function') throw e;
|
|
return e.then(() => _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer), renderNestedChildren);
|
|
}
|
|
};
|
|
|
|
return error.then(renderNestedChildren);
|
|
}
|
|
} // Serialize Element VNodes to HTML
|
|
|
|
|
|
let s = '<' + type,
|
|
html = EMPTY_STR,
|
|
children;
|
|
|
|
for (let name in props) {
|
|
let v = props[name];
|
|
|
|
if (typeof v == 'function' && name !== 'class' && name !== 'className') {
|
|
continue;
|
|
}
|
|
|
|
switch (name) {
|
|
case 'children':
|
|
children = v;
|
|
continue;
|
|
// VDOM-specific props
|
|
|
|
case 'key':
|
|
case 'ref':
|
|
case '__self':
|
|
case '__source':
|
|
continue;
|
|
// prefer for/class over htmlFor/className
|
|
|
|
case 'htmlFor':
|
|
if ('for' in props) continue;
|
|
name = 'for';
|
|
break;
|
|
|
|
case 'className':
|
|
if ('class' in props) continue;
|
|
name = 'class';
|
|
break;
|
|
// Form element reflected properties
|
|
|
|
case 'defaultChecked':
|
|
name = 'checked';
|
|
break;
|
|
|
|
case 'defaultSelected':
|
|
name = 'selected';
|
|
break;
|
|
// Special value attribute handling
|
|
|
|
case 'defaultValue':
|
|
case 'value':
|
|
name = 'value';
|
|
|
|
switch (type) {
|
|
// <textarea value="a&b"> --> <textarea>a&b</textarea>
|
|
case 'textarea':
|
|
children = v;
|
|
continue;
|
|
// <select value> is serialized as a selected attribute on the matching option child
|
|
|
|
case 'select':
|
|
selectValue = v;
|
|
continue;
|
|
// Add a selected attribute to <option> if its value matches the parent <select> value
|
|
|
|
case 'option':
|
|
if (selectValue == v && !('selected' in props)) {
|
|
s = s + ' selected';
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'dangerouslySetInnerHTML':
|
|
html = v && v.__html;
|
|
continue;
|
|
// serialize object styles to a CSS string
|
|
|
|
case 'style':
|
|
if (typeof v === 'object') {
|
|
v = styleObjToCss(v);
|
|
}
|
|
|
|
break;
|
|
|
|
case 'acceptCharset':
|
|
name = 'accept-charset';
|
|
break;
|
|
|
|
case 'httpEquiv':
|
|
name = 'http-equiv';
|
|
break;
|
|
|
|
default:
|
|
{
|
|
if (NAMESPACE_REPLACE_REGEX.test(name)) {
|
|
name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase();
|
|
} else if (UNSAFE_NAME.test(name)) {
|
|
continue;
|
|
} else if ((name[4] === '-' || HTML_ENUMERATED.has(name)) && v != null) {
|
|
// serialize boolean aria-xyz or enumerated attribute values as strings
|
|
v = v + EMPTY_STR;
|
|
} else if (isSvgMode) {
|
|
if (SVG_CAMEL_CASE.test(name)) {
|
|
name = name === 'panose1' ? 'panose-1' : name.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
}
|
|
} else if (HTML_LOWER_CASE.test(name)) {
|
|
name = name.toLowerCase();
|
|
}
|
|
}
|
|
} // write this attribute to the buffer
|
|
|
|
|
|
if (v != null && v !== false) {
|
|
if (v === true || v === EMPTY_STR) {
|
|
s = s + ' ' + name;
|
|
} else {
|
|
s = s + ' ' + name + '="' + (typeof v == 'string' ? encodeEntities(v) : v + EMPTY_STR) + '"';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (UNSAFE_NAME.test(type)) {
|
|
// this seems to performs a lot better than throwing
|
|
// return '<!-- -->';
|
|
throw new Error(`${type} is not a valid HTML tag name in ${s}>`);
|
|
}
|
|
|
|
if (html) ; else if (typeof children === 'string') {
|
|
// single text child
|
|
html = encodeEntities(children);
|
|
} else if (children != null && children !== false && children !== true) {
|
|
// recurse into this element VNode's children
|
|
let childSvgMode = type === 'svg' || type !== 'foreignObject' && isSvgMode;
|
|
html = _renderToString(children, context, childSvgMode, selectValue, vnode, asyncMode, renderer);
|
|
}
|
|
|
|
if (afterDiff) afterDiff(vnode);
|
|
if (ummountHook) ummountHook(vnode); // Emit self-closing tag for empty void elements:
|
|
|
|
if (!html && SELF_CLOSING.has(type)) {
|
|
return s + '/>';
|
|
}
|
|
|
|
const endTag = '</' + type + '>';
|
|
const startTag = s + '>';
|
|
if (isArray(html)) return [startTag, ...html, endTag];else if (typeof html != 'string') return [startTag, html, endTag];
|
|
return startTag + html + endTag;
|
|
}
|
|
|
|
const SELF_CLOSING = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
|
|
/* eslint-disable no-var, key-spacing, object-curly-spacing, prefer-arrow-callback, semi, keyword-spacing */
|
|
// function initPreactIslandElement() {
|
|
// class PreactIslandElement extends HTMLElement {
|
|
// connectedCallback() {
|
|
// var d = this;
|
|
// if (!d.isConnected) return;
|
|
// let i = this.getAttribute('data-target');
|
|
// if (!i) return;
|
|
// var s,
|
|
// e,
|
|
// c = document.createNodeIterator(document, 128);
|
|
// while (c.nextNode()) {
|
|
// let n = c.referenceNode;
|
|
// if (n.data == 'preact-island:' + i) s = n;
|
|
// else if (n.data == '/preact-island:' + i) e = n;
|
|
// if (s && e) break;
|
|
// }
|
|
// if (s && e) {
|
|
// requestAnimationFrame(() => {
|
|
// var p = e.previousSibling;
|
|
// while (p != s) {
|
|
// if (!p || p == s) break;
|
|
// e.parentNode.removeChild(p);
|
|
// p = e.previousSibling;
|
|
// }
|
|
// c = s;
|
|
// while (d.firstChild) {
|
|
// s = d.firstChild;
|
|
// d.removeChild(s);
|
|
// c.after(s);
|
|
// c = s;
|
|
// }
|
|
// d.parentNode.removeChild(d);
|
|
// });
|
|
// }
|
|
// }
|
|
// }
|
|
// customElements.define('preact-island', PreactIslandElement);
|
|
// }
|
|
// To modify the INIT_SCRIPT, uncomment the above code, modify it, and paste it into https://try.terser.org/.
|
|
const INIT_SCRIPT = `class e extends HTMLElement{connectedCallback(){var e=this;if(!e.isConnected)return;let t=this.getAttribute("data-target");if(t){for(var r,a,i=document.createNodeIterator(document,128);i.nextNode();){let e=i.referenceNode;if(e.data=="preact-island:"+t?r=e:e.data=="/preact-island:"+t&&(a=e),r&&a)break}r&&a&&requestAnimationFrame((()=>{for(var t=a.previousSibling;t!=r&&t&&t!=r;)a.parentNode.removeChild(t),t=a.previousSibling;for(i=r;e.firstChild;)r=e.firstChild,e.removeChild(r),i.after(r),i=r;e.parentNode.removeChild(e)}))}}}customElements.define("preact-island",e);`;
|
|
function createInitScript() {
|
|
return `<script>(function(){${INIT_SCRIPT}}())</script>`;
|
|
}
|
|
/**
|
|
* @param {string} id
|
|
* @param {string} content
|
|
* @returns {string}
|
|
*/
|
|
|
|
function createSubtree(id, content) {
|
|
return `<preact-island hidden data-target="${id}">${content}</preact-island>`;
|
|
}
|
|
|
|
/**
|
|
* @param {VNode} vnode
|
|
* @param {RenderToChunksOptions} options
|
|
* @returns {Promise<void>}
|
|
*/
|
|
|
|
async function renderToChunks(vnode, {
|
|
context,
|
|
onWrite,
|
|
abortSignal
|
|
}) {
|
|
context = context || {};
|
|
/** @type {RendererState} */
|
|
|
|
const renderer = {
|
|
start: Date.now(),
|
|
abortSignal,
|
|
onWrite,
|
|
onError: handleError,
|
|
suspended: []
|
|
}; // Synchronously render the shell
|
|
// @ts-ignore - using third internal RendererState argument
|
|
|
|
const shell = renderToString(vnode, context, renderer);
|
|
onWrite(shell); // Wait for any suspended sub-trees if there are any
|
|
|
|
const len = renderer.suspended.length;
|
|
|
|
if (len > 0) {
|
|
onWrite('<div hidden>');
|
|
onWrite(createInitScript()); // We should keep checking all promises
|
|
|
|
await forkPromises(renderer);
|
|
onWrite('</div>');
|
|
}
|
|
}
|
|
|
|
async function forkPromises(renderer) {
|
|
if (renderer.suspended.length > 0) {
|
|
const suspensions = [...renderer.suspended];
|
|
await Promise.all(renderer.suspended.map(s => s.promise));
|
|
renderer.suspended = renderer.suspended.filter(s => !suspensions.includes(s));
|
|
await forkPromises(renderer);
|
|
}
|
|
}
|
|
/** @type {RendererErrorHandler} */
|
|
|
|
|
|
function handleError(error, vnode, renderChild) {
|
|
if (!error || !error.then) return; // walk up to the Suspense boundary
|
|
|
|
while (vnode = vnode[PARENT]) {
|
|
let component = vnode[COMPONENT];
|
|
|
|
if (component && component[CHILD_DID_SUSPEND]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!vnode) return;
|
|
const id = vnode.__v;
|
|
const found = this.suspended.find(x => x.id === id);
|
|
const race = new Deferred();
|
|
const abortSignal = this.abortSignal;
|
|
|
|
if (abortSignal) {
|
|
// @ts-ignore 2554 - implicit undefined arg
|
|
if (abortSignal.aborted) race.resolve();else abortSignal.addEventListener('abort', race.resolve);
|
|
}
|
|
|
|
const promise = error.then(() => {
|
|
if (abortSignal && abortSignal.aborted) return;
|
|
const child = renderChild(vnode.props.children, vnode);
|
|
if (child) this.onWrite(createSubtree(id, child));
|
|
}, // TODO: Abort and send hydration code snippet to client
|
|
// to attempt to recover during hydration
|
|
this.onError);
|
|
this.suspended.push({
|
|
id,
|
|
vnode,
|
|
promise: Promise.race([promise, race.promise])
|
|
});
|
|
const fallback = renderChild(vnode.props.fallback);
|
|
return found ? '' : `<!--preact-island:${id}-->${fallback}<!--/preact-island:${id}-->`;
|
|
}
|
|
|
|
/**
|
|
* @typedef {object} RenderToPipeableStreamOptions
|
|
* @property {() => void} [onShellReady]
|
|
* @property {() => void} [onAllReady]
|
|
* @property {(error) => void} [onError]
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} PipeableStream
|
|
* @property {() => void} abort
|
|
* @property {(writable: import('stream').Writable) => void} pipe
|
|
*/
|
|
|
|
/**
|
|
* @param {import('preact').VNode} vnode
|
|
* @param {RenderToPipeableStreamOptions} options
|
|
* @param {any} [context]
|
|
* @returns {PipeableStream}
|
|
*/
|
|
|
|
function renderToPipeableStream(vnode, options, context) {
|
|
const encoder = new TextEncoder('utf-8');
|
|
const controller = new AbortController();
|
|
const stream = new node_stream.PassThrough();
|
|
renderToChunks(vnode, {
|
|
context,
|
|
abortSignal: controller.signal,
|
|
onError: error => {
|
|
if (options.onError) {
|
|
options.onError(error);
|
|
}
|
|
|
|
controller.abort(error);
|
|
},
|
|
|
|
onWrite(s) {
|
|
stream.write(encoder.encode(s));
|
|
}
|
|
|
|
}).then(() => {
|
|
options.onAllReady && options.onAllReady();
|
|
stream.end();
|
|
}).catch(error => {
|
|
stream.destroy();
|
|
|
|
if (options.onError) {
|
|
options.onError(error);
|
|
} else {
|
|
throw error;
|
|
}
|
|
});
|
|
Promise.resolve().then(() => {
|
|
options.onShellReady && options.onShellReady();
|
|
});
|
|
return {
|
|
/**
|
|
* @param {unknown} [reason]
|
|
*/
|
|
abort(reason = new Error('The render was aborted by the server without a reason.')) {
|
|
controller.abort();
|
|
stream.destroy();
|
|
|
|
if (options.onError) {
|
|
options.onError(reason);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {import("stream").Writable} writable
|
|
*/
|
|
pipe(writable) {
|
|
stream.pipe(writable, {
|
|
end: true
|
|
});
|
|
}
|
|
|
|
};
|
|
}
|
|
|
|
exports.renderToPipeableStream = renderToPipeableStream;
|
|
|
|
})));
|
|
//# sourceMappingURL=index.umd.js.map
|