hyper-dom-expressions 0.50.0-next.2 → 0.50.0-next.3

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # hyper-dom-expressions
2
2
 
3
+ ## 0.50.0-next.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
9
+ 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)`).
10
+
11
+ - 4dae801: Normalize the `repository` field in every package to the standard npm
12
+ convention: a `git+https://github.com/ryansolid/dom-expressions.git` URL
13
+ with a `directory` pointing at the package within the monorepo. Restores
14
+ "View source" / "Open in repo" links on the npm registry and unblocks
15
+ tooling that resolves source from package metadata.
16
+
3
17
  ## 0.50.0-next.2
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -5,66 +5,114 @@
5
5
  ![](https://img.shields.io/bundlephobia/minzip/hyper-dom-expressions.svg?style=flat)
6
6
  ![](https://img.shields.io/npm/dt/hyper-dom-expressions.svg?style=flat)
7
7
 
8
- This package is a Runtime API built for [DOM Expressions](https://github.com/ryansolid/dom-expressions) to provide HyperScript DSL for reactive libraries that do fine grained change detection. While the JSX plugin [Babel Plugin JSX DOM Expressions](https://github.com/ryansolid/dom-expressions/blob/main/packages/babel-plugin-jsx-dom-expressions) is more optimized with precompilation, smaller size, and cleaner syntax, this HyperScript solution has the flexibility of not being precompiled. However, Tagged Template Literals are likely a better choice in terms of performance in non-compiled environments [Lit DOM Expressions](https://github.com/ryansolid/dom-expressions/blob/main/packages/lit-dom-expressions).
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 four 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 [`babel-plugin-jsx-dom-expressions`](https://github.com/ryansolid/dom-expressions/blob/main/packages/babel-plugin-jsx-dom-expressions), or the parse-once-then-clone caching of [`lit-dom-expressions`](https://github.com/ryansolid/dom-expressions/blob/main/packages/lit-dom-expressions). Both hyper and lit run with no build step — if you just want a no-tooling authoring syntax, prefer `lit-dom-expressions`; 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.
9
11
 
10
12
  ## Compatible Libraries
11
- * [Solid](https://github.com/ryansolid/solid): A declarative JavaScript library for building user interfaces.
12
- * [ko-jsx](https://github.com/ryansolid/ko-jsx): Knockout JS with JSX rendering.
13
- * [mobx-jsx](https://github.com/ryansolid/mobx-jsx): Ever wondered how much more performant MobX is without React? A lot.
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)
14
17
 
15
18
  ## Getting Started
16
19
 
17
- Install alongside the DOM Expressions and the fine grained library of your choice. For example with S.js:
20
+ Install alongside DOM Expressions and a reactive library. For Solid:
18
21
 
19
22
  ```sh
20
- > npm install s-js dom-expressions hyper-dom-expressions
23
+ npm install @solidjs/signals dom-expressions hyper-dom-expressions
21
24
  ```
22
- Create your configuration and run dom-expressions command to generate runtime.js. More info [here](https://github.com/ryansolid/dom-expressions).
23
25
 
24
- Use it to initialize your Hyper function
26
+ Initialize `h` against the runtime:
27
+
25
28
  ```js
26
- import { createHyperScript } from 'hyper-dom-expressions';
27
- import * as r from './runtime';
29
+ import { createHyperScript } from "hyper-dom-expressions";
30
+ import * as r from "dom-expressions/src/client";
28
31
 
29
32
  const h = createHyperScript(r);
30
33
  ```
31
34
 
32
- Profit:
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
+
33
41
  ```js
34
- const view = h('table.table.table-hover.table-striped.test-data',
35
- h('tbody', mapSample(() => state.data, row =>
36
- h('tr', [
37
- h('td.col-md-1', row.id),
38
- h('td.col-md-4', h('a', {onClick: [select, row.id]}, () => row.label)),
39
- h('td.col-md-1', h('a', {onClick: [remove, row.id]}, h('span.glyphicon.glyphicon-remove'))),
40
- h('td.col-md-6')
41
- ])
42
- ))
43
- ));
44
-
45
- S.root(() => r.insert(document.getElementById('main'), view());)
42
+ const tree = h("div", h(Counter)); // thunk
43
+ tree(); // materializes DOM
46
44
  ```
47
45
 
48
- Libraries may expose access to h in different ways. For example Solid has it's own entry point 'solid-js/h'.
46
+ Mount via `r.render`. Pass the thunk directly; `render` invokes it inside its root so the whole tree materializes under that owner:
49
47
 
50
- ## Differences from JSX
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.
51
57
 
52
- There are also several small differences but generally follows HyperScript conventions. Ref work by passing a function. Keep in mind you need to wrap expressions in functions if you want them to be observed. For attributes since wrapping in a function is the only indicator of reactivity, passing a non-event function as a value requires wrapping it in a function.
58
+ ## Components, props, and children
53
59
 
54
- Fragments are just arrays. Components are handled by passing a Function to the first argument of the h function. Ie:
55
- ```jsx
56
- const view = <>
57
- <Component prop={value} />
58
- {someValue()}
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. Event props (`on…`) and higher-arity functions (e.g. a `For` render callback) pass through unchanged.
60
61
 
61
- // is equivalent to:
62
- const view = [
63
- h(Component, {prop: value}),
64
- () => someValue()
65
- ]
62
+ - **`props.children`** mirrors the caller's input:
63
+
64
+ | call shape | `props.children` |
65
+ | --- | --- |
66
+ | `h(Comp)` | `undefined` |
67
+ | `h(Comp, { children: v })` | `v` |
68
+ | `h(Comp, null, a)` | `a` |
69
+ | `h(Comp, null, a, b, c)` | `[a, b, c]` |
70
+
71
+ Nested `h(...)` thunks flow through as-is and auto-invoke once when consumed.
72
+
73
+ - **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)`.
74
+
75
+ - **Fragments** are either plain arrays (`[h(...), h(...)]`) or the built-in `h.Fragment` component.
76
+
77
+ - **JSX-compiler interop.** Components compiled by `babel-plugin-jsx-dom-expressions` 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.
78
+
79
+ ## Example
80
+
81
+ ```js
82
+ import { createHyperScript } from "hyper-dom-expressions";
83
+ import * as r from "dom-expressions/src/client";
84
+ import { createSignal, mapArray } from "@solidjs/signals";
85
+
86
+ const h = createHyperScript(r);
87
+
88
+ const For = props => mapArray(() => props.each, props.children);
89
+
90
+ const App = () => {
91
+ const [rows, setRows] = createSignal([
92
+ { id: 1, label: "one" },
93
+ { id: 2, label: "two" }
94
+ ]);
95
+ return h(
96
+ "table.table",
97
+ h(
98
+ "tbody",
99
+ h(For, { each: rows }, row =>
100
+ h(
101
+ "tr",
102
+ h("td.col-md-1", () => row().id),
103
+ h("td.col-md-4", () => row().label)
104
+ )
105
+ )
106
+ )
107
+ );
108
+ };
109
+
110
+ r.render(h(App), document.getElementById("main"));
66
111
  ```
67
112
 
68
- ## Status
113
+ ## Differences from JSX
69
114
 
70
- I'm still working out API details and performance profiling.
115
+ - Refs are passed as a function prop (`ref: el => { … }`).
116
+ - Reactivity is explicit: wrap expressions in a function when they should be tracked (`() => count()`), including when forwarding a component prop (`h("p", () => props.foo)`).
117
+ - Fragments are arrays (or `h.Fragment`).
118
+ - Tag selectors understand `#id` and `.class` shorthands (`h("div#main.sel", …)`).
@@ -1,14 +1,36 @@
1
+ // Tags h() thunks so consumers can distinguish them from user-written
2
+ // accessors and invoke them once under the correct owner rather than
3
+ // wrapping them in an effect.
4
+ const $ELEMENT = Symbol("hyper-element");
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
1
16
  // Inspired by https://github.com/hyperhype/hyperscript
2
17
  function createHyperScript(r) {
3
- function h() {
4
- let args = [].slice.call(arguments),
5
- e,
6
- classes = [],
7
- multiExpression = false;
18
+ function h(...rawArgs) {
19
+ if (rawArgs.length === 1 && Array.isArray(rawArgs[0])) return rawArgs[0];
20
+ const thunk = () => r.untrack(() => materialize(rawArgs));
21
+ thunk[$ELEMENT] = true;
22
+ return thunk;
23
+ }
24
+
25
+ function materialize(args) {
26
+ let e;
27
+ let classes = [];
28
+ let multiExpression = false;
29
+ args = args.slice();
8
30
 
9
31
  function item(l) {
32
+ if (l == null) return;
10
33
  const type = typeof l;
11
- if (l == null) ;else
12
34
  if ("string" === type) {
13
35
  if (!e) parseClass(l);else
14
36
  e.appendChild(document.createTextNode(l));
@@ -47,40 +69,34 @@ function createHyperScript(r) {
47
69
  r.assign(e, l, !!args.length);
48
70
  } else if ("function" === type) {
49
71
  if (!e) {
50
- let props,
51
- next = args[0];
52
- if (
53
- next == null ||
54
- typeof next === "object" && !Array.isArray(next) && !(next instanceof Element))
55
-
56
- props = args.shift();
57
- props || (props = {});
58
- if (args.length) {
59
- props.children = args.length > 1 ? args : args[0];
60
- }
61
- const d = Object.getOwnPropertyDescriptors(props);
62
- for (const k in d) {
63
- if (typeof d[k].value === "function" && !d[k].value.length) r.dynamicProperty(props, k);
72
+ const first = args[0];
73
+ const props =
74
+ first == null ||
75
+ typeof first === "object" && !Array.isArray(first) && !(first instanceof Element) ?
76
+ args.shift() || {} :
77
+ {};
78
+ if (args.length) props.children = args.length > 1 ? args : args[0];
79
+ for (const k in props) {
80
+ const v = props[k];
81
+ if (typeof v === "function" && !v.length) r.dynamicProperty(props, k);
64
82
  }
65
83
  e = r.createComponent(l, props);
84
+ // Drain nested h() thunks so downstream sees the real render result.
85
+ while (typeof e === "function" && e[$ELEMENT]) e = e();
66
86
  args = [];
67
- } else {
68
- r.insert(e, l, multiExpression ? null : undefined);
69
- }
87
+ } else if (l[$ELEMENT]) item(l());else
88
+ r.insert(e, l, multiExpression ? null : undefined);
70
89
  }
71
90
  }
72
91
 
73
- if (args.length === 1 && Array.isArray(args[0])) return args[0];
74
- typeof args[0] === "string" && detectMultiExpression(args);
92
+ if (typeof args[0] === "string") detectMultiExpression(args);
75
93
  while (args.length) item(args.shift());
76
94
  if (e instanceof Element && classes.length) e.classList.add(...classes);
77
95
  return e;
78
96
 
79
97
  function parseClass(string) {
80
- // Our minimal parser doesn’t understand escaping CSS special
81
- // characters like `#`. Don’t use them. More reading:
82
- // https://mathiasbynens.be/notes/css-escapes .
83
-
98
+ // Does not understand escaped CSS specials; see
99
+ // https://mathiasbynens.be/notes/css-escapes.
84
100
  const m = string.split(/([\.#]?[^\s#.]+)/);
85
101
  if (/^\.|#/.test(m[1])) e = document.createElement("div");
86
102
  for (let i = 0; i < m.length; i++) {
@@ -1,16 +1,38 @@
1
1
  'use strict';
2
2
 
3
+ // Tags h() thunks so consumers can distinguish them from user-written
4
+ // accessors and invoke them once under the correct owner rather than
5
+ // wrapping them in an effect.
6
+ const $ELEMENT = Symbol("hyper-element");
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
3
18
  // Inspired by https://github.com/hyperhype/hyperscript
4
19
  function createHyperScript(r) {
5
- function h() {
6
- let args = [].slice.call(arguments),
7
- e,
8
- classes = [],
9
- multiExpression = false;
20
+ function h(...rawArgs) {
21
+ if (rawArgs.length === 1 && Array.isArray(rawArgs[0])) return rawArgs[0];
22
+ const thunk = () => r.untrack(() => materialize(rawArgs));
23
+ thunk[$ELEMENT] = true;
24
+ return thunk;
25
+ }
26
+
27
+ function materialize(args) {
28
+ let e;
29
+ let classes = [];
30
+ let multiExpression = false;
31
+ args = args.slice();
10
32
 
11
33
  function item(l) {
34
+ if (l == null) return;
12
35
  const type = typeof l;
13
- if (l == null) ;else
14
36
  if ("string" === type) {
15
37
  if (!e) parseClass(l);else
16
38
  e.appendChild(document.createTextNode(l));
@@ -49,40 +71,34 @@ function createHyperScript(r) {
49
71
  r.assign(e, l, !!args.length);
50
72
  } else if ("function" === type) {
51
73
  if (!e) {
52
- let props,
53
- next = args[0];
54
- if (
55
- next == null ||
56
- typeof next === "object" && !Array.isArray(next) && !(next instanceof Element))
57
-
58
- props = args.shift();
59
- props || (props = {});
60
- if (args.length) {
61
- props.children = args.length > 1 ? args : args[0];
62
- }
63
- const d = Object.getOwnPropertyDescriptors(props);
64
- for (const k in d) {
65
- if (typeof d[k].value === "function" && !d[k].value.length) r.dynamicProperty(props, k);
74
+ const first = args[0];
75
+ const props =
76
+ first == null ||
77
+ typeof first === "object" && !Array.isArray(first) && !(first instanceof Element) ?
78
+ args.shift() || {} :
79
+ {};
80
+ if (args.length) props.children = args.length > 1 ? args : args[0];
81
+ for (const k in props) {
82
+ const v = props[k];
83
+ if (typeof v === "function" && !v.length) r.dynamicProperty(props, k);
66
84
  }
67
85
  e = r.createComponent(l, props);
86
+ // Drain nested h() thunks so downstream sees the real render result.
87
+ while (typeof e === "function" && e[$ELEMENT]) e = e();
68
88
  args = [];
69
- } else {
70
- r.insert(e, l, multiExpression ? null : undefined);
71
- }
89
+ } else if (l[$ELEMENT]) item(l());else
90
+ r.insert(e, l, multiExpression ? null : undefined);
72
91
  }
73
92
  }
74
93
 
75
- if (args.length === 1 && Array.isArray(args[0])) return args[0];
76
- typeof args[0] === "string" && detectMultiExpression(args);
94
+ if (typeof args[0] === "string") detectMultiExpression(args);
77
95
  while (args.length) item(args.shift());
78
96
  if (e instanceof Element && classes.length) e.classList.add(...classes);
79
97
  return e;
80
98
 
81
99
  function parseClass(string) {
82
- // Our minimal parser doesn’t understand escaping CSS special
83
- // characters like `#`. Don’t use them. More reading:
84
- // https://mathiasbynens.be/notes/css-escapes .
85
-
100
+ // Does not understand escaped CSS specials; see
101
+ // https://mathiasbynens.be/notes/css-escapes.
86
102
  const m = string.split(/([\.#]?[^\s#.]+)/);
87
103
  if (/^\.|#/.test(m[1])) e = document.createElement("div");
88
104
  for (let i = 0; i < m.length; i++) {
package/package.json CHANGED
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "hyper-dom-expressions",
3
3
  "description": "A Fine-Grained Rendering Runtime API using HyperScript",
4
- "version": "0.50.0-next.2",
4
+ "version": "0.50.0-next.3",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/ryansolid/dom-expressions/blob/main/packages/hyper-dom-expressions"
9
+ "url": "git+https://github.com/ryansolid/dom-expressions.git",
10
+ "directory": "packages/hyper-dom-expressions"
10
11
  },
11
12
  "main": "lib/hyper-dom-expressions.js",
12
13
  "module": "dist/hyper-dom-expressions.js",
13
14
  "types": "types/index.d.ts",
14
15
  "sideEffects": false,
15
16
  "devDependencies": {
16
- "dom-expressions": "0.50.0-next.2"
17
+ "dom-expressions": "0.50.0-next.3"
17
18
  },
18
19
  "scripts": {
19
20
  "build": "rollup -c --bundleConfigAsCjs && tsc",
package/types/index.d.ts CHANGED
@@ -5,6 +5,7 @@ interface Runtime {
5
5
  assign(node: Element, props: any, skipChildren?: Boolean): void;
6
6
  createComponent(Comp: (props: any) => any, props: any): any;
7
7
  dynamicProperty(props: any, key: string): any;
8
+ untrack<T>(fn: () => T): T;
8
9
  SVGElements: Set<string>;
9
10
  MathMLElements: Set<string>;
10
11
  Namespaces: Record<string, string>;
@@ -12,11 +13,16 @@ interface Runtime {
12
13
  type ExpandableNode = Node & {
13
14
  [key: string]: any;
14
15
  };
16
+ declare const $ELEMENT: unique symbol;
17
+ export type HyperElement = {
18
+ (): ExpandableNode | ExpandableNode[];
19
+ [$ELEMENT]: true;
20
+ };
15
21
  export type HyperScript = {
16
- (...args: any[]): ExpandableNode | ExpandableNode[];
22
+ (...args: any[]): HyperElement | ExpandableNode[];
17
23
  Fragment: (props: {
18
- children: ExpandableNode | ExpandableNode[];
19
- }) => ExpandableNode | ExpandableNode[];
24
+ children: any;
25
+ }) => any;
20
26
  };
21
27
  export declare function createHyperScript(r: Runtime): HyperScript;
22
28
  export {};