├── .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`
`
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] = `${tag}>`
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, /)
612 | if (boundary === -1) boundary = fields[ix].length - 1
613 | } while (boundary === fields[ix].length - 1 && --ix >= 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, /) + 1, tPos).join('')
652 | }
653 |
654 | function generate (fields, values, snips, attrPos, tree, offset = 0) {
655 | var valdex = 0
656 | var priorCmpBounds = {}
657 | const rootElement = tree.find(([tag]) => 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, /) - 1)
771 | }
772 | // this handles where the props object being spread MAY HAVE a `children`
773 | // property and the element itself has no children:
774 | if (fields[closeIx][closePos + 1][0] === '<' || fields[closeIx][closePos + 1] === '') {
775 | // when this.spread is called, if childMap is empty (so, no children)
776 | // childMap[0] is set to the value of the children attribute, now we can
777 | // so we can inject that value from the childMap
778 | // we shift the childMap so that the value is cleared, so that future renderings
779 | // don't have old state
780 | fields[closeIx][closePos + 1] = `\${this.snips[${ix}][0][2].length === 1 ? this.snips[${ix}][0][2].shift() : ''}${fields[closeIx][closePos + 1]}`
781 | }
782 | }
783 | } else if (valdex < values.length) {
784 | let optionMayBeSelected = false
785 | const tag = getTag(fields, i)
786 |
787 | if (tag === 'option') {
788 | let c = i
789 | let select = null
790 | const predicate = ([tag]) => 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, /) + tag.length + 1
819 | field[pos] = `\${this.selected(values[${offset + valdex - 1}] + '${text}')}${field[pos]}`
820 | }
821 | }
822 |
823 | if (i in snips) {
824 | snips[i].forEach((snip, ix) => {
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 += '' + tag + '>'
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 | [](https://travis-ci.org/esxjs/esx)
4 | [](https://codecov.io/gh/esxjs/esx)
5 | [](http://standardjs.com/)
6 |
7 | High throughput React Server Side Rendering
8 |
9 |
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 |
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 |
`
146 | const Footer = props => esx``
147 |
148 | esx.register({ Header, Footer })
149 |
150 | module.exports = App
151 | ```
152 |
153 | In a client entry point this can be rendered the usual way:
154 |
155 | ```js
156 | const App = require('./App')
157 | const container = document.getElementById('app')
158 | const { hydrate } = require('react-dom') // using hydrate because we have SSR
159 | const esx = require('esx')({ App })
160 | hydrate(esx ``, container)
161 | ```
162 |
163 | And the server entry point can use `esx.renderToToString` for high speed
164 | server-side rendering:
165 |
166 | ```js
167 | const { createServer } = require('http')
168 | const App = require('./App')
169 | createServer((req, res) => {
170 | res.end(`
171 |
172 | Todo
173 |
174 |
175 | ${esx.renderToString ``}
176 |
177 |
178 |
179 | `)
180 | }).listen(3000)
181 | ```
182 |
183 | ## API
184 |
185 | The `esx` module exports an initializer function, which
186 | returns a template string tag function.
187 |
188 | ### Initializer: `createEsx(components = {}) => esx`
189 |
190 | The default export is a function that when called initializes an
191 | instance of `esx`.
192 |
193 | ```js
194 | import createEsx from 'esx'
195 | ```
196 |
197 | ```js
198 | const createEsx = require('esx')
199 | ```
200 |
201 | The initializer takes an object of component mappings which
202 | it then uses to look up component references within the template.
203 |
204 | When called, the Initializer returns a Template Engine instance.
205 |
206 | ### Template Engine: ``esx`` => React Element``
207 |
208 | The result of the Initializer is a Template Engine which
209 | should always be assigned to `esx`. This is important
210 | for editor syntax support. The Template Engine instance
211 | is a [template tag function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates).
212 |
213 | ```js
214 | import createEsx from 'esx'
215 | import App from 'components/App'
216 | const esx = createEsx({ App }) // same as {App: App}
217 | // `esx` is the Template Engine
218 | console.log(esx ``) // exactly same result as React.createElement(App)
219 | ```
220 |
221 | ### Component Registration
222 |
223 | A component must be one of the following
224 |
225 | * function
226 | * class
227 | * symbol
228 | * object with a $$typeof key
229 | * string representing an element (e.g. 'div')
230 |
231 | #### `createEsx(components = {})`
232 |
233 | Components passed to the Initializer are registered
234 | and validated at initialization time. Each key in the
235 | `components` object should correspond to the name of
236 | a component referenced within an ESX template literal.
237 |
238 | #### `esx.register(components = {})`
239 |
240 | Components can also be registered after initialization with the
241 | `esx.register` method:
242 |
243 | ```js
244 | import createEsx from 'esx'
245 | import App from 'components/App'
246 | const esx = createEsx()
247 | esx.register({ App })
248 | // exactly same result as React.createElement(App)
249 | console.log(esx ``)
250 | ```
251 |
252 | Each key in the `components` object should correspond to the name of a component as referenced within an ESX template literal.
253 |
254 | #### `esx.register.one(name, component)`
255 |
256 | A single component can be registered with the `esx.register.one` method.
257 | The supplied `name` parameter must correspond to the name of a component
258 | referenced within an ESX template literal and the `component` parameter
259 | will be validated.
260 |
261 | #### `esx.register.lax(components = {})`
262 |
263 | **Advanced use only**. Use with care. This is a performance escape hatch.
264 | This method will register components without validating. This may be used
265 | for performance reasons such as when needing to register a component within a function. It is recommended to use the non-lax methods unless component validation
266 | in a specific scenario is measured as a bottleneck.
267 |
268 | #### `esx.register.lax.one(name, component)`
269 |
270 | **Advanced use only**. Use with care. This is a performance escape hatch.
271 | Will register one component without validating.
272 |
273 | ### Server-Side Rendering: ``esx.renderToString`` => String``
274 |
275 | On the server side every Template Engine instance also has a
276 | `renderToString` method. The `esx.renderToString` method is
277 | also a template literal tag function.
278 |
279 | This **must** be used in place of the `react-dom/server` packages
280 | `renderToString` method in order to obtain the speed benefits.
281 |
282 | ```js
283 | import createEsx from 'esx'
284 | import App from 'components/App'
285 | const esx = createEsx()
286 | esx.register({ App })
287 | // same, but faster, result as ReactDomServer.renderToString()
288 | console.log(esx.renderToString ``)
289 | ```
290 |
291 | **Alias**: `esx.ssr`
292 |
293 | #### `esx.renderToString(EsxElement) => String`
294 |
295 | The `esx.renderToString` method can also accept an element as its only
296 | parameter.
297 |
298 | ```js
299 | import createEsx from 'esx'
300 | import App from 'components/App'
301 | const esx = createEsx()
302 | esx.register({ App })
303 | const app = esx ``
304 | // same, but faster, result as ReactDomServer.renderToString(app)
305 | console.log(esx.renderToString(app))
306 | ```
307 |
308 | Elements created with `esx` contain template information and can
309 | be used for high performance rendering, whereas a plain React element
310 | at the root could only ever be rendered with `ReactDomServer.renderToString`.
311 |
312 | That is why `esx.renderToString` *will throw* if passed a plain
313 | React element:
314 |
315 | ```js
316 | // * DON'T DO THIS!: *
317 | esx.renderToString(React.createElement('div')) // => throws Error
318 | // instead do this:
319 | esx.renderToString ``
320 | // or this:
321 | esx.renderToString(esx ``)
322 | ```
323 |
324 | ### Plugins
325 |
326 | Pre and Post plugins are also provided to allow for additional manipulation of templates and output. A Post plugin
327 | could be used to write output directly to a stream, or inject additional
328 | HTML.
329 |
330 | #### `esx.plugins.pre((strings, ...values) => [strings, values])`
331 |
332 | The `esx.plugins.pre` method registers a Pre plugin. Plugins
333 | will be executed in the order that there are registered.
334 |
335 | A Pre plugin should be passed a function that has the same signature
336 | as a [tagged template function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates).
337 |
338 | It must return an array containing two arrays. The first is an array of
339 | strings, the second is an array of values.
340 |
341 | The pre plugin can be used to reshape the strings array and/or apply mutations to the interpolated values. An example of a Pre plugin
342 | could be to apply a transform, turning an alternative template syntax
343 | (such as Pug) into ESX syntax.
344 |
345 | #### `esx.plugins.post((htmlString) => htmlString))`
346 |
347 | The `esx.plugins.post` method registers a Post plugin. Plugins
348 | will be executed in the order that there are registered. Unlike
349 | Pre plugins, Post plugins can only be used Server Side and will
350 | only be invoked for components that are rendered via `esx.renderToString`.
351 |
352 | A Post plugin is passed the HTML string output of a component
353 | at the time it is rendered. A Post plugin can be used to inject
354 | extra HTML, apply additional transforms to the HTML, or capture
355 | the HTML as each component is being rendered.
356 |
357 |
358 | ### SSR Options
359 |
360 | On the server side the Initializer has an `ssr` property, which
361 | has an `options` method. The follow options are supported:
362 |
363 | #### `createEsx.ssr.option('hooks-mode', 'compatible'|'stateful')`
364 |
365 | By default the `hooks-mode` option is `compatible` with React
366 | server side rendering. This means that any stateful hooks,
367 | e.g. `useState` and `useReducer` do not actually retain state
368 | between renders.
369 |
370 | The following will set `hooks-mode` to `stateful`:
371 |
372 | ```js
373 | createEsx.ssr.option('hooks-mode', 'stateful')
374 | ```
375 |
376 | This means that `useState`, `useReducer`, `useMemo` and
377 | `useCallback` have the same stateful behaviour as their
378 | client-side counterpart hooks. The state is retained
379 | between `renderToString` calls, instead of always returning
380 | the initial state as with `compatible` mode. This can be useful
381 | where a server-side render-to-hydrate strategy is employed and
382 | a great fit with rendering on server initialize.
383 |
384 |
385 | ## Contributions
386 |
387 | `esx` is an **OPEN Open Source Project**. This means that:
388 |
389 | > 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.
390 |
391 | See the [CONTRIBUTING.md](https://github.com/esxjs/esx/blob/master/CONTRIBUTING.md) file for more details.
392 |
393 | ## The Team
394 |
395 | ### David Mark Clements
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 | ## Prior Art
404 |
405 | ### ESX
406 | `esx` was preceded by... `esx`. The `esx` namespace was registered four years ago
407 | by a prior author [Mattéo Delabre ](https://github.com/matteodelabre), with a similar
408 | idea. He kindly donated the namespace to this particular manifestation of the idea.
409 | For this reason, `esx` versioning begins at v2.x.x. Versions 0.x.x and 1.x.x are deprecated.
410 |
411 | ### Hyperx
412 |
413 | `esx` is directly inspired by [`hyperx`](https://npm.im/hyperx), which
414 | was the first known library to this authors knowledge to make the point
415 | that template strings are perfect for generating both virtual doms
416 | and server side rendering. What `hyperx` lacks, however, is a way
417 | to represent React components within its template syntax. It is *only*
418 | for generating HTML nodes.
419 |
420 | ### HTM
421 |
422 | It's not uncommon for similar ideas to be had and implemented concurrently
423 | without either party knowing of the other.
424 | While [`htm`](https://github.com/developit/htm) was first released early 2019,
425 | work on `esx` had already been on-going some months prior. However the
426 | mission of `esx` is slightly broader, with a primary objective being to speed up
427 | server side rendering, so it took longer to release.
428 |
429 | ## License
430 |
431 | [MIT](./LICENSE)
432 |
433 | ## Sponsors
434 |
435 | * [nearForm](https://nearform.com)
436 |
--------------------------------------------------------------------------------
/test/create.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | /* eslint-env node */
3 | let test = require('aquatap')
4 | const renderer = require('react-test-renderer')
5 | const PropTypes = require('prop-types')
6 | const render = (o) => renderer.create(o).toJSON()
7 | const React = require('react')
8 | const { createElement } = React
9 | const init = process.env.TEST_CLIENT_CODE ? require('../browser') : require('..')
10 | const { MODE } = process.env
11 | if (typeof window === 'undefined') {
12 | if (!MODE) {
13 | process.env.MODE = 'development'
14 | const error = console.error.bind(console)
15 | console.error = (s, ...args) => {
16 | if (/Warning:/.test(s)) return
17 | return error(s, ...args)
18 | }
19 | delete require.cache[require.resolve(__filename)]
20 | require(__filename)
21 | test = () => {}
22 | test.only = test
23 | } else {
24 | process.env.NODE_ENV = MODE
25 | if (MODE === 'development') {
26 | process.nextTick(() => {
27 | process.env.MODE = 'production'
28 | delete require.cache[require.resolve(__filename)]
29 | require(__filename)
30 | })
31 | }
32 | }
33 | }
34 |
35 | test('components parameter must be a plain object or undefined', async ({ throws, doesNotThrow }) => {
36 | throws(() => init(null), Error('ESX: supplied components must be a plain object'))
37 | throws(() => init(() => {}), Error('ESX: supplied components must be a plain object'))
38 | throws(() => init([]), Error('ESX: supplied components must be a plain object'))
39 | throws(() => init(Symbol('test')), Error('ESX: supplied components must be a plain object'))
40 | throws(() => init(1), Error('ESX: supplied components must be a plain object'))
41 | throws(() => init('str'), Error('ESX: supplied components must be a plain object'))
42 | throws(() => init(new (class {})()))
43 | doesNotThrow(() => init())
44 | doesNotThrow(() => init(undefined))
45 | doesNotThrow(() => init({}))
46 | })
47 |
48 | test('components object must contain only uppercase property keys', async ({ throws, doesNotThrow }) => {
49 | throws(() => init({ component: () => {} }), Error(`ESX: component is not valid. All components should use PascalCase`))
50 | doesNotThrow(() => init({ Component: () => {} }))
51 | })
52 |
53 | test('components object values must be function,classes,symbols,strings or objects with a $$typeof key', async ({ throws, doesNotThrow }) => {
54 | throws(() => init({ Component: undefined }), Error(`ESX: Component is not a valid component`))
55 | throws(() => init({ Component: null }), Error(`ESX: Component is not a valid component`))
56 | throws(() => init({ Component: 1 }), Error(`ESX: Component is not a valid component`))
57 | throws(() => init({ Component: {} }), Error(`ESX: Component is not a valid component`))
58 | throws(() => init({ Component: [] }), Error(`ESX: Component is not a valid component`))
59 | doesNotThrow(() => init({ Component: Symbol('test') }))
60 | doesNotThrow(() => init({ Component: () => {} }))
61 | doesNotThrow(() => init({ Component: class {} }))
62 | doesNotThrow(() => init({ Component: { $$typeof: Symbol('test') } }))
63 | doesNotThrow(() => init({ Component: 'div' }))
64 | })
65 |
66 | test('register: components object values must be function,classes,symbols,strings or objects with a $$typeof key', async ({ throws, doesNotThrow }) => {
67 | throws(() => init().register({ Component: undefined }), Error(`ESX: Component is not a valid component`))
68 | throws(() => init().register({ Component: null }), Error(`ESX: Component is not a valid component`))
69 | throws(() => init().register({ Component: 1 }), Error(`ESX: Component is not a valid component`))
70 | throws(() => init().register({ Component: {} }), Error(`ESX: Component is not a valid component`))
71 | throws(() => init().register({ Component: [] }), Error(`ESX: Component is not a valid component`))
72 | doesNotThrow(() => init().register({ Component: Symbol('test') }))
73 | doesNotThrow(() => init().register({ Component: () => {} }))
74 | doesNotThrow(() => init().register({ Component: class {} }))
75 | doesNotThrow(() => init().register({ Component: { $$typeof: Symbol('test') } }))
76 | doesNotThrow(() => init().register({ Component: 'div' }))
77 | })
78 |
79 | test('register.lax: skips validation', async ({ doesNotThrow }) => {
80 | doesNotThrow(() => init().register.lax({ Component: undefined }))
81 | doesNotThrow(() => init().register.lax({ Component: null }))
82 | doesNotThrow(() => init().register.lax({ Component: 1 }))
83 | doesNotThrow(() => init().register.lax({ Component: {} }))
84 | doesNotThrow(() => init().register.lax({ Component: [] }))
85 | doesNotThrow(() => init().register.lax({ Component: Symbol('test') }))
86 | doesNotThrow(() => init().register.lax({ Component: () => {} }))
87 | doesNotThrow(() => init().register.lax({ Component: class {} }))
88 | doesNotThrow(() => init().register.lax({ Component: { $$typeof: Symbol('test') } }))
89 | doesNotThrow(() => init().register.lax({ Component: 'div' }))
90 | })
91 |
92 | test('register.one: components object values must be function,classes,symbols,strings or objects with a $$typeof key', async ({ throws, doesNotThrow }) => {
93 | throws(() => init().register.one('Component', undefined), Error(`ESX: Component is not a valid component`))
94 | throws(() => init().register.one('Component', null), Error(`ESX: Component is not a valid component`))
95 | throws(() => init().register.one('Component', 1), Error(`ESX: Component is not a valid component`))
96 | throws(() => init().register.one('Component', {}), Error(`ESX: Component is not a valid component`))
97 | throws(() => init().register.one('Component', []), Error(`ESX: Component is not a valid component`))
98 | doesNotThrow(() => init().register.one('Component', Symbol('test')))
99 | doesNotThrow(() => init().register.one('Component', () => {}))
100 | doesNotThrow(() => init().register.one('Component', class {}))
101 | doesNotThrow(() => init().register.one('Component', { $$typeof: Symbol('test') }))
102 | doesNotThrow(() => init().register.one('Component', 'div'))
103 | })
104 |
105 | test('register.one.lax: skips validation', async ({ doesNotThrow }) => {
106 | doesNotThrow(() => init().register.one.lax('Component', undefined))
107 | doesNotThrow(() => init().register.one.lax('Component', null))
108 | doesNotThrow(() => init().register.one.lax('Component', 'str'))
109 | doesNotThrow(() => init().register.one.lax('Component', 1))
110 | doesNotThrow(() => init().register.one.lax('Component', {}))
111 | doesNotThrow(() => init().register.one.lax('Component', []))
112 | doesNotThrow(() => init().register.one.lax('Component', Symbol('test')))
113 | doesNotThrow(() => init().register.one.lax('Component', () => {}))
114 | doesNotThrow(() => init().register.one.lax('Component', class {}))
115 | doesNotThrow(() => init().register.one.lax('Component', { $$typeof: Symbol('test') }))
116 | })
117 |
118 | test('all registrations throws if any component is using legacy context API', async ({ throws }) => {
119 | const esx = init()
120 | // simulate react-router 4 Link
121 | class Link extends React.Component {
122 | render () {
123 | return esx``
124 | }
125 | }
126 |
127 | Link.contextTypes = {
128 | router: PropTypes.shape({
129 | history: PropTypes.shape({
130 | push: PropTypes.func.isRequired,
131 | replace: PropTypes.func.isRequired,
132 | createHref: PropTypes.func.isRequired
133 | }).isRequired
134 | }).isRequired
135 | }
136 | const err = Error(`ESX: Link has a contextTypes property. Legacy context API is not supported – https://reactjs.org/docs/legacy-context.html`)
137 | throws(() => { init({ Link }) }, err)
138 | throws(() => { esx.register({ Link }) }, err)
139 | throws(() => { esx.register.lax({ Link }) }, err)
140 | throws(() => { esx.register.one('Link', Link) }, err)
141 | throws(() => { esx.register.one.lax('Link', Link) }, err)
142 | })
143 |
144 | test('empty string returns null', async ({ same }) => {
145 | const esx = init()
146 | same(render(esx``), null)
147 | })
148 |
149 | test('text at the root is ignored, returns null', async ({ same }) => {
150 | const esx = init()
151 | same(render(esx`ignore me`), null)
152 | })
153 |
154 | test('text outside elements is ignored', async ({ same }) => {
155 | const esx = init()
156 | same(render(esx`ignore me
`),
525 | render(createElement(
526 | 'div',
527 | null,
528 | createElement('div', null),
529 | createElement('p', null, 'hi')
530 | )
531 | )
532 | )
533 | })
534 |
535 | test('className', async ({ same }) => {
536 | const esx = init()
537 | same(render(esx``), render(createElement('img', { className: 'x' })))
538 | same(render(esx``), render(createElement('img', { className: 'x' })))
539 | })
540 |
541 | test('unexpected token, expression in open element', async ({ throws }) => {
542 | const esx = init()
543 | throws(() => esx``, SyntaxError('ESX: Unexpected token in element. Expressions may only be spread, embedded in attributes be included as children.'))
544 | })
545 |
546 | test('unexpected token, quotes around expression', async ({ throws }) => {
547 | const esx = init()
548 | throws(
549 | () => esx``,
550 | SyntaxError('Unexpected token. Attribute expressions must not be surrounded in quotes.')
551 | )
552 | })
553 |
554 | test('void elements must not have children', async ({ throws }) => {
555 | const esx = init()
556 | throws(() => esx`child`, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
557 | throws(() => esx`${'child'}`, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
558 | throws(() => esx`
hi
`, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
559 | esx.register({ Cmp () { return esx`
hi
` } })
560 | throws(() => esx``, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
561 | throws(() => esx``, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
562 | throws(() => esx``, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
563 | throws(() => esx``, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
564 | throws(() => esx``, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
565 | })
566 |
567 | test('void elements must not use dangerouslySetInnerHTML', async ({ throws }) => {
568 | const esx = init()
569 | throws(() => esx`no' }}/>`, SyntaxError('ESX: Void elements must not have children or use dangerouslySetInnerHTML.'))
570 | })
571 |
572 | test('elements can only have either children or dangerouslySetInnerHTML', async ({ throws }) => {
573 | const esx = init()
574 | throws(() => esx`
no' }}>
`, SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.'))
575 | throws(() => esx`
no' }} children='no'>
`, SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.'))
576 | throws(() => esx`
no' }}>${'no'}
`, SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.'))
577 | throws(() => esx`
no' }}>no
`, SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.'))
578 | throws(() => esx`
no' }} children='no'/>`, SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.'))
579 | throws(() => esx`
no' }} children=${'no'}/>`, SyntaxError('ESX: Can only set one of children or dangerouslySetInnerHTML.'))
580 | })
581 |
582 | test('unexpected token', async ({ throws }) => {
583 | const esx = init()
584 | const props = {}
585 | throws(() => esx``, SyntaxError('ESX: Unexpected token.'))
586 | })
587 |
588 | test('lack of component closing tag causes syntax error', async ({ throws }) => {
589 | const esx = init({ App: () => {} })
590 | throws(() => esx``, SyntaxError(`Expected corresponding ESX closing tag for `))
591 | throws(() => esx`
`, SyntaxError(`Expected corresponding ESX closing tag for `))
592 | })
593 |
594 | test('lack of closing tag for elements other than auto closing elements causes syntax error', async ({ throws }) => {
595 | const esx = init({ App: () => {} })
596 | throws(() => esx`
`, SyntaxError(`Expected corresponding ESX closing tag for
`))
597 | throws(() => esx`
`, SyntaxError(`Expected corresponding ESX closing tag for
`))
598 | })
599 |
600 | test('lack of closing tag for auto closing elements does not throw', async ({ doesNotThrow }) => {
601 | const esx = init({ App: ({ children }) => children })
602 | doesNotThrow(() => esx``, SyntaxError(`Expected corresponding ESX closing tag for `))
603 | doesNotThrow(() => esx``, SyntaxError(`Expected corresponding ESX closing tag for `))
604 | doesNotThrow(() => esx``, SyntaxError(`Expected corresponding ESX closing tag for `))
605 | doesNotThrow(() => esx``, SyntaxError(`Expected corresponding ESX closing tag for `))
606 | doesNotThrow(() => esx` `, SyntaxError(`Expected corresponding ESX closing tag for `))
607 | doesNotThrow(() => esx` `, SyntaxError(`Expected corresponding ESX closing tag for `))
608 | doesNotThrow(() => esx`
`, SyntaxError(`Expected corresponding ESX closing tag for
`))
609 | doesNotThrow(() => esx`
`, SyntaxError(`Expected corresponding ESX closing tag for