@dom-expressions/hyperscript 0.50.0-next.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/hyper-dom-expressions.js +189 -0
- package/lib/hyper-dom-expressions.js +191 -0
- package/package.json +24 -0
- package/types/index.d.ts +28 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# hyper-dom-expressions
|
|
2
|
+
|
|
3
|
+
## 0.50.0-next.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- df03fb8: Move all packages under the `@dom-expressions` npm scope with new names:
|
|
8
|
+
- `dom-expressions` → `@dom-expressions/runtime`
|
|
9
|
+
- `babel-plugin-jsx-dom-expressions` → `@dom-expressions/babel-plugin-jsx`
|
|
10
|
+
- `jsx-dom-expressions-compiler` → `@dom-expressions/jsx-compiler`
|
|
11
|
+
- `hyper-dom-expressions` → `@dom-expressions/hyperscript`
|
|
12
|
+
- `tagged-jsx-dom-expressions` → `@dom-expressions/tagged-jsx`
|
|
13
|
+
|
|
14
|
+
The old unscoped names stop receiving `next` prereleases and remain in use
|
|
15
|
+
only by the Solid 1.x maintenance line published from `main`.
|
|
16
|
+
|
|
17
|
+
`lit-dom-expressions` is dropped from the prerelease line; it has been
|
|
18
|
+
superseded by `@dom-expressions/tagged-jsx`.
|
|
19
|
+
|
|
20
|
+
`@dom-expressions/jsx-compiler` now distributes prebuilt native binaries
|
|
21
|
+
through per-platform packages (`@dom-expressions/jsx-compiler-darwin-x64`,
|
|
22
|
+
`-darwin-arm64`, `-linux-x64-gnu`, `-linux-arm64-gnu`, `-win32-x64-msvc`)
|
|
23
|
+
resolved automatically via `optionalDependencies`, instead of shipping a
|
|
24
|
+
binary inside the main package.
|
|
25
|
+
|
|
26
|
+
## 0.50.0-next.14
|
|
27
|
+
|
|
28
|
+
## 0.50.0-next.13
|
|
29
|
+
|
|
30
|
+
## 0.50.0-next.12
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 64e9aee: Delegated events are now owned by render roots instead of the document by default. `render()` installs and disposes its delegated listeners with the root, `delegateEvents()` now only declares event demand, and additional listener containers can be registered explicitly for framework features that render outside the root.
|
|
35
|
+
|
|
36
|
+
## 0.50.0-next.11
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- d5cd499: Remove `on:` namespace event support from compiler, runtime, JSX types, and renderer packages.
|
|
41
|
+
|
|
42
|
+
## 0.50.0-next.10
|
|
43
|
+
|
|
44
|
+
## 0.50.0-next.9
|
|
45
|
+
|
|
46
|
+
## 0.50.0-next.8
|
|
47
|
+
|
|
48
|
+
## 0.50.0-next.7
|
|
49
|
+
|
|
50
|
+
## 0.50.0-next.6
|
|
51
|
+
|
|
52
|
+
## 0.50.0-next.5
|
|
53
|
+
|
|
54
|
+
## 0.50.0-next.4
|
|
55
|
+
|
|
56
|
+
### Patch Changes
|
|
57
|
+
|
|
58
|
+
- ecd7803: Fix render-prop consumers re-mounting stable children on parent updates in hyperscript. `mapArray`-style helpers (`For`, `Index`, `Show`, etc.) and any third-party JSX-compiled component that re-invokes a callback prop with arguments store whatever the callback returns and re-flatten it on every parent change. With the lazy / tagged-thunk `h(...)` design, returning `h(Row, ...)` from such a callback would store a thunk — re-flattening would re-invoke it and re-run the row component, re-create its DOM, and fire its `onCleanup` even for rows the diff would have kept.
|
|
59
|
+
|
|
60
|
+
`h(...)` now materializes any tagged thunks that function props with arity ≥ 1 return — covering 1-arity render-callbacks (`children: row => h(Row, …)`, `header: tab => h(Tab, …)`), 2-arity `mapArray` row callbacks (`(item, index) => h(Row, …)`), event handlers, and higher-arity callbacks alike — so what the consumer stores is already the rendered tree, matching what JSX-compiled call sites produce. Behavior of zero-arity function props (`dynamicProperty` wrap) is unchanged. Arity (so consumers that introspect `cb.length`, e.g. `mapArray` deciding whether to allocate an index signal, see the original signature), `this`-binding when forwarded as a DOM event handler, and identity once the wrap is in place are all preserved. The wrap is idempotent across nested components.
|
|
61
|
+
|
|
62
|
+
## 0.50.0-next.3
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- 4c4fb65: Fix ownership leak when composing control-flow components (`For`, `Show`, etc.) in hyperscript (solidjs/solid#2453). `h(...)` now returns a tagged zero-arity thunk whose body runs under `untrack`, so render effects created inside a child component — notably per-row effects created by `mapArray` inside `For` — are rooted under the child's owner instead of whichever `r.insert` effect happens to consume it. Mutating a parent list signal no longer disposes sibling rows' render effects.
|
|
67
|
+
|
|
68
|
+
The exported surface changes shape: `h(...)` previously returned a DOM node (or array) eagerly; it now returns a thunk you invoke. Recommended usage is `r.render(h(App), mountEl)` — `render` calls the thunk inside its root. Nested `h(...)` calls compose freely; tagged thunks auto-invoke at consumption and user accessors (`() => expr`) continue to route through `r.insert`. `props.children` is uniformly wrapped via `dynamicProperty` the same way other zero-arity function props are, matching Solid JSX's getter convention — consumers that want reactive re-reads wrap the access in a thunk (`h("p", () => props.children)`).
|
|
69
|
+
|
|
70
|
+
- 4dae801: Normalize the `repository` field in every package to the standard npm
|
|
71
|
+
convention: a `git+https://github.com/ryansolid/dom-expressions.git` URL
|
|
72
|
+
with a `directory` pointing at the package within the monorepo. Restores
|
|
73
|
+
"View source" / "Open in repo" links on the npm registry and unblocks
|
|
74
|
+
tooling that resolves source from package metadata.
|
|
75
|
+
|
|
76
|
+
## 0.50.0-next.2
|
|
77
|
+
|
|
78
|
+
### Patch Changes
|
|
79
|
+
|
|
80
|
+
- 39c207c: Fix a SyntaxError when an element has 222+ merged dynamic attributes
|
|
81
|
+
(solidjs/solid#2682). The internal identifier generator produced `in` at
|
|
82
|
+
index 221, and since these identifiers are emitted as object shorthand
|
|
83
|
+
destructuring bindings, the resulting `({ …, in }) => …` could not be parsed.
|
|
84
|
+
`getNumberedId` now shifts past any natural index that would encode to a JS
|
|
85
|
+
reserved word, keeping the mapping injective and the output at 2 characters
|
|
86
|
+
for all practical dynamic counts.
|
|
87
|
+
- 305d9ce: - SSR: Duplicate attributes in JSX without spreads are now deduplicated —
|
|
88
|
+
`<div class="a" class="b" />` correctly renders as `<div class="b" />`
|
|
89
|
+
(last-wins), matching client behavior. Previously the compiler kept both
|
|
90
|
+
attributes in the output.
|
|
91
|
+
- Client: `setAttributeNS` / `removeAttributeNS` now use matching names when
|
|
92
|
+
clearing namespaced attributes (e.g. `xlink:href`). Previously removal could
|
|
93
|
+
leave the attribute in place because it used the local name while the set
|
|
94
|
+
used the qualified name.
|
|
95
|
+
- Expanded test coverage across all four packages; no other behavior changes.
|
|
96
|
+
|
|
97
|
+
## 0.50.0-next.1
|
|
98
|
+
|
|
99
|
+
### Patch Changes
|
|
100
|
+
|
|
101
|
+
- ee365e0: - `insert()` accepts an optional 5th `options` argument that is forwarded to the
|
|
102
|
+
internal `effect()` call, letting callers (e.g. Solid's `render()`) opt into
|
|
103
|
+
transition-aware initial mounts without otherwise changing `insert`'s
|
|
104
|
+
behavior.
|
|
105
|
+
- SSR: `$dflj(ids)` now materializes every id in the list in a single call
|
|
106
|
+
instead of stopping after the first successful `$dfl`. Callers pass only the
|
|
107
|
+
keys they intend to materialize, which simplifies the primitive and composes
|
|
108
|
+
cleanly for bulk-uncollapse cases (e.g. a group activation revealing several
|
|
109
|
+
held fallbacks at once).
|
|
110
|
+
- SSR: Fix cascading async root holes in the streaming shell. When an inner
|
|
111
|
+
Loading boundary resolved its first chunk while the outer shell was still
|
|
112
|
+
pending, `flushEnd` could call `serializer.flush()` before `doShell()` had
|
|
113
|
+
written the root `_assets` module map, causing seroval to silently drop the
|
|
114
|
+
writes and client hydration to fail with "module was not preloaded". Root
|
|
115
|
+
asset serialization is now memoized and gated on both paths.
|
|
116
|
+
- Type formatting cleanup in `jsx-properties.d.ts`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Ryan Carniato
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Hyper DOM Expressions
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ryansolid/dom-expressions/actions/workflows/main-ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@dom-expressions/hyperscript)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
HyperScript DSL for [DOM Expressions](https://github.com/ryansolid/dom-expressions), targeting fine-grained reactive libraries that want a no-build authoring syntax.
|
|
9
|
+
|
|
10
|
+
> **Performance note.** Of the DOM-expressions frontends, hyperscript is the **slowest**. Every `h(...)` call materializes a small tree at runtime, versus the precompiled constants and cloned templates emitted by [`@dom-expressions/babel-plugin-jsx`](https://github.com/ryansolid/dom-expressions/blob/main/packages/babel-plugin-jsx), or the parse-once-then-clone caching of [`@dom-expressions/tagged-jsx`](https://github.com/ryansolid/dom-expressions/blob/main/packages/tagged-jsx). Both hyper and tagged-jsx run with no build step — if you just want a no-tooling authoring syntax, prefer `@dom-expressions/tagged-jsx`; it's considerably faster. Hyperscript's niche is interop with React-style JSX transforms and other ecosystems that already emit `h(tag, props, …children)` calls.
|
|
11
|
+
|
|
12
|
+
## Compatible Libraries
|
|
13
|
+
|
|
14
|
+
- [Solid](https://github.com/ryansolid/solid)
|
|
15
|
+
- [ko-jsx](https://github.com/ryansolid/ko-jsx)
|
|
16
|
+
- [mobx-jsx](https://github.com/ryansolid/mobx-jsx)
|
|
17
|
+
|
|
18
|
+
## Getting Started
|
|
19
|
+
|
|
20
|
+
Install alongside DOM Expressions and a reactive library. For Solid:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @solidjs/signals dom-expressions @dom-expressions/hyperscript
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Initialize `h` against the runtime:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { createHyperScript } from "@dom-expressions/hyperscript";
|
|
30
|
+
import * as r from "dom-expressions/src/client";
|
|
31
|
+
|
|
32
|
+
const h = createHyperScript(r);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Consumers typically re-export a pre-wired `h` — Solid exposes `solid-js/h`.
|
|
36
|
+
|
|
37
|
+
## The `h` contract
|
|
38
|
+
|
|
39
|
+
`h(...)` is **lazy**. Every call returns a zero-arity thunk tagged with an internal symbol; the thunk materializes DOM (or invokes the component) under the current reactive owner when called. Laziness is what keeps per-row render effects inside `For`/`mapArray` rooted under their own owners rather than the parent `insert` effect.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const tree = h("div", h(Counter)); // thunk
|
|
43
|
+
tree(); // materializes DOM
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Mount via `r.render`. Pass the thunk directly; `render` invokes it inside its root so the whole tree materializes under that owner:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import { render } from "dom-expressions/src/client";
|
|
50
|
+
|
|
51
|
+
const App = () => h("main", h(Home));
|
|
52
|
+
|
|
53
|
+
render(h(App), document.getElementById("app"));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Inside `h(...)` composition works without ceremony: nested `h(...)` children are invoked once at consumption, and user-supplied accessors (`() => expr`) route through `r.insert` so they stay reactive.
|
|
57
|
+
|
|
58
|
+
## Components, props, and children
|
|
59
|
+
|
|
60
|
+
- **Props are uniform.** Zero-arity function props are routed through `dynamicProperty` so reading them invokes the accessor and returns the current value — the same getter-style convention Solid's JSX compiler produces. Function props with arity ≥ 1 (render-callbacks like `children: row => h(Row, …)`, `header: tab => h(Tab, …)`, a `mapArray` `(item, index) => h(Row, …)` row callback, event handlers like `onClick: e => …`, etc.) are wrapped so any nested `h(...)` thunks in the return value are materialized at the call site, matching what JSX-compiled call sites store; this is what keeps `mapArray`-style consumers (`For`, `Index`, `Show`, etc.) from re-running stable row components on list mutations and gives the same property to any third-party JSX-compiled component that re-invokes a callback prop with arguments. Arity (so `cb.length` introspection still works), `this`-binding, and identity once the wrap is in place are all preserved; the wrap is idempotent across nested components.
|
|
61
|
+
|
|
62
|
+
> **Footgun (events on components).** Because zero-arity function props on components are wrapped as getters, an event handler written `onClick: () => doStuff()` is invoked at render time and its return value (`undefined`) becomes the prop — the click never fires. Take the unused argument to mark it as a 1-arity callback: `onClick: e => doStuff()`. The same goes for any other component prop you want passed by reference. Solid's `@solidjs/h` README documents this same caveat.
|
|
63
|
+
|
|
64
|
+
- **`props.children`** mirrors the caller's input:
|
|
65
|
+
|
|
66
|
+
| call shape | `props.children` |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `h(Comp)` | `undefined` |
|
|
69
|
+
| `h(Comp, { children: v })` | `v` |
|
|
70
|
+
| `h(Comp, null, a)` | `a` |
|
|
71
|
+
| `h(Comp, null, a, b, c)` | `[a, b, c]` |
|
|
72
|
+
|
|
73
|
+
Nested `h(...)` thunks flow through as-is and auto-invoke once when consumed.
|
|
74
|
+
|
|
75
|
+
- **Reactive consumption.** `h("p", props.children)` reads `props.children` once at render time. For reactive re-reads, the consumer wraps in its own accessor: `h("p", () => props.children)`. `r.insert` tracks the read and re-runs on change. This mirrors Solid JSX, which compiles `{props.children}` to `insert(el, () => props.children)`.
|
|
76
|
+
|
|
77
|
+
- **Fragments** are either plain arrays (`[h(...), h(...)]`) or the built-in `h.Fragment` component.
|
|
78
|
+
|
|
79
|
+
- **JSX-compiler interop.** Components compiled by `@dom-expressions/babel-plugin-jsx` can be invoked from inside `h(...)`. Their bodies see the same `props` shape they expect from compiled call sites (getters for dynamic props, `children` as a value / function / array), so typical library components (e.g. Solid Router routes) work. The reverse direction — passing a hyperscript thunk to compiled call sites expecting an element — is not supported.
|
|
80
|
+
|
|
81
|
+
## Example
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { createHyperScript } from "@dom-expressions/hyperscript";
|
|
85
|
+
import * as r from "dom-expressions/src/client";
|
|
86
|
+
import { createSignal, mapArray } from "@solidjs/signals";
|
|
87
|
+
|
|
88
|
+
const h = createHyperScript(r);
|
|
89
|
+
|
|
90
|
+
const For = props => mapArray(() => props.each, props.children);
|
|
91
|
+
|
|
92
|
+
const App = () => {
|
|
93
|
+
const [rows, setRows] = createSignal([
|
|
94
|
+
{ id: 1, label: "one" },
|
|
95
|
+
{ id: 2, label: "two" }
|
|
96
|
+
]);
|
|
97
|
+
return h(
|
|
98
|
+
"table.table",
|
|
99
|
+
h(
|
|
100
|
+
"tbody",
|
|
101
|
+
h(For, { each: rows }, row =>
|
|
102
|
+
h(
|
|
103
|
+
"tr",
|
|
104
|
+
h("td.col-md-1", () => row().id),
|
|
105
|
+
h("td.col-md-4", () => row().label)
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
r.render(h(App), document.getElementById("main"));
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Differences from JSX
|
|
116
|
+
|
|
117
|
+
- Refs are passed as a function prop (`ref: el => { … }`).
|
|
118
|
+
- Reactivity is explicit: wrap expressions in a function when they should be tracked (`() => count()`), including when forwarding a component prop (`h("p", () => props.foo)`).
|
|
119
|
+
- Fragments are arrays (or `h.Fragment`).
|
|
120
|
+
- Tag selectors understand `#id` and `.class` shorthands (`h("div#main.sel", …)`).
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Tags h() thunks so consumers can distinguish them from user-written
|
|
2
|
+
// accessors and invoke them once rather than wrapping them in an effect.
|
|
3
|
+
const $ELEMENT = Symbol("hyper-element");
|
|
4
|
+
// Marks callback props that have already been wrapped (see the
|
|
5
|
+
// prop-loop below) so re-passing them through nested `h(Comp, ...)`
|
|
6
|
+
// calls is idempotent — important for identity stability across
|
|
7
|
+
// component boundaries.
|
|
8
|
+
const $WRAPPED = Symbol("hyper-wrapped");
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// Recursively invokes h() thunks (and walks arrays containing them) so a
|
|
21
|
+
// callback's return value lands in the consumer pre-rendered.
|
|
22
|
+
function resolveThunks(value) {
|
|
23
|
+
if (typeof value === "function" && value[$ELEMENT]) return resolveThunks(value());
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
const out = new Array(value.length);
|
|
26
|
+
for (let i = 0; i < value.length; i++) out[i] = resolveThunks(value[i]);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Common arities (1 and 2) are hand-shaped so the wrapped function has
|
|
33
|
+
// the right native `length` without paying for `defineProperty`. 1-arity
|
|
34
|
+
// covers most render-props and 1-arity event handlers; 2-arity covers
|
|
35
|
+
// `mapArray` row callbacks that take an index. Anything higher falls
|
|
36
|
+
// back to rest-args + `apply` and a one-shot `defineProperty`.
|
|
37
|
+
function wrapCallback(orig) {
|
|
38
|
+
let w;
|
|
39
|
+
if (orig.length === 1) {
|
|
40
|
+
w = function (a) {
|
|
41
|
+
return resolveThunks(orig.call(this, a));
|
|
42
|
+
};
|
|
43
|
+
} else if (orig.length === 2) {
|
|
44
|
+
w = function (a, b) {
|
|
45
|
+
return resolveThunks(orig.call(this, a, b));
|
|
46
|
+
};
|
|
47
|
+
} else {
|
|
48
|
+
w = function (...args) {
|
|
49
|
+
return resolveThunks(orig.apply(this, args));
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(w, "length", { value: orig.length });
|
|
52
|
+
}
|
|
53
|
+
w[$WRAPPED] = true;
|
|
54
|
+
return w;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Inspired by https://github.com/hyperhype/hyperscript
|
|
58
|
+
function createHyperScript(r) {
|
|
59
|
+
function h(...rawArgs) {
|
|
60
|
+
if (rawArgs.length === 1 && Array.isArray(rawArgs[0])) return rawArgs[0];
|
|
61
|
+
const thunk = () => r.untrack(() => materialize(rawArgs));
|
|
62
|
+
thunk[$ELEMENT] = true;
|
|
63
|
+
return thunk;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function materialize(args) {
|
|
67
|
+
let e;
|
|
68
|
+
let classes = [];
|
|
69
|
+
let multiExpression = false;
|
|
70
|
+
args = args.slice();
|
|
71
|
+
|
|
72
|
+
function item(l) {
|
|
73
|
+
if (l == null) return;
|
|
74
|
+
const type = typeof l;
|
|
75
|
+
if ("string" === type) {
|
|
76
|
+
if (!e) parseClass(l);else
|
|
77
|
+
e.appendChild(document.createTextNode(l));
|
|
78
|
+
} else if (
|
|
79
|
+
"number" === type ||
|
|
80
|
+
"boolean" === type ||
|
|
81
|
+
"bigint" === type ||
|
|
82
|
+
"symbol" === type ||
|
|
83
|
+
l instanceof Date ||
|
|
84
|
+
l instanceof RegExp)
|
|
85
|
+
{
|
|
86
|
+
e.appendChild(document.createTextNode(l.toString()));
|
|
87
|
+
} else if (Array.isArray(l)) {
|
|
88
|
+
for (let i = 0; i < l.length; i++) item(l[i]);
|
|
89
|
+
} else if (l instanceof Element) {
|
|
90
|
+
r.insert(e, l, multiExpression ? null : undefined);
|
|
91
|
+
} else if ("object" === type) {
|
|
92
|
+
let dynamic = false;
|
|
93
|
+
const d = Object.getOwnPropertyDescriptors(l);
|
|
94
|
+
for (const k in d) {
|
|
95
|
+
if (k === "class" && classes.length !== 0) {
|
|
96
|
+
const value =
|
|
97
|
+
typeof d["class"].value === "function" ?
|
|
98
|
+
() => [...classes, d["class"].value()] :
|
|
99
|
+
[...classes, l["class"]];
|
|
100
|
+
Object.defineProperty(l, "class", { ...d[k], value });
|
|
101
|
+
classes = [];
|
|
102
|
+
}
|
|
103
|
+
if (k !== "ref" && k.slice(0, 2) !== "on" && typeof d[k].value === "function") {
|
|
104
|
+
r.dynamicProperty(l, k);
|
|
105
|
+
dynamic = true;
|
|
106
|
+
} else if (d[k].get) dynamic = true;
|
|
107
|
+
}
|
|
108
|
+
dynamic ?
|
|
109
|
+
r.spread(e, l, !!args.length) :
|
|
110
|
+
r.assign(e, l, !!args.length);
|
|
111
|
+
} else if ("function" === type) {
|
|
112
|
+
if (!e) {
|
|
113
|
+
const first = args[0];
|
|
114
|
+
const props =
|
|
115
|
+
first == null ||
|
|
116
|
+
typeof first === "object" && !Array.isArray(first) && !(first instanceof Element) ?
|
|
117
|
+
args.shift() || {} :
|
|
118
|
+
{};
|
|
119
|
+
if (args.length) props.children = args.length > 1 ? args : args[0];
|
|
120
|
+
// Zero-arity props become getters (JSX-getter parity).
|
|
121
|
+
// Higher-arity callbacks get wrapped so any tagged thunks
|
|
122
|
+
// they return are materialized at the call site — otherwise
|
|
123
|
+
// a render-prop consumer (`mapArray`-style `For`/`Index`,
|
|
124
|
+
// any third-party JSX-compiled component that re-invokes a
|
|
125
|
+
// callback with arguments) would store the raw thunk and
|
|
126
|
+
// re-invoke it on every parent change, re-mounting stable
|
|
127
|
+
// children. `this` is preserved via `.call`/`.apply`; arity
|
|
128
|
+
// is preserved so consumers that introspect `cb.length`
|
|
129
|
+
// (e.g. `mapArray` deciding whether to allocate an index
|
|
130
|
+
// signal) see the original signature.
|
|
131
|
+
for (const k in props) {
|
|
132
|
+
const v = props[k];
|
|
133
|
+
if (typeof v === "function") {
|
|
134
|
+
if (!v.length) r.dynamicProperty(props, k);else
|
|
135
|
+
if (!v[$ELEMENT] && !v[$WRAPPED]) {
|
|
136
|
+
props[k] = wrapCallback(v);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
e = r.createComponent(l, props);
|
|
141
|
+
// Drain nested h() thunks so downstream sees the real render result.
|
|
142
|
+
while (typeof e === "function" && e[$ELEMENT]) e = e();
|
|
143
|
+
args = [];
|
|
144
|
+
} else if (l[$ELEMENT]) item(l());else
|
|
145
|
+
r.insert(e, l, multiExpression ? null : undefined);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof args[0] === "string") detectMultiExpression(args);
|
|
150
|
+
while (args.length) item(args.shift());
|
|
151
|
+
if (e instanceof Element && classes.length) e.classList.add(...classes);
|
|
152
|
+
return e;
|
|
153
|
+
|
|
154
|
+
function parseClass(string) {
|
|
155
|
+
// Does not understand escaped CSS specials; see
|
|
156
|
+
// https://mathiasbynens.be/notes/css-escapes.
|
|
157
|
+
const m = string.split(/([\.#]?[^\s#.]+)/);
|
|
158
|
+
if (/^\.|#/.test(m[1])) e = document.createElement("div");
|
|
159
|
+
for (let i = 0; i < m.length; i++) {
|
|
160
|
+
const v = m[i],
|
|
161
|
+
s = v.substring(1, v.length);
|
|
162
|
+
if (!v) continue;
|
|
163
|
+
if (!e)
|
|
164
|
+
e = r.SVGElements.has(v) ?
|
|
165
|
+
document.createElementNS(r.Namespaces.svg, v) :
|
|
166
|
+
r.MathMLElements.has(v) ?
|
|
167
|
+
document.createElementNS(r.Namespaces.mathml, v) :
|
|
168
|
+
document.createElement(v);else
|
|
169
|
+
if (v[0] === ".") classes.push(s);else
|
|
170
|
+
if (v[0] === "#") e.setAttribute("id", s);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function detectMultiExpression(list) {
|
|
174
|
+
for (let i = 1; i < list.length; i++) {
|
|
175
|
+
if (typeof list[i] === "function") {
|
|
176
|
+
multiExpression = true;
|
|
177
|
+
return;
|
|
178
|
+
} else if (Array.isArray(list[i])) {
|
|
179
|
+
detectMultiExpression(list[i]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
h.Fragment = (props) => props.children;
|
|
186
|
+
return h;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { createHyperScript };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tags h() thunks so consumers can distinguish them from user-written
|
|
4
|
+
// accessors and invoke them once rather than wrapping them in an effect.
|
|
5
|
+
const $ELEMENT = Symbol("hyper-element");
|
|
6
|
+
// Marks callback props that have already been wrapped (see the
|
|
7
|
+
// prop-loop below) so re-passing them through nested `h(Comp, ...)`
|
|
8
|
+
// calls is idempotent — important for identity stability across
|
|
9
|
+
// component boundaries.
|
|
10
|
+
const $WRAPPED = Symbol("hyper-wrapped");
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// Recursively invokes h() thunks (and walks arrays containing them) so a
|
|
23
|
+
// callback's return value lands in the consumer pre-rendered.
|
|
24
|
+
function resolveThunks(value) {
|
|
25
|
+
if (typeof value === "function" && value[$ELEMENT]) return resolveThunks(value());
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
const out = new Array(value.length);
|
|
28
|
+
for (let i = 0; i < value.length; i++) out[i] = resolveThunks(value[i]);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Common arities (1 and 2) are hand-shaped so the wrapped function has
|
|
35
|
+
// the right native `length` without paying for `defineProperty`. 1-arity
|
|
36
|
+
// covers most render-props and 1-arity event handlers; 2-arity covers
|
|
37
|
+
// `mapArray` row callbacks that take an index. Anything higher falls
|
|
38
|
+
// back to rest-args + `apply` and a one-shot `defineProperty`.
|
|
39
|
+
function wrapCallback(orig) {
|
|
40
|
+
let w;
|
|
41
|
+
if (orig.length === 1) {
|
|
42
|
+
w = function (a) {
|
|
43
|
+
return resolveThunks(orig.call(this, a));
|
|
44
|
+
};
|
|
45
|
+
} else if (orig.length === 2) {
|
|
46
|
+
w = function (a, b) {
|
|
47
|
+
return resolveThunks(orig.call(this, a, b));
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
w = function (...args) {
|
|
51
|
+
return resolveThunks(orig.apply(this, args));
|
|
52
|
+
};
|
|
53
|
+
Object.defineProperty(w, "length", { value: orig.length });
|
|
54
|
+
}
|
|
55
|
+
w[$WRAPPED] = true;
|
|
56
|
+
return w;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Inspired by https://github.com/hyperhype/hyperscript
|
|
60
|
+
function createHyperScript(r) {
|
|
61
|
+
function h(...rawArgs) {
|
|
62
|
+
if (rawArgs.length === 1 && Array.isArray(rawArgs[0])) return rawArgs[0];
|
|
63
|
+
const thunk = () => r.untrack(() => materialize(rawArgs));
|
|
64
|
+
thunk[$ELEMENT] = true;
|
|
65
|
+
return thunk;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function materialize(args) {
|
|
69
|
+
let e;
|
|
70
|
+
let classes = [];
|
|
71
|
+
let multiExpression = false;
|
|
72
|
+
args = args.slice();
|
|
73
|
+
|
|
74
|
+
function item(l) {
|
|
75
|
+
if (l == null) return;
|
|
76
|
+
const type = typeof l;
|
|
77
|
+
if ("string" === type) {
|
|
78
|
+
if (!e) parseClass(l);else
|
|
79
|
+
e.appendChild(document.createTextNode(l));
|
|
80
|
+
} else if (
|
|
81
|
+
"number" === type ||
|
|
82
|
+
"boolean" === type ||
|
|
83
|
+
"bigint" === type ||
|
|
84
|
+
"symbol" === type ||
|
|
85
|
+
l instanceof Date ||
|
|
86
|
+
l instanceof RegExp)
|
|
87
|
+
{
|
|
88
|
+
e.appendChild(document.createTextNode(l.toString()));
|
|
89
|
+
} else if (Array.isArray(l)) {
|
|
90
|
+
for (let i = 0; i < l.length; i++) item(l[i]);
|
|
91
|
+
} else if (l instanceof Element) {
|
|
92
|
+
r.insert(e, l, multiExpression ? null : undefined);
|
|
93
|
+
} else if ("object" === type) {
|
|
94
|
+
let dynamic = false;
|
|
95
|
+
const d = Object.getOwnPropertyDescriptors(l);
|
|
96
|
+
for (const k in d) {
|
|
97
|
+
if (k === "class" && classes.length !== 0) {
|
|
98
|
+
const value =
|
|
99
|
+
typeof d["class"].value === "function" ?
|
|
100
|
+
() => [...classes, d["class"].value()] :
|
|
101
|
+
[...classes, l["class"]];
|
|
102
|
+
Object.defineProperty(l, "class", { ...d[k], value });
|
|
103
|
+
classes = [];
|
|
104
|
+
}
|
|
105
|
+
if (k !== "ref" && k.slice(0, 2) !== "on" && typeof d[k].value === "function") {
|
|
106
|
+
r.dynamicProperty(l, k);
|
|
107
|
+
dynamic = true;
|
|
108
|
+
} else if (d[k].get) dynamic = true;
|
|
109
|
+
}
|
|
110
|
+
dynamic ?
|
|
111
|
+
r.spread(e, l, !!args.length) :
|
|
112
|
+
r.assign(e, l, !!args.length);
|
|
113
|
+
} else if ("function" === type) {
|
|
114
|
+
if (!e) {
|
|
115
|
+
const first = args[0];
|
|
116
|
+
const props =
|
|
117
|
+
first == null ||
|
|
118
|
+
typeof first === "object" && !Array.isArray(first) && !(first instanceof Element) ?
|
|
119
|
+
args.shift() || {} :
|
|
120
|
+
{};
|
|
121
|
+
if (args.length) props.children = args.length > 1 ? args : args[0];
|
|
122
|
+
// Zero-arity props become getters (JSX-getter parity).
|
|
123
|
+
// Higher-arity callbacks get wrapped so any tagged thunks
|
|
124
|
+
// they return are materialized at the call site — otherwise
|
|
125
|
+
// a render-prop consumer (`mapArray`-style `For`/`Index`,
|
|
126
|
+
// any third-party JSX-compiled component that re-invokes a
|
|
127
|
+
// callback with arguments) would store the raw thunk and
|
|
128
|
+
// re-invoke it on every parent change, re-mounting stable
|
|
129
|
+
// children. `this` is preserved via `.call`/`.apply`; arity
|
|
130
|
+
// is preserved so consumers that introspect `cb.length`
|
|
131
|
+
// (e.g. `mapArray` deciding whether to allocate an index
|
|
132
|
+
// signal) see the original signature.
|
|
133
|
+
for (const k in props) {
|
|
134
|
+
const v = props[k];
|
|
135
|
+
if (typeof v === "function") {
|
|
136
|
+
if (!v.length) r.dynamicProperty(props, k);else
|
|
137
|
+
if (!v[$ELEMENT] && !v[$WRAPPED]) {
|
|
138
|
+
props[k] = wrapCallback(v);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
e = r.createComponent(l, props);
|
|
143
|
+
// Drain nested h() thunks so downstream sees the real render result.
|
|
144
|
+
while (typeof e === "function" && e[$ELEMENT]) e = e();
|
|
145
|
+
args = [];
|
|
146
|
+
} else if (l[$ELEMENT]) item(l());else
|
|
147
|
+
r.insert(e, l, multiExpression ? null : undefined);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof args[0] === "string") detectMultiExpression(args);
|
|
152
|
+
while (args.length) item(args.shift());
|
|
153
|
+
if (e instanceof Element && classes.length) e.classList.add(...classes);
|
|
154
|
+
return e;
|
|
155
|
+
|
|
156
|
+
function parseClass(string) {
|
|
157
|
+
// Does not understand escaped CSS specials; see
|
|
158
|
+
// https://mathiasbynens.be/notes/css-escapes.
|
|
159
|
+
const m = string.split(/([\.#]?[^\s#.]+)/);
|
|
160
|
+
if (/^\.|#/.test(m[1])) e = document.createElement("div");
|
|
161
|
+
for (let i = 0; i < m.length; i++) {
|
|
162
|
+
const v = m[i],
|
|
163
|
+
s = v.substring(1, v.length);
|
|
164
|
+
if (!v) continue;
|
|
165
|
+
if (!e)
|
|
166
|
+
e = r.SVGElements.has(v) ?
|
|
167
|
+
document.createElementNS(r.Namespaces.svg, v) :
|
|
168
|
+
r.MathMLElements.has(v) ?
|
|
169
|
+
document.createElementNS(r.Namespaces.mathml, v) :
|
|
170
|
+
document.createElement(v);else
|
|
171
|
+
if (v[0] === ".") classes.push(s);else
|
|
172
|
+
if (v[0] === "#") e.setAttribute("id", s);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function detectMultiExpression(list) {
|
|
176
|
+
for (let i = 1; i < list.length; i++) {
|
|
177
|
+
if (typeof list[i] === "function") {
|
|
178
|
+
multiExpression = true;
|
|
179
|
+
return;
|
|
180
|
+
} else if (Array.isArray(list[i])) {
|
|
181
|
+
detectMultiExpression(list[i]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
h.Fragment = (props) => props.children;
|
|
188
|
+
return h;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
exports.createHyperScript = createHyperScript;
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dom-expressions/hyperscript",
|
|
3
|
+
"description": "A Fine-Grained Rendering Runtime API using HyperScript",
|
|
4
|
+
"version": "0.50.0-next.15",
|
|
5
|
+
"author": "Ryan Carniato",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ryansolid/dom-expressions.git",
|
|
10
|
+
"directory": "packages/hyperscript"
|
|
11
|
+
},
|
|
12
|
+
"main": "lib/hyper-dom-expressions.js",
|
|
13
|
+
"module": "dist/hyper-dom-expressions.js",
|
|
14
|
+
"types": "types/index.d.ts",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@dom-expressions/runtime": "0.50.0-next.15"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "rollup -c --bundleConfigAsCjs && tsc",
|
|
21
|
+
"test": "pnpm run build && jest",
|
|
22
|
+
"test:coverage": "pnpm run build && jest --coverage"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type MountableElement = Element | Document | ShadowRoot | DocumentFragment | Node;
|
|
2
|
+
interface Runtime {
|
|
3
|
+
insert(parent: MountableElement, accessor: any, marker?: Node | null, init?: any): any;
|
|
4
|
+
spread(node: Element, accessor: any, skipChildren?: Boolean): void;
|
|
5
|
+
assign(node: Element, props: any, skipChildren?: Boolean): void;
|
|
6
|
+
createComponent(Comp: (props: any) => any, props: any): any;
|
|
7
|
+
dynamicProperty(props: any, key: string): any;
|
|
8
|
+
untrack<T>(fn: () => T): T;
|
|
9
|
+
SVGElements: Set<string>;
|
|
10
|
+
MathMLElements: Set<string>;
|
|
11
|
+
Namespaces: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
type ExpandableNode = Node & {
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
};
|
|
16
|
+
declare const $ELEMENT: unique symbol;
|
|
17
|
+
export type HyperElement = {
|
|
18
|
+
(): ExpandableNode | ExpandableNode[];
|
|
19
|
+
[$ELEMENT]: true;
|
|
20
|
+
};
|
|
21
|
+
export type HyperScript = {
|
|
22
|
+
(...args: any[]): HyperElement | ExpandableNode[];
|
|
23
|
+
Fragment: (props: {
|
|
24
|
+
children: any;
|
|
25
|
+
}) => any;
|
|
26
|
+
};
|
|
27
|
+
export declare function createHyperScript(r: Runtime): HyperScript;
|
|
28
|
+
export {};
|