diff --git a/.babelrc b/.babelrc index cff5232..8762b0e 100644 --- a/.babelrc +++ b/.babelrc @@ -1,12 +1,23 @@ { "presets": [ - ["@babel/preset-env", { - "targets": { - "node": "current" + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } } - }] + ] ], "plugins": [ - "@babel/plugin-transform-react-jsx" + [ + "@babel/plugin-transform-react-jsx", + { + "pragma": "h", + "pragmaFrag": "Fragment", + "useBuiltIns": true, + "throwIfNamespace": false + } + ] ] } diff --git a/.editorconfig b/.editorconfig index a082ad7..fd375c4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,11 +6,8 @@ root = true [*] -# Change these settings to your own preference indent_style = space indent_size = 2 - -# We recommend you to keep these unchanged end_of_line = lf charset = utf-8 trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc index 6f45e23..483ef4f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,11 +1,15 @@ { - "extends": [ - "airbnb", - "eslint-config-prettier", - "eslint-config-prettier/react" - ], + "extends": ["airbnb", "eslint-config-prettier", "eslint-config-prettier/react"], "rules": { + "import/prefer-default-export": "off", + "no-continue": "off", "no-plusplus": "off", + "no-restricted-syntax": "off", "prefer-template": "off" + }, + "settings": { + "react": { + "pragma": "h" + } } } diff --git a/.gitattributes b/.gitattributes index c667fa9..d88a0db 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,5 +11,8 @@ .* text eol=lf *.js text eol=lf *.json text eol=lf +*.map text eol=lf *.md text eol=lf *.svg text eol=lf +*.ts text eol=lf +*.tsx text eol=lf diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e81b4d..e363895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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][unreleased] + +- [BREAKING] Remove higher-order app `withRender` from the library due to redundancy. +- Support for `className` attribute and allow to use array and object as a value. +- Compatibility with upcoming [Hyperapp V2](https://github.com/hyperapp/hyperapp/pull/726). +- Various performance optimizations. + ## [2.1.0] - 2018-07-11 - Add [TypeScript](https://www.typescriptlang.org/) typings. diff --git a/README.md b/README.md index fca5527..710e6f7 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,17 @@ [![library size](https://img.shields.io/bundlephobia/minzip/hyperapp-render.svg)](https://bundlephobia.com/result?p=hyperapp-render) [![slack chat](https://hyperappjs.herokuapp.com/badge.svg)](https://hyperappjs.herokuapp.com 'Join us') -A [Hyperapp](https://github.com/hyperapp/hyperapp) higher-order `app` -that allows you to render views to an HTML string. +This library is allowing you to render +[Hyperapp](https://github.com/hyperapp/hyperapp) views to an HTML string. - **User experience** — Generate HTML on the server and send the markup down on the initial request for faster page loads. Built-in - [mounting](https://github.com/hyperapp/hyperapp/tree/1.2.0#mounting) + [mounting](https://github.com/hyperapp/hyperapp/tree/1.2.9#mounting) feature in Hyperapp is allowing you to have a very performant first-load experience. - **Accessibility** — Allow search engines to crawl your pages for [SEO](https://en.wikipedia.org/wiki/Search_engine_optimization) purposes. - **Testability** — [Check HTML validity](https://en.wikipedia.org/wiki/Validator) and use - [snapshot testing](https://facebook.github.io/jest/docs/en/snapshot-testing.html) + [snapshot testing](https://jestjs.io/docs/en/snapshot-testing.html) to improve quality of your software. ## Getting Started @@ -24,8 +24,8 @@ Our first example is an interactive app from which you can generate an HTML mark Go ahead and [try it online](https://codepen.io/frenzzy/pen/zpmRQY/left/?editors=0010). ```jsx -import { h, app } from 'hyperapp' -import { withRender } from 'hyperapp-render' +import { h } from 'hyperapp' +import { renderToString } from 'hyperapp-render' const state = { text: 'Hello' @@ -42,14 +42,13 @@ const view = (state, actions) => ( ) -const main = withRender(app)(state, actions, view) +const html = renderToString(view(state, actions)) -main.toString() // =>

Hello

-main.setText('World') // <= any sync or async action call -main.toString() // =>

World

+console.log(html) // =>

Hello

``` -Looking for a boilerplate? Try [Hyperapp Starter](https://github.com/frenzzy/hyperapp-starter) +Looking for a boilerplate? +Try [Hyperapp Starter](https://github.com/kriasoft/hyperapp-starter) with pre-configured server-side rendering and many more. ## Installation @@ -74,110 +73,56 @@ You can find the library in `window.hyperappRender`. We support all ES5-compliant browsers, including Internet Explorer 9 and above, but depending on your target browsers you may need to include [polyfills]() for -[`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set), -[`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and -[`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) +[`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) and +[`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) before any other code. ## Usage -The library provides a few functions which you can use depending on your needs or personal preferences. +The library provides two functions +which you can use depending on your needs or personal preferences: ```jsx -import { withRender, renderToString, renderToStream } from 'hyperapp-render' - -const main = withRender(app)(state, actions, view, container) +import { renderToString, renderToStream } from 'hyperapp-render' -main.toString() // => renderToString() // => +renderToString(view(state, actions)) // => renderToString(view, state, actions) // => -main.toStream() // => => renderToStream() // => => +renderToStream(view(state, actions)) // => => renderToStream(view, state, actions) // => => ``` -**Note:** functions `toStream` and `renderToStream` are available in +**Note:** `renderToStream` is available from [Node.js](https://nodejs.org/en/) environment only (v6 or newer). ## Overview -The library exposes three functions. The first of these is `withRender` high-order function, -which adds the `toString` action to be able to render your application to an HTML string at any given time. -This can be useful for server-side rendering or creating HTML snippets based on current application state. - -```jsx -import { h, app } from 'hyperapp' -import { withRender } from 'hyperapp-render' - -const state = { name: 'World' } -const actions = { setName: name => ({ name }) } -const view = (state, actions) =>

Hello {state.name}

- -const main = withRender(app)(state, actions, view) +You can use `renderToString` function to generate HTML on the server +and send the markup down on the initial request for faster page loads +and to allow search engines to crawl your pages for +[SEO](https://en.wikipedia.org/wiki/Search_engine_optimization) purposes. -main.toString() // =>

Hello World

-main.setName('Hyperapp') // <= any sync or async action call -main.toString() // =>

Hello Hyperapp

-``` - -The second `renderToString` function generates HTML markup from any of your views without -app initialization. That could be useful to generate HTML markup from static views. - -```jsx -import { renderToString } from 'hyperapp-render' +If you call [`hyperapp.app()`](https://github.com/hyperapp/hyperapp/tree/1.2.9#mounting) +on a node that already has this server-rendered markup, +Hyperapp will preserve it and only attach event handlers, allowing you +to have a very performant first-load experience. -const Component = ({ name }) =>

Hello {name}

- -renderToString() -// =>

Hello World

-``` - -The last `renderToStream` function and `toStream` equivalent return a -[Readable stream](https://nodejs.org/api/stream.html#stream_readable_streams) that outputs an HTML string. -The HTML output by this stream is exactly equal to what `toString` or `renderToString` would return. -They are designed for more performant server-side rendering and here are examples how they could be used -with [Express](http://expressjs.com/) or [Koa](http://koajs.com/): - -```jsx -app.get('/', (req, res) => { - res.write('') - res.write('Page') - res.write('
') - const main = withRender(app)(state, actions, view) - const stream = main.toStream() - stream.pipe(res, { end: false }) - stream.on('end', () => { - res.write('
') - res.end() - }) -}) -``` - -```jsx -app.get('/', (req, res) => { - res.write('') - const stream = renderToStream( - - Page - -
{view(state, actions)}
- - - ) - stream.pipe(res) -}) -``` +The `renderToStream` function returns a +[Readable stream](https://nodejs.org/api/stream.html#stream_readable_streams) +that outputs an HTML string. +The HTML output by this stream is exactly equal to what `renderToString` would return. +By using this function you can reduce [TTFB](https://en.wikipedia.org/wiki/Time_to_first_byte) +and improve user experience even more. ## Caveats The library automatically escapes text content and attribute values -of [virtual DOM nodes](https://github.com/hyperapp/hyperapp/tree/1.2.0#view) -to protect your application against [XSS](https://en.wikipedia.org/wiki/Cross-site_scripting) attacks. - -However, it is not safe to allow "user input" for node names or attribute keys because -the library does not reject injection attack on markup due to performance reasons. -See: +of [virtual DOM nodes](https://github.com/hyperapp/hyperapp/tree/1.2.9#view) +to protect your application against +[XSS](https://en.wikipedia.org/wiki/Cross-site_scripting) attacks. +However, it is not safe to allow "user input" for node names or attribute keys: ```jsx const Node = 'div onclick="alert()"' diff --git a/benchmark/benchmark.js b/benchmark/benchmark.js index 28a76de..68690d6 100644 --- a/benchmark/benchmark.js +++ b/benchmark/benchmark.js @@ -1,129 +1,135 @@ /** @jsx h */ import { h } from 'hyperapp' -import { renderToString } from '../src/index' +import { escapeHtml, concatClassNames, stringifyStyles, renderToString } from '../src/index' -suite('escape', () => { - benchmark('empty', () => { - renderToString(
{null}
) +suite('escapeHtml(value)', () => { + benchmark('numeric value', () => { + escapeHtml(123456789.012) }) - benchmark('number', () => { - renderToString(
{1.23}
) + benchmark('no special characters', () => { + escapeHtml('hello world') }) - benchmark('string', () => { - renderToString(
text
) + benchmark('single special character', () => { + escapeHtml('hello wor&d') }) - benchmark('special characters', () => { - renderToString(
{`"&'<>`}
) + benchmark('many special characters', () => { + escapeHtml('"&&"') }) }) -suite('styles', () => { - benchmark('empty', () => { - const style = { +suite('concatClassNames(value)', () => { + benchmark('string value', () => { + concatClassNames('foo bar baz') + }) + + benchmark('values array', () => { + concatClassNames(['foo', 'bar', 'baz', null]) + }) + + benchmark('values map', () => { + concatClassNames({ foo: true, bar: 'ok', baz: 1, qux: null }) + }) + + benchmark('mixed values', () => { + concatClassNames(['foo', false, 0, null, { bar: 'ok', baz: 1, qux: null }]) + }) + + benchmark('nested values', () => { + concatClassNames(['foo', ['bar', { baz: 1 }, [false, { qux: null }]]]) + }) +}) + +suite('stringifyStyles(style)', () => { + benchmark('no values', () => { + stringifyStyles({ color: null, border: null, opacity: null, - } - renderToString(
) + }) }) - benchmark('basic', () => { - const style = { + benchmark('basic styles', () => { + stringifyStyles({ color: '#000', border: '1px solid', opacity: 0.5, - } - renderToString(
) + }) }) - benchmark('camel-case', () => { - const style = { + benchmark('camel-case styles', () => { + stringifyStyles({ backgroundColor: '#000', borderTop: '1px solid', lineHeight: 1.23, - } - renderToString(
) + }) }) benchmark('vendor specific', () => { - const style = { + stringifyStyles({ + webkitTransform: 'rotate(5deg)', MozTransform: 'rotate(5deg)', msTransform: 'rotate(5deg)', - transform: 'rotate(5deg)', - } - renderToString(
) + }) }) }) -suite('attributes', () => { - benchmark('empty', () => { - renderToString(
) +suite('renderAttributes(props)', () => { + benchmark('no value', () => { + renderToString(
) }) - benchmark('boolean', () => { - renderToString(
) + benchmark('boolean value', () => { + renderToString(
) }) - benchmark('regular', () => { - renderToString(
) + benchmark('string value', () => { + renderToString(
) }) - benchmark('special', () => { - renderToString(
) + benchmark('special attributes', () => { + renderToString(
) }) }) -suite('elements', () => { - const Fragment = (attributes, children) => h('', attributes, children) +suite('renderToString(node)', () => { + const Fragment = '' const Component = (attributes, children) =>

{children}

benchmark('basic', () => { - renderToString( -
-

Hello World

-
, - ) + renderToString(

Hello World

) }) benchmark('fragment', () => { - renderToString( -
- Hello World -
, - ) + renderToString(Hello World) }) benchmark('component', () => { - renderToString( -
- Hello World -
, - ) + renderToString(Hello World) }) benchmark('array', () => { renderToString( -
+ A B C -
, + , ) }) benchmark('nested', () => { renderToString( -
+ A BC -
, + , ) }) }) diff --git a/dist/hyperapp-render.js b/dist/hyperapp-render.js index 1d7002d..0936151 100644 --- a/dist/hyperapp-render.js +++ b/dist/hyperapp-render.js @@ -6,90 +6,161 @@ (factory((global.hyperappRender = {}))); }(this, (function (exports) { 'use strict'; + var isArray = Array.isArray; + var hasOwnProperty = Object.prototype.hasOwnProperty; var styleNameCache = new Map(); - var uppercasePattern = /([A-Z])/g; + var uppercasePattern = /[A-Z]/g; var msPattern = /^ms-/; + var escapeRegExp = /["&'<>]/; var voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']); - var ignoreAttributes = new Set(['key', 'innerHTML', '__source']); - var escapeRegExp = /["&'<>]/g; - var escapeLookup = new Map([['"', '"'], ['&', '&'], ["'", '''], ['<', '<'], ['>', '>']]); + function escapeHtml(value) { + var str = '' + value; + + if (typeof value === 'number') { + return str; + } + + var match = escapeRegExp.exec(str); + + if (!match) { + return str; + } + + var index = match.index; + var lastIndex = 0; + var out = ''; + + for (var _escape = ''; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: + _escape = '"'; + break; + + case 38: + _escape = '&'; + break; - function escaper(match) { - return escapeLookup.get(match); + case 39: + _escape = '''; + break; + + case 60: + _escape = '<'; + break; + + case 62: + _escape = '>'; + break; + + default: + continue; + } + + if (lastIndex !== index) { + out += str.substring(lastIndex, index); + } + + lastIndex = index + 1; + out += _escape; + } + + return lastIndex !== index ? out + str.substring(lastIndex, index) : out; } + function concatClassNames(value) { + if (typeof value === 'string' || typeof value === 'number') { + return value || ''; + } - function escapeHtml(value) { - if (typeof value === 'number') { - return '' + value; + var out = ''; + var delimiter = ''; + + if (isArray(value)) { + for (var i = 0; i < value.length; i++) { + var name = concatClassNames(value[i]); + + if (name !== '') { + out += delimiter + name; + delimiter = ' '; + } + } + } else { + for (var _name in value) { + if (hasOwnProperty.call(value, _name) && value[_name]) { + out += delimiter + _name; + delimiter = ' '; + } + } } - return ('' + value).replace(escapeRegExp, escaper); + return out; } function hyphenateStyleName(styleName) { return styleNameCache.get(styleName) || styleNameCache.set(styleName, styleName.replace(uppercasePattern, '-$&').toLowerCase().replace(msPattern, '-ms-')).get(styleName); } - function stringifyStyles(styles) { + function stringifyStyles(style) { var out = ''; var delimiter = ''; - var styleNames = Object.keys(styles); - for (var i = 0; i < styleNames.length; i++) { - var styleName = styleNames[i]; - var styleValue = styles[styleName]; + for (var name in style) { + if (hasOwnProperty.call(style, name)) { + var value = style[name]; - if (styleValue != null) { - if (styleName === 'cssText') { - out += delimiter + styleValue; - } else { - out += delimiter + hyphenateStyleName(styleName) + ':' + styleValue; - } + if (value != null) { + if (name === 'cssText') { + out += delimiter + value; + } else { + out += delimiter + hyphenateStyleName(name) + ':' + value; + } - delimiter = ';'; + delimiter = ';'; + } } } - return out || null; + return out; } - function renderFragment(_ref, stack) { - var nodeName = _ref.nodeName, - attributes = _ref.attributes, - children = _ref.children; + function renderFragment(name, props, children, stack) { var out = ''; var footer = ''; - if (nodeName) { - out += '<' + nodeName; - var keys = Object.keys(attributes); + if (name) { + out += '<' + name; - for (var i = 0; i < keys.length; i++) { - var name = keys[i]; - var value = attributes[name]; + for (var prop in props) { + if (hasOwnProperty.call(props, prop)) { + var value = props[prop]; - if (name === 'style' && value && typeof value === 'object') { - value = stringifyStyles(value); - } + if (value != null && prop !== 'key' && prop !== 'innerHTML' && prop !== '__source' && !(prop[0] === 'o' && prop[1] === 'n')) { + if (prop === 'class' || prop === 'className') { + prop = 'class'; + value = concatClassNames(value) || false; + } else if (prop === 'style' && value && typeof value === 'object') { + value = stringifyStyles(value) || false; + } - if (value != null && value !== false && typeof value !== 'function' && !ignoreAttributes.has(name)) { - out += ' ' + name; + if (value !== false) { + out += ' ' + prop; - if (value !== true) { - out += '="' + escapeHtml(value) + '"'; + if (value !== true) { + out += '="' + escapeHtml(value) + '"'; + } + } } } } - if (voidElements.has(nodeName)) { + if (voidElements.has(name)) { out += '/>'; } else { out += '>'; - footer = ''; + footer = ''; } } - var innerHTML = attributes.innerHTML; + var innerHTML = props.innerHTML; if (innerHTML != null) { out += innerHTML; @@ -145,14 +216,16 @@ var node = resolveNode(frame.children[frame.childIndex++], state, actions); if (node != null && typeof node !== 'boolean') { - if (node.pop) { + if (isArray(node)) { stack.push({ childIndex: 0, children: node, footer: '' }); - } else if (node.attributes) { - out += renderFragment(node, stack); + } else if (node.type === 3) { + out += escapeHtml(node.name); + } else if (typeof node === 'object') { + out += renderFragment(node.name || node.nodeName, node.props || node.attributes, node.children, stack); } else { out += escapeHtml(node); } @@ -167,26 +240,7 @@ return renderer(view, state, actions)(Infinity); } - function withRender(nextApp) { - return function (initialState, actionsTemplate, view, container) { - var actions = nextApp(initialState, Object.assign({}, actionsTemplate, { - getState: function getState() { - return function (state) { - return state; - }; - } - }), view, container); - - actions.toString = function () { - return renderToString(view, actions.getState(), actions); - }; - - return actions; - }; - } - exports.renderToString = renderToString; - exports.withRender = withRender; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/dist/hyperapp-render.js.map b/dist/hyperapp-render.js.map index b60acc3..1c57c38 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","src/browser.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 [\"'\", '''], // shorter than \"'\" and \"'\" plus supports HTML4\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\n// https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\nfunction renderFragment({ nodeName, attributes, children }, stack) {\n let out = ''\n let footer = ''\n\n if (nodeName) {\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 // 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","import { renderToString } from './index'\n\nexport { renderToString }\n\nexport function withRender(nextApp) {\n return (initialState, actionsTemplate, view, container) => {\n const actions = nextApp(\n initialState,\n { ...actionsTemplate, getState: () => (state) => state },\n view,\n container,\n )\n\n actions.toString = () => renderToString(view, actions.getState(), actions)\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","childIndex","resolveNode","node","state","actions","renderer","view","end","bytes","frame","pop","renderToString","Infinity","withRender","nextApp","initialState","actionsTemplate","container","getState","toString"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"hyperapp-render.js","sources":["src/index.js"],"sourcesContent":["const { isArray } = Array\nconst { hasOwnProperty } = Object.prototype\nconst styleNameCache = new Map()\nconst uppercasePattern = /[A-Z]/g\nconst msPattern = /^ms-/\n\n// https://www.w3.org/International/questions/qa-escapes#use\nconst escapeRegExp = /[\"&'<>]/\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\n// credits to https://github.com/component/escape-html\nexport function escapeHtml(value) {\n const str = '' + value\n if (typeof value === 'number') {\n // better performance for safe values\n return str\n }\n\n const match = escapeRegExp.exec(str)\n if (!match) {\n return str\n }\n\n let { index } = match\n let lastIndex = 0\n let out = ''\n\n for (let escape = ''; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"'\n break\n case 38: // &\n escape = '&'\n break\n case 39: // '\n escape = ''' // shorter than \"'\" and \"'\" plus supports HTML4\n break\n case 60: // <\n escape = '<'\n break\n case 62: // >\n escape = '>'\n break\n default:\n continue\n }\n\n if (lastIndex !== index) {\n out += str.substring(lastIndex, index)\n }\n\n lastIndex = index + 1\n out += escape\n }\n\n return lastIndex !== index ? out + str.substring(lastIndex, index) : out\n}\n\n// credits to https://github.com/jorgebucaran/classcat\nexport function concatClassNames(value) {\n if (typeof value === 'string' || typeof value === 'number') {\n return value || ''\n }\n\n let out = ''\n let delimiter = ''\n\n if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const name = concatClassNames(value[i])\n if (name !== '') {\n out += delimiter + name\n delimiter = ' '\n }\n }\n } else {\n for (const name in value) {\n if (hasOwnProperty.call(value, name) && value[name]) {\n out += delimiter + name\n delimiter = ' '\n }\n }\n }\n\n return out\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\nexport function stringifyStyles(style) {\n let out = ''\n let delimiter = ''\n\n for (const name in style) {\n if (hasOwnProperty.call(style, name)) {\n const value = style[name]\n\n if (value != null) {\n if (name === 'cssText') {\n out += delimiter + value\n } else {\n out += delimiter + hyphenateStyleName(name) + ':' + value\n }\n delimiter = ';'\n }\n }\n }\n\n return out\n}\n\n// https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\nfunction renderFragment(name, props, children, stack) {\n let out = ''\n let footer = ''\n\n if (name) {\n out += '<' + name\n\n for (let prop in props) {\n if (hasOwnProperty.call(props, prop)) {\n let value = props[prop]\n\n if (\n value != null &&\n prop !== 'key' &&\n prop !== 'innerHTML' &&\n prop !== '__source' && // babel-plugin-transform-react-jsx-source\n !(prop[0] === 'o' && prop[1] === 'n')\n ) {\n if (prop === 'class' || prop === 'className') {\n prop = 'class'\n value = concatClassNames(value) || false\n } else if (prop === 'style' && value && typeof value === 'object') {\n value = stringifyStyles(value) || false\n }\n\n if (value !== false) {\n out += ' ' + prop\n\n if (value !== true) {\n out += '=\"' + escapeHtml(value) + '\"'\n }\n }\n }\n }\n }\n\n if (voidElements.has(name)) {\n out += '/>'\n } else {\n out += '>'\n footer = ''\n }\n }\n\n const { innerHTML } = props\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\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 (isArray(node)) {\n stack.push({\n childIndex: 0,\n children: node,\n footer: '',\n })\n } else if (node.type === 3) {\n out += escapeHtml(node.name)\n } else if (typeof node === 'object') {\n out += renderFragment(\n node.name || node.nodeName,\n node.props || node.attributes,\n node.children,\n stack,\n )\n } else {\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"],"names":["isArray","Array","hasOwnProperty","Object","prototype","styleNameCache","Map","uppercasePattern","msPattern","escapeRegExp","voidElements","Set","escapeHtml","value","str","match","exec","index","lastIndex","out","escape","length","charCodeAt","substring","concatClassNames","delimiter","i","name","call","hyphenateStyleName","styleName","get","set","replace","toLowerCase","stringifyStyles","style","renderFragment","props","children","stack","footer","prop","has","innerHTML","push","childIndex","resolveNode","node","state","actions","renderer","view","end","bytes","frame","pop","type","nodeName","attributes","renderToString","Infinity"],"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 c9deaba..73ab8ef 100644 --- a/dist/hyperapp-render.min.js +++ b/dist/hyperapp-render.min.js @@ -1,3 +1,3 @@ /*! Hyperapp Render | MIT Licence | https://github.com/kriasoft/hyperapp-render */ -!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(e.hyperappRender={})}(this,function(e){"use strict";var l=new Map,f=/([A-Z])/g,a=/^ms-/,p=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),s=new Set(["key","innerHTML","__source"]),n=/["&'<>]/g,t=new Map([['"',"""],["&","&"],["'","'"],["<","<"],[">",">"]]);function r(e){return t.get(e)}function h(e){return"number"==typeof e?""+e:(""+e).replace(n,r)}function g(e){for(var n,t="",r="",o=Object.keys(e),i=0;i":(i+=">",u="")}var d=r.innerHTML;return null!=d&&(i+=d),0=t.children.length)n+=t.footer,u.pop();else{var r=b(t.children[t.childIndex++],o,i);null!=r&&"boolean"!=typeof r&&(r.pop?u.push({childIndex:0,children:r,footer:""}):r.attributes?n+=d(r,u):n+=h(r))}}return n}(1/0);var o,i,u,c}e.renderToString=u,e.withRender=function(i){return function(e,n,t,r){var o=i(e,Object.assign({},n,{getState:function(){return function(e){return e}}}),t,r);return o.toString=function(){return u(t,o.getState(),o)},o}},Object.defineProperty(e,"__esModule",{value:!0})}); +!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r(e.hyperappRender={})}(this,function(e){"use strict";var l=Array.isArray,f=Object.prototype.hasOwnProperty,i=new Map,c=/[A-Z]/g,u=/^ms-/,s=/["&'<>]/,p=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);function d(e){var r=""+e;if("number"==typeof e)return r;var n=s.exec(r);if(!n)return r;for(var t=n.index,a=0,o="",i="";t":(a+=">",o="")}var l=r.innerHTML;return null!=l&&(a+=l),0=n.children.length)r+=n.footer,i.pop();else{var t=y(n.children[n.childIndex++],a,o);null!=t&&"boolean"!=typeof t&&(l(t)?i.push({childIndex:0,children:t,footer:""}):3===t.type?r+=d(t.name):r+="object"==typeof t?v(t.name||t.nodeName,t.props||t.attributes,t.children,i):d(t))}}return r}(1/0);var a,o,i,c},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 0496cc2..91de8c2 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","src/browser.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 [\"'\", '''], // shorter than \"'\" and \"'\" plus supports HTML4\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\n// https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\nfunction renderFragment({ nodeName, attributes, children }, stack) {\n let out = ''\n let footer = ''\n\n if (nodeName) {\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 // 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","import { renderToString } from './index'\n\nexport { renderToString }\n\nexport function withRender(nextApp) {\n return (initialState, actionsTemplate, view, container) => {\n const actions = nextApp(\n initialState,\n { ...actionsTemplate, getState: () => (state) => state },\n view,\n container,\n )\n\n actions.toString = () => renderToString(view, actions.getState(), actions)\n\n return actions\n }\n}\n"],"names":["styleNameCache","Map","uppercasePattern","msPattern","voidElements","Set","ignoreAttributes","escapeRegExp","escapeLookup","escaper","match","get","escapeHtml","value","replace","stringifyStyles","styles","styleName","out","delimiter","styleNames","Object","keys","i","length","styleValue","set","toLowerCase","renderFragment","stack","nodeName","attributes","children","footer","name","has","innerHTML","push","childIndex","resolveNode","node","state","actions","renderToString","view","end","bytes","frame","pop","renderer","Infinity","nextApp","initialState","actionsTemplate","container","getState","toString"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"hyperapp-render.min.js","sources":["src/index.js"],"sourcesContent":["const { isArray } = Array\nconst { hasOwnProperty } = Object.prototype\nconst styleNameCache = new Map()\nconst uppercasePattern = /[A-Z]/g\nconst msPattern = /^ms-/\n\n// https://www.w3.org/International/questions/qa-escapes#use\nconst escapeRegExp = /[\"&'<>]/\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\n// credits to https://github.com/component/escape-html\nexport function escapeHtml(value) {\n const str = '' + value\n if (typeof value === 'number') {\n // better performance for safe values\n return str\n }\n\n const match = escapeRegExp.exec(str)\n if (!match) {\n return str\n }\n\n let { index } = match\n let lastIndex = 0\n let out = ''\n\n for (let escape = ''; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"'\n break\n case 38: // &\n escape = '&'\n break\n case 39: // '\n escape = ''' // shorter than \"'\" and \"'\" plus supports HTML4\n break\n case 60: // <\n escape = '<'\n break\n case 62: // >\n escape = '>'\n break\n default:\n continue\n }\n\n if (lastIndex !== index) {\n out += str.substring(lastIndex, index)\n }\n\n lastIndex = index + 1\n out += escape\n }\n\n return lastIndex !== index ? out + str.substring(lastIndex, index) : out\n}\n\n// credits to https://github.com/jorgebucaran/classcat\nexport function concatClassNames(value) {\n if (typeof value === 'string' || typeof value === 'number') {\n return value || ''\n }\n\n let out = ''\n let delimiter = ''\n\n if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const name = concatClassNames(value[i])\n if (name !== '') {\n out += delimiter + name\n delimiter = ' '\n }\n }\n } else {\n for (const name in value) {\n if (hasOwnProperty.call(value, name) && value[name]) {\n out += delimiter + name\n delimiter = ' '\n }\n }\n }\n\n return out\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\nexport function stringifyStyles(style) {\n let out = ''\n let delimiter = ''\n\n for (const name in style) {\n if (hasOwnProperty.call(style, name)) {\n const value = style[name]\n\n if (value != null) {\n if (name === 'cssText') {\n out += delimiter + value\n } else {\n out += delimiter + hyphenateStyleName(name) + ':' + value\n }\n delimiter = ';'\n }\n }\n }\n\n return out\n}\n\n// https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\nfunction renderFragment(name, props, children, stack) {\n let out = ''\n let footer = ''\n\n if (name) {\n out += '<' + name\n\n for (let prop in props) {\n if (hasOwnProperty.call(props, prop)) {\n let value = props[prop]\n\n if (\n value != null &&\n prop !== 'key' &&\n prop !== 'innerHTML' &&\n prop !== '__source' && // babel-plugin-transform-react-jsx-source\n !(prop[0] === 'o' && prop[1] === 'n')\n ) {\n if (prop === 'class' || prop === 'className') {\n prop = 'class'\n value = concatClassNames(value) || false\n } else if (prop === 'style' && value && typeof value === 'object') {\n value = stringifyStyles(value) || false\n }\n\n if (value !== false) {\n out += ' ' + prop\n\n if (value !== true) {\n out += '=\"' + escapeHtml(value) + '\"'\n }\n }\n }\n }\n }\n\n if (voidElements.has(name)) {\n out += '/>'\n } else {\n out += '>'\n footer = ''\n }\n }\n\n const { innerHTML } = props\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\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 (isArray(node)) {\n stack.push({\n childIndex: 0,\n children: node,\n footer: '',\n })\n } else if (node.type === 3) {\n out += escapeHtml(node.name)\n } else if (typeof node === 'object') {\n out += renderFragment(\n node.name || node.nodeName,\n node.props || node.attributes,\n node.children,\n stack,\n )\n } else {\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"],"names":["isArray","Array","hasOwnProperty","Object","prototype","styleNameCache","Map","uppercasePattern","msPattern","escapeRegExp","voidElements","Set","escapeHtml","value","str","match","exec","index","lastIndex","out","escape","length","charCodeAt","substring","concatClassNames","delimiter","i","name","call","stringifyStyles","style","styleName","get","set","replace","toLowerCase","renderFragment","props","children","stack","footer","prop","has","innerHTML","push","childIndex","resolveNode","node","state","actions","view","end","bytes","frame","pop","type","nodeName","attributes","renderer","Infinity"],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index 44fab22..55d63e4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ ], "main": "node/index.js", "module": "node/module.js", - "typings": "src/node.d.ts", + "types": "src/node.d.ts", "esnext": "src/node.js", "browser": { "node/index.js": "./browser/index.js", @@ -26,33 +26,33 @@ "src/node.js": "./src/browser.js" }, "dependencies": { - "@types/node": "*", - "hyperapp": "^1.2.0" + "@types/node": "*" }, "devDependencies": { - "@babel/core": "^7.1.2", + "@babel/core": "^7.1.5", "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/preset-env": "^7.1.0", + "@babel/preset-env": "^7.1.5", "@babel/register": "^7.0.0", "babel-core": "^7.0.0-0", "babel-jest": "^23.6.0", "benchr": "^4.3.0", - "eslint": "^5.6.1", + "eslint": "^5.8.0", "eslint-config-airbnb": "^17.1.0", "eslint-config-prettier": "^3.1.0", "eslint-plugin-import": "^2.14.0", "eslint-plugin-jsx-a11y": "^6.1.2", "eslint-plugin-react": "^7.11.1", - "fs-extra": "^7.0.0", - "husky": "^1.1.1", + "fs-extra": "^7.0.1", + "husky": "^1.1.3", + "hyperapp": "^1.2.9", "jest": "^23.6.0", - "prettier": "^1.14.3", - "rollup": "^0.66.4", + "prettier": "^1.15.1", + "rollup": "^0.67.0", "rollup-plugin-babel": "^4.0.3", - "rollup-plugin-commonjs": "^9.1.8", + "rollup-plugin-commonjs": "^9.2.0", "rollup-plugin-node-resolve": "^3.4.0", "rollup-plugin-uglify": "^6.0.0", - "typescript": "^3.1.1" + "typescript": "^3.1.6" }, "scripts": { "lint": "node tools/lint", diff --git a/src/browser.d.ts b/src/browser.d.ts index a18839d..a366467 100644 --- a/src/browser.d.ts +++ b/src/browser.d.ts @@ -1,30 +1,3 @@ -export as namespace hyperappRender - -import { ActionsType, View } from 'hyperapp' import { renderToString } from './index' export { renderToString } - -export type RenderActions = { - toString(): string -} - -export interface Render { - ( - state: State, - actions: ActionsType, - view: View, - container: Element | null, - ): RenderActions -} - -export interface App { - ( - state: State, - actions: ActionsType, - view: View, - container: Element | null, - ): Actions -} - -export function withRender(app: App): Render diff --git a/src/browser.js b/src/browser.js index 6775505..a366467 100644 --- a/src/browser.js +++ b/src/browser.js @@ -1,18 +1,3 @@ import { renderToString } from './index' export { renderToString } - -export function withRender(nextApp) { - return (initialState, actionsTemplate, view, container) => { - const actions = nextApp( - initialState, - { ...actionsTemplate, getState: () => (state) => state }, - view, - container, - ) - - actions.toString = () => renderToString(view, actions.getState(), actions) - - return actions - } -} diff --git a/src/index.d.ts b/src/index.d.ts index 89d087e..234a6c8 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,4 +1,8 @@ -import { View } from 'hyperapp' +export function escapeHtml(value: any): string + +export function concatClassNames(value: any): string + +export function stringifyStyles(style: any): string export function renderer( view: View, diff --git a/src/index.js b/src/index.js index b8ef664..64ea0bd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,12 @@ +const { isArray } = Array +const { hasOwnProperty } = Object.prototype const styleNameCache = new Map() -const uppercasePattern = /([A-Z])/g +const uppercasePattern = /[A-Z]/g const msPattern = /^ms-/ +// https://www.w3.org/International/questions/qa-escapes#use +const escapeRegExp = /["&'<>]/ + // https://www.w3.org/TR/html/syntax.html#void-elements const voidElements = new Set([ 'area', @@ -20,33 +25,82 @@ const voidElements = new Set([ 'wbr', ]) -const ignoreAttributes = new Set([ - 'key', - 'innerHTML', - '__source', // https://babeljs.io/docs/plugins/transform-react-jsx-source/ -]) +// credits to https://github.com/component/escape-html +export function escapeHtml(value) { + const str = '' + value + if (typeof value === 'number') { + // better performance for safe values + return str + } -// https://www.w3.org/International/questions/qa-escapes#use -const escapeRegExp = /["&'<>]/g -const escapeLookup = new Map([ - ['"', '"'], - ['&', '&'], - ["'", '''], // shorter than "'" and "'" plus supports HTML4 - ['<', '<'], - ['>', '>'], -]) + const match = escapeRegExp.exec(str) + if (!match) { + return str + } -function escaper(match) { - return escapeLookup.get(match) + let { index } = match + let lastIndex = 0 + let out = '' + + for (let escape = ''; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: // " + escape = '"' + break + case 38: // & + escape = '&' + break + case 39: // ' + escape = ''' // shorter than "'" and "'" plus supports HTML4 + break + case 60: // < + escape = '<' + break + case 62: // > + escape = '>' + break + default: + continue + } + + if (lastIndex !== index) { + out += str.substring(lastIndex, index) + } + + lastIndex = index + 1 + out += escape + } + + return lastIndex !== index ? out + str.substring(lastIndex, index) : out } -function escapeHtml(value) { - if (typeof value === 'number') { - // better performance for safe values - return '' + value +// credits to https://github.com/jorgebucaran/classcat +export function concatClassNames(value) { + if (typeof value === 'string' || typeof value === 'number') { + return value || '' + } + + let out = '' + let delimiter = '' + + if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const name = concatClassNames(value[i]) + if (name !== '') { + out += delimiter + name + delimiter = ' ' + } + } + } else { + for (const name in value) { + if (hasOwnProperty.call(value, name) && value[name]) { + out += delimiter + name + delimiter = ' ' + } + } } - return ('' + value).replace(escapeRegExp, escaper) + return out } // "backgroundColor" => "background-color" @@ -67,69 +121,74 @@ function hyphenateStyleName(styleName) { ) } -function stringifyStyles(styles) { +export function stringifyStyles(style) { let out = '' let delimiter = '' - const styleNames = Object.keys(styles) - for (let i = 0; i < styleNames.length; i++) { - const styleName = styleNames[i] - const styleValue = styles[styleName] + for (const name in style) { + if (hasOwnProperty.call(style, name)) { + const value = style[name] - if (styleValue != null) { - if (styleName === 'cssText') { - out += delimiter + styleValue - } else { - out += delimiter + hyphenateStyleName(styleName) + ':' + styleValue + if (value != null) { + if (name === 'cssText') { + out += delimiter + value + } else { + out += delimiter + hyphenateStyleName(name) + ':' + value + } + delimiter = ';' } - - delimiter = ';' } } - return out || null + return out } // https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments -function renderFragment({ nodeName, attributes, children }, stack) { +function renderFragment(name, props, children, stack) { let out = '' let footer = '' - if (nodeName) { - out += '<' + nodeName - const keys = Object.keys(attributes) - - for (let i = 0; i < keys.length; i++) { - const name = keys[i] - let value = attributes[name] - - if (name === 'style' && value && typeof value === 'object') { - value = stringifyStyles(value) - } + if (name) { + out += '<' + name + + for (let prop in props) { + if (hasOwnProperty.call(props, prop)) { + let value = props[prop] + + if ( + value != null && + prop !== 'key' && + prop !== 'innerHTML' && + prop !== '__source' && // babel-plugin-transform-react-jsx-source + !(prop[0] === 'o' && prop[1] === 'n') + ) { + if (prop === 'class' || prop === 'className') { + prop = 'class' + value = concatClassNames(value) || false + } else if (prop === 'style' && value && typeof value === 'object') { + value = stringifyStyles(value) || false + } - if ( - value != null && - value !== false && - typeof value !== 'function' && - !ignoreAttributes.has(name) - ) { - out += ' ' + name + if (value !== false) { + out += ' ' + prop - if (value !== true) { - out += '="' + escapeHtml(value) + '"' + if (value !== true) { + out += '="' + escapeHtml(value) + '"' + } + } } } } - if (voidElements.has(nodeName)) { + if (voidElements.has(name)) { out += '/>' } else { out += '>' - footer = '' + footer = '' } } - const { innerHTML } = attributes + const { innerHTML } = props if (innerHTML != null) { out += innerHTML @@ -165,6 +224,7 @@ export function renderer(view, state, actions) { }, ] let end = false + return (bytes) => { if (end) { return null @@ -187,18 +247,22 @@ export function renderer(view, state, actions) { const node = resolveNode(frame.children[frame.childIndex++], state, actions) if (node != null && typeof node !== 'boolean') { - if (node.pop) { - // array + if (isArray(node)) { stack.push({ childIndex: 0, children: node, footer: '', }) - } else if (node.attributes) { - // element - out += renderFragment(node, stack) + } else if (node.type === 3) { + out += escapeHtml(node.name) + } else if (typeof node === 'object') { + out += renderFragment( + node.name || node.nodeName, + node.props || node.attributes, + node.children, + stack, + ) } else { - // text node out += escapeHtml(node) } } diff --git a/src/node.d.ts b/src/node.d.ts index 148fb69..38525dc 100644 --- a/src/node.d.ts +++ b/src/node.d.ts @@ -1,6 +1,3 @@ -export as namespace hyperappRender - -import { ActionsType, View } from 'hyperapp' import { Readable } from 'stream' import { renderToString } from './index' @@ -11,28 +8,3 @@ export function renderToStream( state?: State, actions?: Actions, ): Readable - -export type RenderActions = { - toString(): string - toStream(): Readable -} - -export interface Render { - ( - state: State, - actions: ActionsType, - view: View, - container: Element | null, - ): RenderActions -} - -export interface App { - ( - state: State, - actions: ActionsType, - view: View, - container: Element | null, - ): Actions -} - -export function withRender(app: App): Render diff --git a/src/node.js b/src/node.js index 0f010d6..67a931a 100644 --- a/src/node.js +++ b/src/node.js @@ -17,19 +17,3 @@ export function renderToStream(view, state, actions) { }, }) } - -export function withRender(nextApp) { - return (initialState, actionsTemplate, view, container) => { - const actions = nextApp( - initialState, - { ...actionsTemplate, getState: () => (state) => state }, - view, - container, - ) - - actions.toString = () => renderToString(view, actions.getState(), actions) - actions.toStream = () => renderToStream(view, actions.getState(), actions) - - return actions - } -} diff --git a/test/browser.test.js b/test/browser.test.js index 0ccbd0e..132a0c4 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -1,70 +1,10 @@ /** @jsx h */ -import { h, app } from 'hyperapp' -import { renderToString, withRender } from '../src/browser' +import { h } from 'hyperapp' +import { renderToString } from '../src/browser' describe('renderToString(view, state, actions)', () => { - it('should render markup', () => { + it('should render simple markup', () => { const html = renderToString(
hello world
) expect(html).toBe('
hello world
') }) }) - -describe('withRender(app)(state, actions, view, container)', () => { - const testState = { count: 0 } - const testActions = { - up: (count = 1) => (state) => ({ count: state.count + count }), - getState: () => (state) => state, - } - const testView = (state) =>

{state.count}

- - it('should create a higher-order app', () => { - const mockApp = jest.fn(() => ({ result: true })) - const renderApp = withRender(mockApp) - expect(renderApp).toBeInstanceOf(Function) - 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).toHaveProperty('result', true) - }) - - it('should not mutate original actions', () => { - withRender(app)(testState, testActions, testView) - expect(testActions).toEqual({ - up: testActions.up, - getState: testActions.getState, - }) - }) - - it('should not mutate store', () => { - const actions = withRender(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 actions = withRender(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 = withRender(app)(testState, testActions, view) - expect(actions.toString()).toBe('

0

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

5

') - }) -}) diff --git a/test/browser.test.tsx b/test/browser.test.tsx new file mode 100644 index 0000000..be53ac0 --- /dev/null +++ b/test/browser.test.tsx @@ -0,0 +1,6 @@ +import { h } from 'hyperapp' +import { renderToString } from '../src/browser' +import { Counter, print } from './index.test' + +print(renderToString(Counter.view, Counter.state, Counter.actions)) +print(renderToString(

hello world

)) diff --git a/test/index.test.js b/test/index.test.js index 6b4c02b..685a693 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -65,12 +65,89 @@ describe('escape', () => { ) }) + it('should escape url', () => { + const html = renderToString(ref) + expect(html).toBe('ref') + }) + it('should not escape innerHTML', () => { const html = renderToString(
'} />) expect(html).toBe('
') }) }) +describe('class', () => { + it('should render a string', () => { + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should not render an empty string', () => { + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should not render an empty object', () => { + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should not render an empty array', () => { + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should not render falsy values', () => { + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should render an array of values', () => { + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should support nested arrays', () => { + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should render an object of class names', () => { + const className = { + foo: true, + bar: true, + quux: false, + baz: true, + } + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should render a mix of array of object values', () => { + const className = [ + 'foo', + 'foo-bar', + { + 'foo-baz': true, + }, + ['fum', 'bam', 'pow'], + ] + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should render className as class', () => { + const html = renderToString(
) + expect(html).toBe('
') + }) + + it('should not throw an exception', () => { + const className = Object.create({ hasOwnProperty: null }) + const html = renderToString(
) + expect(html).toBe('
') + }) +}) + describe('styles', () => { it('should generate markup for style attribute', () => { const styles = { @@ -95,11 +172,14 @@ describe('styles', () => { it('should create vendor-prefixed markup correctly', () => { const styles = { - msTransition: 'none', + WebkitTransition: 'none', MozTransition: 'none', + msTransition: 'none', } const html = renderToString(
) - expect(html).toBe('
') + expect(html).toBe( + '
', + ) }) it('should render style attribute when styles exist', () => { @@ -174,6 +254,12 @@ describe('styles', () => { const html = renderToString(
) expect(html).toBe('
') }) + + it('should not throw an exception', () => { + const style = Object.create({ hasOwnProperty: null }) + const html = renderToString(
) + expect(html).toBe('
') + }) }) describe('attributes', () => { @@ -221,7 +307,7 @@ describe('attributes', () => { expect(html).toBe('
') }) - it('should not render attribute with falsey value', () => { + it('should not render attribute with falsy value', () => { const html = renderToString(
) expect(html).toBe('
') }) @@ -256,6 +342,12 @@ describe('attributes', () => { const html = renderToString(') }) + + it('should not throw an exception', () => { + const attributes = Object.create({ hasOwnProperty: null }) + const html = renderToString(
) + expect(html).toBe('
') + }) }) describe('renderer(view, state, actions)(bytes)', () => { @@ -331,17 +423,65 @@ describe('renderToString(view, state, actions)', () => { }) it('should render content of JSX fragment', () => { - const html = renderToString(h('', {}, [, ])) + const Fragment = '' + const html = renderToString( + + + + , + ) expect(html).toBe('') }) it('should render raw html without extra markup', () => { - const html = renderToString(h('', { innerHTML: 'alert("hello world")' })) - expect(html).toBe('alert("hello world")') + const Fragment = '' + const html = renderToString() + expect(html).toBe(`alert('hello world')`) }) it('should render an array of elements', () => { const html = renderToString([, ]) expect(html).toBe('') }) + + it('should support Hyperapp V2', () => { + const VNode = { + name: 'div', + props: {}, + children: [ + { + name: 'foo bar baz', + props: {}, + children: [], + element: null, + key: null, + type: 3, + }, + ], + element: null, + key: null, + type: 1, + } + const html = renderToString(VNode) + expect(html).toBe('
foo bar baz
') + }) + + it('should render counter', () => { + const testState = { count: 100 } + const testActions = { + up: () => (state) => ({ count: state.count + 1 }), + getState: () => (state) => state, + } + const testView = (state, actions) => { + expect(state).toBe(testState) + expect(actions).toBe(testActions) + return ( + + ) + } + const html = renderToString(testView, testState, testActions) + expect(html).toBe('') + }) }) diff --git a/test/index.test.tsx b/test/index.test.tsx new file mode 100644 index 0000000..10c9873 --- /dev/null +++ b/test/index.test.tsx @@ -0,0 +1,49 @@ +import { h, ActionsType, View } from 'hyperapp' +import { + escapeHtml, + concatClassNames, + stringifyStyles, + renderer, + renderToString, +} from '../src/index' + +export namespace Counter { + export interface State { + count: number + } + + export interface Actions { + up(): State + } + + export const state: State = { + count: 0, + } + + export const actions: ActionsType = { + up: () => (state) => ({ count: state.count + 1 }), + } + + export const view: View = (state, actions) => ( + + ) +} + +export function print(message: string) { + console.log(message) +} + +print(escapeHtml(100500)) +print(escapeHtml('hello world')) + +print(concatClassNames('foo bar baz')) +print(concatClassNames({ foo: true, bar: 'ok', baz: null })) +print(concatClassNames(['foo', ['bar', 0], { baz: 'ok' }, false, null])) + +print(stringifyStyles({ color: 'red', backgroundColor: null })) + +print(renderer(Counter.view, Counter.state, Counter.actions)(Infinity)) +print(renderToString(Counter.view, Counter.state, Counter.actions)) +print(renderToString(

hello world

)) diff --git a/test/node.test.js b/test/node.test.js index 89af2e1..1c13189 100644 --- a/test/node.test.js +++ b/test/node.test.js @@ -1,7 +1,7 @@ /** @jsx h */ -import { h, app } from 'hyperapp' +import { h } from 'hyperapp' import { Readable, Writable } from 'stream' -import { renderToString, renderToStream, withRender } from '../src/node' +import { renderToString, renderToStream } from '../src/node' function readFromStream(stream) { return new Promise((resolve, reject) => { @@ -19,7 +19,7 @@ function readFromStream(stream) { } describe('renderToString(view, state, actions)', () => { - it('should render markup', () => { + it('should render simple markup', () => { const html = renderToString(
hello world
) expect(html).toBe('
hello world
') }) @@ -50,50 +50,3 @@ describe('renderToStream(view, state, actions)', () => { expect(html).toBe('
hello world
') }) }) - -describe('withRender(app)(state, actions, view, container)', () => { - const testState = { count: 0 } - const testActions = { - up: (count = 1) => (state) => ({ count: state.count + count }), - } - const testView = (state) =>

{state.count}

- - it('should create a higher-order app', () => { - const mockApp = jest.fn(() => ({ result: true })) - const renderApp = withRender(mockApp) - expect(renderApp).toBeInstanceOf(Function) - 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).toHaveProperty('result', true) - }) - - it('should not mutate original actions', () => { - withRender(app)(testState, testActions, testView) - expect(testActions).toEqual({ up: testActions.up }) - }) - - it('should render app with current state to string', () => { - const acitons = withRender(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 actions = withRender(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

') - }) -}) diff --git a/test/node.test.tsx b/test/node.test.tsx new file mode 100644 index 0000000..186a0ab --- /dev/null +++ b/test/node.test.tsx @@ -0,0 +1,14 @@ +import { Readable } from 'stream' +import { h } from 'hyperapp' +import { renderToStream, renderToString } from '../src/node' +import { Counter, print } from './index.test' + +function printStream(stream: Readable) { + stream.pipe(process.stdout) +} + +print(renderToString(Counter.view, Counter.state, Counter.actions)) +print(renderToString(

hello world

)) + +printStream(renderToStream(Counter.view, Counter.state, Counter.actions)) +printStream(renderToStream(

hello world

)) diff --git a/test/ts/browser.tsx b/test/ts/browser.tsx deleted file mode 100644 index dbaa13c..0000000 --- a/test/ts/browser.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { h, app, ActionsType, View } from 'hyperapp' -import { withRender, App, Render, renderToString } from '../../src/browser' - -namespace Counter { - export interface State { - count: number - } - - export interface Actions { - down(): State - up(value: number): State - } - - export const state: State = { - count: 0, - } - - export const actions: ActionsType = { - down: () => (state) => ({ count: state.count - 1 }), - up: (value: number) => (state) => ({ - count: state.count + value, - }), - } -} - -const view: View = (state, actions) => ( -
-
{state.count}
- - -
-) - -const counterString = renderToString(view, Counter.state, Counter.actions) - -const counterRender = withRender< - App, - Render ->(app)(Counter.state, Counter.actions, view, document.body) - -console.log(counterString) -console.log(counterRender.toString()) diff --git a/test/ts/index.tsx b/test/ts/index.tsx deleted file mode 100644 index 7b3a11a..0000000 --- a/test/ts/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { h, ActionsType, View } from 'hyperapp' -import { renderer, renderToString } from '../../src/index' - -namespace Counter { - export interface State { - count: number - } - - export interface Actions { - down(): State - up(value: number): State - } - - export const state: State = { - count: 0, - } - - export const actions: ActionsType = { - down: () => (state) => ({ count: state.count - 1 }), - up: (value: number) => (state) => ({ - count: state.count + value, - }), - } -} - -const view: View = (state, actions) => ( -
-
{state.count}
- - -
-) - -const counterRendererString = renderer(view, Counter.state, Counter.actions)(Infinity) - -const counterString = renderToString(view, Counter.state, Counter.actions) - -console.log(counterRendererString) -console.log(counterString) diff --git a/test/ts/node.tsx b/test/ts/node.tsx deleted file mode 100644 index 3622f85..0000000 --- a/test/ts/node.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { h, app, ActionsType, View } from 'hyperapp' -import { renderToStream, withRender, App, Render, renderToString } from '../../src/node' - -namespace Counter { - export interface State { - count: number - } - - export interface Actions { - down(): State - up(value: number): State - } - - export const state: State = { - count: 0, - } - - export const actions: ActionsType = { - down: () => (state) => ({ count: state.count - 1 }), - up: (value: number) => (state) => ({ - count: state.count + value, - }), - } -} - -const view: View = (state, actions) => ( -
-
{state.count}
- - -
-) - -const counterString = renderToString(view, Counter.state, Counter.actions) - -const counterStream = renderToStream(view, Counter.state, Counter.actions) - -const counterRender = withRender< - App, - Render ->(app)(Counter.state, Counter.actions, view, document.body) - -console.log(counterString) -console.log(counterStream) -console.log(counterRender.toString()) -console.log(counterRender.toStream()) diff --git a/test/ts/tsconfig.json b/test/ts/tsconfig.json deleted file mode 100644 index aabdd59..0000000 --- a/test/ts/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "noEmit": true, - "jsx": "react", - "jsxFactory": "h", - "baseUrl": "../..", - "typeRoots": [ - "node_modules/@types/node", - "node_modules/hyperapp" - ], - "moduleResolution": "node", - "paths": { - "render": ["src"] - }, - "types": [ - "node", - "hyperapp", - ], - "target": "es6" - }, - "include": ["*.ts", "*.tsx", "**/*.ts", "**/*.tsx"] -} diff --git a/tools/build.js b/tools/build.js index e28ed2b..40ea0b0 100644 --- a/tools/build.js +++ b/tools/build.js @@ -103,7 +103,7 @@ async function build() { version: pkg.version, main: 'index.js', module: 'module.js', - typings: '../src/browser.d.ts', + types: '../src/browser.d.ts', esnext: '../src/browser.js', }, { spaces: 2 }, @@ -118,7 +118,7 @@ async function build() { version: pkg.version, main: 'index.js', module: 'module.js', - typings: '../src/node.d.ts', + types: '../src/node.d.ts', esnext: '../src/node.js', }, { spaces: 2 }, diff --git a/tools/test.js b/tools/test.js index 62557aa..7ee528f 100644 --- a/tools/test.js +++ b/tools/test.js @@ -1,7 +1,3 @@ -process.on('unhandledRejection', (error) => { - throw error -}) - const cp = require('child_process') const jest = require('jest') @@ -23,7 +19,7 @@ function spawn(command, args) { } async function test() { - await spawn('node_modules/typescript/bin/tsc', ['--project', 'test/ts']) + await spawn('tsc', ['--project', '.']) await jest.run(['--config', JSON.stringify(jestConfig), ...process.argv.slice(2)]) } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5c90ad1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "jsx": "react", + "jsxFactory": "h", + "module": "commonjs", + "noEmit": true, + "strict": true + }, + "include": ["src", "test"] +}