diff --git a/package-lock.json b/package-lock.json index 8ac19e7..e4d3c80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@babel/register": "^7.12.10", "@changesets/changelog-github": "^0.4.1", "@changesets/cli": "^2.18.0", - "baseline-rts": "npm:preact-render-to-string@latest", + "baseline-rts": "npm:preact-render-to-string@6.5.11", "benchmarkjs-pretty": "^2.0.1", "chai": "^4.2.0", "check-export-map": "^1.3.1", @@ -2561,11 +2561,10 @@ }, "node_modules/baseline-rts": { "name": "preact-render-to-string", - "version": "6.5.10", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.10.tgz", - "integrity": "sha512-BJdypTQaBA5UbTF9NKZS3zP93Sw33tZOxNXIfuHofqOZFoMdsquNkVebs/HkEw0in/Qbi6Ep/Anngnj+VsHeBQ==", + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", "dev": true, - "license": "MIT", "peerDependencies": { "preact": ">=10" } diff --git a/package.json b/package.json index f0d7a1b..91c67b2 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "bench": "BABEL_ENV=test node -r @babel/register benchmarks index.js", "bench:v8": "BABEL_ENV=test microbundle benchmarks/index.js -f modern --alias benchmarkjs-pretty=benchmarks/lib/benchmark-lite.js --external none --target node --no-compress --no-sourcemap --raw -o benchmarks/.v8.mjs && v8 --module benchmarks/.v8.modern.js", "build": "npm run -s transpile && npm run -s transpile:jsx && npm run -s transpile:stream && npm run -s transpile:stream-node && npm run -s copy-typescript-definition", - "postbuild": "node ./config/node-13-exports.js && node ./config/node-commonjs.js && node ./config/node-verify-exports.js && check-export-map", "transpile": "microbundle src/index.js -f es,cjs,umd", "transpile:stream": "microbundle src/stream.js -o dist/stream/index.js -f es,cjs,umd", "transpile:stream-node": "microbundle src/stream-node.js -o dist/stream/node/index.js -f es,cjs,umd --target node", @@ -133,7 +132,7 @@ "@babel/register": "^7.12.10", "@changesets/changelog-github": "^0.4.1", "@changesets/cli": "^2.18.0", - "baseline-rts": "npm:preact-render-to-string@latest", + "baseline-rts": "npm:preact-render-to-string@6.5.11", "benchmarkjs-pretty": "^2.0.1", "chai": "^4.2.0", "check-export-map": "^1.3.1", diff --git a/src/index.js b/src/index.js index c648b4f..07ab07f 100644 --- a/src/index.js +++ b/src/index.js @@ -202,199 +202,199 @@ function renderClassComponent(vnode, context) { 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 +const NO_ACTION = 1 << 0; +const DOM_NODE = 1 << 1; +const TEXT_NODE = 1 << 2; +const FUNCTION_NODE = 1 << 3; +const LIST_NODE = 1 << 4; + +const getType = (vnode) => { if ( vnode == null || vnode === true || vnode === false || vnode === EMPTY_STR ) { - return EMPTY_STR; + return NO_ACTION; + } else if (typeof vnode != 'object') { + return TEXT_NODE; + } else if (isArray(vnode)) { + return LIST_NODE; + } else if (vnode.constructor !== undefined) { + return NO_ACTION; + } else if (typeof vnode.type === 'function') { + return FUNCTION_NODE; } + return DOM_NODE; +}; - 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; - } +const handleTemplate = ( + vnode, + props, + context, + isSvgMode, + selectValue, + asyncMode, + renderer +) => { + let out = EMPTY_STR; + for (let i = 0; i < props.tpl.length; i++) { + out = out + props.tpl[i]; - // Recurse into children / Arrays - if (isArray(vnode)) { - let rendered = EMPTY_STR, - renderArray; - parent[CHILDREN] = vnode; - for (let i = 0; i < vnode.length; i++) { - let child = vnode[i]; - if (child == null || typeof child == 'boolean') continue; - - const childRender = _renderToString( - child, - context, - isSvgMode, - selectValue, - parent, - asyncMode, - renderer - ); + if (props.exprs && i < props.exprs.length) { + const value = props.exprs[i]; + if (value == null) continue; - if (typeof childRender == 'string') { - rendered = rendered + childRender; + // 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 { - if (!renderArray) { - renderArray = []; - } - - if (rendered) renderArray.push(rendered); - - rendered = EMPTY_STR; - - if (isArray(childRender)) { - renderArray.push(...childRender); - } else { - renderArray.push(childRender); - } + // Values are pre-escaped by the JSX transform + out = out + value; } } - - 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; + return out; +}; +const handleFunctionNode = ( + vnode, + context, + isSvgMode, + selectValue, + parent, + asyncMode, + renderer +) => { 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 === 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; - } - } - } + let cctx = context, + contextType, + rendered, + component; + if (type === Fragment) { + // Serialized precompiled JSX. + if ('tpl' in props) { + return /**#__NOINLINE__**/ handleTemplate( + vnode, + props, + context, + isSvgMode, + selectValue, + asyncMode, + renderer + ); + } 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 ''; + } - 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.__; + } - rendered = props.children; + let isClassComponent = + type.prototype && typeof type.prototype.render == 'function'; + if (isClassComponent) { + rendered = /**#__NOINLINE__**/ renderClassComponent(vnode, cctx); + component = vnode[COMPONENT]; } else { - contextType = type.contextType; - if (contextType != null) { - let provider = context[contextType.__c]; - cctx = provider ? provider.props.value : contextType.__; + 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; + } - 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( + if (component.getChildContext != null) { + context = assign({}, context, component.getChildContext()); + } + + if ( + isClassComponent && + 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 === Fragment && + rendered.key == null && + rendered.props.tpl == null; + rendered = isTopLevelFragment ? rendered.props.children : rendered; + + try { + return _renderToString( + rendered, + context, + isSvgMode, + selectValue, vnode, - cctx + asyncMode, + renderer ); + } catch (err) { + if (type.getDerivedStateFromError) { + component[NEXT_STATE] = type.getDerivedStateFromError(err); + } - // 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 (component.componentDidCatch) { + component.componentDidCatch(err, EMPTY_OBJ); + } - if (renderHook) renderHook(vnode); + if (component[DIRTY]) { + rendered = renderClassComponent(vnode, context); + component = vnode[COMPONENT]; - rendered = type.call(component, props, cctx); - } - component[DIRTY] = true; - } + if (component.getChildContext != null) { + context = assign({}, context, component.getChildContext()); + } - if (component.getChildContext != null) { - context = assign({}, context, component.getChildContext()); - } + let isTopLevelFragment = + rendered != null && + rendered.type === Fragment && + rendered.key == null && + rendered.props.tpl == null; + rendered = isTopLevelFragment ? rendered.props.children : rendered; - if ( - isClassComponent && - 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 === Fragment && - rendered.key == null && - rendered.props.tpl == null; - rendered = isTopLevelFragment ? rendered.props.children : rendered; - - try { return _renderToString( rendered, context, @@ -404,137 +404,119 @@ function _renderToString( asyncMode, renderer ); - } catch (err) { - if (type.getDerivedStateFromError) { - component[NEXT_STATE] = type.getDerivedStateFromError(err); - } + } - if (component.componentDidCatch) { - component.componentDidCatch(err, EMPTY_OBJ); - } + return EMPTY_STR; + } finally { + if (afterDiff) afterDiff(vnode); + vnode[PARENT] = null; - if (component[DIRTY]) { - rendered = renderClassComponent(vnode, context); - component = vnode[COMPONENT]; + if (ummountHook) ummountHook(vnode); + } + } + } - if (component.getChildContext != null) { - context = assign({}, context, component.getChildContext()); - } + // 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 === Fragment && + rendered.key == null && + rendered.props.tpl == null; + rendered = isTopLevelFragment ? rendered.props.children : rendered; - let isTopLevelFragment = - rendered != null && - rendered.type === 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 + ); - return _renderToString( - rendered, - context, - isSvgMode, - selectValue, - vnode, - asyncMode, - renderer - ); - } + if (afterDiff) afterDiff(vnode); + // when we are dealing with suspense we can't do this... + vnode[PARENT] = null; + + if (options.unmount) options.unmount(vnode); + + return str; + } catch (error) { + if (!asyncMode && renderer && renderer.onError) { + let res = renderer.onError(error, vnode, (child) => + _renderToString( + child, + context, + isSvgMode, + selectValue, + vnode, + asyncMode, + renderer + ) + ); - return EMPTY_STR; - } finally { - if (afterDiff) afterDiff(vnode); - vnode[PARENT] = null; + if (res !== undefined) return res; - if (ummountHook) ummountHook(vnode); - } - } + let errorHook = options[CATCH_ERROR]; + if (errorHook) errorHook(error, vnode); + return EMPTY_STR; } - // 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 === 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... - vnode[PARENT] = null; + if (!asyncMode) throw error; - if (options.unmount) options.unmount(vnode); + if (!error || typeof error.then != 'function') throw error; - return str; - } catch (error) { - if (!asyncMode && renderer && renderer.onError) { - let res = renderer.onError(error, vnode, (child) => - _renderToString( - child, - context, - isSvgMode, - selectValue, - vnode, - asyncMode, - renderer - ) + const renderNestedChildren = () => { + try { + return _renderToString( + rendered, + context, + isSvgMode, + selectValue, + vnode, + asyncMode, + renderer ); + } catch (e) { + if (!e || typeof e.then != 'function') throw e; - if (res !== undefined) return res; - - let errorHook = options[CATCH_ERROR]; - if (errorHook) errorHook(error, vnode); - return EMPTY_STR; + return e.then( + () => + _renderToString( + rendered, + context, + isSvgMode, + selectValue, + vnode, + asyncMode, + renderer + ), + renderNestedChildren + ); } + }; - if (!asyncMode) throw error; - - if (!error || typeof error.then != 'function') throw error; + return error.then(renderNestedChildren); + } +}; - 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 - ); - } - }; +const handleDomNode = ( + vnode, + context, + isSvgMode, + selectValue, + parent, + asyncMode, + renderer +) => { + vnode[PARENT] = parent; + if (beforeDiff) beforeDiff(vnode); - return error.then(renderNestedChildren); - } - } + let type = vnode.type, + props = vnode.props; // Serialize Element VNodes to HTML let s = '<' + type, @@ -703,6 +685,127 @@ function _renderToString( if (isArray(html)) return [startTag, ...html, endTag]; else if (typeof html != 'string') return [startTag, html, endTag]; return startTag + html + endTag; +}; + +const handleTextNode = (vnode) => { + let vnodeType = typeof vnode; + if (vnodeType == 'function') return EMPTY_STR; + return vnodeType == 'string' ? encodeEntities(vnode) : vnode + EMPTY_STR; +}; + +const handleListNode = ( + vnode, + context, + isSvgMode, + selectValue, + parent, + asyncMode, + renderer +) => { + let rendered = EMPTY_STR, + renderArray; + parent[CHILDREN] = vnode; + for (let i = 0; i < vnode.length; 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 = []; + } + + 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; +}; + +/** + * 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 +) { + switch (getType(vnode)) { + case NO_ACTION: + return EMPTY_STR; + case TEXT_NODE: { + return /**#__NOINLINE__**/ handleTextNode(vnode); + } + case LIST_NODE: { + return /**#__NOINLINE__**/ handleListNode( + vnode, + context, + isSvgMode, + selectValue, + parent, + asyncMode, + renderer + ); + } + case FUNCTION_NODE: { + return /**#__NOINLINE__**/ handleFunctionNode( + vnode, + context, + isSvgMode, + selectValue, + parent, + asyncMode, + renderer + ); + } + case DOM_NODE: { + return /**#__NOINLINE__**/ handleDomNode( + vnode, + context, + isSvgMode, + selectValue, + parent, + asyncMode, + renderer + ); + } + } } const SELF_CLOSING = new Set([