(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} */ 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} context */ function renderClassComponent(vnode, context) { let type = /** @type {import("preact").ComponentClass} */ 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 | Promise)[]} */ 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 ''; } 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) { // case 'textarea': children = v; continue; // 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 = ''; 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 ``; } /** * @param {string} id * @param {string} content * @returns {string} */ function createSubtree(id, content) { return ``; } /** * @param {VNode} vnode * @param {RenderToChunksOptions} options * @returns {Promise} */ 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(''); } } 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 ? '' : `${fallback}`; } /** * @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