├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── assets ├── esx .png └── jsx-vs-esx.png ├── benchmarks ├── cloned-children │ ├── createElement-app.js │ ├── esx-app.js │ └── index.js ├── component-spread │ ├── createElement-app.js │ ├── esx-app.js │ └── index.js ├── element-spread │ ├── createElement-app.js │ ├── esx-app.js │ └── index.js ├── injected-children-multiple-leaves │ ├── createElement-app.js │ ├── esx-app.js │ └── index.js ├── injected-children │ ├── createElement-app.js │ ├── esx-app.js │ └── index.js ├── small-app │ ├── createElement-app.js │ ├── createElement-server.js │ ├── esx-app.js │ ├── esx-server.js │ └── index.js └── tiny-app │ ├── createElement-app.js │ ├── esx-app.js │ └── index.js ├── browser.js ├── index.js ├── lib ├── attr.js ├── browser.js ├── constants.js ├── escape.js ├── get.js ├── hooks │ ├── compatible.js │ └── stateful.js ├── parse.js ├── plugins.js ├── symbols.js └── validate.js ├── optimize.js ├── package.json ├── readme.md └── test ├── create.test.js └── ssr.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # 0x 36 | .__browserify_string_empty.js 37 | profile-* 38 | 39 | # tap --cov 40 | .nyc_output/ 41 | 42 | # JetBrains IntelliJ IDEA 43 | .idea 44 | *.iml 45 | 46 | # VS Code 47 | .vscode/ 48 | 49 | # lock files 50 | yarn.lock 51 | package-lock.json 52 | 53 | #macOs 54 | 55 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | assets 2 | benchmarks 3 | .nyc_output 4 | coverage 5 | *.0x 6 | .DS_Store -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | node_js: 4 | - 10 5 | - 12 6 | os: 7 | - windows 8 | - linux 9 | - osx 10 | script: 11 | - npm run ci -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at david.clements@nearform.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # esx is an OPEN Open Source Project 2 | 3 | ## What? 4 | 5 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. 6 | 7 | ## Rules 8 | 9 | There are a few basic ground-rules for contributors: 10 | 11 | 1. **No `--force` pushes** on `master` or modifying the Git history in any way after a PR has been merged. 12 | 1. **Non-master branches** ought to be used for ongoing work. 13 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 14 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 15 | 1. Contributors should attempt to adhere to the prevailing code-style. 16 | 17 | ## Releases 18 | 19 | Declaring formal releases remains the prerogative of the project maintainer. 20 | 21 | ## Changes to this arrangement 22 | 23 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. 24 | 25 | ----------------------------------------- 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 David Mark Clements 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/esx .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esxjs/esx/a4a295883d1a5f987f239884eff59f8739ab68f5/assets/esx .png -------------------------------------------------------------------------------- /assets/jsx-vs-esx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esxjs/esx/a4a295883d1a5f987f239884eff59f8739ab68f5/assets/jsx-vs-esx.png -------------------------------------------------------------------------------- /benchmarks/cloned-children/createElement-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const React = require('react') 3 | const { createElement } = require('react') 4 | const Cmp2 = ({ children }) => { 5 | const cloned = React.Children.map(children, (el) => React.cloneElement(el, { new: 'prop' })) 6 | return createElement('p', null, cloned) 7 | } 8 | 9 | const Cmp1 = (props) => { 10 | return createElement( 11 | 'div', 12 | { a: props.a }, 13 | createElement(Cmp2, null, createElement('div', { attr: 'hi' }, props.text)) 14 | ) 15 | } 16 | 17 | const value = 'hia' 18 | 19 | module.exports = () => createElement(Cmp1, { a: value, text: 'hi' }) 20 | -------------------------------------------------------------------------------- /benchmarks/cloned-children/esx-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const esx = require('../..')() 4 | const React = require('react') 5 | const Cmp2 = ({ children }) => { 6 | const cloned = React.Children.map(children, (el) => React.cloneElement(el, { new: 'prop' }, 'hi')) 7 | return esx`

${cloned}

` 8 | } 9 | esx.register({ Cmp2 }) 10 | const Cmp1 = (props) => { 11 | return esx`
${props.text}
` 12 | } 13 | esx.register({ Cmp1 }) 14 | const value = 'hia' 15 | 16 | module.exports = () => esx`` 17 | -------------------------------------------------------------------------------- /benchmarks/cloned-children/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | process.env.NODE_ENV = 'production' 3 | const bench = require('fastbench') 4 | const { createElement } = require('react') 5 | const { renderToString } = require('react-dom/server') 6 | const esx = require('../..')() 7 | const EsxApp = require('./esx-app') 8 | const CreateElementApp = require('./createElement-app') 9 | const assert = require('assert') 10 | 11 | esx.register({ EsxApp }) 12 | 13 | assert.strict.equal(esx.renderToString``, renderToString(createElement(CreateElementApp))) 14 | 15 | const max = 1000 16 | const run = bench([ 17 | function esxRenderToString (cb) { 18 | for (var i = 0; i < max; i++) { 19 | esx.renderToString`` 20 | } 21 | setImmediate(cb) 22 | }, 23 | function reactRenderToString (cb) { 24 | for (var i = 0; i < max; i++) { 25 | renderToString(createElement(CreateElementApp)) 26 | } 27 | setImmediate(cb) 28 | } 29 | ], 1000) 30 | 31 | run(run) 32 | -------------------------------------------------------------------------------- /benchmarks/component-spread/createElement-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { createElement } = require('react') 3 | const esx = require('../..')() 4 | const Cmp2 = ({ a, b, pid, text }) => { 5 | return createElement( 6 | 'p', 7 | { a: a, b: b, id: pid }, 8 | text 9 | ) 10 | } 11 | esx.register({ Cmp2 }) 12 | const Cmp1 = props => { 13 | return createElement( 14 | 'div', 15 | null, 16 | createElement(Cmp2, Object.assign({ a: '1' }, props, { b: '2' })) 17 | ) 18 | } 19 | esx.register({ Cmp1 }) 20 | const value = 'hia' 21 | 22 | module.exports = () => createElement(Cmp1, { pid: value, text: 'hi' }) 23 | 24 | /* JSX: 25 | const esx = require('../..')() 26 | const Cmp2 = ({a, b, pid, text}) => { 27 | return

{text}

28 | } 29 | esx.register({Cmp2}) 30 | const Cmp1 = (props) => { 31 | return
32 | } 33 | esx.register({Cmp1}) 34 | const value = 'hia' 35 | 36 | module.exports = () => 37 | 38 | const { createElement } = require('react') 39 | const Cmp2 = ({text}) => { 40 | return createElement('p', null, text) 41 | } 42 | */ 43 | -------------------------------------------------------------------------------- /benchmarks/component-spread/esx-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const esx = require('../..')() 4 | const Cmp2 = ({ a, b, pid, text }) => { 5 | return esx`

${text}

` 6 | } 7 | esx.register({ Cmp2 }) 8 | const Cmp1 = (props) => { 9 | return esx`
` 10 | } 11 | esx.register({ Cmp1 }) 12 | const value = 'hia' 13 | 14 | module.exports = () => esx`` 15 | -------------------------------------------------------------------------------- /benchmarks/component-spread/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | process.env.NODE_ENV = 'production' 3 | const bench = require('fastbench') 4 | const { createElement } = require('react') 5 | const { renderToString } = require('react-dom/server') 6 | const esx = require('../..')() 7 | const EsxApp = require('./esx-app') 8 | const CreateElementApp = require('./createElement-app') 9 | const assert = require('assert') 10 | 11 | esx.register({ EsxApp }) 12 | 13 | assert.strict.equal(esx.renderToString``, renderToString(createElement(CreateElementApp))) 14 | 15 | const max = 1000 16 | const run = bench([ 17 | function esxRenderToString (cb) { 18 | for (var i = 0; i < max; i++) { 19 | esx.renderToString`` 20 | } 21 | setImmediate(cb) 22 | }, 23 | function reactRenderToString (cb) { 24 | for (var i = 0; i < max; i++) { 25 | renderToString(createElement(CreateElementApp)) 26 | } 27 | setImmediate(cb) 28 | } 29 | ], 1000) 30 | 31 | run(run) 32 | -------------------------------------------------------------------------------- /benchmarks/element-spread/createElement-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { createElement } = require('react') 3 | const esx = require('../..')() 4 | const Cmp2 = props => { 5 | return createElement('p', Object.assign({ a: '1' }, props, { b: '1' })) 6 | } 7 | esx.register({ Cmp2 }) 8 | const Cmp1 = ({ pid, value, text }) => { 9 | return createElement( 10 | 'div', 11 | null, 12 | createElement(Cmp2, { pid: pid, value: value, text: text }) 13 | ) 14 | } 15 | esx.register({ Cmp1 }) 16 | const value = 'hia' 17 | 18 | module.exports = () => createElement(Cmp1, { pid: value, text: 'hi' }) 19 | 20 | /* JSX: 21 | const esx = require('../..')() 22 | const Cmp2 = (props) => { 23 | return

24 | } 25 | esx.register({Cmp2}) 26 | const Cmp1 = ({pid, value, text}) => { 27 | return
28 | } 29 | esx.register({Cmp1}) 30 | const value = 'hia' 31 | 32 | module.exports = () => 33 | */ 34 | -------------------------------------------------------------------------------- /benchmarks/element-spread/esx-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const esx = require('../..')() 4 | const Cmp2 = (props) => { 5 | return esx`

` 6 | } 7 | esx.register({ Cmp2 }) 8 | const Cmp1 = ({ pid, value, text }) => { 9 | return esx`
` 10 | } 11 | esx.register({ Cmp1 }) 12 | const value = 'hia' 13 | 14 | module.exports = () => esx`` 15 | -------------------------------------------------------------------------------- /benchmarks/element-spread/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | process.env.NODE_ENV = 'production' 3 | const bench = require('fastbench') 4 | const { createElement } = require('react') 5 | const { renderToString } = require('react-dom/server') 6 | const esx = require('../..')() 7 | const EsxApp = require('./esx-app') 8 | const CreateElementApp = require('./createElement-app') 9 | const assert = require('assert') 10 | 11 | esx.register({ EsxApp }) 12 | 13 | assert.strict.equal(esx.renderToString``, renderToString(createElement(CreateElementApp))) 14 | 15 | const max = 1000 16 | const run = bench([ 17 | function esxRenderToString (cb) { 18 | for (var i = 0; i < max; i++) { 19 | esx.renderToString`` 20 | } 21 | setImmediate(cb) 22 | }, 23 | function reactRenderToString (cb) { 24 | for (var i = 0; i < max; i++) { 25 | renderToString(createElement(CreateElementApp)) 26 | } 27 | setImmediate(cb) 28 | } 29 | ], 1000) 30 | 31 | run(run) 32 | -------------------------------------------------------------------------------- /benchmarks/injected-children-multiple-leaves/createElement-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createElement } = require('react') 4 | const Cmp4 = ({ children }) => { 5 | return createElement('p', null, children) 6 | } 7 | const Cmp3 = ({ children }) => { 8 | return createElement('p', null, children) 9 | } 10 | const Cmp2 = ({ children }) => { 11 | return createElement('p', null, children) 12 | } 13 | const Cmp1 = (props) => { 14 | return createElement( 15 | 'div', 16 | { a: props.a }, 17 | [ 18 | createElement(Cmp2, null, createElement('div', null, props.text)), 19 | createElement(Cmp3, null, createElement('div', null, props.text)), 20 | createElement(Cmp4, null, createElement('div', null, props.text)) 21 | ] 22 | ) 23 | } 24 | 25 | const value = 'hia' 26 | 27 | module.exports = () => createElement(Cmp1, { a: value, text: 'hi' }) 28 | -------------------------------------------------------------------------------- /benchmarks/injected-children-multiple-leaves/esx-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const esx = require('../..')() 4 | const Cmp4 = ({ children }) => { 5 | return esx`

${children}

` 6 | } 7 | const Cmp3 = ({ children }) => { 8 | return esx`

${children}

` 9 | } 10 | const Cmp2 = ({ children }) => { 11 | return esx`

${children}

` 12 | } 13 | esx.register({ Cmp2, Cmp3, Cmp4 }) 14 | const Cmp1 = (props) => { 15 | return esx` 16 |
17 |
${props.text}
18 |
${props.text}
19 |
${props.text}
20 |
21 | ` 22 | } 23 | esx.register({ Cmp1 }) 24 | const value = 'hia' 25 | 26 | module.exports = () => esx`` 27 | -------------------------------------------------------------------------------- /benchmarks/injected-children-multiple-leaves/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | process.env.NODE_ENV = 'production' 3 | const bench = require('fastbench') 4 | const { createElement } = require('react') 5 | const { renderToString } = require('react-dom/server') 6 | const esx = require('../..')() 7 | const EsxApp = require('./esx-app') 8 | const CreateElementApp = require('./createElement-app') 9 | const assert = require('assert') 10 | 11 | esx.register({ EsxApp }) 12 | 13 | assert.strict.equal(esx.renderToString``, renderToString(createElement(CreateElementApp))) 14 | 15 | const max = 1000 16 | const run = bench([ 17 | function esxRenderToString (cb) { 18 | for (var i = 0; i < max; i++) { 19 | esx.renderToString`` 20 | } 21 | setImmediate(cb) 22 | }, 23 | function reactRenderToString (cb) { 24 | for (var i = 0; i < max; i++) { 25 | renderToString(createElement(CreateElementApp)) 26 | } 27 | setImmediate(cb) 28 | } 29 | ], 1000) 30 | 31 | run(run) 32 | -------------------------------------------------------------------------------- /benchmarks/injected-children/createElement-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createElement } = require('react') 4 | const Cmp2 = ({ children }) => { 5 | return createElement('p', null, children) 6 | } 7 | 8 | const Cmp1 = (props) => { 9 | return createElement( 10 | 'div', 11 | { a: props.a }, 12 | createElement(Cmp2, null, createElement('div', null, props.text)) 13 | ) 14 | } 15 | 16 | const value = 'hia' 17 | 18 | module.exports = () => createElement(Cmp1, { a: value, text: 'hi' }) 19 | -------------------------------------------------------------------------------- /benchmarks/injected-children/esx-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const esx = require('../..')() 4 | const Cmp2 = ({ children }) => { 5 | return esx`

${children}

` 6 | } 7 | esx.register({ Cmp2 }) 8 | const Cmp1 = (props) => { 9 | return esx`
${props.text}
` 10 | } 11 | esx.register({ Cmp1 }) 12 | const value = 'hia' 13 | 14 | module.exports = () => esx`` 15 | -------------------------------------------------------------------------------- /benchmarks/injected-children/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | process.env.NODE_ENV = 'production' 3 | const bench = require('fastbench') 4 | const { createElement } = require('react') 5 | const { renderToString } = require('react-dom/server') 6 | const esx = require('../..')() 7 | const EsxApp = require('./esx-app') 8 | const CreateElementApp = require('./createElement-app') 9 | const assert = require('assert') 10 | 11 | esx.register({ EsxApp }) 12 | 13 | assert.strict.equal(esx.renderToString``, renderToString(createElement(CreateElementApp))) 14 | 15 | const max = 1000 16 | const run = bench([ 17 | function esxRenderToString (cb) { 18 | for (var i = 0; i < max; i++) { 19 | esx.renderToString`` 20 | } 21 | setImmediate(cb) 22 | }, 23 | function reactRenderToString (cb) { 24 | for (var i = 0; i < max; i++) { 25 | renderToString(createElement(CreateElementApp)) 26 | } 27 | setImmediate(cb) 28 | } 29 | ], 1000) 30 | 31 | run(run) 32 | -------------------------------------------------------------------------------- /benchmarks/small-app/createElement-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createElement } = require('react') 4 | const Cmp2 = ({ text }) => { 5 | return createElement( 6 | 'div', 7 | null, 8 | createElement( 9 | 'aside', 10 | null, 11 | ' foo ' 12 | ), 13 | createElement( 14 | 'main', 15 | null, 16 | createElement( 17 | 'div', 18 | null, 19 | createElement( 20 | 'h2', 21 | null, 22 | ' something ' 23 | ), 24 | createElement( 25 | 'div', 26 | null, 27 | [ 28 | createElement( 29 | 'a', 30 | { href: 'http://www.example.com' }, 31 | 'a link ' 32 | ), 33 | createElement( 34 | 'span', 35 | null, 36 | createElement('em', null, 'some'), 37 | ' text' 38 | ) 39 | ] 40 | ), 41 | createElement( 42 | 'p', 43 | null, 44 | text 45 | ), 46 | createElement( 47 | 'p', 48 | null, 49 | ' more text, ', 50 | createElement( 51 | 'small', 52 | null, 53 | ' small print ' 54 | ) 55 | ) 56 | ) 57 | ) 58 | ) 59 | } 60 | 61 | const Cmp1 = (props) => { 62 | return createElement( 63 | 'div', 64 | { a: props.a }, 65 | [ 66 | createElement(Cmp2, { text: props.text }), 67 | createElement(Cmp2, { text: props.text }), 68 | createElement(Cmp2, { text: props.text }), 69 | createElement(Cmp2, { text: props.text }), 70 | createElement(Cmp2, { text: props.text }), 71 | createElement(Cmp2, { text: props.text }), 72 | createElement(Cmp2, { text: props.text }), 73 | createElement(Cmp2, { text: props.text }), 74 | createElement(Cmp2, { text: props.text }), 75 | createElement(Cmp2, { text: props.text }) 76 | ] 77 | ) 78 | } 79 | 80 | const value = 'hia' 81 | 82 | module.exports = () => createElement(Cmp1, { a: value, text: 'hi' }) 83 | -------------------------------------------------------------------------------- /benchmarks/small-app/createElement-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { renderToString } = require('react-dom/server') 3 | const { createElement } = require('react') 4 | const CreateElementApp = require('./createElement-app') 5 | 6 | const { createServer } = require('http') 7 | 8 | createServer((req, res) => { 9 | res.end(renderToString(createElement(CreateElementApp))) 10 | }).listen(3000) 11 | -------------------------------------------------------------------------------- /benchmarks/small-app/esx-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const esx = require('../..')() 4 | const Cmp2 = ({ text }) => { 5 | return esx` 6 |
7 | 8 |
9 |
10 |

something

11 |
12 | a link 13 | some text 14 |
15 |

${text}

16 |

more text, small print

17 |
18 |
19 |
20 | ` 21 | } 22 | esx.register({ Cmp2 }) 23 | const Cmp1 = (props) => { 24 | return esx` 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | ` 38 | } 39 | esx.register({ Cmp1 }) 40 | const value = 'hia' 41 | 42 | module.exports = () => esx`` 43 | -------------------------------------------------------------------------------- /benchmarks/small-app/esx-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const EsxApp = require('./esx-app') 3 | const esx = require('../..')({ EsxApp }) 4 | const { createServer } = require('http') 5 | 6 | createServer((req, res) => { 7 | res.end(esx.renderToString``) 8 | }).listen(3000) 9 | -------------------------------------------------------------------------------- /benchmarks/small-app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | process.env.NODE_ENV = 'production' 3 | const bench = require('fastbench') 4 | const { createElement } = require('react') 5 | const { renderToString } = require('react-dom/server') 6 | const esx = require('../..')() 7 | const EsxApp = require('./esx-app') 8 | const CreateElementApp = require('./createElement-app') 9 | const assert = require('assert') 10 | 11 | esx.register({ EsxApp }) 12 | 13 | assert.strict.equal(esx.renderToString``, renderToString(createElement(CreateElementApp))) 14 | 15 | const max = 1000 16 | const run = bench([ 17 | function esxRenderToStringAsTag (cb) { 18 | for (var i = 0; i < max; i++) { 19 | esx.renderToString`` 20 | } 21 | setImmediate(cb) 22 | }, 23 | function esxRenderToStringPassedElement (cb) { 24 | for (var i = 0; i < max; i++) { 25 | const element = esx`` 26 | esx.renderToString(element) 27 | } 28 | setImmediate(cb) 29 | }, 30 | function reactRenderToString (cb) { 31 | for (var i = 0; i < max; i++) { 32 | renderToString(createElement(CreateElementApp)) 33 | } 34 | setImmediate(cb) 35 | } 36 | ], 100) 37 | 38 | run(run) 39 | -------------------------------------------------------------------------------- /benchmarks/tiny-app/createElement-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createElement } = require('react') 4 | const Cmp2 = ({ text }) => { 5 | return createElement('p', null, text) 6 | } 7 | 8 | const Cmp1 = (props) => { 9 | return createElement('div', { a: props.a }, createElement(Cmp2, { text: props.text })) 10 | } 11 | 12 | const value = 'hia' 13 | 14 | module.exports = () => createElement(Cmp1, { a: value, text: 'hi' }) 15 | -------------------------------------------------------------------------------- /benchmarks/tiny-app/esx-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const esx = require('../..')() 4 | const Cmp2 = ({ text }) => { 5 | return esx`

${text}

` 6 | } 7 | esx.register({ Cmp2 }) 8 | const Cmp1 = (props) => { 9 | return esx`
` 10 | } 11 | esx.register({ Cmp1 }) 12 | const value = 'hia' 13 | 14 | module.exports = () => esx`` 15 | -------------------------------------------------------------------------------- /benchmarks/tiny-app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | process.env.NODE_ENV = 'production' 3 | const bench = require('fastbench') 4 | const { createElement } = require('react') 5 | const { renderToString } = require('react-dom/server') 6 | const esx = require('../..')() 7 | const EsxApp = require('./esx-app') 8 | const CreateElementApp = require('./createElement-app') 9 | const assert = require('assert') 10 | 11 | esx.register({ EsxApp }) 12 | 13 | assert.strict.equal(esx.renderToString``, renderToString(createElement(CreateElementApp))) 14 | 15 | const max = 1000 16 | const run = bench([ 17 | function esxRenderToString (cb) { 18 | for (var i = 0; i < max; i++) { 19 | esx.renderToString`` 20 | } 21 | setImmediate(cb) 22 | }, 23 | function reactRenderToString (cb) { 24 | for (var i = 0; i < max; i++) { 25 | renderToString(createElement(CreateElementApp)) 26 | } 27 | setImmediate(cb) 28 | } 29 | ], 1000) 30 | 31 | run(run) 32 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { createElement, Fragment } = require('react') 3 | const parse = require('./lib/parse') 4 | const { 5 | validate, validateOne, supported 6 | } = require('./lib/validate') 7 | const { marker, ties } = require('./lib/symbols') 8 | const plugins = require('./lib/plugins') 9 | 10 | function esx (components = {}) { 11 | validate(components) 12 | components = Object.assign({}, components) 13 | components[ties] = {} 14 | const cache = new WeakMap() 15 | const render = (strings, ...values) => { 16 | const key = strings 17 | const state = cache.has(key) 18 | ? cache.get(key) 19 | : cache.set(key, parse(components, strings, values)).get(key) 20 | const { tree } = state 21 | var i = tree.length 22 | var root = null 23 | const map = {} 24 | while (i--) { 25 | const [, props, childMap, meta] = tree[i] 26 | const { isComponent, name } = meta 27 | const tag = isComponent ? components[meta.name] || Fragment : name 28 | const children = new Array(childMap.length) 29 | const { dynAttrs, dynChildren, spread } = meta 30 | const spreads = spread && Object.keys(spread).map(Number) 31 | for (var c in childMap) { 32 | if (typeof childMap[c] === 'number') { 33 | children[c] = map[childMap[c]] 34 | } else { 35 | children[c] = childMap[c] 36 | } 37 | } 38 | if (spread) { 39 | for (var sp in spread) { 40 | const keys = Object.keys(values[sp]) 41 | for (var k in keys) { 42 | if (spread[sp].after.indexOf(keys[k]) > -1) continue 43 | props[keys[k]] = values[sp][keys[k]] 44 | } 45 | } 46 | } 47 | if (dynAttrs) { 48 | for (var p in dynAttrs) { 49 | const overridden = spread && spreads.filter(n => { 50 | return dynAttrs[p] < n 51 | }).some((n) => { 52 | return p in values[n] && spread[n].before.indexOf(p) > -1 53 | }) 54 | if (overridden) continue 55 | if (props[p] !== marker) continue // this means later static property, should override 56 | props[p] = values[dynAttrs[p]] 57 | } 58 | } 59 | if (dynChildren) { 60 | for (var n in dynChildren) { 61 | children[n] = values[dynChildren[n]] 62 | } 63 | } 64 | const reactChildren = children.length === 0 ? (props.children || null) : (children.length === 1 ? children[0] : children) 65 | root = reactChildren === null ? createElement(tag, props) : createElement(tag, props, reactChildren) 66 | map[i] = root 67 | } 68 | return root 69 | } 70 | render.createElement = createElement 71 | const merge = (additionalComponents) => { 72 | Object.assign(components, additionalComponents) 73 | } 74 | const set = (key, component) => { 75 | supported(key, component) 76 | components[key] = component 77 | } 78 | render.register = (additionalComponents) => { 79 | validate(additionalComponents) 80 | merge(additionalComponents) 81 | } 82 | render.register.one = (key, component) => { 83 | validateOne(key, component) 84 | set(key, component) 85 | } 86 | render.register.lax = (cmps) => { 87 | for (var k in cmps) supported(k, cmps[k]) 88 | merge(cmps) 89 | } 90 | render.register.one.lax = set 91 | return render 92 | } 93 | 94 | esx.plugins = plugins 95 | esx.plugins.post = () => { 96 | throw Error('Post Plugins can only be used server side') 97 | } 98 | 99 | module.exports = esx 100 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const debug = require('debug')('esx') 3 | const { createElement, Fragment } = tryToLoad('react') 4 | const { 5 | renderToStaticMarkup, 6 | renderToString: reactRenderToString 7 | } = tryToLoad('react-dom/server') 8 | const escapeHtml = require('./lib/escape') 9 | const parse = require('./lib/parse') 10 | const { 11 | validate, validateOne, supported 12 | } = require('./lib/validate') 13 | const attr = require('./lib/attr') 14 | const plugins = require('./lib/plugins') 15 | 16 | var hooks = require('./lib/hooks/compatible') 17 | const { 18 | REACT_PROVIDER_TYPE, 19 | REACT_CONSUMER_TYPE, 20 | REACT_MEMO_TYPE, 21 | REACT_ELEMENT_TYPE, 22 | REACT_FORWARD_REF_TYPE, 23 | VOID_ELEMENTS 24 | } = require('./lib/constants') 25 | const { 26 | ns, marker, skip, provider, esxValues, parent, owner, template, ties, runners 27 | } = require('./lib/symbols') 28 | const { pre, post } = plugins[runners]() 29 | // singleton state for ssr 30 | const cache = new WeakMap() 31 | var ssr = false 32 | var ssrReactRootAdded = false 33 | var currentValues = null 34 | var lastChildProp = null 35 | function selected (val, wasSelected) { 36 | if (Array.isArray(selected.defaultValue)) { 37 | return selected.defaultValue.includes(val) ? ' selected=""' : '' 38 | } 39 | return selected.defaultValue != null 40 | ? (val === selected.defaultValue ? ' selected=""' : '') 41 | : (wasSelected ? ' selected=""' : '') 42 | } 43 | 44 | selected.defaultValue = null 45 | 46 | selected.register = function (val) { 47 | selected.defaultValue = Array.isArray(val) ? val : escapeHtml(val) 48 | return '' 49 | } 50 | selected.deregister = function () { 51 | selected.defaultValue = null 52 | return '' 53 | } 54 | 55 | const elementToMarkup = (el) => { 56 | if (ssrReactRootAdded === false) { 57 | return reactRenderToString(el) 58 | } 59 | return renderToStaticMarkup(el) 60 | } 61 | const postprocess = post 62 | 63 | const spread = (ix, [tag, props, childMap, meta], values, strBefore, strAfter = '') => { 64 | const object = values[ix] 65 | const keys = Object.keys(object) 66 | const { spread, spreadIndices } = meta 67 | const spreadCount = spreadIndices.length 68 | var priorSpreadKeys = spreadCount > 0 && new Set() 69 | var result = '' 70 | var dirtyBefore = false 71 | for (var si = 0; si < spreadCount; si++) { 72 | const sIx = spreadIndices[si] 73 | if (sIx >= ix) break 74 | priorSpreadKeys.add(...spread[sIx].dynamic) 75 | } 76 | spread[ix].dynamic = keys 77 | for (var k in keys) { 78 | const key = keys[k] 79 | if (attr.reserved(key)) continue 80 | if (spread[ix].after.indexOf(key) > -1) continue 81 | if (tag === 'select' && key === 'defaultValue') { 82 | selected.register(object[key]) 83 | continue 84 | } 85 | const keyIsDSIH = key === 'dangerouslySetInnerHTML' 86 | if (keyIsDSIH || key === 'children' || (tag === 'textarea' && key === 'defaultValue')) { 87 | const forbiddenKey = keyIsDSIH ? 'children' : 'dangerouslySetInnerHTML' 88 | const collision = spread[ix].before.indexOf(forbiddenKey) > -1 || 89 | spread[ix].after.indexOf(forbiddenKey) > -1 || 90 | priorSpreadKeys.has(forbiddenKey) || 91 | forbiddenKey in object || 92 | (keyIsDSIH && childMap.length > 0) 93 | if (collision) { 94 | throw SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.') 95 | } 96 | const overrideChildren = spread[ix].before.indexOf(key) > -1 || 97 | (priorSpreadKeys && priorSpreadKeys.has(key)) || 98 | childMap.length === 0 99 | 100 | if (overrideChildren) { 101 | const rootAdded = ssrReactRootAdded 102 | ssrReactRootAdded = true 103 | childMap[0] = (key === 'children' || 104 | (tag === 'textarea' && key === 'defaultValue')) 105 | ? inject(object[key]) 106 | : object[key].__html 107 | ssrReactRootAdded = rootAdded 108 | } 109 | continue 110 | } 111 | const val = typeof object[key] === 'number' ? object[key] + '' : object[key] 112 | const mappedKey = attr.mapping(key, tag) 113 | if (mappedKey.length === 0) continue 114 | if (mappedKey === 'style') result += style(val) 115 | else result += attribute(val, mappedKey, key) 116 | if (spread[ix].before.indexOf(key) > -1) { 117 | dirtyBefore = true 118 | continue 119 | } 120 | if (priorSpreadKeys && priorSpreadKeys.has(key)) { 121 | dirtyBefore = true 122 | } 123 | } 124 | if (dirtyBefore) { 125 | strBefore = '' 126 | for (var i = 0; i < spread[ix].before.length; i++) { 127 | const key = spread[ix].before[i] 128 | if (keys.indexOf(key) > -1) continue 129 | const mappedKey = attr.mapping(key, tag) 130 | if (mappedKey.length === 0) continue 131 | if (props[key] === marker) { 132 | strBefore += attribute(values[meta.dynAttrs[key]], mappedKey, key) 133 | } else { 134 | strBefore += attribute(props[key], mappedKey, key) 135 | } 136 | } 137 | } 138 | if (strAfter.length > 0 && strAfter[0] !== ' ') strAfter = ' ' + strAfter 139 | const out = `${strBefore}${result}${strAfter}` 140 | return (out.length === 0 || out[0] === ' ') ? out : ' ' + out 141 | } 142 | 143 | const attribute = (val, attrKey, propKey, replace = null) => { 144 | if (replace !== null && propKey in replace) return replace[propKey] 145 | if (val == null) return '' 146 | const type = typeof val 147 | if (type === 'function' || type === 'symbol') return '' 148 | if (type === 'boolean' && attrKey.length > 0) { 149 | const serialized = attr.bool(propKey) ? attr.serializeBool(propKey, val) : '' 150 | val = serialized.length > 0 ? ` ${attrKey}=${serialized}` : '' 151 | } else { 152 | if (attr.bool(propKey, true)) val = '' 153 | val = ` ${attrKey}="${escapeHtml(val)}"` 154 | } 155 | return val 156 | } 157 | 158 | const injectObject = (val) => { 159 | if (val.$$typeof === REACT_ELEMENT_TYPE) { 160 | // if the element does not have an ns value, it may have been cloned in which 161 | // case we've slipped a method through the cloning process that pulls the esx 162 | // state in from the old element 163 | const state = val[ns] || (val._owner && val._owner[owner] && val._owner()) 164 | 165 | if (!state) { 166 | return elementToMarkup(val) 167 | } 168 | return state.tmpl(state.values, state.extra, state.replace) 169 | } 170 | debug('Objects are not valid as an elements child', val) 171 | return '' 172 | } 173 | const injectArray = (val) => { 174 | const stack = val.slice() 175 | var priorItemWasString = false 176 | var result = '' 177 | while (stack.length > 0) { 178 | const item = stack.shift() 179 | if (item == null) continue 180 | if (Array.isArray(item)) { 181 | stack.unshift(...item) 182 | continue 183 | } 184 | const type = typeof item 185 | if (type === 'function' || type === 'symbol') continue 186 | if (type === 'object') { 187 | result += injectObject(item) 188 | priorItemWasString = false 189 | } 190 | if (type === 'string' || type === 'number') { 191 | if (priorItemWasString) result += '' 192 | result += escapeHtml(item) 193 | priorItemWasString = true 194 | } 195 | } 196 | return result 197 | } 198 | const inject = (val) => { 199 | if (val == null) return '' 200 | const type = typeof val 201 | if (type === 'string') return escapeHtml(val) 202 | if (type === 'function' || type === 'symbol') return '' 203 | if (type !== 'object') return val 204 | if (Array.isArray(val)) return injectArray(val) 205 | return injectObject(val) 206 | } 207 | 208 | function EsxElementUnopt (item) { 209 | this.$$typeof = REACT_ELEMENT_TYPE 210 | const [type, props] = item 211 | this.type = type 212 | this.props = props 213 | this.key = props.key || null 214 | this.ref = props.ref || null 215 | this.esxUnopt = true 216 | } 217 | 218 | function EsxElement (item, tmpl, values, replace = null) { 219 | this.$$typeof = REACT_ELEMENT_TYPE 220 | const [type, props] = item 221 | this.type = type 222 | this.props = props 223 | this.key = props.key || null 224 | this.ref = props.ref || null 225 | this[ns] = { tmpl, values, item, replace } 226 | } 227 | const ownerDesc = { 228 | get: function _owner () { 229 | var lastProps = this.props 230 | var state = this[ns] 231 | var type = this.type 232 | var children = lastChildProp 233 | 234 | const propagate = typeof type === 'string' ? function propagate () { 235 | var extra = '' 236 | var replace = null 237 | var rewrite = '' 238 | var same = true 239 | if (this.props.children !== children) { 240 | // cloneElement is overriding children, bail: 241 | return null 242 | } 243 | for (var k in this.props) { 244 | const mappedKey = attr.mapping(k, type) 245 | if (mappedKey === 'children' || !mappedKey) continue 246 | if (lastProps[k] !== this.props[k]) { 247 | same = false 248 | if (k in lastProps) { 249 | if (!rewrite) rewrite = state.tmpl.body 250 | if (replace === null) replace = {} 251 | replace[mappedKey] = attribute(this.props[k], mappedKey, k) 252 | rewrite = rewrite.replace(attribute(lastProps[k], mappedKey, k), replace[mappedKey]) 253 | } else { 254 | extra += attribute(this.props[k], mappedKey, k) 255 | } 256 | } 257 | } 258 | if (same === false) { 259 | state.extra = extra 260 | state.replace = replace 261 | if (rewrite) state.tmpl = compileTmpl(rewrite, state.tmpl.state) 262 | } 263 | const result = state 264 | // if we don't clear state from scope, memory will grow infinitely 265 | // that means the propagate function can only be called once, the second 266 | // time it will return null (which will force a deopt to standard react rendering) 267 | lastProps = state = type = children = null 268 | return result 269 | } : function propogate () { 270 | const el = renderComponent([this.type, this.props, {}, {}]) 271 | // if we don't clear state from scope, memory will grow infinitely 272 | // that means the propagate function can only be called once 273 | lastProps = state = type = children = null 274 | return el[ns] 275 | } 276 | propagate[owner] = true 277 | return propagate 278 | } 279 | } 280 | Object.defineProperty(EsxElement.prototype, '_owner', ownerDesc) 281 | 282 | function esx (components = {}) { 283 | validate(components) 284 | components = Object.assign({}, components) 285 | components[ties] = {} 286 | 287 | const raw = (strings, ...values) => { 288 | const key = strings 289 | ;[strings, values] = pre(strings, values) 290 | const state = cache.has(key) 291 | ? cache.get(key) 292 | : cache.set(key, parse(components, strings, values)).get(key) 293 | const { tree } = state 294 | var i = tree.length 295 | var root = null 296 | const map = {} 297 | while (i--) { 298 | const [, props, childMap, meta] = tree[i] 299 | const { isComponent, name } = meta 300 | const tag = isComponent ? components[meta.name] || Fragment : name 301 | const children = new Array(childMap.length) 302 | const { dynAttrs, dynChildren, spread } = meta 303 | const spreads = spread && Object.keys(spread).map(Number) 304 | for (var c in childMap) { 305 | if (typeof childMap[c] === 'number') { 306 | children[c] = map[childMap[c]] 307 | } else { 308 | children[c] = childMap[c] || null 309 | } 310 | } 311 | if (spread) { 312 | for (var sp in spread) { 313 | const keys = Object.keys(values[sp]) 314 | for (var k in keys) { 315 | if (spread[sp].after.indexOf(keys[k]) > -1) continue 316 | props[keys[k]] = values[sp][keys[k]] 317 | } 318 | } 319 | } 320 | for (var p in dynAttrs) { 321 | const overridden = spread && spreads.filter(n => { 322 | return dynAttrs[p] < n 323 | }).some((n) => { 324 | return p in values[n] && spread[n].before.indexOf(p) > -1 325 | }) 326 | if (overridden) continue 327 | if (props[p] !== marker) continue // this means later static property, should override 328 | props[p] = values[dynAttrs[p]] 329 | } 330 | for (var n in dynChildren) { 331 | children[n] = values[dynChildren[n]] 332 | } 333 | const reactChildren = children.length === 0 ? (props.children || null) : (children.length === 1 ? children[0] : children) 334 | root = reactChildren === null ? createElement(tag, props) : createElement(tag, props, reactChildren) 335 | map[i] = root 336 | } 337 | if (root) { 338 | try { // production scenario -- faster 339 | root[template] = { strings, values } 340 | } catch (e) { // development scenario (work around frozen objects) 341 | root = { 342 | [template]: { strings, values }, 343 | __proto__: root 344 | } 345 | } 346 | } 347 | return root 348 | } 349 | const render = function (strings, ...values) { 350 | if (ssr === false) return raw(strings, ...values) 351 | const key = strings 352 | ;[strings, values] = pre(strings, values) 353 | currentValues = values 354 | const state = cache.has(key) 355 | ? cache.get(key) 356 | : cache.set(key, parse(components, strings, values)).get(key) 357 | const { tree } = state 358 | const item = tree[0] 359 | if (item === undefined) return null 360 | const meta = item[3] 361 | const { recompile } = meta 362 | meta.values = values 363 | tree[esxValues] = values 364 | const { tmpl } = loadTmpl(state, values, recompile) 365 | if (recompile) meta.recompile = false 366 | const el = new EsxElement(item, tmpl, values) 367 | if (!(parent in el.props)) el.props[parent] = item 368 | return el 369 | } 370 | 371 | function renderToString (strings, ...args) { 372 | if (strings[template]) { 373 | args = strings[template].values 374 | strings = strings[template].strings 375 | } else if ('$$typeof' in strings) { 376 | throw Error('esx.renderToString is either a tag function or can accept esx elements. But not plain React elements.') 377 | } 378 | hooks.install() 379 | ssr = true 380 | currentValues = null 381 | ssrReactRootAdded = false 382 | const result = render(strings, ...args) 383 | if (result === null) return '' 384 | const rootIsComponent = typeof result.type !== 'string' 385 | const treeRoot = rootIsComponent ? cache.get(strings).tree[0] : null 386 | if (treeRoot !== null) hooks.rendering(treeRoot) 387 | const { tmpl, values, extra, replace } = result[ns] 388 | const output = tmpl(values, extra, replace) 389 | currentValues = null 390 | ssrReactRootAdded = false 391 | ssr = false 392 | if (treeRoot !== null) hooks.after(treeRoot) 393 | hooks.uninstall() 394 | return output 395 | } 396 | const set = (key, component) => { 397 | const current = components[key] 398 | if (current === component) return render 399 | supported(key, component) 400 | components[key] = component 401 | const lastType = typeof current 402 | const type = typeof component 403 | const recompile = lastType !== type 404 | const references = components[ties][key] 405 | if (references) { 406 | for (var i = 0; i < references.length; i++) { 407 | const item = references[i] 408 | item[0] = components[key] // update the tag to the new component 409 | prepare(item) 410 | if (recompile) { 411 | const root = item[3].tree[0] 412 | root[3].recompile = true 413 | } 414 | } 415 | } 416 | return render 417 | } 418 | render.register = (additionalComponents) => { 419 | for (var key in additionalComponents) { 420 | const component = additionalComponents[key] 421 | validateOne(key, component) 422 | set(key, component) 423 | } 424 | return render 425 | } 426 | render.register.one = (key, component) => { 427 | validateOne(key, component) 428 | return set(key, component) 429 | } 430 | render.register.lax = (additionalComponents) => { 431 | for (var key in additionalComponents) { 432 | const component = additionalComponents[key] 433 | set(key, component) 434 | } 435 | return render 436 | } 437 | render.register.one.lax = set 438 | render._r = render.register.one.lax 439 | render.renderToString = render.ssr = renderToString 440 | render.createElement = createElement 441 | return render 442 | } 443 | 444 | function renderComponent (item, values) { 445 | hooks.rendering(item) 446 | const [tag, props, childMap, meta] = item 447 | try { props[parent] = item } catch (e) {} // try/catch is for dev scenarios where object is frozen 448 | if (tag.$$typeof === REACT_PROVIDER_TYPE) { 449 | for (const p in meta.dynAttrs) { 450 | if (p === 'children') { 451 | meta.dynChildren[0] = meta.dynAttrs[p] 452 | childMap[0] = marker 453 | } else { 454 | props[p] = currentValues[meta.dynAttrs[p]] 455 | } 456 | } 457 | const result = resolveChildren(childMap, meta.dynChildren, meta.tree, item) 458 | if (meta.hooksUsed) hooks.after(item) 459 | return result 460 | } 461 | 462 | const { dynAttrs, dynChildren } = meta 463 | if (values) { 464 | for (const p in dynAttrs) { 465 | if (p[0] === '…') { 466 | const ix = dynAttrs[p] 467 | for (var sp in values[ix]) { 468 | if (meta.spread[ix].after.indexOf(sp) > -1) continue 469 | if (values[ix].hasOwnProperty(sp)) { 470 | if (sp === 'children') { 471 | Object.defineProperty(props, 'children', { 472 | value: values[ix][sp] 473 | }) 474 | } else { 475 | props[sp] = values[ix][sp] 476 | } 477 | } 478 | } 479 | } else { 480 | props[p] = values[dynAttrs[p]] 481 | } 482 | if (p === 'ref' || p === 'key') { 483 | values[dynAttrs[p]] = skip 484 | } 485 | } 486 | } 487 | 488 | const context = tag.contextType 489 | ? (tag.contextType[provider] 490 | ? tag.contextType[provider][1].value 491 | : tag.contextType._currentValue2 492 | ) : {} 493 | 494 | if (tag.$$typeof === REACT_CONSUMER_TYPE) { 495 | const tagContext = tag._context 496 | const context = tagContext[provider] 497 | ? tagContext[provider][1].value 498 | : tagContext._currentValue2 499 | const props = Object.assign({ children: values[dynChildren[0]] }, item[1]) 500 | const result = props.children(context) 501 | if (meta.hooksUsed) hooks.after(item) 502 | return result 503 | } 504 | if (tag.$$typeof === REACT_MEMO_TYPE) { 505 | const result = tag.type(props, context) 506 | if (meta.hooksUsed) hooks.after(item) 507 | return result 508 | } 509 | if (tag.$$typeof === REACT_FORWARD_REF_TYPE) { 510 | const result = tag.render(props, props.ref) 511 | if (meta.hooksUsed) hooks.after(item) 512 | return result 513 | } 514 | if (tag.prototype && tag.prototype.render) { 515 | const Tag = tag 516 | const element = new Tag(props, context) 517 | if ('componentWillMount' in element) element.componentWillMount() 518 | if ('UNSAFE_componentWillMount' in element) element.UNSAFE_componentWillMount() 519 | const result = element.render() 520 | if (meta.hooksUsed) hooks.after(item) 521 | return result 522 | } 523 | const result = tag(props, context) 524 | if (meta.hooksUsed) hooks.after(item) 525 | return result 526 | } 527 | 528 | function childPropsGetter () { 529 | if (!ssr) return null 530 | const item = this[parent] 531 | const [ , , childMap, meta ] = item 532 | const { dynChildren } = meta 533 | return (lastChildProp = resolveChildren(childMap, dynChildren, meta.tree, item)) 534 | } 535 | 536 | function prepare (item) { 537 | const [ tag, , , meta ] = item 538 | if (meta.isComponent && typeof tag.defaultProps === 'object') { 539 | item[1] = Object.assign({}, tag.defaultProps, meta.attributes) 540 | } 541 | const props = item[1] 542 | if (!('children' in props)) { 543 | Object.defineProperty(props, 'children', { get: childPropsGetter, enumerable: true, configurable: true }) 544 | } 545 | meta.isProvider = tag.$$typeof === REACT_PROVIDER_TYPE 546 | if (meta.isProvider) tag._context[provider] = item 547 | return item 548 | } 549 | 550 | function loadTmpl (state, values, recompile = false) { 551 | if (state.tmpl && recompile === false) return state 552 | const { tree, fields, attrPos } = state 553 | const snips = {} 554 | for (var cmi = 0; cmi < tree.length; cmi++) { 555 | const [ tag, , , meta ] = prepare(tree[cmi]) 556 | const ix = meta.openTagStart[0] 557 | if (meta.isComponent === false) { 558 | const [ ix, pos ] = meta.openTagEnd 559 | const isVoidElement = VOID_ELEMENTS.has(tag) 560 | if (isVoidElement === true && meta.selfClosing === false) { 561 | fields[ix][pos - 1] = '/>' 562 | meta.selfClosing = true 563 | if (meta.closeTagStart) { 564 | const [ ix, pos ] = meta.closeTagStart 565 | replace(fields[ix], pos, meta.closeTagEnd[1]) 566 | } 567 | } 568 | if (isVoidElement === false && meta.selfClosing === true && !meta.closeTagStart) { 569 | const [ ix, pos ] = meta.openTagEnd 570 | fields[ix][pos - 1] = '>' 571 | fields[ix][pos] = `` 572 | } 573 | } 574 | if (snips[ix]) snips[ix].push(tree[cmi]) 575 | else snips[ix] = [tree[cmi]] 576 | } 577 | 578 | const body = generate(fields.map((f) => f.slice()), values, snips, attrPos, tree) 579 | const tmpl = compileTmpl(body.join(''), { 580 | inject, attribute, style, spread, snips, renderComponent, addRoot, selected, postprocess 581 | }) 582 | state.tmpl = tmpl 583 | state.snips = snips 584 | return state 585 | } 586 | 587 | function replace (array, s, e, ch = '') { 588 | while (s <= e) { 589 | array[s] = ch 590 | s++ 591 | } 592 | } 593 | function seek (array, pos, rx) { 594 | var i = pos - 1 595 | const end = array.length - 1 596 | while (i++ < end) { 597 | if (rx.test(array[i])) return i 598 | } 599 | return -1 600 | } 601 | function reverseSeek (array, pos, rx) { 602 | var i = pos 603 | while (i-- >= 0) { 604 | if (rx.test(array[i])) return i 605 | } 606 | return -1 607 | } 608 | 609 | function seekToEndOfTagName (fields, ix) { 610 | do { 611 | var boundary = reverseSeek(fields[ix], fields[ix].length - 1, /= 0) 614 | 615 | while (ix < fields.length - 1) { 616 | var pos = seek(fields[ix], boundary, /(^[\s/>]$)|^\$|^$/) - 1 617 | if (pos !== boundary) break 618 | boundary = 0 619 | ix++ 620 | } 621 | 622 | pos++ 623 | return [ix, pos] 624 | } 625 | 626 | function seekToEndOfOpeningTag (fields, ix) { 627 | const rx = /\/?>/ 628 | do { 629 | var pos = seek(fields[ix], 0, rx) 630 | } while (rx.test(fields[ix][pos]) === false && ++ix < fields.length) 631 | 632 | if (/\//.test(fields[ix][pos - 1])) pos -= 1 633 | 634 | return [ix, pos] 635 | } 636 | 637 | function style (obj) { 638 | if (typeof obj !== 'object' && obj != null) { 639 | throw TypeError('The `style` prop expects a mapping from style properties to values, not a string.') 640 | } 641 | const str = renderToStaticMarkup({ 642 | $$typeof: REACT_ELEMENT_TYPE, 643 | type: 'x', 644 | props: { style: obj } 645 | }).slice(3, -5) 646 | return str.length > 0 ? ' ' + str : str 647 | } 648 | 649 | function getTag (fields, i) { 650 | const [ix, tPos] = seekToEndOfTagName(fields, i) 651 | return fields[ix].slice(reverseSeek(fields[ix], fields[ix].length - 1, / typeof tag === 'string') 658 | 659 | for (var i = 0; i < fields.length; i++) { 660 | const field = fields[i] 661 | const fLen = field.length 662 | const priorChar = field[fLen - 1] 663 | if (priorChar === '') continue 664 | if (priorChar === '=') { 665 | const { s, e } = attrPos[i + offset] 666 | const key = field.slice(s, e).join('') 667 | const pos = s === 0 ? 0 : reverseSeek(field, s, /^[^\s]$/) + 1 668 | if (key === 'style') { 669 | replace(field, pos, e) 670 | field[s] = `\${this.style(values[${offset + valdex++}])}` 671 | } else if (key === 'dangerouslySetInnerHTML') { 672 | replace(field, pos, e) 673 | const [ix, p] = seekToEndOfOpeningTag(fields, i + 1) 674 | fields[ix][p + 1] = `\${values[${offset + valdex++}].__html}${fields[ix][p + 1]}` 675 | } else if (key === 'defaultValue' && getTag(fields, i) === 'select') { 676 | replace(field, pos, e) 677 | field[pos] = `\${this.selected.register(values[${offset + valdex++}])}` 678 | } else if (key === 'selected' && getTag(fields, i) === 'option') { 679 | replace(field, pos, e) 680 | const [ ix, p ] = seekToEndOfTagName(fields, i) 681 | const wasSelectedPos = seek(fields[ix], p, /§/) 682 | if (wasSelectedPos === -1) { 683 | field[field.length - 1] = `\${this.attribute(values[${offset + valdex++}], '${key}', '${key}', replace)}` 684 | } else { 685 | const sanity = fields[ix][wasSelectedPos + 2] === ')' && fields[ix][wasSelectedPos + 3] === '}' && 686 | fields[ix][wasSelectedPos - 1] === ' ' && fields[ix][wasSelectedPos - 2] === ',' 687 | if (sanity) { 688 | const selectIndex = fields[ix][wasSelectedPos + 1].codePointAt(0) 689 | fields[ix][wasSelectedPos + 1] = '' 690 | const selectWithDefaultValue = 'defaultValue' in tree[selectIndex][1] 691 | if (selectWithDefaultValue) { 692 | fields[ix][wasSelectedPos] = `values[${offset + valdex++}]` 693 | } else { 694 | fields[ix][wasSelectedPos] = 'false' 695 | field[field.length - 1] = `\${this.attribute(values[${offset + valdex++}], '${key}', '${key}', replace)}` 696 | } 697 | } else { 698 | valdex++ 699 | } 700 | } 701 | } else if (key === 'children' || attr.mapping(key, getTag(fields, i)) === 'children') { 702 | replace(field, pos, e) 703 | const [ix, p] = seekToEndOfOpeningTag(fields, i + 1) 704 | if (fields[ix][p + 1][0] === '<') { 705 | fields[ix][p] = fields[ix][p] + `\${this.inject(values[${offset + valdex++}])}` 706 | } else { 707 | // children attribute has clashed with element that has children, 708 | // increase valdex to ignore attribute 709 | valdex++ 710 | } 711 | } else if (attr.reserved(key) === false) { 712 | const tag = getTag(fields, i) 713 | const mappedKey = attr.mapping(key, tag) 714 | if (mappedKey.length > 0) { 715 | if (snips[i] && snips[i][0] === rootElement) { 716 | field[pos] = `\${this.attribute(values[${offset + valdex++}], '${mappedKey}', '${key}', replace)}` 717 | } else { 718 | field[pos] = `\${this.attribute(values[${offset + valdex++}], '${mappedKey}', '${key}')}` 719 | } 720 | } else { 721 | // if the mapped key is empty, clear the attribute from the output and ignore the value 722 | replace(field, pos, e) 723 | valdex++ 724 | } 725 | replace(field, pos + 1, e) 726 | if (pos > 0) replace(field, 0, seek(field, 0, /^[^\s]$/) - 1) // trim left 727 | 728 | if (key === 'value' && tag === 'option') { 729 | const p = seekToEndOfTagName(fields, i)[1] 730 | field[p] = `\${this.selected(values[${offset + valdex - 1}])}${field[p]}` 731 | } 732 | } else { 733 | replace(field, pos, e) 734 | valdex++ 735 | } 736 | } else if (priorChar === '…') { 737 | var ix = i 738 | var item = snips[ix] 739 | while (ix >= 0) { 740 | if (item) break 741 | item = snips[--ix] 742 | } 743 | item = item[0] 744 | const [ tag, props, childMap, meta ] = item 745 | if (typeof tag === 'function') { 746 | // setting the field to a space instead of ellipsis 747 | // and rewinding i allows for a second pass where it's 748 | // recognized as a component, the space will be wiped 749 | // away with the component overwrite (don't set it to 750 | // empty string since this indicates "don't process") 751 | // when it's a prior char 752 | field[field.length - 1] = ' ' 753 | i-- 754 | continue 755 | } 756 | const [openIx, openPos] = seekToEndOfTagName(fields, i) 757 | const [closeIx, closePos] = seekToEndOfOpeningTag(fields, i + 1) 758 | 759 | if (field[field.length - 2] === ' ') field[field.length - 2] = '' 760 | 761 | field[field.length - 1] = '`, `' 762 | const str = fields[openIx][openPos] 763 | fields[openIx][openPos] = `\${this.spread(${offset + valdex++}, this.snips[${ix}][${snips[ix].length - 1}], values, \`` 764 | if (str[0] === '$') fields[openIx][openPos] += str 765 | 766 | fields[closeIx][closePos] = '`)}' + fields[closeIx][closePos] 767 | if (VOID_ELEMENTS.has(tag) === false && childMap.length === 0) { 768 | if (meta.spread[ix] && meta.spread[ix].before.indexOf('children') > -1) { 769 | childMap[0] = props.children 770 | replace(fields[closeIx], closePos + 1, seek(fields[closeIx], 0, / tag === 'select' 791 | while (c >= 0) { 792 | select = snips[c].reverse().find(predicate) 793 | if (select) break 794 | c-- 795 | } 796 | optionMayBeSelected = 'defaultValue' in select[1] || select[3].spreadIndices.length > 0 797 | } 798 | const output = `\${this.inject(values[${offset + valdex++}])}` 799 | const prefix = (priorChar !== '>') && !optionMayBeSelected 800 | ? '' 801 | : '' 802 | const suffix = (fields[i + 1] && fields[i + 1].join('').trimLeft()[0] !== '<') && 803 | !optionMayBeSelected 804 | ? '' 805 | : '' 806 | 807 | if (field.length > 0) { 808 | field[field.length - 1] = `${field[field.length - 1]}${prefix}${output}${suffix}` 809 | } else { 810 | // this happens when there are multiple adjacent interpolated children 811 | // eg.
${'a'}${'b'}
- field.length will be 0 for the second 812 | // child because it's represented as an empty string in callSite param 813 | field[0] = `${output}${suffix}` 814 | } 815 | 816 | if (optionMayBeSelected) { 817 | const text = fields[i + 1].slice(0, fields[i + 1].findIndex((c) => c === '<')).join('') 818 | const pos = reverseSeek(fields[i], fields[i].length - 1, / { 825 | const { openTagStart, openTagEnd, selfClosing, closeTagEnd, isComponent, name } = snip[3] 826 | if (!isComponent) return 827 | 828 | const [ from, start ] = openTagStart 829 | const [ to, end ] = selfClosing ? openTagEnd : closeTagEnd 830 | const [ tag ] = snip 831 | const type = typeof tag 832 | if (type === 'string') { 833 | replace(fields[from], start + 1, start + name.length) 834 | fields[from][start + 1] = `\${this.snips[${i}][${ix}][0]}` 835 | if (selfClosing === false) { 836 | replace(fields[to], end - name.length, end - 1) 837 | fields[to][end - 1] = `\${this.snips[${i}][${ix}][0]}` 838 | } 839 | priorCmpBounds = { to, end } 840 | return 841 | } 842 | if (priorCmpBounds.to > from || (priorCmpBounds.to === from && priorCmpBounds.end > start)) { 843 | return 844 | } 845 | priorCmpBounds = { to, end } 846 | 847 | if (type === 'symbol') { 848 | tree[esxValues] = values 849 | snip[2] = resolveChildren(snip[2], snip[3].dynChildren, tree, tree[0]) 850 | field[start] = `\${this.inject(this.snips[${i}][${ix}][2])}` 851 | } else { 852 | field[start] = `\${this.inject(this.renderComponent(this.snips[${i}][${ix}], values))}` 853 | } 854 | replace(field, start + 1, from === to ? end : field.length - 1) 855 | if (from < to) { 856 | valdex = to 857 | var c = from 858 | while (c++ < to && fields[c]) { 859 | replace(fields[c], 0, c === to ? end : fields[c].length - 1) 860 | } 861 | } 862 | }) 863 | } 864 | } 865 | 866 | const body = fields.map((f) => f.join('')) 867 | if (rootElement) { 868 | const { keys } = rootElement[3] 869 | const attrs = keys.map((k) => { 870 | k = attr.mapping(k, rootElement[0]) 871 | if (k === 'dangerouslySetInnerHTML') return '' 872 | return k.length === 0 || attr.reserved(k) ? '' : ` (${k})=".*"` 873 | }).filter(Boolean) 874 | const rx = RegExp(attrs.join('|'), 'g') 875 | 876 | for (var fi = 0; fi < body.length; fi++) { 877 | const field = body[fi] 878 | const match = field.match(/\/>|>/) 879 | if (match === null) continue 880 | if (attrs.length > 0) { 881 | body.slice(0, fi + 1).forEach((f, i) => { 882 | if (i === fi) { 883 | const pre = f.slice(0, match.index).trimRight() 884 | const post = f.slice(match.index).trimLeft() 885 | const clonableRootElement = makeClonable(pre, rx) 886 | const offset = clonableRootElement.length - pre.length 887 | match.index += offset 888 | body[i] = clonableRootElement + post 889 | return 890 | } 891 | const clonableRootElement = makeClonable(f, rx) 892 | body[i] = clonableRootElement 893 | }) 894 | } 895 | const pre = body[fi].slice(0, match.index).trimRight() 896 | const post = body[fi].slice(match.index).trimLeft() 897 | body[fi] = `${pre}\${extra}\${this.addRoot()}${post}` 898 | break 899 | } 900 | } 901 | return body 902 | } 903 | 904 | function makeClonable (str, rx) { 905 | return str.replace(rx, (m, ...args) => { 906 | const k = args.slice(0, -1).find((k) => !!k) 907 | return `\${replace !== null && '${k}' in replace ? replace['${k}'] : \`${m}\`}` 908 | }) 909 | } 910 | 911 | function addRoot () { 912 | const result = ssrReactRootAdded ? '' : ' data-reactroot=""' 913 | ssrReactRootAdded = true 914 | return result 915 | } 916 | 917 | function compileTmpl (body, state) { 918 | const fn = state.postprocess 919 | ? Function('values', `extra=''`, 'replace=null', 'return this.postprocess(`' + body + '`)').bind(state) : // eslint-disable-line 920 | Function('values', `extra=''`, 'replace=null', 'return `' + body + '`').bind(state) // eslint-disable-line 921 | fn.body = body 922 | fn.state = state 923 | return fn 924 | } 925 | 926 | function compileChildTmpl (item, tree) { 927 | const meta = item[3] 928 | const { openTagStart, openTagEnd, selfClosing, closeTagEnd, attrPos } = meta 929 | const to = selfClosing ? openTagEnd[0] : closeTagEnd[0] 930 | const from = openTagStart[0] 931 | const fields = meta.fields.map((f) => f.split('')) 932 | const snips = {} 933 | const root = tree[0] 934 | const dynamicChildIndices = Object.keys(root[3].dynChildren) 935 | const offset = dynamicChildIndices.filter((i) => (i < from)).length 936 | const posOffset = root[3].fields.slice(0, offset).join('').length 937 | for (var cmi = 0; cmi < tree.length; cmi++) { 938 | const cur = tree[cmi][3] 939 | const ix = cur.openTagStart[0] + offset 940 | const sPos = cur.openTagStart[1] + posOffset 941 | if (ix < from || ix > to) continue 942 | if (sPos < openTagEnd[1]) continue 943 | const ePos = (cur.selfClosing ? cur.openTagEnd[1] : cur.closeTagEnd[1]) 944 | if (ePos > closeTagEnd[1]) continue 945 | if (snips[ix - offset - from]) snips[ix - offset - from].push(tree[cmi]) 946 | else snips[ix - offset - from] = [tree[cmi]] 947 | } 948 | const values = tree[esxValues].slice(from, to) 949 | replace(fields[from], 0, openTagStart[1] - 1) 950 | fields[to].length = (selfClosing ? openTagEnd[1] : closeTagEnd[1]) + 1 951 | const body = generate(fields.slice(from, to + 1), values, snips, attrPos, tree, from) 952 | const tmpl = compileTmpl(body.join(''), { 953 | inject, attribute, style, spread, snips, renderComponent, addRoot, selected 954 | }) 955 | return tmpl 956 | } 957 | 958 | function resolveChildren (childMap, dynChildren, tree, top) { 959 | const children = [] 960 | for (var i = 0; i < childMap.length; i++) { 961 | if (typeof childMap[i] === 'number') { 962 | const [ tag, props, , elMeta ] = tree[childMap[i]] 963 | if (typeof tag === 'function') { 964 | const element = renderComponent(tree[childMap[i]], tree[esxValues]) 965 | const state = element[ns] || (element._owner && element._owner[owner] && element._owner()) 966 | if (state) { 967 | children[i] = new EsxElement(tree[childMap[i]], state.tmpl, state.values, state.replace) 968 | } else { 969 | children[i] = new EsxElementUnopt(tree[childMap[i]]) 970 | } 971 | } else { 972 | for (var p in elMeta.dynAttrs) { 973 | if (!(p in props)) { 974 | props[p] = tree[esxValues][elMeta.dynAttrs[p]] 975 | } 976 | } 977 | tree[childMap[i]][3][ns] = tree[childMap[i]][3][ns] || { 978 | tmpl: compileChildTmpl(tree[childMap[i]], tree, top), 979 | values: tree[esxValues] 980 | } 981 | 982 | try { 983 | props[parent] = tree[childMap[i]] 984 | } catch (e) { 985 | // this element has at some point been passed 986 | // through React.renderToString (or renderToStaticMarkup), 987 | // because props[parent] is there and has become readOnly. 988 | // So there's nothing to do here, we just need to avoid 989 | // a throw. 990 | } 991 | children[i] = new EsxElement(tree[childMap[i]], tree[childMap[i]][3][ns].tmpl, tree[esxValues]) 992 | } 993 | } else { 994 | children[i] = childMap[i] 995 | } 996 | for (var n in dynChildren) { 997 | const val = tree[esxValues][dynChildren[n]] 998 | children[n] = val 999 | } 1000 | } 1001 | if (children.length === 0) return null 1002 | if (children.length === 1) return children[0] 1003 | return children 1004 | } 1005 | 1006 | function tryToLoad (peer) { 1007 | try { 1008 | // explicit requires for webpacks benefit 1009 | if (peer === 'react') return require('react') 1010 | if (peer === 'react-dom/server') return require('react-dom/server') 1011 | } catch (e) { 1012 | console.error(` 1013 | esx depends on ${peer} as a peer dependency, 1014 | ensure that ${peer} (or preact-compat) is 1015 | installed in your application 1016 | `) 1017 | process.exit(1) 1018 | } 1019 | } 1020 | 1021 | esx.ssr = { 1022 | option (key, value) { 1023 | if (key !== 'hooks-mode') { 1024 | throw Error('invalid option') 1025 | } 1026 | if (value !== 'compatible' && value !== 'stateful') { 1027 | throw Error('invalid option') 1028 | } 1029 | hooks = require(`./lib/hooks/${value}`) 1030 | } 1031 | } 1032 | 1033 | esx.plugins = plugins 1034 | 1035 | module.exports = esx 1036 | -------------------------------------------------------------------------------- /lib/attr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function mapping (key, tag) { 4 | switch (key) { 5 | // prop->attr 6 | case 'className': return 'class' 7 | case 'htmlFor': return 'for' 8 | case 'defaultChecked': return 'checked' 9 | case 'defaultValue': return tag === 'input' ? 'value' : (tag === 'textarea' ? 'children' : '') 10 | // lowercase 11 | case 'contentEditable': return 'contenteditable' 12 | case 'crossOrigin': return 'crossorigin' 13 | case 'spellCheck': return 'spellcheck' 14 | case 'allowFullScreen': return 'allowfullscreen' 15 | case 'autoPlay': return 'autoplay' 16 | case 'autoFocus': return 'autofocus' 17 | case 'formNoValidate': return 'formnovalidate' 18 | case 'noModule': return 'nomodule' 19 | case 'noValidate': return 'novalidate' 20 | case 'playsInline': return 'playsinline' 21 | case 'readOnly': return 'readonly' 22 | case 'rowSpan': return 'rowspan' 23 | case 'itemScope': return 'itemscope' 24 | case 'tabIndex': return 'tabindex' 25 | // hyphenated 26 | case 'httpEquiv': return 'http-equiv' 27 | case 'acceptCharset': return 'accept-charset' 28 | case 'accentHeight': return 'accent-height' 29 | case 'alignmentBaseline': return 'alignment-baseline' 30 | case 'arabicForm': return 'arabic-form' 31 | case 'baselineShift': return 'baseline-shift' 32 | case 'capHeight': return 'cap-height' 33 | case 'clipPath': return 'clip-path' 34 | case 'clipRule': return 'clip-rule' 35 | case 'colorInterpolation': return 'color-interpolation' 36 | case 'colorInterpolationFilters': return 'color-interpolation-filters' 37 | case 'colorProfile': return 'color-profile' 38 | case 'colorRendering': return 'color-rendering' 39 | case 'dominantBaseline': return 'dominant-baseline' 40 | case 'enableBackground': return 'enable-background' 41 | case 'fillOpacity': return 'fill-opacity' 42 | case 'fillRule': return 'fill-rule' 43 | case 'floodColor': return 'flood-color' 44 | case 'floodOpacity': return 'flood-opacity' 45 | case 'fontFamily': return 'font-family' 46 | case 'fontSize': return 'font-size' 47 | case 'fontSizeAdjust': return 'font-size-adjust' 48 | case 'fontStretch': return 'font-stretch' 49 | case 'fontStyle': return 'font-style' 50 | case 'fontVariant': return 'font-variant' 51 | case 'fontWeight': return 'font-weight' 52 | case 'glyphName': return 'glyph-name' 53 | case 'glyphOrientationHorizontal': return 'glyph-orientation-horizontal' 54 | case 'glyphOrientationVertical': return 'glyph-orientation-vertical' 55 | case 'horizAdvX': return 'horiz-adv-x' 56 | case 'horizOriginX': return 'horiz-origin-x' 57 | case 'imageRendering': return 'image-rendering' 58 | case 'letterSpacing': return 'letter-spacing' 59 | case 'lightingColor': return 'lighting-color' 60 | case 'markerEnd': return 'marker-end' 61 | case 'markerMid': return 'marker-mid' 62 | case 'markerStart': return 'marker-start' 63 | case 'overlinePosition': return 'overline-position' 64 | case 'overlineThickness': return 'overline-thickness' 65 | case 'paintOrder': return 'paint-order' 66 | case 'panose-1': return 'panose-1' 67 | case 'pointerEvents': return 'pointer-events' 68 | case 'renderingIntent': return 'rendering-intent' 69 | case 'shapeRendering': return 'shape-rendering' 70 | case 'stopColor': return 'stop-color' 71 | case 'stopOpacity': return 'stop-opacity' 72 | case 'strikethroughPosition': return 'strikethrough-position' 73 | case 'strikethroughThickness': return 'strikethrough-thickness' 74 | case 'strokeDasharray': return 'stroke-dasharray' 75 | case 'strokeDashoffset': return 'stroke-dashoffset' 76 | case 'strokeLinecap': return 'stroke-linecap' 77 | case 'strokeLinejoin': return 'stroke-linejoin' 78 | case 'strokeMiterlimit': return 'stroke-miterlimit' 79 | case 'strokeOpacity': return 'stroke-opacity' 80 | case 'strokeWidth': return 'stroke-width' 81 | case 'textAnchor': return 'text-anchor' 82 | case 'textDecoration': return 'text-decoration' 83 | case 'textRendering': return 'text-rendering' 84 | case 'underlinePosition': return 'underline-position' 85 | case 'underlineThickness': return 'underline-thickness' 86 | case 'unicodeBidi': return 'unicode-bidi' 87 | case 'unicodeRange': return 'unicode-range' 88 | case 'unitsPerEm': return 'units-per-em' 89 | case 'vAlphabetic': return 'v-alphabetic' 90 | case 'vHanging': return 'v-hanging' 91 | case 'vIdeographic': return 'v-ideographic' 92 | case 'vMathematical': return 'v-mathematical' 93 | case 'vectorEffect': return 'vector-effect' 94 | case 'vertAdvY': return 'vert-adv-y' 95 | case 'vertOriginX': return 'vert-origin-x' 96 | case 'vertOriginY': return 'vert-origin-y' 97 | case 'wordSpacing': return 'word-spacing' 98 | case 'writingMode': return 'writing-mode' 99 | case 'xHeight': return 'x-height' 100 | // xml namespace 101 | case 'xmlnsXlink': return 'xmlns:xlink' 102 | case 'xmlBase': return 'xml:base' 103 | case 'xmlLang': return 'xml:lang' 104 | case 'xmlSpace': return 'xml:space' 105 | case 'xlinkActuate': return 'xlink:actuate' 106 | case 'xlinkArcrole': return 'xlink:arcrole' 107 | case 'xlinkHref': return 'xlink:href' 108 | case 'xlinkRole': return 'xlink:role' 109 | case 'xlinkShow': return 'xlink:show' 110 | case 'xlinkTitle': return 'xlink:title' 111 | case 'xlinkType': return 'xlink:type' 112 | default: return key 113 | } 114 | } 115 | 116 | function reserved (key) { 117 | /* eslint-disable no-fallthrough */ 118 | switch (key) { 119 | case 'key': 120 | case 'ref': 121 | case 'innerHTML': 122 | case 'suppressContentEditableWarning': 123 | case 'suppressHydrationWarning': return true 124 | default: return false 125 | } 126 | /* eslint-enable no-fallthrough */ 127 | } 128 | 129 | function serializeBool (key, val) { 130 | return enumeratedBool(key) ? `"${val.toString()}"` : (val ? '""' : '') 131 | } 132 | 133 | function enumeratedBool (key) { 134 | /* eslint-disable no-fallthrough */ 135 | switch (key) { 136 | // enumerated HTML attributes (must have boolean strings) 137 | case 'contentEditable': 138 | case 'draggable': 139 | case 'spellCheck': 140 | case 'value': 141 | // enumerated SVG attributes (must have boolean strings) 142 | case 'autoReverse': 143 | case 'externalResourcesRequired': 144 | case 'focusable': 145 | case 'preserveAlpha': return true 146 | default: return false 147 | } 148 | /* eslint-enable no-fallthrough */ 149 | } 150 | 151 | function bool (key, strict = false) { 152 | /* eslint-disable no-fallthrough */ 153 | switch (key) { 154 | // props 155 | case 'defaultChecked': 156 | // true HTML boolean attributes 157 | case 'allowFullScreen': 158 | case 'async': 159 | case 'autoPlay': 160 | case 'autoFocus': 161 | case 'controls': 162 | case 'default': 163 | case 'defer': 164 | case 'disabled': 165 | case 'formNoValidate': 166 | case 'hidden': 167 | case 'loop': 168 | case 'noModule': 169 | case 'noValidate': 170 | case 'open': 171 | case 'playsInline': 172 | case 'readOnly': 173 | case 'required': 174 | case 'reversed': 175 | case 'scoped': 176 | case 'seamless': 177 | case 'itemScope': 178 | // DOM properties 179 | case 'checked': 180 | case 'multiple': 181 | case 'muted': 182 | case 'selected': return true 183 | // overloaded booleans 184 | case 'capture': 185 | case 'download': return !strict 186 | default: return strict ? false : enumeratedBool(key) 187 | } 188 | /* eslint-enable no-fallthrough */ 189 | } 190 | 191 | module.exports = { 192 | mapping, 193 | reserved, 194 | bool, 195 | enumeratedBool, 196 | serializeBool 197 | } 198 | -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { createElement } = require('react') 3 | const parse = require('./lib/parse') 4 | const validate = require('./lib/validate') 5 | const { marker } = require('./lib/symbols') 6 | 7 | function esx (components = {}) { 8 | validate(components) 9 | const cache = new WeakMap() 10 | const render = (strings, ...values) => { 11 | const key = strings 12 | const state = cache.has(key) 13 | ? cache.get(key) 14 | : cache.set(key, parse(components, strings, values)).get(key) 15 | const { tree } = state 16 | var i = tree.length 17 | var root = null 18 | const map = {} 19 | while (i--) { 20 | const [tag, props, childMap, meta] = tree[i] 21 | const children = new Array(childMap.length) 22 | const { dynAttrs, dynChildren, spread } = meta 23 | const spreads = spread && Object.keys(spread).map(Number) 24 | for (var c in childMap) { 25 | if (typeof childMap[c] === 'number') { 26 | children[c] = map[childMap[c]] 27 | } else { 28 | children[c] = childMap[c] 29 | } 30 | } 31 | if (spread) { 32 | for (var sp in spread) { 33 | const keys = Object.keys(values[sp]) 34 | for (var k in keys) { 35 | if (spread[sp].after.indexOf(keys[k]) > -1) continue 36 | props[keys[k]] = values[sp][keys[k]] 37 | } 38 | } 39 | } 40 | if (dynAttrs) { 41 | for (var p in dynAttrs) { 42 | const overridden = spread && spreads.filter(n => { 43 | return dynAttrs[p] < n 44 | }).some((n) => { 45 | return p in values[n] && spread[n].before.indexOf(p) > -1 46 | }) 47 | if (overridden) continue 48 | if (props[p] !== marker) continue // this means later static property, should override 49 | props[p] = values[dynAttrs[p]] 50 | } 51 | } 52 | if (dynChildren) { 53 | for (var n in dynChildren) { 54 | children[n] = values[dynChildren[n]] 55 | } 56 | } 57 | const reactChildren = children.length === 0 ? (props.children || null) : (children.length === 1 ? children[0] : children) 58 | root = reactChildren === null ? createElement(tag, props) : createElement(tag, props, reactChildren) 59 | map[i] = root 60 | } 61 | return root 62 | } 63 | 64 | render.register = (additionalComponents) => { 65 | validate(additionalComponents) 66 | Object.assign(components, additionalComponents) 67 | } 68 | render.createElement = createElement 69 | return render 70 | } 71 | 72 | module.exports = esx 73 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createElement, createContext, memo, forwardRef } = require('react') 4 | const { Provider, Consumer } = createContext() 5 | /* istanbul ignore next */ 6 | const noop = () => {} 7 | const REACT_PROVIDER_TYPE = Provider.$$typeof 8 | const REACT_CONSUMER_TYPE = Consumer.$$typeof 9 | const REACT_MEMO_TYPE = memo(noop).$$typeof 10 | const REACT_ELEMENT_TYPE = createElement('div').$$typeof 11 | const REACT_FORWARD_REF_TYPE = forwardRef(noop).$$typeof 12 | const VOID_ELEMENTS = new Set([ 13 | 'area', 14 | 'base', 15 | 'br', 16 | 'col', 17 | 'embed', 18 | 'hr', 19 | 'img', 20 | 'input', 21 | 'link', 22 | 'meta', 23 | 'param', 24 | 'source', 25 | 'track', 26 | 'wbr' 27 | ]) 28 | const AUTO_CLOSING_ELEMENTS = new Set([ 29 | // html spec: closing illegal 30 | 'area', 31 | 'base', 32 | 'br', 33 | 'col', 34 | 'embed', 35 | 'hr', 36 | 'img', 37 | 'input', 38 | 'link', 39 | 'meta', 40 | 'param', 41 | 'source', 42 | 'track', 43 | 'wbr', 44 | 'command', 45 | 'keygen', 46 | 'menuitem', 47 | // html spec: may be unclosed 48 | 'html', 49 | 'head', 50 | 'body', 51 | 'p', 52 | 'dt', 53 | 'dd', 54 | 'li', 55 | 'option', 56 | 'thead', 57 | 'th', 58 | 'tbody', 59 | 'tr', 60 | 'td', 61 | 'tfoot', 62 | 'colgroup' 63 | ]) 64 | 65 | const PROPERTIES_RX = /[^.[\]]+|\[(?:(\d+(?:\.\d+)?)((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(\.|\[\])(?:\4|$))/g 66 | 67 | module.exports = { 68 | REACT_PROVIDER_TYPE, 69 | REACT_CONSUMER_TYPE, 70 | REACT_MEMO_TYPE, 71 | REACT_ELEMENT_TYPE, 72 | REACT_FORWARD_REF_TYPE, 73 | VOID_ELEMENTS, 74 | AUTO_CLOSING_ELEMENTS, 75 | PROPERTIES_RX 76 | } 77 | -------------------------------------------------------------------------------- /lib/escape.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // React took the escape-html code 4 | // and included it into their code base 5 | // and then changed the escape code for 6 | // apostrophes from decimal form to 7 | // hexadecimal form. In order to provide consistent 8 | // escaping, this does the same. 9 | 10 | /** 11 | * Copyright (c) David Mark Clements 12 | * 13 | * This source code is licensed under the MIT license found in the 14 | * LICENSE file in the root directory of this source tree. 15 | * 16 | * Based on the escape-html library, which is used under the MIT License below: 17 | * 18 | * Copyright (c) 2012-2013 TJ Holowaychuk 19 | * Copyright (c) 2015 Andreas Lubbe 20 | * Copyright (c) 2015 Tiancheng "Timothy" Gu 21 | * 22 | * Permission is hereby granted, free of charge, to any person obtaining 23 | * a copy of this software and associated documentation files (the 24 | * 'Software'), to deal in the Software without restriction, including 25 | * without limitation the rights to use, copy, modify, merge, publish, 26 | * distribute, sublicense, and/or sell copies of the Software, and to 27 | * permit persons to whom the Software is furnished to do so, subject to 28 | * the following conditions: 29 | * 30 | * The above copyright notice and this permission notice shall be 31 | * included in all copies or substantial portions of the Software. 32 | * 33 | * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 34 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 35 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 36 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 37 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 38 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 39 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 40 | */ 41 | 42 | // Original code from escape-html module 43 | 44 | const matchHtmlRegExp = /["'&<>]/ 45 | 46 | function escape (val) { 47 | if (typeof val === 'number' || typeof val === 'boolean') return val + '' 48 | var str = '' + val 49 | var match = matchHtmlRegExp.exec(str) 50 | 51 | if (!match) { 52 | return str 53 | } 54 | 55 | var escape 56 | var html = '' 57 | var index = 0 58 | var lastIndex = 0 59 | 60 | for (index = match.index; index < str.length; index++) { 61 | switch (str.charCodeAt(index)) { 62 | case 34: // " 63 | escape = '"' 64 | break 65 | case 38: // & 66 | escape = '&' 67 | break 68 | case 39: // ' 69 | escape = ''' 70 | break 71 | case 60: // < 72 | escape = '<' 73 | break 74 | case 62: // > 75 | escape = '>' 76 | break 77 | default: 78 | continue 79 | } 80 | 81 | if (lastIndex !== index) { 82 | html += str.substring(lastIndex, index) 83 | } 84 | 85 | lastIndex = index + 1 86 | html += escape 87 | } 88 | 89 | return lastIndex !== index 90 | ? html + str.substring(lastIndex, index) 91 | : html 92 | } 93 | 94 | module.exports = escape 95 | -------------------------------------------------------------------------------- /lib/get.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function get (o, p) { 4 | var i = -1 5 | var l = p.length 6 | var n = o 7 | while (n != null && ++i < l) { 8 | n = n[p[i]] 9 | } 10 | return n 11 | } 12 | 13 | module.exports = get 14 | -------------------------------------------------------------------------------- /lib/hooks/compatible.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { 3 | // I'll be fine: 4 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: internals 5 | } = require('react') 6 | const { provider, ns } = require('../symbols') 7 | function noop () {} 8 | const esxDispatcher = { 9 | [ns]: true, 10 | useContext, 11 | useMemo, 12 | useReducer, 13 | useRef, 14 | useState, 15 | useCallback, 16 | useLayoutEffect: noop, 17 | useImperativeHandle: noop, 18 | useEffect: noop, 19 | useDebugValue: noop 20 | } 21 | var reactDispatcher = null 22 | var dispatcher = null 23 | Object.defineProperty(internals.ReactCurrentDispatcher, 'current', { 24 | get () { 25 | return dispatcher 26 | }, 27 | set (d) { 28 | if (d !== null && d[ns] !== true) { 29 | reactDispatcher = Object.assign({}, d) 30 | Object.assign(d, esxDispatcher) 31 | Object.defineProperty(internals.ReactCurrentDispatcher, 'current', { 32 | value: d, configurable: true, writable: true 33 | }) 34 | return d 35 | } 36 | return (dispatcher = d) 37 | } 38 | }, { configurable: true }) 39 | 40 | function install () { 41 | dispatcher = esxDispatcher 42 | internals.ReactCurrentDispatcher.current = dispatcher 43 | } 44 | function uninstall () { 45 | dispatcher = esxDispatcher 46 | } 47 | 48 | function useContext (context) { 49 | return context[provider] 50 | ? context[provider][1].value 51 | : reactDispatcher.useContext(context) 52 | } 53 | function useMemo (fn, deps) { 54 | return fn() 55 | } 56 | function useReducer (reducer, initialState, init) { 57 | if (typeof init === 'function') initialState = init(initialState) 58 | return [initialState, noop] 59 | } 60 | function useRef (val) { 61 | return { current: val } 62 | } 63 | function useState (initialState) { 64 | return [initialState, noop] 65 | } 66 | function useCallback (cb) { 67 | return cb 68 | } 69 | 70 | module.exports = { 71 | install, uninstall, rendering: noop, after: noop 72 | } 73 | -------------------------------------------------------------------------------- /lib/hooks/stateful.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { 3 | // I'll be fine: 4 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: internals 5 | } = require('react') 6 | const { provider, ns } = require('../symbols') 7 | function noop () {} 8 | const esxDispatcher = { 9 | [ns]: true, 10 | useContext, 11 | useMemo, 12 | useReducer, 13 | useRef, 14 | useState, 15 | useCallback, 16 | useLayoutEffect: noop, 17 | useImperativeHandle: noop, 18 | useEffect: noop, 19 | useDebugValue: noop 20 | } 21 | var reactDispatcher = null 22 | var dispatcher = null 23 | Object.defineProperty(internals.ReactCurrentDispatcher, 'current', { 24 | get () { 25 | return dispatcher 26 | }, 27 | set (d) { 28 | if (d !== null && d[ns] !== true) { 29 | reactDispatcher = Object.assign({}, d) 30 | Object.assign(d, esxDispatcher) 31 | Object.defineProperty(internals.ReactCurrentDispatcher, 'current', { 32 | value: d, configurable: true, writable: true 33 | }) 34 | return d 35 | } 36 | return (dispatcher = d) 37 | } 38 | }, { configurable: true }) 39 | 40 | const current = { 41 | renderingItem: null, 42 | index: 0 43 | } 44 | function rendering (item) { 45 | current.renderingItem = item 46 | current.index = 0 47 | } 48 | function after (item) { 49 | item[3].hooksSetup = true 50 | } 51 | function install () { 52 | dispatcher = esxDispatcher 53 | internals.ReactCurrentDispatcher.current = dispatcher 54 | } 55 | function uninstall () { 56 | dispatcher = esxDispatcher 57 | current.renderingItem = null 58 | current.index = 0 59 | } 60 | function useStateDispatcher (states) { 61 | return Function( // eslint-disable-line 62 | 'newState', 63 | `this.states[${current.index}] = newState` 64 | ).bind({ states }) 65 | } 66 | function useReducerDispatcher (states, reducer) { 67 | return Function( // eslint-disable-line 68 | 'action', 69 | `this.states[${current.index}] = this.reducer(this.states[${current.index}], action)` 70 | ).bind({ states, reducer }) 71 | } 72 | function useMemoDispatcher (states) { 73 | return Function( // eslint-disable-line 74 | 'fn', 75 | 'deps', 76 | ` 77 | const lastDeps = this.states[${current.index}] 78 | if (!('val' in this)) return (this.val = fn()) 79 | if (deps.length !== lastDeps.length) { 80 | this.states[${current.index}] = deps 81 | return (this.val = fn()) 82 | } 83 | 84 | for (var i = 0; i < deps.length; i++) { 85 | if (!Object.is(deps[i], lastDeps[i])) { 86 | this.states[${current.index}] = deps 87 | return (this.val = fn()) 88 | } 89 | } 90 | return this.val 91 | ` 92 | ).bind({ states }) 93 | } 94 | function getState (initialState, makeDispatcher, opts = null) { 95 | const meta = current.renderingItem[3] 96 | const { index } = current 97 | if (meta.hooksSetup === false) { 98 | meta.hooksUsed = true 99 | meta.hooks = meta.hooks || { 100 | states: [], 101 | dispatchers: [] 102 | } 103 | meta.hooks.states[index] = initialState 104 | const dispatch = makeDispatcher(meta.hooks.states, opts) 105 | meta.hooks.dispatchers[index] = dispatch 106 | current.index++ 107 | return [initialState, dispatch] 108 | } 109 | const state = meta.hooks.states[index] 110 | const dispatch = meta.hooks.dispatchers[index] 111 | current.index++ 112 | return [state, dispatch] 113 | } 114 | 115 | function useContext (context) { 116 | return context[provider] 117 | ? context[provider][1].value 118 | : reactDispatcher.useContext(context) 119 | } 120 | function useMemo (fn, deps) { 121 | const [ , getVal ] = getState(deps, useMemoDispatcher) 122 | return getVal(fn, deps) 123 | } 124 | function useReducer (reducer, initialState, init) { 125 | if (typeof init === 'function') initialState = init(initialState) 126 | return getState(initialState, useReducerDispatcher, reducer) 127 | } 128 | 129 | function useRef (val) { 130 | return { current: val } 131 | } 132 | 133 | function useState (initialState) { 134 | return getState(initialState, useStateDispatcher) 135 | } 136 | 137 | function useCallback (cb, deps) { 138 | return useMemo(() => cb, deps) 139 | } 140 | 141 | module.exports = { 142 | install, uninstall, rendering, after 143 | } 144 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('esx') 2 | const { Fragment } = require('react') 3 | const escapeHtml = require('./escape') 4 | const attr = require('./attr') 5 | const get = require('./get') 6 | const { 7 | VOID_ELEMENTS, 8 | AUTO_CLOSING_ELEMENTS, 9 | PROPERTIES_RX 10 | } = require('./constants') 11 | const { marker, ties } = require('./symbols') 12 | const tokens = debug.extend('tokens') 13 | 14 | /* istanbul ignore next */ 15 | if (process.env.TRACE) { 16 | tokens.log = (...args) => { 17 | console.log(...args, Error('trace').stack) 18 | } 19 | } 20 | 21 | /* istanbul ignore next */ 22 | const [ 23 | VAR, TEXT, OPEN, CLOSE, 24 | ATTR, KEY, KW, VW, VAL, 25 | SQ, DQ, EQ, BRK, SC, SPREAD 26 | ] = tokens.enabled === false ? Array.from(Array(14)).map((_, i) => i) : [ 27 | 'VAR', 'TEXT', 'OPEN', 'CLOSE', 28 | 'ATTR', 'KEY', 'KW', 'VW', 'VAL', 29 | 'SQ', 'DQ', 'EQ', 'BRK', 'SC', 'SPREAD' 30 | ] 31 | 32 | function addInterpolatedChild (tree, valuesIndex) { 33 | const node = getParent(tree) 34 | const children = node[2] 35 | const meta = node[3] 36 | meta.dynChildren[children.length] = valuesIndex 37 | children.push(marker) 38 | } 39 | 40 | function addInterpolatedAttr (tree, valuesIndex) { 41 | const node = tree[tree.length - 1] 42 | const props = node[1] 43 | const meta = node[3] 44 | props[meta.lastKey] = marker 45 | meta.dynAttrs[meta.lastKey] = valuesIndex 46 | } 47 | 48 | function addValue (tree, val) { 49 | const node = tree[tree.length - 1] 50 | const props = node[1] 51 | const meta = node[3] 52 | props[meta.lastKey] = val 53 | if (meta.lastKey === 'children') { 54 | meta.childrenAttribute = true 55 | } 56 | } 57 | 58 | function addProp (tree, name) { 59 | tokens(KEY, name) 60 | const node = tree[tree.length - 1] 61 | const tag = node[0] 62 | const props = node[1] 63 | const meta = node[3] 64 | if ((name === 'dangerouslySetInnerHTML' || name === 'children') && VOID_ELEMENTS.has(tag)) { 65 | throw SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.') 66 | } 67 | if (name === 'children') { 68 | if ('dangerouslySetInnerHTML' in props) { 69 | throw SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.') 70 | } 71 | Object.defineProperty(props, name, { value: true, writable: true, enumerable: true }) 72 | } else props[name] = true 73 | 74 | if (name === 'dangerouslySetInnerHTML' && 'children' in props) { 75 | throw SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.') 76 | } 77 | 78 | meta.lastKey = name 79 | meta.keys.push(name) 80 | } 81 | 82 | function valuelessAttr (tree, field, key, suffix = '') { 83 | addProp(tree, key) 84 | if (attr.bool(key)) { 85 | // the fact that it's present and a legit boolean attribute 86 | // means that it's value is true: 87 | addValue(tree, true) 88 | return `${field.slice(0, 0 - key.length - suffix.length)}${attr.mapping(key)}=${attr.serializeBool(key, true)}${suffix}` 89 | } 90 | debug(`the attribute "${key}" has no value and is not a boolean attribute. This attribute will not be rendered.`) 91 | return field.slice(0, -1 - key.length - suffix.length) + suffix 92 | } 93 | 94 | function getParent (tree) { 95 | var top = tree.length 96 | while (top-- > 0) { 97 | const el = tree[top] 98 | if (el[3].closed === false) return el 99 | } 100 | return null 101 | } 102 | 103 | function grow (r, index, c, tree, components, fields, attrPos) { 104 | const parent = getParent(tree) 105 | if (parent !== null) parent[2].push(tree.length) 106 | if (!(r in components)) { 107 | const propPath = r.match(PROPERTIES_RX).map((p) => { 108 | return p.replace(/'|"|`/g, '') 109 | }) 110 | if (propPath.length > 1) { 111 | components[r] = get(components, propPath) 112 | if (components[r] === undefined) { 113 | throw ReferenceError(`ESX: ${r} not found in registered components`) 114 | } 115 | } else if (r[0].toUpperCase() === r[0] && !(r in components) && r !== 'Fragment') { 116 | throw ReferenceError(`ESX: ${r} not found in registered components`) 117 | } 118 | } 119 | const isComponent = r in components || r === 'Fragment' 120 | const meta = { 121 | name: r, 122 | closed: false, 123 | selfClosing: false, 124 | isComponent, 125 | openTagStart: [c, index], 126 | fields, 127 | attrPos, 128 | lasKey: '', 129 | spreadIndices: [], 130 | spread: null, 131 | keys: [], 132 | dynAttrs: {}, 133 | dynChildren: {}, 134 | hooks: null, 135 | hooksSetup: false, 136 | hooksUsed: false, 137 | attributes: {}, 138 | tree 139 | } 140 | const tag = isComponent ? (components[r] || Fragment) : r 141 | const props = meta.attributes 142 | const item = [tag, props, [], meta] 143 | components[ties][r] = components[ties][r] || [] 144 | components[ties][r].push(item) 145 | tree.push(item) 146 | return meta 147 | } 148 | 149 | function parse (components, strings, values) { 150 | var ctx = TEXT 151 | const tree = [] 152 | const fields = [] 153 | const attrPos = {} 154 | for (var c = 0; c < strings.length; c++) { 155 | const s = ((ctx === VW ? strings[c].trimRight() : strings[c].trim()) 156 | .replace(/<(\s+)?(\/)?>/g, '<$2Fragment>')) 157 | 158 | var field = '' 159 | var r = '' 160 | if (ctx === VW) { 161 | ctx = ATTR 162 | tokens(ATTR) 163 | } 164 | for (var i = 0; i < s.length; i++) { 165 | var ch = s[i] 166 | if (/\s/.test(ch) && /\s/.test(s[i - 1])) { 167 | continue 168 | } 169 | if (ctx === OPEN && r === '/' && /\s/.test(ch)) { 170 | if (!tree[tree.length - 1][3].isComponent) { 171 | continue 172 | } 173 | } 174 | switch (true) { 175 | case (ctx === TEXT && ch === '<'): 176 | const trim = field.trimRight() 177 | if (trim.slice(-1) === '>') field = trim 178 | if (r.length > 0 && !/^\s$|-/.test(r)) { 179 | const parent = getParent(tree) 180 | tokens(TEXT, r) 181 | if (parent !== null) { 182 | if (VOID_ELEMENTS.has(parent[0])) { 183 | throw SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.') 184 | } 185 | if ('dangerouslySetInnerHTML' in parent[1]) { 186 | throw SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.') 187 | } 188 | 189 | if (s.length < strings[c].length) { 190 | const match = strings[c].match(/^\s+/) 191 | if (match !== null && s[0] !== '<') { 192 | r = ' ' + r 193 | field = ' ' + field 194 | } 195 | } 196 | parent[2].push(r) 197 | } 198 | } 199 | ctx = OPEN 200 | r = '' 201 | field += ch 202 | break 203 | case (ch === '>' && s[i - 1] === '/'): 204 | const node = tree[tree.length - 1][3] 205 | node.closed = true 206 | node.selfClosing = true 207 | const parent = getParent(tree) 208 | if (parent !== null) { 209 | if (VOID_ELEMENTS.has(parent[0])) { 210 | throw SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.') 211 | } 212 | } 213 | ctx = TEXT 214 | tokens(SC) 215 | const { isComponent, childrenAttribute } = node 216 | if (!isComponent && /\s/.test(s[i - 2])) { 217 | field = field.slice(0, -1).trimRight() + '/' 218 | } 219 | if (r.length && r !== '/') { 220 | field = valuelessAttr(tree, field, r.slice(0, r.length - 1), '/') 221 | } 222 | r = '' 223 | const textAreaWithDefaultValue = tree[tree.length - 1][0] === 'textarea' && ('defaultValue' in tree[tree.length - 1][1]) 224 | if (isComponent === false && (childrenAttribute || textAreaWithDefaultValue)) { 225 | const [tag, props] = tree[tree.length - 1] 226 | field = field.slice(0, -1).trimRight() + ch 227 | node.openTagEnd = [c, field.length - 1] 228 | node.selfClosing = false 229 | const children = textAreaWithDefaultValue ? props.defaultValue : props.children 230 | if (children !== marker) field += escapeHtml(children) 231 | node.closeTagStart = [c, field.length] 232 | field += '' 233 | node.closeTagEnd = [c, field.length] 234 | } else { 235 | field += ch 236 | node.openTagEnd = [c, field.length - 1] 237 | } 238 | if (node.spread) { 239 | node.spread[Object.keys(node.spread).pop()].after = node.keys 240 | node.keys = [] 241 | } 242 | break 243 | case (ch === '>' && ctx !== DQ && ctx !== SQ): 244 | if (ctx === KEY) { 245 | field = valuelessAttr(tree, field, r) 246 | } else if (ctx === OPEN) { 247 | tokens(OPEN, r) 248 | var top = tree.length 249 | if (r[0] !== '/') { 250 | const parent = getParent(tree) 251 | if (parent !== null && VOID_ELEMENTS.has(parent[0])) { 252 | throw SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.') 253 | } 254 | grow(r, field.length - r.length - 1, c, tree, components, fields, attrPos) 255 | } else { 256 | top = tree.length 257 | while (top-- > 0) { 258 | const meta = tree[top][3] 259 | const tag = tree[top][0] 260 | if (meta.closed === false) { 261 | const name = r.slice(1) 262 | if (name === tag || components[name] === tag || tag === Fragment) { 263 | meta.closed = true 264 | meta.closeTagStart = [c, field.length - r.length - 1] 265 | meta.closeTagEnd = [c, field.length] 266 | break 267 | } else { 268 | if (AUTO_CLOSING_ELEMENTS.has(tag) === false) { 269 | throw SyntaxError(`Expected corresponding ESX closing tag for <${tag.name || tag.toString()}>`) 270 | } 271 | } 272 | } 273 | } 274 | } 275 | } 276 | 277 | tokens(CLOSE, r) 278 | const el = tree[tree.length - 1] 279 | const props = el[1] 280 | const meta = el[3] 281 | if (meta.isComponent === false) { 282 | const tag = el[0] 283 | field = field.trimRight() 284 | const textAreaWithDefaultValue = tag === 'textarea' && 'defaultValue' in el[1] 285 | const selectWithDefaultValue = tag === 'select' && 'defaultValue' in el[1] 286 | if (meta.closed && (meta.childrenAttribute || textAreaWithDefaultValue)) { 287 | const hasNoChildren = field[field.length - r.length - 2] === '>' 288 | if (hasNoChildren) { 289 | const children = tag === 'textarea' ? props.defaultValue : props.children 290 | if (children !== marker) field = field.slice(0, field.length - r.length - 1) + escapeHtml(children) + field.slice(-r.length - 1) 291 | } 292 | } 293 | if (meta.closed === false && selectWithDefaultValue) { 294 | if (props.defaultValue !== marker) field += `\${this.selected.register("${props.defaultValue}")}` 295 | } 296 | if (r === '/option') { 297 | const parentSelect = getParent(tree) 298 | const parentSelectIndex = tree.findIndex((el) => el === parentSelect) 299 | const encodedPsi = String.fromCharCode(parentSelectIndex) 300 | const wasSelected = props.selected !== marker ? props.selected : '§' + encodedPsi 301 | const optionValue = typeof props.value === 'string' ? props.value 302 | : typeof el[2][0] === 'string' ? el[2][0] : '' 303 | if (parentSelectIndex > -1) { 304 | if (optionValue) { 305 | const [ ix, pos ] = meta.openTagStart 306 | const priorField = ix in fields 307 | let str = priorField ? fields[ix] : field 308 | str = str.replace(/ selected=""/g, '') 309 | const pre = str.slice(0, pos + r.length) 310 | const aft = str.slice(pos + r.length) 311 | const inject = `\${this.selected(${JSON.stringify(optionValue)}, ${wasSelected})}` 312 | str = `${pre}${inject}${aft}` 313 | if (priorField) { 314 | const min = pre.length - 1 315 | const offset = inject.length 316 | fields[ix] = str 317 | if (ix in meta.attrPos) { 318 | if (meta.attrPos[ix].s > min) { 319 | meta.attrPos[ix].s += offset 320 | meta.attrPos[ix].e += offset 321 | } 322 | } 323 | } else field = str 324 | } 325 | } 326 | } 327 | if (r === '/select') { 328 | field += `\${this.selected.deregister()}` 329 | } 330 | } 331 | if (!meta.openTagEnd) { 332 | meta.openTagEnd = [c, field.length + 1] 333 | if (meta.spread) { 334 | meta.spread[Object.keys(meta.spread).pop()].after = meta.keys 335 | meta.keys = [] 336 | } 337 | } 338 | r = '' 339 | ctx = TEXT 340 | field += ch 341 | break 342 | case (ctx === TEXT): 343 | r += ch 344 | field += ch 345 | break 346 | case (ctx === OPEN && ch === '/' && r.length > 0): 347 | tokens(OPEN, r) 348 | grow(r, field.length - r.length - 1, c, tree, components, fields, attrPos) 349 | r = '' 350 | ctx = TEXT 351 | field += ch 352 | break 353 | case (ctx === OPEN && /\s/.test(ch)): 354 | // ignore all whitespace in closing elements 355 | if (r[0] === '/') { 356 | continue 357 | } 358 | // ignore whitespace in opening tags prior to the tag name 359 | if (r.length === 0) { 360 | continue 361 | } 362 | tokens(OPEN, r) 363 | grow(r, field.length - r.length - 1, c, tree, components, fields, attrPos) 364 | r = '' 365 | ctx = ATTR 366 | tokens(ATTR) 367 | field += ch 368 | break 369 | case (ctx === OPEN): 370 | r += ch 371 | field += ch 372 | break 373 | case (ctx === ATTR): 374 | if (/[^\s=]/.test(ch)) { 375 | ctx = KEY 376 | r = ch 377 | field += ch 378 | break 379 | } 380 | field += ch 381 | break 382 | case (ctx === KEY): 383 | if (/\s/.test(ch)) { 384 | field = valuelessAttr(tree, field, r) 385 | r = '' 386 | ctx = KW 387 | tokens(KW) 388 | field += ch 389 | break 390 | } 391 | if (ch === '=') { 392 | addProp(tree, r) 393 | tokens(EQ) 394 | r = '' 395 | ctx = VW 396 | field += ch 397 | break 398 | } 399 | r += ch 400 | if (r === '...' && i === s.length - 1) { 401 | ctx = SPREAD 402 | field = field.slice(0, -2) 403 | ch = '…' 404 | tokens(SPREAD) 405 | } 406 | field += ch 407 | break 408 | case (ctx === KW || ctx === ATTR): 409 | tokens(BRK) 410 | if (/[\w-]/.test(ch)) { 411 | r += ch 412 | ctx = KEY 413 | field += ch 414 | break 415 | } 416 | ctx = ATTR 417 | tokens(ATTR) 418 | field += ch 419 | break 420 | case (ctx === VW): 421 | if (ch === `"`) { 422 | ctx = DQ 423 | field += ch 424 | break 425 | } 426 | if (ch === `'`) { 427 | ctx = SQ 428 | field += '"' 429 | break 430 | } 431 | ctx = VAL 432 | i -= 1 433 | field += ch 434 | break 435 | case (ctx === VAL && /\s/.test(ch)): 436 | throw SyntaxError('ESX: attribute value should be either an expression or quoted text') 437 | case (ctx === DQ && ch === `"`): // eslint-disable-line no-fallthrough 438 | case (ctx === SQ && ch === `'`): 439 | tokens(BRK) 440 | const tag = tree[tree.length - 1][0] 441 | const { lastKey } = tree[tree.length - 1][3] 442 | const esc = r.length > 0 ? (attr.bool(lastKey, true) ? '' : escapeHtml(r)) : '' 443 | if (lastKey === 'style' && esc.length > 0) { 444 | throw TypeError('The `style` prop expects a mapping from style properties to values, not a string.') 445 | } 446 | tokens(VAL, r, BRK) 447 | const val = r 448 | addValue(tree, val) 449 | if (val !== esc) field = field.replace(RegExp(`${val}$`, 'm'), esc) 450 | ctx = ATTR 451 | tokens(ATTR) 452 | const tKey = attr.mapping(lastKey, tag) 453 | if (tKey !== lastKey) { 454 | field = field.slice(0, field.length - 1 - esc.length - 1 - lastKey.length) + tKey + field.slice(field.length - 1 - esc.length - 1) 455 | if (tKey.length === 0) field = field.slice(0, 0 - 2 - esc.length) 456 | } 457 | if (tree[tree.length - 1][3].isComponent === false && tKey === 'children') { 458 | field = field.replace(RegExp(`children="${esc}$`), '') 459 | break 460 | } 461 | if (attr.reserved(tKey)) { 462 | field = field.slice(0, field.length - esc.length - lastKey.length - 3) 463 | } else if (tKey.length > 0) { 464 | if (ch === `'`) ch = '"' 465 | field += ch 466 | } 467 | r = '' 468 | break 469 | case (ctx === VAL || ctx === DQ || ctx === SQ): 470 | r += ch 471 | if (ch === `'`) ch = '"' 472 | field += ch 473 | break 474 | } 475 | } 476 | if (r.length > 0) { 477 | if (ctx === TEXT) { 478 | const parent = getParent(tree) 479 | if (parent !== null) { 480 | if (s.length < strings[c].length) { 481 | const match = strings[c].match(/\s+$/) 482 | if (match !== null) { 483 | r += match[0] 484 | field += match[0] 485 | } 486 | } 487 | parent[2].push(r) 488 | } 489 | tokens(TEXT, r) 490 | } 491 | } 492 | fields[c] = field 493 | if (c in values === false) break 494 | if (ctx === ATTR) throw SyntaxError('Unexpected token. Attributes must have a name.') 495 | tokens(VAR) 496 | if (ctx === VW) { 497 | tokens(VAL, values[c], VW) 498 | const { lastKey } = tree[tree.length - 1][3] 499 | attrPos[c] = { s: field.length - 1 - lastKey.length, e: field.length - 1 } 500 | addInterpolatedAttr(tree, c, i) 501 | } else if (ctx === VAL || ctx === SQ || ctx === DQ) { // value interpolated between quote marks 502 | throw SyntaxError('Unexpected token. Attribute expressions must not be surrounded in quotes.') 503 | } else if (ctx === TEXT) { // dynamic children 504 | tokens(TEXT, values[c]) 505 | if (VOID_ELEMENTS.has(tree[tree.length - 1][0])) { 506 | throw SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.') 507 | } 508 | if ('dangerouslySetInnerHTML' in tree[tree.length - 1][1]) { 509 | throw SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.') 510 | } 511 | addInterpolatedChild(tree, c) 512 | } else if (ctx === SPREAD) { 513 | ctx = ATTR 514 | r = '' 515 | const meta = tree[tree.length - 1][3] 516 | meta.spread = meta.spread || {} 517 | meta.spread[c] = { 518 | before: meta.keys, 519 | after: [] 520 | } 521 | meta.spreadIndices.push(c) 522 | meta.dynAttrs['…' + c] = c 523 | meta.keys = [] 524 | tokens(ATTR) 525 | } else { 526 | tokens(ctx, values[c]) 527 | switch (ctx) { 528 | case OPEN: 529 | throw SyntaxError('ESX: Unexpected token in element. Expressions may only be spread, embedded in attributes be included as children.') 530 | default: 531 | throw SyntaxError('ESX: Unexpected token.') 532 | } 533 | } 534 | } 535 | 536 | const root = tree[0] 537 | if (root && root[3].selfClosing === false && !('closeTagStart' in root[3])) { 538 | const [ tag ] = root 539 | if (AUTO_CLOSING_ELEMENTS.has(tag) === false) { 540 | throw SyntaxError(`Expected corresponding ESX closing tag for <${tag.name || tag.toString()}>`) 541 | } 542 | } 543 | 544 | debug('tree parsed from string') 545 | return { tree, fields: fields.map((f) => f.split('')), attrPos, fn: null } 546 | } 547 | 548 | module.exports = parse 549 | -------------------------------------------------------------------------------- /lib/plugins.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { prototype } = require('events') 3 | const { runners } = require('./symbols') 4 | const preState = Symbol('esx.pre-state') 5 | const postState = Symbol('esx.post-state') 6 | 7 | function pre (strings, values) { 8 | this[preState] = [strings, values] 9 | this.emit('pre') 10 | return this[preState] 11 | } 12 | function post (string) { 13 | this[postState] = string 14 | this.emit('post') 15 | return this[postState] 16 | } 17 | 18 | module.exports = { 19 | __proto__: prototype, 20 | [preState]: [], 21 | [postState]: '', 22 | [runners]: function () { 23 | return { 24 | pre: pre.bind(this), 25 | post: post.bind(this) 26 | } 27 | }, 28 | pre (fn) { 29 | const listener = () => { 30 | const [ strings, values ] = this[preState] 31 | this[preState] = fn(strings, ...values) 32 | } 33 | this.on('pre', listener) 34 | return () => { 35 | this.removeListener('pre', listener) 36 | } 37 | }, 38 | post (fn) { 39 | const listener = () => { 40 | const string = this[postState] 41 | this[postState] = fn(string) 42 | } 43 | this.on('post', listener) 44 | return () => { 45 | this.removeListener('post', listener) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/symbols.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const ns = Symbol('esx') 3 | const marker = Symbol('esx.marker') 4 | const skip = Symbol('esx.skip') 5 | const provider = Symbol('esx.provider') 6 | const esxValues = Symbol('esx.values') 7 | const parent = Symbol('esx.parent') 8 | const owner = Symbol('esx.owner') 9 | const template = Symbol('esx.template') 10 | const ties = Symbol('esx.ties') 11 | const separator = Symbol('esx.separator') 12 | const runners = Symbol('esx.plugin-runners') 13 | module.exports = { 14 | ns, marker, skip, provider, esxValues, parent, owner, template, ties, separator, runners 15 | } 16 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PROPERTIES_RX } = require('./constants') 4 | const { getPrototypeOf, prototype } = Object 5 | const validated = { 6 | primitives: new Set(), 7 | objects: new WeakSet() 8 | } 9 | function validateOne (key, cmp, path = '') { 10 | const primitive = typeof cmp === 'symbol' || typeof cmp === 'string' 11 | if (primitive && validated.primitives.has(cmp)) return 12 | else if (validated.objects.has(cmp)) return 13 | const name = key.match(PROPERTIES_RX).pop() 14 | if (name[0].toUpperCase() !== name[0]) { 15 | if (cmp !== null && typeof cmp === 'object') { 16 | validate(cmp, path + key + '.') 17 | return 18 | } else { 19 | throw Error(`ESX: ${path}${key} is not valid. All components should use PascalCase`) 20 | } 21 | } 22 | if (primitive) { 23 | validated.primitives.add(cmp) 24 | return 25 | } 26 | 27 | const valid = typeof cmp === 'function' || (cmp != null && 28 | typeof cmp === 'object' && Array.isArray(cmp) === false && '$$typeof' in cmp) 29 | 30 | if (valid) validated.objects.add(cmp) 31 | else throw Error(`ESX: ${path}${key} is not a valid component`) 32 | } 33 | 34 | function supported (key, cmp, path = '') { 35 | try { var unsupported = 'contextTypes' in cmp } catch (e) {} 36 | if (unsupported) { 37 | throw Error(`ESX: ${path}${key} has a contextTypes property. Legacy context API is not supported – https://reactjs.org/docs/legacy-context.html`) 38 | } 39 | } 40 | 41 | function validate (components, path = '') { 42 | const invalidType = components === null || getPrototypeOf(components) !== prototype 43 | if (invalidType) throw Error('ESX: supplied components must be a plain object') 44 | for (var key in components) { 45 | const cmp = components[key] 46 | supported(key, cmp, path) 47 | validateOne(key, cmp, path) 48 | } 49 | } 50 | 51 | module.exports = { validate, validateOne, supported } 52 | -------------------------------------------------------------------------------- /optimize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = require('esx-optimize') 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esx", 3 | "version": "2.3.3", 4 | "description": "High throughput React Server Side Rendering", 5 | "main": "index.js", 6 | "browser": "./browser.js", 7 | "engines": { 8 | "node": ">= 10.0.0" 9 | }, 10 | "scripts": { 11 | "test": "tap test/*.test.js", 12 | "test:browser": "NODE_ENV=development airtap -w --local test/create.test.js", 13 | "test:client": "TEST_CLIENT_CODE=1 tap test/create.test.js", 14 | "cov": "tap --100 --coverage-report=html test/*.test.js", 15 | "ci": "tap --cov --lines=90 --branches=90 --functions=100 --statements=90 --coverage-report=text-lcov test/*.test.js | codecov --pipe" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "jsx", 20 | "es6", 21 | "templates", 22 | "performance", 23 | "speed", 24 | "fast", 25 | "esnext", 26 | "ssr", 27 | "server-side rendering", 28 | "rendering" 29 | ], 30 | "author": "David Mark Clements ", 31 | "license": "MIT", 32 | "devDependencies": { 33 | "airtap": "^2.0.1", 34 | "aquatap": "^1.0.1", 35 | "codecov": "^3.3.0", 36 | "fastbench": "^1.0.1", 37 | "react": "^16.8.4", 38 | "react-dom": "^16.8.4", 39 | "react-test-renderer": "^16.8.4", 40 | "standard": "^12.0.1" 41 | }, 42 | "dependencies": { 43 | "debug": "^4.1.0", 44 | "esx-optimize": "^1.0.0" 45 | }, 46 | "directories": { 47 | "lib": "lib", 48 | "test": "test" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/esxjs/esx.git" 53 | }, 54 | "bugs": { 55 | "url": "https://github.com/esxjs/esx/issues" 56 | }, 57 | "homepage": "https://github.com/esxjs/esx#readme" 58 | } 59 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # esx 2 | 3 | [![Build Status](https://img.shields.io/travis/esxjs/esx.svg)](https://travis-ci.org/esxjs/esx) 4 | [![Coverage](https://img.shields.io/codecov/c/github/esxjs/esx.svg)](https://codecov.io/gh/esxjs/esx) 5 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) 6 | 7 | High throughput React Server Side Rendering 8 | 9 | esx demo 10 | 11 | 12 | For a simplified example of `esx` in action, check out [esx-demo](https://github.com/esxjs/esx-demo). 13 | 14 | `esx` is designed to be a **high speed SSR template engine for React**. 15 | 16 | It can be used with **absolutely no code base changes**. 17 | 18 | Use it with a [preloader flag](https://nodejs.org/api/cli.html#cli_r_require_module) like so: 19 | 20 | ```sh 21 | node -r esx/optimize my-app.js 22 | ``` 23 | 24 | *Note: transpiling is still experimental*. 25 | 26 | Alternatively [babel-plugin-esx-ssr](https://github.com/esxjs/babel-plugin-esx-ssr) can be used to transpile for the same performance gains. The babel plugin would be a preferred option where speed of process initialization is important (such as serverless). 27 | 28 | Optionally, `esx` is also a universal **JSX-like syntax in plain JavaScript** that allows for the elimination of transpilation in development environments. 29 | 30 | * For the server side, using `esx` syntax will yield the same high speed results as the optimizing preloader 31 | * For client side development, using `esx` syntax can enhance development workflow by removing the need for browser transpilation when developing in modern browsers 32 | * For client side production `esx` can be compiled away for production with [babel-plugin-esx-browser](https://github.com/esxjs/babel-plugin-esx-browser), resulting in zero-byte payload overhead. 33 | 34 | It uses native [Tagged Templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates) and works in all modern browsers. 35 | 36 |

37 | esx demo 38 |

39 | 40 | ## Status 41 | 42 | Not only is this project on-going, it's also following a moving 43 | target (the React implementation). 44 | 45 | This should only be used in production when: 46 | 47 | * It has been verified to yield significant enough performance gains 48 | * It has been thoroughly verified against your current implementation 49 | 50 | `esx` needs use cases and battle testing. All issues are very welcome, 51 | PR's are extremely welcome and Collaborators are exceptionally, extraordinarily, exceedingly welcome. 52 | 53 | ## Install 54 | 55 | ```js 56 | npm i esx 57 | ``` 58 | 59 | ## Tests 60 | 61 | There are close to 3000 passing tests. 62 | 63 | ```sh 64 | git clone https://github.com/esxjs/esx 65 | cd esx 66 | npm i 67 | npm test 68 | npm run test:client # test client-side implementation in node 69 | npm tun test:browser # test client-side implementation in browser 70 | ``` 71 | 72 | ## Syntax 73 | 74 | Creating HTML with `esx` syntax is as close as possible to JSX: 75 | 76 | - Spread props: `
` 77 | - Self-closing tags: `
` 78 | - Attributes: `` and `` 79 | - Boolean attributes: `
` 80 | - Components: `` 81 | - Components must be registered with `esx`: `esx.register({Foo})` 82 | 83 | ## Compatibility 84 | 85 | * `react` v16.8+ is required as a peer dependency 86 | * `react-dom` v16.8+ is required as a peer dependency 87 | * `esx` is built for Node 10+ 88 | * Supported Operating Systems: Windows, Linux, macOS 89 | 90 | ## Limitations 91 | 92 | `esx` should cover the API surface of all *non-deprecated* React features. 93 | 94 | Notably, `esx` will not work with the [Legacy Context API](https://reactjs.org/docs/legacy-context.html), 95 | but it will work with the [New Context API](https://reactjs.org/docs/context.html). 96 | 97 | While the legacy API is being phased out, there still may be modules in a 98 | projects depedency tree that rely on the legacy API. If you desperately need 99 | support for the legacy API, contact me.. 100 | 101 | ## Usage 102 | 103 | ### As an optimizer 104 | 105 | Preload `esx/optimize` like so: 106 | 107 | ```sh 108 | node -r esx/optimize my-app.js 109 | ``` 110 | 111 | That's it. This will convert all `JSX` and `createElement` calls to ESX format, 112 | unlocking the throughput benefits of SSR template rendering. 113 | 114 | ### As a JSX replacement 115 | 116 | Additionally, `esx` can be written by hand for great ergonomic benefit 117 | in both server and client development contexts. Here's the example 118 | from the [`htm`](https://github.com/developit/htm) readme converted 119 | to `esx` (`htm` is discussed at the bottom of this readme): 120 | 121 | ```js 122 | // using require instead of import allows for no server transpilation 123 | const { Component } = require('react') 124 | const esx = require('esx')() 125 | class App extends Component { 126 | addTodo() { 127 | const { todos = [] } = this.state; 128 | this.setState({ todos: todos.concat(`Item ${todos.length}`) }); 129 | } 130 | render({ page }, { todos = [] }) { 131 | return esx` 132 |
133 |
134 |
    135 | ${todos.map(todo => esx` 136 |
  • ${todo}
  • 137 | `)} 138 |
139 | 140 |
footer content here
141 |
142 | ` 143 | } 144 | } 145 | const Header = ({ name }) => esx`

${name} List

` 146 | const Footer = props => esx`