From 1d60dbe565b491b4b060a8a20603dfebd776613e Mon Sep 17 00:00:00 2001 From: jdecroock Date: Thu, 21 Jul 2022 10:15:14 +0200 Subject: [PATCH 1/2] split up pretty and create hot paths --- package-lock.json | 4 +- package.json | 3 +- src/index.js | 381 +++++++++++++++++++++++++++++++++++++++++----- src/util.js | 3 +- 4 files changed, 343 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96391807..e504348e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "preact-render-to-string", - "version": "5.1.19", + "version": "5.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "preact-render-to-string", - "version": "5.1.19", + "version": "5.2.1", "license": "MIT", "dependencies": { "pretty-format": "^3.8.0" diff --git a/package.json b/package.json index 06abcb0a..9faec549 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ "browser": "./dist/jsx.module.js", "require": "./dist/jsx.js" }, - "./package.json": "./package.json", - "./": "./" + "./package.json": "./package.json" }, "scripts": { "bench": "BABEL_ENV=test node -r @babel/register benchmarks index.js", diff --git a/src/index.js b/src/index.js index 7a740a45..b16fbbfc 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,6 @@ import { indent, isLargeString, styleObjToCss, - assign, getChildren } from './util'; import { options, Fragment } from 'preact'; @@ -15,10 +14,10 @@ const SHALLOW = { shallow: true }; // components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names. const UNNAMED = []; -const VOID_ELEMENTS = - /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; +const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; const UNSAFE_NAME = /[\s\n\\/='"\0<>]/; +const XLINK = /^xlink:?./; function markAsDirty() { this.__d = true; @@ -59,7 +58,12 @@ function renderToString(vnode, context, opts) { const previousSkipEffects = options.__s; options.__s = true; - const res = _renderToString(vnode, context, opts); + let res; + if (opts.pretty || opts.sortAttributes) { + res = _renderToStringPretty(vnode, context, opts); + } else { + res = _renderToString(vnode, context, opts); + } // options._commit, we don't schedule any effects in this library right now, // so we can pass an empty queue to this hook. @@ -69,6 +73,32 @@ function renderToString(vnode, context, opts) { return res; } +function createComponent(vnode, context) { + return { + __v: vnode, + context, + props: vnode.props, + // silently drop state updates + setState: markAsDirty, + forceUpdate: markAsDirty, + __d: true, + // hooks + __h: [] + }; +} + +// Necessary for createContext api. Setting this property will pass +// the context value as `this.context` just for this component. +function getContext(nodeName, context) { + let cxType = nodeName.contextType; + let provider = cxType && context[cxType.__c]; + return cxType != null + ? provider + ? provider.props.value + : cxType.__ + : context; +} + /** The default export is an alias of `render()`. */ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { if (vnode == null || typeof vnode === 'boolean') { @@ -80,6 +110,292 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { return encodeEntities(vnode); } + if (Array.isArray(vnode)) { + const rendered = []; + for (let i = 0; i < vnode.length; i++) { + rendered.push( + _renderToString(vnode[i], context, opts, inner, isSvgMode, selectValue) + ); + } + return rendered.join(''); + } + + let nodeName = vnode.type, + props = vnode.props, + isComponent = false; + + // components + if (typeof nodeName === 'function') { + isComponent = true; + if (opts.shallow && (inner || opts.renderRootComponent === false)) { + nodeName = getComponentName(nodeName); + } else if (nodeName === Fragment) { + const children = []; + getChildren(children, vnode.props.children); + return _renderToString( + children, + context, + opts, + opts.shallowHighOrder !== false, + isSvgMode, + selectValue + ); + } else { + let rendered; + + let c = (vnode.__c = createComponent(vnode, context)); + + // options._diff + if (options.__b) options.__b(vnode); + + // options._render + let renderHook = options.__r; + + if ( + !nodeName.prototype || + typeof nodeName.prototype.render !== 'function' + ) { + let cctx = getContext(nodeName, context); + + // 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 (c.__d && count++ < 25) { + c.__d = false; + + if (renderHook) renderHook(vnode); + + // stateless functional components + rendered = nodeName.call(vnode.__c, props, cctx); + } + } else { + let cctx = getContext(nodeName, context); + + // c = new nodeName(props, context); + c = vnode.__c = new nodeName(props, cctx); + c.__v = vnode; + // turn off stateful re-rendering: + c._dirty = c.__d = true; + c.props = props; + if (c.state == null) c.state = {}; + + if (c._nextState == null && c.__s == null) { + c._nextState = c.__s = c.state; + } + + c.context = cctx; + if (nodeName.getDerivedStateFromProps) + c.state = Object.assign( + {}, + c.state, + nodeName.getDerivedStateFromProps(c.props, c.state) + ); + else if (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._nextState !== c.state + ? c._nextState + : c.__s !== c.state + ? c.__s + : c.state; + } + + if (renderHook) renderHook(vnode); + + rendered = c.render(c.props, c.state, c.context); + } + + if (c.getChildContext) { + context = Object.assign({}, context, c.getChildContext()); + } + + if (options.diffed) options.diffed(vnode); + return _renderToString( + rendered, + context, + opts, + opts.shallowHighOrder !== false, + isSvgMode, + selectValue + ); + } + } + + // render JSX to HTML + let s = `<${nodeName}`, + propChildren, + html; + + if (props) { + for (let name in props) { + let v = props[name]; + if (name === 'children') { + propChildren = v; + continue; + } + + if (UNSAFE_NAME.test(name)) continue; + + if ( + !(opts && opts.allAttributes) && + (name === 'key' || + name === 'ref' || + name === '__self' || + name === '__source') + ) + continue; + + if (name === 'defaultValue') { + name = 'value'; + } else if (name === 'defaultChecked') { + name = 'checked'; + } else if (name === 'defaultSelected') { + name = 'selected'; + } else if (name === 'className') { + if (typeof props.class !== 'undefined') continue; + name = 'class'; + } else if (isSvgMode && XLINK.test(name)) { + name = name.toLowerCase().replace(/^xlink:?/, 'xlink:'); + } + + if (name === 'htmlFor') { + if (props.for) continue; + name = 'for'; + } + + if (name === 'style' && v && typeof v === 'object') { + v = styleObjToCss(v); + } + + // always use string values instead of booleans for aria attributes + // also see https://github.com/preactjs/preact/pull/2347/files + if (name[0] === 'a' && name['1'] === 'r' && typeof v === 'boolean') { + v = String(v); + } + + let hooked = + opts.attributeHook && + opts.attributeHook(name, v, context, opts, isComponent); + if (hooked || hooked === '') { + s = s + hooked; + continue; + } + + if (name === 'dangerouslySetInnerHTML') { + html = v && v.__html; + } else if (nodeName === 'textarea' && name === 'value') { + // + propChildren = v; + } else if ((v || v === 0 || v === '') && typeof v !== 'function') { + if (v === true || v === '') { + v = name; + // in non-xml mode, allow boolean attributes + if (!opts || !opts.xml) { + s = s + ' ' + name; + continue; + } + } + + if (name === 'value') { + if (nodeName === 'select') { + selectValue = v; + continue; + } else if ( + // If we're looking at an