From f17f6eb478956f12aaace86a8d3c545eccffe1ba Mon Sep 17 00:00:00 2001 From: Vladimir Kutepov Date: Tue, 27 Feb 2018 18:26:33 +0700 Subject: [PATCH] Lazy components --- CHANGELOG.md | 4 ++ dist/hyperapp-render.js | 81 +++++++++++++++++----------- dist/hyperapp-render.js.map | 2 +- dist/hyperapp-render.min.js | 2 +- dist/hyperapp-render.min.js.map | 2 +- src/index.js | 95 +++++++++++++++++++++------------ src/server.js | 24 +++++---- test/index.test.js | 78 ++++++++++++++++++--------- test/server.test.js | 66 +++++++++++------------ 9 files changed, 217 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0c83b..a9996bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + +* [Components](https://github.com/hyperapp/hyperapp/tree/1.1.2#components) support. + ## [1.3.0] - 2018-02-24 * Scoped npm package name. Use `@hyperapp/render` instead of `hyperapp-render`. diff --git a/dist/hyperapp-render.js b/dist/hyperapp-render.js index 5015e13..72953f2 100644 --- a/dist/hyperapp-render.js +++ b/dist/hyperapp-render.js @@ -35,7 +35,7 @@ function stringifyStyles(styles) { var delimiter = ''; var styleNames = Object.keys(styles); - for (var i = 0, len = styleNames.length; i < len; i++) { + for (var i = 0; i < styleNames.length; i++) { var styleName = styleNames[i]; var styleValue = styles[styleName]; @@ -53,26 +53,18 @@ function stringifyStyles(styles) { return out || null; } -function renderFragment(node, stack) { - if (node == null || typeof node === 'boolean') { - return ''; - } - - var attributes = node.attributes; - - if (!attributes) { - return escapeHtml(node); - } - - var tag = node.nodeName; +function renderFragment(_ref, stack) { + var nodeName = _ref.nodeName, + attributes = _ref.attributes, + children = _ref.children; var out = ''; var footer = ''; - if (tag) { - out += '<' + tag; + if (nodeName) { + out += '<' + nodeName; var keys = Object.keys(attributes); - for (var i = 0, len = keys.length; i < len; i++) { + for (var i = 0; i < keys.length; i++) { var name = keys[i]; var value = attributes[name]; @@ -89,11 +81,11 @@ function renderFragment(node, stack) { } } - if (voidElements.has(tag)) { + if (voidElements.has(nodeName)) { out += '/>'; } else { out += '>'; - footer = ''; + footer = ''; } } @@ -103,8 +95,6 @@ function renderFragment(node, stack) { out += innerHTML; } - var children = node.children; - if (children.length > 0) { stack.push({ childIndex: 0, @@ -118,10 +108,18 @@ function renderFragment(node, stack) { return out; } -function renderer(node) { +function resolveNode(node, state, actions) { + if (typeof node === 'function') { + return resolveNode(node(state, actions), state, actions); + } + + return node; +} + +function renderer(view, state, actions) { var stack = [{ childIndex: 0, - children: [node], + children: [view], footer: '' }]; var end = false; @@ -144,26 +142,47 @@ function renderer(node) { out += frame.footer; stack.pop(); } else { - var child = frame.children[frame.childIndex++]; - out += renderFragment(child, stack); + var node = resolveNode(frame.children[frame.childIndex++], state, actions); + + if (node != null && typeof node !== 'boolean') { + if (node.pop) { + stack.push({ + childIndex: 0, + children: node, + footer: '' + }); + } else if (node.attributes) { + out += renderFragment(node, stack); + } else { + out += escapeHtml(node); + } + } } } return out; }; } -function renderToString(node) { - return renderer(node)(Infinity); +function renderToString(view, state, actions) { + return renderer(view, state, actions)(Infinity); } -function render(app) { +function render(nextApp) { return function (initialState, actionsTemplate, view, container) { - return app(initialState, Object.assign({}, actionsTemplate, { - toString: function toString() { - return function (state, actions) { - return renderToString(view(state, actions)); + var actions = nextApp(initialState, Object.assign({}, actionsTemplate, { + getState: function getState() { + return function (state) { + return state; }; } }), view, container); + + if (typeof actions === 'object') { + actions.toString = function () { + return renderToString(view, actions.getState(), actions); + }; + } + + return actions; }; } diff --git a/dist/hyperapp-render.js.map b/dist/hyperapp-render.js.map index f36c20c..6e832c1 100644 --- a/dist/hyperapp-render.js.map +++ b/dist/hyperapp-render.js.map @@ -1 +1 @@ -{"version":3,"file":"hyperapp-render.js","sources":["src/index.js"],"sourcesContent":["const styleNameCache = new Map()\nconst uppercasePattern = /([A-Z])/g\nconst msPattern = /^ms-/\n\n// https://www.w3.org/TR/html/syntax.html#void-elements\nconst voidElements = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\nconst ignoreAttributes = new Set([\n 'key',\n 'innerHTML',\n '__source', // https://babeljs.io/docs/plugins/transform-react-jsx-source/\n])\n\n// https://www.w3.org/International/questions/qa-escapes#use\nconst escapeRegExp = /[\"&'<>]/g\nconst escapeLookup = new Map([\n ['\"', '"'],\n ['&', '&'],\n [\"'\", '''],\n ['<', '<'],\n ['>', '>'],\n])\n\nfunction escaper(match) {\n return escapeLookup.get(match)\n}\n\nfunction escapeHtml(value) {\n if (typeof value === 'number') {\n // better performance for safe values\n return '' + value\n }\n return ('' + value).replace(escapeRegExp, escaper)\n}\n\n// \"backgroundColor\" => \"background-color\"\n// \"MozTransition\" => \"-moz-transition\"\n// \"msTransition\" => \"-ms-transition\"\nfunction hyphenateStyleName(styleName) {\n return (\n styleNameCache.get(styleName) ||\n styleNameCache\n .set(\n styleName,\n styleName\n .replace(uppercasePattern, '-$&')\n .toLowerCase()\n .replace(msPattern, '-ms-'),\n )\n .get(styleName)\n )\n}\n\nfunction stringifyStyles(styles) {\n let out = ''\n let delimiter = ''\n const styleNames = Object.keys(styles)\n for (let i = 0, len = styleNames.length; i < len; i++) {\n const styleName = styleNames[i]\n const styleValue = styles[styleName]\n\n // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L135\n if (styleValue != null) {\n if (styleName === 'cssText') {\n out += delimiter + styleValue\n } else {\n out += delimiter + hyphenateStyleName(styleName) + ':' + styleValue\n }\n delimiter = ';'\n }\n }\n return out || null\n}\n\nfunction renderFragment(node, stack) {\n // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L150\n if (node == null || typeof node === 'boolean') {\n return ''\n }\n\n const { attributes } = node\n if (!attributes) {\n // text node\n return escapeHtml(node)\n }\n\n const tag = node.nodeName\n let out = ''\n let footer = ''\n if (tag) {\n // https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\n out += '<' + tag\n const keys = Object.keys(attributes)\n for (let i = 0, len = keys.length; i < len; i++) {\n const name = keys[i]\n let value = attributes[name]\n\n if (name === 'style' && value && typeof value === 'object') {\n value = stringifyStyles(value)\n }\n\n // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L131\n if (\n value != null &&\n value !== false &&\n typeof value !== 'function' &&\n !ignoreAttributes.has(name)\n ) {\n out += ' ' + name\n if (value !== true) {\n out += '=\"' + escapeHtml(value) + '\"'\n }\n }\n }\n\n if (voidElements.has(tag)) {\n out += '/>'\n } else {\n out += '>'\n footer = ''\n }\n }\n\n const { innerHTML } = attributes\n if (innerHTML != null) {\n out += innerHTML\n }\n\n const { children } = node\n if (children.length > 0) {\n stack.push({\n childIndex: 0,\n children,\n footer,\n })\n } else {\n out += footer\n }\n\n return out\n}\n\nexport function renderer(node) {\n const stack = [\n {\n childIndex: 0,\n children: [node],\n footer: '',\n },\n ]\n let end = false\n return (bytes) => {\n if (end) {\n return null\n }\n let out = ''\n while (out.length < bytes) {\n if (stack.length === 0) {\n end = true\n break\n }\n const frame = stack[stack.length - 1]\n if (frame.childIndex >= frame.children.length) {\n out += frame.footer\n stack.pop()\n } else {\n const child = frame.children[frame.childIndex++]\n out += renderFragment(child, stack)\n }\n }\n return out\n }\n}\n\nexport function renderToString(node) {\n return renderer(node)(Infinity)\n}\n\nexport function render(app) {\n return (initialState, actionsTemplate, view, container) =>\n app(\n initialState,\n Object.assign({}, actionsTemplate, {\n toString: () => (state, actions) => renderToString(view(state, actions)),\n }),\n view,\n container,\n )\n}\n"],"names":["styleNameCache","Map","uppercasePattern","msPattern","voidElements","Set","ignoreAttributes","escapeRegExp","escapeLookup","escaper","match","get","escapeHtml","value","replace","hyphenateStyleName","styleName","set","toLowerCase","stringifyStyles","styles","out","delimiter","styleNames","Object","keys","i","len","length","styleValue","renderFragment","node","stack","attributes","tag","nodeName","footer","name","has","innerHTML","children","push","renderer","end","bytes","frame","childIndex","pop","child","renderToString","Infinity","render","app","initialState","actionsTemplate","view","container","assign","state","actions"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"hyperapp-render.js","sources":["src/index.js"],"sourcesContent":["const styleNameCache = new Map()\nconst uppercasePattern = /([A-Z])/g\nconst msPattern = /^ms-/\n\n// https://www.w3.org/TR/html/syntax.html#void-elements\nconst voidElements = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\nconst ignoreAttributes = new Set([\n 'key',\n 'innerHTML',\n '__source', // https://babeljs.io/docs/plugins/transform-react-jsx-source/\n])\n\n// https://www.w3.org/International/questions/qa-escapes#use\nconst escapeRegExp = /[\"&'<>]/g\nconst escapeLookup = new Map([\n ['\"', '"'],\n ['&', '&'],\n [\"'\", '''],\n ['<', '<'],\n ['>', '>'],\n])\n\nfunction escaper(match) {\n return escapeLookup.get(match)\n}\n\nfunction escapeHtml(value) {\n if (typeof value === 'number') {\n // better performance for safe values\n return '' + value\n }\n\n return ('' + value).replace(escapeRegExp, escaper)\n}\n\n// \"backgroundColor\" => \"background-color\"\n// \"MozTransition\" => \"-moz-transition\"\n// \"msTransition\" => \"-ms-transition\"\nfunction hyphenateStyleName(styleName) {\n return (\n styleNameCache.get(styleName) ||\n styleNameCache\n .set(\n styleName,\n styleName\n .replace(uppercasePattern, '-$&')\n .toLowerCase()\n .replace(msPattern, '-ms-'),\n )\n .get(styleName)\n )\n}\n\nfunction stringifyStyles(styles) {\n let out = ''\n let delimiter = ''\n const styleNames = Object.keys(styles)\n\n for (let i = 0; i < styleNames.length; i++) {\n const styleName = styleNames[i]\n const styleValue = styles[styleName]\n\n if (styleValue != null) {\n if (styleName === 'cssText') {\n out += delimiter + styleValue\n } else {\n out += delimiter + hyphenateStyleName(styleName) + ':' + styleValue\n }\n\n delimiter = ';'\n }\n }\n\n return out || null\n}\n\nfunction renderFragment({ nodeName, attributes, children }, stack) {\n let out = ''\n let footer = ''\n\n if (nodeName) {\n // https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\n out += '<' + nodeName\n const keys = Object.keys(attributes)\n\n for (let i = 0; i < keys.length; i++) {\n const name = keys[i]\n let value = attributes[name]\n\n if (name === 'style' && value && typeof value === 'object') {\n value = stringifyStyles(value)\n }\n\n if (\n value != null &&\n value !== false &&\n typeof value !== 'function' &&\n !ignoreAttributes.has(name)\n ) {\n out += ' ' + name\n\n if (value !== true) {\n out += '=\"' + escapeHtml(value) + '\"'\n }\n }\n }\n\n if (voidElements.has(nodeName)) {\n out += '/>'\n } else {\n out += '>'\n footer = ''\n }\n }\n\n const { innerHTML } = attributes\n\n if (innerHTML != null) {\n out += innerHTML\n }\n\n if (children.length > 0) {\n stack.push({\n childIndex: 0,\n children,\n footer,\n })\n } else {\n out += footer\n }\n\n return out\n}\n\nfunction resolveNode(node, state, actions) {\n if (typeof node === 'function') {\n return resolveNode(node(state, actions), state, actions)\n }\n\n return node\n}\n\nexport function renderer(view, state, actions) {\n const stack = [\n {\n childIndex: 0,\n children: [view],\n footer: '',\n },\n ]\n let end = false\n return (bytes) => {\n if (end) {\n return null\n }\n\n let out = ''\n\n while (out.length < bytes) {\n if (stack.length === 0) {\n end = true\n break\n }\n\n const frame = stack[stack.length - 1]\n\n if (frame.childIndex >= frame.children.length) {\n out += frame.footer\n stack.pop()\n } else {\n const node = resolveNode(frame.children[frame.childIndex++], state, actions)\n\n if (node != null && typeof node !== 'boolean') {\n if (node.pop) {\n // is array\n stack.push({\n childIndex: 0,\n children: node,\n footer: '',\n })\n } else if (node.attributes) {\n // element\n out += renderFragment(node, stack)\n } else {\n // text node\n out += escapeHtml(node)\n }\n }\n }\n }\n\n return out\n }\n}\n\nexport function renderToString(view, state, actions) {\n return renderer(view, state, actions)(Infinity)\n}\n\nexport function render(nextApp) {\n return (initialState, actionsTemplate, view, container) => {\n const actions = nextApp(\n initialState,\n Object.assign({}, actionsTemplate, { getState: () => (state) => state }),\n view,\n container,\n )\n\n if (typeof actions === 'object') {\n actions.toString = () => renderToString(view, actions.getState(), actions)\n }\n\n return actions\n }\n}\n"],"names":["styleNameCache","Map","uppercasePattern","msPattern","voidElements","Set","ignoreAttributes","escapeRegExp","escapeLookup","escaper","match","get","escapeHtml","value","replace","hyphenateStyleName","styleName","set","toLowerCase","stringifyStyles","styles","out","delimiter","styleNames","Object","keys","i","length","styleValue","renderFragment","stack","nodeName","attributes","children","footer","name","has","innerHTML","push","resolveNode","node","state","actions","renderer","view","end","bytes","frame","childIndex","pop","renderToString","Infinity","render","nextApp","initialState","actionsTemplate","container","assign","toString","getState"],"mappings":"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"} \ No newline at end of file diff --git a/dist/hyperapp-render.min.js b/dist/hyperapp-render.min.js index 9418a98..e110918 100644 --- a/dist/hyperapp-render.min.js +++ b/dist/hyperapp-render.min.js @@ -1,3 +1,3 @@ /*! Hyperapp Render | MIT Licence | https://github.com/hyperapp/render */ -!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(e.self=e.self||{})}(this,function(e){"use strict";var n=new Map,r=/([A-Z])/g,t=/^ms-/,o=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),u=new Set(["key","innerHTML","__source"]),i=/["&'<>]/g,f=new Map([['"',"""],["&","&"],["'","'"],["<","<"],[">",">"]]);function l(e){return f.get(e)}function c(e){return"number"==typeof e?""+e:(""+e).replace(i,l)}function a(e){return n.get(e)||n.set(e,e.replace(r,"-$&").toLowerCase().replace(t,"-ms-")).get(e)}function s(e){for(var n="",r="",t=Object.keys(e),o=0,u=t.length;o":(i+=">",f="")}var g=r.innerHTML;null!=g&&(i+=g);var b=e.children;return b.length>0?n.push({childIndex:0,children:b,footer:f}):i+=f,i}function p(e){var n=[{childIndex:0,children:[e],footer:""}],r=!1;return function(e){if(r)return null;for(var t="";t.length=o.children.length)t+=o.footer,n.pop();else t+=d(o.children[o.childIndex++],n)}return t}}function h(e){return p(e)(1/0)}e.renderer=p,e.renderToString=h,e.render=function(e){return function(n,r,t,o){return e(n,Object.assign({},r,{toString:function(){return function(e,n){return h(t(e,n))}}}),t,o)}},Object.defineProperty(e,"__esModule",{value:!0})}); +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(e.self=e.self||{})}(this,function(e){"use strict";var n=new Map,t=/([A-Z])/g,r=/^ms-/,o=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),u=new Set(["key","innerHTML","__source"]),i=/["&'<>]/g,c=new Map([['"',"""],["&","&"],["'","'"],["<","<"],[">",">"]]);function f(e){return c.get(e)}function l(e){return"number"==typeof e?""+e:(""+e).replace(i,f)}function a(e){return n.get(e)||n.set(e,e.replace(t,"-$&").toLowerCase().replace(r,"-ms-")).get(e)}function d(e){for(var n="",t="",r=Object.keys(e),o=0;o":(c+=">",f="")}var g=r.innerHTML;return null!=g&&(c+=g),i.length>0?n.push({childIndex:0,children:i,footer:f}):c+=f,c}function p(e,n,t){return"function"==typeof e?p(e(n,t),n,t):e}function h(e,n,t){var r=[{childIndex:0,children:[e],footer:""}],o=!1;return function(e){if(o)return null;for(var u="";u.length=i.children.length)u+=i.footer,r.pop();else{var c=p(i.children[i.childIndex++],n,t);null!=c&&"boolean"!=typeof c&&(c.pop?r.push({childIndex:0,children:c,footer:""}):c.attributes?u+=s(c,r):u+=l(c))}}return u}}function g(e,n,t){return h(e,n,t)(1/0)}e.renderer=h,e.renderToString=g,e.render=function(e){return function(n,t,r,o){var u=e(n,Object.assign({},t,{getState:function(){return function(e){return e}}}),r,o);return"object"==typeof u&&(u.toString=function(){return g(r,u.getState(),u)}),u}},Object.defineProperty(e,"__esModule",{value:!0})}); //# sourceMappingURL=hyperapp-render.min.js.map diff --git a/dist/hyperapp-render.min.js.map b/dist/hyperapp-render.min.js.map index 85bf6d6..8878b4a 100644 --- a/dist/hyperapp-render.min.js.map +++ b/dist/hyperapp-render.min.js.map @@ -1 +1 @@ -{"version":3,"file":"hyperapp-render.min.js","sources":["src/index.js"],"sourcesContent":["const styleNameCache = new Map()\nconst uppercasePattern = /([A-Z])/g\nconst msPattern = /^ms-/\n\n// https://www.w3.org/TR/html/syntax.html#void-elements\nconst voidElements = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\nconst ignoreAttributes = new Set([\n 'key',\n 'innerHTML',\n '__source', // https://babeljs.io/docs/plugins/transform-react-jsx-source/\n])\n\n// https://www.w3.org/International/questions/qa-escapes#use\nconst escapeRegExp = /[\"&'<>]/g\nconst escapeLookup = new Map([\n ['\"', '"'],\n ['&', '&'],\n [\"'\", '''],\n ['<', '<'],\n ['>', '>'],\n])\n\nfunction escaper(match) {\n return escapeLookup.get(match)\n}\n\nfunction escapeHtml(value) {\n if (typeof value === 'number') {\n // better performance for safe values\n return '' + value\n }\n return ('' + value).replace(escapeRegExp, escaper)\n}\n\n// \"backgroundColor\" => \"background-color\"\n// \"MozTransition\" => \"-moz-transition\"\n// \"msTransition\" => \"-ms-transition\"\nfunction hyphenateStyleName(styleName) {\n return (\n styleNameCache.get(styleName) ||\n styleNameCache\n .set(\n styleName,\n styleName\n .replace(uppercasePattern, '-$&')\n .toLowerCase()\n .replace(msPattern, '-ms-'),\n )\n .get(styleName)\n )\n}\n\nfunction stringifyStyles(styles) {\n let out = ''\n let delimiter = ''\n const styleNames = Object.keys(styles)\n for (let i = 0, len = styleNames.length; i < len; i++) {\n const styleName = styleNames[i]\n const styleValue = styles[styleName]\n\n // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L135\n if (styleValue != null) {\n if (styleName === 'cssText') {\n out += delimiter + styleValue\n } else {\n out += delimiter + hyphenateStyleName(styleName) + ':' + styleValue\n }\n delimiter = ';'\n }\n }\n return out || null\n}\n\nfunction renderFragment(node, stack) {\n // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L150\n if (node == null || typeof node === 'boolean') {\n return ''\n }\n\n const { attributes } = node\n if (!attributes) {\n // text node\n return escapeHtml(node)\n }\n\n const tag = node.nodeName\n let out = ''\n let footer = ''\n if (tag) {\n // https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\n out += '<' + tag\n const keys = Object.keys(attributes)\n for (let i = 0, len = keys.length; i < len; i++) {\n const name = keys[i]\n let value = attributes[name]\n\n if (name === 'style' && value && typeof value === 'object') {\n value = stringifyStyles(value)\n }\n\n // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L131\n if (\n value != null &&\n value !== false &&\n typeof value !== 'function' &&\n !ignoreAttributes.has(name)\n ) {\n out += ' ' + name\n if (value !== true) {\n out += '=\"' + escapeHtml(value) + '\"'\n }\n }\n }\n\n if (voidElements.has(tag)) {\n out += '/>'\n } else {\n out += '>'\n footer = ''\n }\n }\n\n const { innerHTML } = attributes\n if (innerHTML != null) {\n out += innerHTML\n }\n\n const { children } = node\n if (children.length > 0) {\n stack.push({\n childIndex: 0,\n children,\n footer,\n })\n } else {\n out += footer\n }\n\n return out\n}\n\nexport function renderer(node) {\n const stack = [\n {\n childIndex: 0,\n children: [node],\n footer: '',\n },\n ]\n let end = false\n return (bytes) => {\n if (end) {\n return null\n }\n let out = ''\n while (out.length < bytes) {\n if (stack.length === 0) {\n end = true\n break\n }\n const frame = stack[stack.length - 1]\n if (frame.childIndex >= frame.children.length) {\n out += frame.footer\n stack.pop()\n } else {\n const child = frame.children[frame.childIndex++]\n out += renderFragment(child, stack)\n }\n }\n return out\n }\n}\n\nexport function renderToString(node) {\n return renderer(node)(Infinity)\n}\n\nexport function render(app) {\n return (initialState, actionsTemplate, view, container) =>\n app(\n initialState,\n Object.assign({}, actionsTemplate, {\n toString: () => (state, actions) => renderToString(view(state, actions)),\n }),\n view,\n container,\n )\n}\n"],"names":["styleNameCache","Map","uppercasePattern","msPattern","voidElements","Set","ignoreAttributes","escapeRegExp","escapeLookup","escaper","match","get","escapeHtml","value","replace","hyphenateStyleName","styleName","set","toLowerCase","stringifyStyles","styles","out","delimiter","styleNames","Object","keys","i","len","length","styleValue","renderFragment","node","stack","attributes","tag","nodeName","footer","name","has","innerHTML","children","push","renderer","end","bytes","frame","childIndex","pop","renderToString","Infinity","app","initialState","actionsTemplate","view","container","assign","state","actions"],"mappings":";+LAAA,IAAMA,EAAiB,IAAIC,IACrBC,EAAmB,WACnBC,EAAY,OAGZC,EAAe,IAAIC,KACvB,OACA,OACA,KACA,MACA,QACA,KACA,MACA,QACA,OACA,OACA,QACA,SACA,QACA,QAGIC,EAAmB,IAAID,KAC3B,MACA,YACA,aAIIE,EAAe,WACfC,EAAe,IAAIP,MACtB,IAAK,WACL,IAAK,UACL,IAAK,UACL,IAAK,SACL,IAAK,UAGR,SAASQ,EAAQC,UACRF,EAAaG,IAAID,GAG1B,SAASE,EAAWC,SACG,iBAAVA,EAEF,GAAKA,GAEN,GAAKA,GAAOC,QAAQP,EAAcE,GAM5C,SAASM,EAAmBC,UAExBhB,EAAeW,IAAIK,IACnBhB,EACGiB,IACCD,EACAA,EACGF,QAAQZ,EAAkB,OAC1BgB,cACAJ,QAAQX,EAAW,SAEvBQ,IAAIK,GAIX,SAASG,EAAgBC,WACnBC,EAAM,GACNC,EAAY,GACVC,EAAaC,OAAOC,KAAKL,GACtBM,EAAI,EAAGC,EAAMJ,EAAWK,OAAQF,EAAIC,EAAKD,IAAK,KAC/CV,EAAYO,EAAWG,GACvBG,EAAaT,EAAOJ,GAGR,MAAda,OACgB,YAAdb,EACKM,EAAYO,EAEZP,EAAYP,EAAmBC,GAAa,IAAMa,IAE/C,YAGTR,GAAO,KAGhB,SAASS,EAAeC,EAAMC,MAEhB,MAARD,GAAgC,kBAATA,QAClB,OAGDE,EAAeF,EAAfE,eACHA,SAEIrB,EAAWmB,OAGdG,EAAMH,EAAKI,SACbd,EAAM,GACNe,EAAS,MACTF,EAAK,IAEA,IAAMA,UACPT,EAAOD,OAAOC,KAAKQ,GAChBP,EAAI,EAAGC,EAAMF,EAAKG,OAAQF,EAAIC,EAAKD,IAAK,KACzCW,EAAOZ,EAAKC,GACdb,EAAQoB,EAAWI,GAEV,UAATA,GAAoBxB,GAA0B,iBAAVA,MAC9BM,EAAgBN,IAKf,MAATA,IACU,IAAVA,GACiB,mBAAVA,GACNP,EAAiBgC,IAAID,QAEf,IAAMA,GACC,IAAVxB,OACK,KAAOD,EAAWC,GAAS,MAKpCT,EAAakC,IAAIJ,MACZ,SAEA,MACE,KAAOA,EAAM,SAIlBK,EAAcN,EAAdM,UACS,MAAbA,OACKA,OAGDC,EAAaT,EAAbS,gBACJA,EAASZ,OAAS,IACda,iBACQ,2BAKPL,EAGFf,EAGT,SAAgBqB,EAASX,OACjBC,eAEU,YACDD,UACH,KAGRY,GAAM,SACH,SAACC,MACFD,SACK,aAELtB,EAAM,GACHA,EAAIO,OAASgB,GAAO,IACJ,IAAjBZ,EAAMJ,OAAc,IAChB,YAGFiB,EAAQb,EAAMA,EAAMJ,OAAS,MAC/BiB,EAAMC,YAAcD,EAAML,SAASZ,UAC9BiB,EAAMT,SACPW,cAGCjB,EADOe,EAAML,SAASK,EAAMC,cACNd,UAG1BX,GAIX,SAAgB2B,EAAejB,UACtBW,EAASX,EAATW,CAAeO,EAAAA,4CAGxB,SAAuBC,UACd,SAACC,EAAcC,EAAiBC,EAAMC,UAC3CJ,EACEC,EACA3B,OAAO+B,UAAWH,YACN,kBAAM,SAACI,EAAOC,UAAYT,EAAeK,EAAKG,EAAOC,QAEjEJ,EACAC"} \ No newline at end of file +{"version":3,"file":"hyperapp-render.min.js","sources":["src/index.js"],"sourcesContent":["const styleNameCache = new Map()\nconst uppercasePattern = /([A-Z])/g\nconst msPattern = /^ms-/\n\n// https://www.w3.org/TR/html/syntax.html#void-elements\nconst voidElements = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\nconst ignoreAttributes = new Set([\n 'key',\n 'innerHTML',\n '__source', // https://babeljs.io/docs/plugins/transform-react-jsx-source/\n])\n\n// https://www.w3.org/International/questions/qa-escapes#use\nconst escapeRegExp = /[\"&'<>]/g\nconst escapeLookup = new Map([\n ['\"', '"'],\n ['&', '&'],\n [\"'\", '''],\n ['<', '<'],\n ['>', '>'],\n])\n\nfunction escaper(match) {\n return escapeLookup.get(match)\n}\n\nfunction escapeHtml(value) {\n if (typeof value === 'number') {\n // better performance for safe values\n return '' + value\n }\n\n return ('' + value).replace(escapeRegExp, escaper)\n}\n\n// \"backgroundColor\" => \"background-color\"\n// \"MozTransition\" => \"-moz-transition\"\n// \"msTransition\" => \"-ms-transition\"\nfunction hyphenateStyleName(styleName) {\n return (\n styleNameCache.get(styleName) ||\n styleNameCache\n .set(\n styleName,\n styleName\n .replace(uppercasePattern, '-$&')\n .toLowerCase()\n .replace(msPattern, '-ms-'),\n )\n .get(styleName)\n )\n}\n\nfunction stringifyStyles(styles) {\n let out = ''\n let delimiter = ''\n const styleNames = Object.keys(styles)\n\n for (let i = 0; i < styleNames.length; i++) {\n const styleName = styleNames[i]\n const styleValue = styles[styleName]\n\n if (styleValue != null) {\n if (styleName === 'cssText') {\n out += delimiter + styleValue\n } else {\n out += delimiter + hyphenateStyleName(styleName) + ':' + styleValue\n }\n\n delimiter = ';'\n }\n }\n\n return out || null\n}\n\nfunction renderFragment({ nodeName, attributes, children }, stack) {\n let out = ''\n let footer = ''\n\n if (nodeName) {\n // https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\n out += '<' + nodeName\n const keys = Object.keys(attributes)\n\n for (let i = 0; i < keys.length; i++) {\n const name = keys[i]\n let value = attributes[name]\n\n if (name === 'style' && value && typeof value === 'object') {\n value = stringifyStyles(value)\n }\n\n if (\n value != null &&\n value !== false &&\n typeof value !== 'function' &&\n !ignoreAttributes.has(name)\n ) {\n out += ' ' + name\n\n if (value !== true) {\n out += '=\"' + escapeHtml(value) + '\"'\n }\n }\n }\n\n if (voidElements.has(nodeName)) {\n out += '/>'\n } else {\n out += '>'\n footer = ''\n }\n }\n\n const { innerHTML } = attributes\n\n if (innerHTML != null) {\n out += innerHTML\n }\n\n if (children.length > 0) {\n stack.push({\n childIndex: 0,\n children,\n footer,\n })\n } else {\n out += footer\n }\n\n return out\n}\n\nfunction resolveNode(node, state, actions) {\n if (typeof node === 'function') {\n return resolveNode(node(state, actions), state, actions)\n }\n\n return node\n}\n\nexport function renderer(view, state, actions) {\n const stack = [\n {\n childIndex: 0,\n children: [view],\n footer: '',\n },\n ]\n let end = false\n return (bytes) => {\n if (end) {\n return null\n }\n\n let out = ''\n\n while (out.length < bytes) {\n if (stack.length === 0) {\n end = true\n break\n }\n\n const frame = stack[stack.length - 1]\n\n if (frame.childIndex >= frame.children.length) {\n out += frame.footer\n stack.pop()\n } else {\n const node = resolveNode(frame.children[frame.childIndex++], state, actions)\n\n if (node != null && typeof node !== 'boolean') {\n if (node.pop) {\n // is array\n stack.push({\n childIndex: 0,\n children: node,\n footer: '',\n })\n } else if (node.attributes) {\n // element\n out += renderFragment(node, stack)\n } else {\n // text node\n out += escapeHtml(node)\n }\n }\n }\n }\n\n return out\n }\n}\n\nexport function renderToString(view, state, actions) {\n return renderer(view, state, actions)(Infinity)\n}\n\nexport function render(nextApp) {\n return (initialState, actionsTemplate, view, container) => {\n const actions = nextApp(\n initialState,\n Object.assign({}, actionsTemplate, { getState: () => (state) => state }),\n view,\n container,\n )\n\n if (typeof actions === 'object') {\n actions.toString = () => renderToString(view, actions.getState(), actions)\n }\n\n return actions\n }\n}\n"],"names":["styleNameCache","Map","uppercasePattern","msPattern","voidElements","Set","ignoreAttributes","escapeRegExp","escapeLookup","escaper","match","get","escapeHtml","value","replace","hyphenateStyleName","styleName","set","toLowerCase","stringifyStyles","styles","out","delimiter","styleNames","Object","keys","i","length","styleValue","renderFragment","stack","nodeName","attributes","children","footer","name","has","innerHTML","push","resolveNode","node","state","actions","renderer","view","end","bytes","frame","childIndex","pop","renderToString","Infinity","nextApp","initialState","actionsTemplate","container","assign","toString","getState"],"mappings":";+LAAA,IAAMA,EAAiB,IAAIC,IACrBC,EAAmB,WACnBC,EAAY,OAGZC,EAAe,IAAIC,KACvB,OACA,OACA,KACA,MACA,QACA,KACA,MACA,QACA,OACA,OACA,QACA,SACA,QACA,QAGIC,EAAmB,IAAID,KAC3B,MACA,YACA,aAIIE,EAAe,WACfC,EAAe,IAAIP,MACtB,IAAK,WACL,IAAK,UACL,IAAK,UACL,IAAK,SACL,IAAK,UAGR,SAASQ,EAAQC,UACRF,EAAaG,IAAID,GAG1B,SAASE,EAAWC,SACG,iBAAVA,EAEF,GAAKA,GAGN,GAAKA,GAAOC,QAAQP,EAAcE,GAM5C,SAASM,EAAmBC,UAExBhB,EAAeW,IAAIK,IACnBhB,EACGiB,IACCD,EACAA,EACGF,QAAQZ,EAAkB,OAC1BgB,cACAJ,QAAQX,EAAW,SAEvBQ,IAAIK,GAIX,SAASG,EAAgBC,WACnBC,EAAM,GACNC,EAAY,GACVC,EAAaC,OAAOC,KAAKL,GAEtBM,EAAI,EAAGA,EAAIH,EAAWI,OAAQD,IAAK,KACpCV,EAAYO,EAAWG,GACvBE,EAAaR,EAAOJ,GAER,MAAdY,OACgB,YAAdZ,EACKM,EAAYM,EAEZN,EAAYP,EAAmBC,GAAa,IAAMY,IAG/C,YAITP,GAAO,KAGhB,SAASQ,IAAmDC,OAAlCC,IAAAA,SAAUC,IAAAA,WAAYC,IAAAA,SAC1CZ,EAAM,GACNa,EAAS,MAETH,EAAU,IAEL,IAAMA,UACPN,EAAOD,OAAOC,KAAKO,GAEhBN,EAAI,EAAGA,EAAID,EAAKE,OAAQD,IAAK,KAC9BS,EAAOV,EAAKC,GACdb,EAAQmB,EAAWG,GAEV,UAATA,GAAoBtB,GAA0B,iBAAVA,MAC9BM,EAAgBN,IAIf,MAATA,IACU,IAAVA,GACiB,mBAAVA,GACNP,EAAiB8B,IAAID,QAEf,IAAMA,GAEC,IAAVtB,OACK,KAAOD,EAAWC,GAAS,MAKpCT,EAAagC,IAAIL,MACZ,SAEA,MACE,KAAOA,EAAW,SAIvBM,EAAcL,EAAdK,iBAES,MAAbA,OACKA,GAGLJ,EAASN,OAAS,IACdW,iBACQ,2BAKPJ,EAGFb,EAGT,SAASkB,EAAYC,EAAMC,EAAOC,SACZ,mBAATF,EACFD,EAAYC,EAAKC,EAAOC,GAAUD,EAAOC,GAG3CF,EAGT,SAAgBG,EAASC,EAAMH,EAAOC,OAC9BZ,eAEU,YACDc,UACH,KAGRC,GAAM,SACH,SAACC,MACFD,SACK,aAGLxB,EAAM,GAEHA,EAAIM,OAASmB,GAAO,IACJ,IAAjBhB,EAAMH,OAAc,IAChB,YAIFoB,EAAQjB,EAAMA,EAAMH,OAAS,MAE/BoB,EAAMC,YAAcD,EAAMd,SAASN,UAC9BoB,EAAMb,SACPe,UACD,KACCT,EAAOD,EAAYQ,EAAMd,SAASc,EAAMC,cAAeP,EAAOC,GAExD,MAARF,GAAgC,kBAATA,IACrBA,EAAKS,MAEDX,iBACQ,WACFE,SACF,KAEDA,EAAKR,cAEPH,EAAeW,EAAMV,MAGrBlB,EAAW4B,YAMnBnB,GAIX,SAAgB6B,EAAeN,EAAMH,EAAOC,UACnCC,EAASC,EAAMH,EAAOC,EAAtBC,CAA+BQ,EAAAA,4CAGxC,SAAuBC,UACd,SAACC,EAAcC,EAAiBV,EAAMW,OACrCb,EAAUU,EACdC,EACA7B,OAAOgC,UAAWF,YAA6B,kBAAM,SAACb,UAAUA,MAChEG,EACAW,SAGqB,iBAAZb,MACDe,SAAW,kBAAMP,EAAeN,EAAMF,EAAQgB,WAAYhB,KAG7DA"} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 78011ae..d3ab95e 100644 --- a/src/index.js +++ b/src/index.js @@ -45,6 +45,7 @@ function escapeHtml(value) { // better performance for safe values return '' + value } + return ('' + value).replace(escapeRegExp, escaper) } @@ -70,43 +71,35 @@ function stringifyStyles(styles) { let out = '' let delimiter = '' const styleNames = Object.keys(styles) - for (let i = 0, len = styleNames.length; i < len; i++) { + + for (let i = 0; i < styleNames.length; i++) { const styleName = styleNames[i] const styleValue = styles[styleName] - // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L135 if (styleValue != null) { if (styleName === 'cssText') { out += delimiter + styleValue } else { out += delimiter + hyphenateStyleName(styleName) + ':' + styleValue } + delimiter = ';' } } + return out || null } -function renderFragment(node, stack) { - // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L150 - if (node == null || typeof node === 'boolean') { - return '' - } - - const { attributes } = node - if (!attributes) { - // text node - return escapeHtml(node) - } - - const tag = node.nodeName +function renderFragment({ nodeName, attributes, children }, stack) { let out = '' let footer = '' - if (tag) { + + if (nodeName) { // https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments - out += '<' + tag + out += '<' + nodeName const keys = Object.keys(attributes) - for (let i = 0, len = keys.length; i < len; i++) { + + for (let i = 0; i < keys.length; i++) { const name = keys[i] let value = attributes[name] @@ -114,7 +107,6 @@ function renderFragment(node, stack) { value = stringifyStyles(value) } - // keep in sync with https://github.com/hyperapp/hyperapp/blob/1.1.2/src/index.js#L131 if ( value != null && value !== false && @@ -122,26 +114,27 @@ function renderFragment(node, stack) { !ignoreAttributes.has(name) ) { out += ' ' + name + if (value !== true) { out += '="' + escapeHtml(value) + '"' } } } - if (voidElements.has(tag)) { + if (voidElements.has(nodeName)) { out += '/>' } else { out += '>' - footer = '' + footer = '' } } const { innerHTML } = attributes + if (innerHTML != null) { out += innerHTML } - const { children } = node if (children.length > 0) { stack.push({ childIndex: 0, @@ -155,11 +148,19 @@ function renderFragment(node, stack) { return out } -export function renderer(node) { +function resolveNode(node, state, actions) { + if (typeof node === 'function') { + return resolveNode(node(state, actions), state, actions) + } + + return node +} + +export function renderer(view, state, actions) { const stack = [ { childIndex: 0, - children: [node], + children: [view], footer: '', }, ] @@ -168,37 +169,63 @@ export function renderer(node) { if (end) { return null } + let out = '' + while (out.length < bytes) { if (stack.length === 0) { end = true break } + const frame = stack[stack.length - 1] + if (frame.childIndex >= frame.children.length) { out += frame.footer stack.pop() } else { - const child = frame.children[frame.childIndex++] - out += renderFragment(child, stack) + const node = resolveNode(frame.children[frame.childIndex++], state, actions) + + if (node != null && typeof node !== 'boolean') { + if (node.pop) { + // is array + stack.push({ + childIndex: 0, + children: node, + footer: '', + }) + } else if (node.attributes) { + // element + out += renderFragment(node, stack) + } else { + // text node + out += escapeHtml(node) + } + } } } + return out } } -export function renderToString(node) { - return renderer(node)(Infinity) +export function renderToString(view, state, actions) { + return renderer(view, state, actions)(Infinity) } -export function render(app) { - return (initialState, actionsTemplate, view, container) => - app( +export function render(nextApp) { + return (initialState, actionsTemplate, view, container) => { + const actions = nextApp( initialState, - Object.assign({}, actionsTemplate, { - toString: () => (state, actions) => renderToString(view(state, actions)), - }), + Object.assign({}, actionsTemplate, { getState: () => (state) => state }), view, container, ) + + if (typeof actions === 'object') { + actions.toString = () => renderToString(view, actions.getState(), actions) + } + + return actions + } } diff --git a/src/server.js b/src/server.js index 353db2a..3d13e80 100644 --- a/src/server.js +++ b/src/server.js @@ -3,8 +3,9 @@ import { renderer, renderToString } from './index' export { renderer, renderToString } -export function renderToStream(node) { - const read = renderer(node) +export function renderToStream(view, state, actions) { + const read = renderer(view, state, actions) + // https://nodejs.org/api/stream.html return new Readable({ read(size) { @@ -17,15 +18,20 @@ export function renderToStream(node) { }) } -export function render(app) { - return (initialState, actionsTemplate, view, container) => - app( +export function render(nextApp) { + return (initialState, actionsTemplate, view, container) => { + const actions = nextApp( initialState, - Object.assign({}, actionsTemplate, { - toString: () => (state, actions) => renderToString(view(state, actions)), - toStream: () => (state, actions) => renderToStream(view(state, actions)), - }), + Object.assign({}, actionsTemplate, { getState: () => (state) => state }), view, container, ) + + if (typeof actions === 'object') { + actions.toString = () => renderToString(view, actions.getState(), actions) + actions.toStream = () => renderToStream(view, actions.getState(), actions) + } + + return actions + } } diff --git a/test/index.test.js b/test/index.test.js index dabde6f..1c75fa6 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -258,7 +258,7 @@ describe('attributes', () => { }) }) -describe('renderer(node)(bytes)', () => { +describe('renderer(view, state, actions)(bytes)', () => { it('should create a reader function', () => { const read = renderer(
) expect(read).toBeInstanceOf(Function) @@ -283,7 +283,7 @@ describe('renderer(node)(bytes)', () => { }) }) -describe('renderToString(node)', () => { +describe('renderToString(view, state, actions)', () => { it('should render simple markup', () => { const html = renderToString(
hello world
) expect(html).toBe('
hello world
') @@ -339,43 +339,69 @@ describe('renderToString(node)', () => { const html = renderToString(h('', { innerHTML: 'alert("hello world")' })) expect(html).toBe('alert("hello world")') }) + + it('should render an array of elements', () => { + const html = renderToString([, ]) + expect(html).toBe('') + }) }) describe('render(app)(state, actions, view, container)', () => { - const initialState = { - count: 0, - } - const actions = { + const testState = { count: 0 } + const testActions = { up: (count = 1) => (state) => ({ count: state.count + count }), + getState: () => (state) => state, } - const view = (state) =>

{state.count}

+ const testView = (state) =>

{state.count}

it('should create a higher-order app', () => { - const spyApp = jest.fn(() => 'result') - const renderApp = render(spyApp) + const mockApp = jest.fn(() => 'result') + const renderApp = render(mockApp) expect(renderApp).toBeInstanceOf(Function) - expect(spyApp).not.toBeCalled() - const main = renderApp(initialState, actions, view, 'container') - expect(spyApp).toBeCalled() - expect(spyApp.mock.calls[0][0]).toBe(initialState) - expect(spyApp.mock.calls[0][1]).not.toBe(actions) - expect(spyApp.mock.calls[0][2]).toBe(view) - expect(spyApp.mock.calls[0][3]).toBe('container') - expect(main).toBe('result') + expect(mockApp).not.toBeCalled() + const actions = renderApp(testState, testActions, testView, 'container') + expect(mockApp).toBeCalled() + expect(mockApp.mock.calls[0][0]).toBe(testState) + expect(mockApp.mock.calls[0][1]).not.toBe(testActions) + expect(mockApp.mock.calls[0][2]).toBe(testView) + expect(mockApp.mock.calls[0][3]).toBe('container') + expect(actions).toBe('result') }) it('should not mutate original actions', () => { - render(app)(initialState, actions, view) - expect(actions).toEqual({ up: actions.up }) + render(app)(testState, testActions, testView) + expect(testActions).toEqual({ + up: testActions.up, + getState: testActions.getState, + }) + }) + + it('should not mutate store', () => { + const actions = render(app)(testState, testActions, testView) + expect(actions.getState()).toEqual({ count: 0 }) + expect(actions.toString()).toBe('

0

') + expect(actions.getState()).toEqual({ count: 0 }) }) it('should render app with current state', () => { - const main = render(app)(initialState, actions, view) - expect(main.toString).toBeInstanceOf(Function) - expect(main.toString()).toBe('

0

') - main.up() - expect(main.toString()).toBe('

1

') - main.up(100) - expect(main.toString()).toBe('

101

') + const actions = render(app)(testState, testActions, testView) + expect(actions.toString).toBeInstanceOf(Function) + expect(actions.toString()).toBe('

0

') + actions.up() + expect(actions.toString()).toBe('

1

') + actions.up(100) + expect(actions.toString()).toBe('

101

') + }) + + it('should provide state and actions to nested views', () => { + const Component = () => (state, actions) => { + expect(actions).toBeInstanceOf(Object) + return

{state.count}

+ } + const view = () => + const actions = render(app)(testState, testActions, view) + expect(actions.toString()).toBe('

0

') + actions.up(5) + expect(actions.toString()).toBe('

5

') }) }) diff --git a/test/server.test.js b/test/server.test.js index 3c8a380..77bafcd 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -18,7 +18,7 @@ function readFromStream(stream) { }) } -describe('server/renderer(node)(bytes)', () => { +describe('server/renderer(view, state, actions)(bytes)', () => { it('should render markup', () => { const read = renderer(
) expect(read(Infinity)).toBe('
') @@ -26,14 +26,14 @@ describe('server/renderer(node)(bytes)', () => { }) }) -describe('server/renderToString(node)', () => { +describe('server/renderToString(view, state, actions)', () => { it('should render markup', () => { const html = renderToString(
hello world
) expect(html).toBe('
hello world
') }) }) -describe('server/renderToStream(node)', () => { +describe('server/renderToStream(view, state, actions)', () => { it('should return a readable stream', () => { const stream = renderToStream(
hello world
) expect(stream).toBeInstanceOf(Readable) @@ -60,50 +60,48 @@ describe('server/renderToStream(node)', () => { }) describe('server/render(app)(state, actions, view, container)', () => { - const initialState = { - count: 0, - } - const actions = { + const testState = { count: 0 } + const testActions = { up: (count = 1) => (state) => ({ count: state.count + count }), } - const view = (state) =>

{state.count}

+ const testView = (state) =>

{state.count}

it('should create a higher-order app', () => { - const spyApp = jest.fn(() => 'result') - const renderApp = render(spyApp) + const mockApp = jest.fn(() => 'result') + const renderApp = render(mockApp) expect(renderApp).toBeInstanceOf(Function) - expect(spyApp).not.toBeCalled() - const main = renderApp(initialState, actions, view, 'container') - expect(spyApp).toBeCalled() - expect(spyApp.mock.calls[0][0]).toBe(initialState) - expect(spyApp.mock.calls[0][1]).not.toBe(actions) - expect(spyApp.mock.calls[0][2]).toBe(view) - expect(spyApp.mock.calls[0][3]).toBe('container') - expect(main).toBe('result') + expect(mockApp).not.toBeCalled() + const actions = renderApp(testState, testActions, testView, 'container') + expect(mockApp).toBeCalled() + expect(mockApp.mock.calls[0][0]).toBe(testState) + expect(mockApp.mock.calls[0][1]).not.toBe(testActions) + expect(mockApp.mock.calls[0][2]).toBe(testView) + expect(mockApp.mock.calls[0][3]).toBe('container') + expect(actions).toBe('result') }) it('should not mutate original actions', () => { - render(app)(initialState, actions, view) - expect(actions).toEqual({ up: actions.up }) + render(app)(testState, testActions, testView) + expect(testActions).toEqual({ up: testActions.up }) }) it('should render app with current state to string', () => { - const main = render(app)(initialState, actions, view) - expect(main.toString).toBeInstanceOf(Function) - expect(main.toString()).toBe('

0

') - main.up() - expect(main.toString()).toBe('

1

') - main.up(100) - expect(main.toString()).toBe('

101

') + const acitons = render(app)(testState, testActions, testView) + expect(acitons.toString).toBeInstanceOf(Function) + expect(acitons.toString()).toBe('

0

') + acitons.up() + expect(acitons.toString()).toBe('

1

') + acitons.up(100) + expect(acitons.toString()).toBe('

101

') }) it('should render app with current state to stream', async () => { - const main = render(app)(initialState, actions, view) - expect(main.toStream).toBeInstanceOf(Function) - expect(await readFromStream(main.toStream())).toBe('

0

') - main.up() - expect(await readFromStream(main.toStream())).toBe('

1

') - main.up(100) - expect(await readFromStream(main.toStream())).toBe('

101

') + const actions = render(app)(testState, testActions, testView) + expect(actions.toStream).toBeInstanceOf(Function) + expect(await readFromStream(actions.toStream())).toBe('

0

') + actions.up() + expect(await readFromStream(actions.toStream())).toBe('

1

') + actions.up(100) + expect(await readFromStream(actions.toStream())).toBe('

101

') }) })