@zubra/runtime 5.2.0 → 5.4.0
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/dist/production/deep.js +5 -2
- package/dist/production/effect.js +5 -2
- package/dist/production/element.js +5 -2
- package/dist/production/index.js +1 -1
- package/dist/production/primitives.js +1 -1
- package/dist/production/react.js +1 -1
- package/dist/production/remark.js +46 -7
- package/dist/production/resource.js +1 -1
- package/dist/production/server.js +1 -1
- package/dist/production/signal-impl.js +1 -1
- package/dist/production/signal.js +1 -1
- package/dist/production/spread.js +1 -1
- package/dist/production/vue.js +1 -1
- package/dist/production/zag.js +1 -1
- package/package.json +1 -1
- package/src/deep.js +17 -1
- package/src/devhook.js +41 -0
- package/src/devtools.js +284 -0
- package/src/effect.js +24 -1
- package/src/element.js +30 -1
- package/src/remark.js +45 -6
package/dist/production/deep.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/deep.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/deep.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// Deep reactivity for state: `let items = []` followed by `items.push(x)` updates the DOM.
|
|
3
3
|
//
|
|
4
4
|
// A `state()` holding an array or a plain object holds a proxy of it instead. Each property the
|
|
@@ -29,11 +29,14 @@ const isPlain = (value) => {
|
|
|
29
29
|
/** The object behind a reactive proxy, or the value itself. */
|
|
30
30
|
export const raw = (value) => (value && typeof value === 'object' && value[RAW]) || value;
|
|
31
31
|
|
|
32
|
+
|
|
32
33
|
function track(target, key) {
|
|
33
34
|
let map = signals.get(target);
|
|
34
35
|
if (!map) signals.set(target, (map = new Map()));
|
|
35
36
|
let signal = map.get(key);
|
|
36
|
-
if (!signal)
|
|
37
|
+
if (!signal) {
|
|
38
|
+
map.set(key, (signal = new Signal.State(0)));
|
|
39
|
+
}
|
|
37
40
|
signal.get();
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/effect.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/effect.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// Effects and ownership, built only on the Signals API.
|
|
3
3
|
//
|
|
4
4
|
// The Signals proposal intentionally leaves effects and scheduling to libraries; this is ours.
|
|
@@ -29,6 +29,7 @@ let scheduled = null; // promise for the pending microtask flush
|
|
|
29
29
|
let flushing = false;
|
|
30
30
|
const effectOf = new WeakMap();
|
|
31
31
|
|
|
32
|
+
|
|
32
33
|
const watcher = new Signal.subtle.Watcher(() => {
|
|
33
34
|
scheduled ??= new Promise((resolve) => queueMicrotask(() => {
|
|
34
35
|
try { flush(); } finally { scheduled = null; resolve(); }
|
|
@@ -214,7 +215,9 @@ export function flush() {
|
|
|
214
215
|
for (let round = 0; ; round++) {
|
|
215
216
|
const pending = watcher.getPending();
|
|
216
217
|
watcher.watch(); // re-arm before running, so writes made by effects schedule another round
|
|
217
|
-
if (pending.length === 0)
|
|
218
|
+
if (pending.length === 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
218
221
|
if (round >= MAX_ROUNDS) {
|
|
219
222
|
report(new Error('zubra: effects keep re-triggering each other; stopped after 100 rounds'));
|
|
220
223
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/element.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/element.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// define(): the custom element a compiled component becomes.
|
|
3
3
|
//
|
|
4
4
|
// The compiler hands over a template and a list of parts (see src/emit_js.rs). Each part has a
|
|
@@ -1673,7 +1673,10 @@ function defineElement(tag, definition) {
|
|
|
1673
1673
|
const initial = (name, compute) => {
|
|
1674
1674
|
if (!this.#given.has(name)) this.#signals[name].set(compute());
|
|
1675
1675
|
};
|
|
1676
|
-
try {
|
|
1676
|
+
try {
|
|
1677
|
+
parts = setup ? setup($p, (name, detail) => this.#emit(name, detail), initial
|
|
1678
|
+
) : [];
|
|
1679
|
+
} finally { current = previous; }
|
|
1677
1680
|
// A form control with one two-way prop: that's what it submits, and what a form reset
|
|
1678
1681
|
// restores, unless the script says otherwise.
|
|
1679
1682
|
const models = Object.keys(props).filter((name) => props[name].model);
|
package/dist/production/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/index.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/index.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// @zubra/runtime — everything compiled components and their scripts import.
|
|
3
3
|
export { Signal, isNative } from './signal.js';
|
|
4
4
|
export { effect, scope, flush, tick } from './effect.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/primitives.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/primitives.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// @zubra/runtime/primitives: the two keyboard behaviours the platform doesn't have.
|
|
3
3
|
//
|
|
4
4
|
// The platform now does most of what headless libraries used to: <dialog> traps focus and closes
|
package/dist/production/react.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/react.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/react.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// React adapter: a zubra element as a React component, for React 18 and 19.
|
|
3
3
|
//
|
|
4
4
|
// import Rating from './rating.zb' // the build plugin wraps it for React files
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/remark.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/remark.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// Server markup whose markers were stripped: the element's runtime loads this module only when it
|
|
3
3
|
// meets such markup.
|
|
4
4
|
//
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
// - an zb-if region holds the branch the current values choose, and an zb-each region one row
|
|
11
11
|
// per item, each with the shape of its block (checked element by element);
|
|
12
12
|
// - a light-DOM slot region is the rest of its range, when nothing follows it there; its
|
|
13
|
-
// content is the fallback if it has the fallback's shape, and supplied content otherwise
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
13
|
+
// content is the fallback if it has the fallback's shape, and supplied content otherwise;
|
|
14
|
+
// - an zb-await region holds its zb-then block, for a value known at build time, or its pending
|
|
15
|
+
// block, which the element renders afresh anyway (so only where the region ends matters).
|
|
16
|
+
// Anything else (content projected into a child component, a region the server left to the
|
|
17
|
+
// browser, other than one left empty or a pending zb-await) can't be placed for sure, and the
|
|
18
|
+
// element renders afresh as before.
|
|
17
19
|
|
|
18
20
|
const TEXT = 3;
|
|
19
21
|
const COMMENT = 8;
|
|
@@ -56,7 +58,12 @@ export function remark(root, template, parts, { blueprint, isOpen, isClose, read
|
|
|
56
58
|
let index = 0;
|
|
57
59
|
for (; t; t = t.nextSibling) {
|
|
58
60
|
if (t.nodeType === TEXT) {
|
|
59
|
-
|
|
61
|
+
// Static text (if kept): the same words as the template's. Text that isn't (a page's own
|
|
62
|
+
// `<x-a>Save</x-a>` where the template has only whitespace) isn't server markup.
|
|
63
|
+
if (cursor?.nodeType === TEXT) {
|
|
64
|
+
if (cursor.data.trim() !== t.data.trim()) throw fail;
|
|
65
|
+
cursor = cursor.nextSibling;
|
|
66
|
+
}
|
|
60
67
|
continue;
|
|
61
68
|
}
|
|
62
69
|
const path = [...prefix, index];
|
|
@@ -147,8 +154,40 @@ export function remark(root, template, parts, { blueprint, isOpen, isClose, read
|
|
|
147
154
|
insert(parent, null, ']');
|
|
148
155
|
}
|
|
149
156
|
cursor = null;
|
|
157
|
+
} else if (part.await) {
|
|
158
|
+
// zb-await: the server rendered the zb-then block for a value it knew at build time (a
|
|
159
|
+
// plain value here too), or the pending block in a region it left to the element, which
|
|
160
|
+
// the element renders afresh anyway (`[?`): only its extent has to be found, by the
|
|
161
|
+
// pending block's shape. Failing both, an empty region left to the element, as above.
|
|
162
|
+
const saved = edits.length;
|
|
163
|
+
const start = cursor;
|
|
164
|
+
const value = untrack(() => $r(part.await()));
|
|
165
|
+
const thenable = (item) => item != null && typeof $r(item)?.then === 'function';
|
|
166
|
+
const settled = !(Array.isArray(value) ? value.some(thenable) : thenable(value));
|
|
167
|
+
const attempts = [];
|
|
168
|
+
if (settled && part.blocks[1]) attempts.push(['[', part.blocks[1], [new State(value), new State(() => {})]]);
|
|
169
|
+
if (part.blocks[0]) attempts.push(['[?', part.blocks[0], []]);
|
|
170
|
+
let placed = false;
|
|
171
|
+
for (const [open, chosen, args] of attempts) {
|
|
172
|
+
try {
|
|
173
|
+
insert(parent, cursor, open);
|
|
174
|
+
cursor = block(chosen, args);
|
|
175
|
+
insert(parent, cursor, ']');
|
|
176
|
+
placed = true;
|
|
177
|
+
break;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error !== fail) throw error;
|
|
180
|
+
edits.length = saved;
|
|
181
|
+
cursor = start;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!placed) {
|
|
185
|
+
if (!followsRegion(t, skipText(cursor))) throw fail;
|
|
186
|
+
insert(parent, cursor, '[?');
|
|
187
|
+
insert(parent, cursor, ']');
|
|
188
|
+
}
|
|
150
189
|
} else {
|
|
151
|
-
throw fail;
|
|
190
|
+
throw fail;
|
|
152
191
|
}
|
|
153
192
|
// Past the template's matching close.
|
|
154
193
|
let depth = 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/resource.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/resource.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// Async data as signals: `resource(fetcher)`.
|
|
3
3
|
//
|
|
4
4
|
// const user = resource(({ signal }) => fetch(`/api/users/${p.id}`, { signal }).then((r) => r.json()))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/server.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/server.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// Server rendering of zubra elements, for framework integrations (the Astro renderer, …).
|
|
3
3
|
//
|
|
4
4
|
// A `.zb` module loads on a server: `define()` returns a stand-in that carries the element's
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/signal-impl.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/signal-impl.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// An implementation of the TC39 Signals proposal API (https://github.com/tc39/proposal-signals).
|
|
3
3
|
//
|
|
4
4
|
// This is zubra's polyfill boundary for reactivity: compiled components talk only to this API,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/signal.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/signal.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// The one place compiled zubra code gets reactivity from.
|
|
3
3
|
//
|
|
4
4
|
// If the platform provides the TC39 Signals API natively, use it. Otherwise fall back to our
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/spread.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/spread.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// {...spread} in compiled components: an object's entries as an element's attributes,
|
|
3
3
|
// properties and listeners. Its own entry (`@zubra/runtime/spread`), which a compiled module imports
|
|
4
4
|
// only when its template spreads, so components that don't, don't load it.
|
package/dist/production/vue.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/vue.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/vue.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// Vue 3 adapter: a zubra element as a Vue component.
|
|
3
3
|
//
|
|
4
4
|
// <script setup>
|
package/dist/production/zag.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by scripts/build-production.js from src/zag.js: the runtime without hot reloading. Do not edit.
|
|
1
|
+
// Generated by scripts/build-production.js from src/zag.js: the runtime without hot reloading or the DevTools hook. Do not edit.
|
|
2
2
|
// @zubra/runtime/zag: Zag.js state machines driving zubra components.
|
|
3
3
|
//
|
|
4
4
|
// Zag (https://zagjs.com) is a framework-agnostic set of accessible widget behaviours (combobox,
|
package/package.json
CHANGED
package/src/deep.js
CHANGED
|
@@ -28,17 +28,33 @@ const isPlain = (value) => {
|
|
|
28
28
|
/** The object behind a reactive proxy, or the value itself. */
|
|
29
29
|
export const raw = (value) => (value && typeof value === 'object' && value[RAW]) || value;
|
|
30
30
|
|
|
31
|
+
// @dev
|
|
32
|
+
/**
|
|
33
|
+
* For the DevTools hook (devtools.js), in development only: each property signal's object and
|
|
34
|
+
* key, so it can be named, and `wrote` hears each write through a proxy.
|
|
35
|
+
*/
|
|
36
|
+
export const dev = { propertyOf: new WeakMap(), wrote: null };
|
|
37
|
+
// @dev-end
|
|
38
|
+
|
|
31
39
|
function track(target, key) {
|
|
32
40
|
let map = signals.get(target);
|
|
33
41
|
if (!map) signals.set(target, (map = new Map()));
|
|
34
42
|
let signal = map.get(key);
|
|
35
|
-
if (!signal)
|
|
43
|
+
if (!signal) {
|
|
44
|
+
map.set(key, (signal = new Signal.State(0)));
|
|
45
|
+
// @dev
|
|
46
|
+
dev.propertyOf.set(signal, { target, key });
|
|
47
|
+
// @dev-end
|
|
48
|
+
}
|
|
36
49
|
signal.get();
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
function trigger(target, key) {
|
|
40
53
|
const signal = signals.get(target)?.get(key);
|
|
41
54
|
if (signal) signal.set(signal.get() + 1);
|
|
55
|
+
// @dev
|
|
56
|
+
if (signal) dev.wrote?.(signal);
|
|
57
|
+
// @dev-end
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
const handler = {
|
package/src/devhook.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// What development records for the DevTools, from the page's first element on: which components
|
|
2
|
+
// are on the page, and which listeners each node has. Only element.js imports it, from a
|
|
3
|
+
// `// @dev` block, so the production build (scripts/build-production.js) leaves it out.
|
|
4
|
+
//
|
|
5
|
+
// This part is small and always loaded in development. The reading (devtools.js) is loaded
|
|
6
|
+
// when the browser extension connects, so a page that never opens the pane carries only this.
|
|
7
|
+
|
|
8
|
+
export const elements = new WeakMap(); // host → { tag, source, props, named, scope }
|
|
9
|
+
export const listeners = new WeakMap(); // node → the listener parts bound to it
|
|
10
|
+
export const hooks = { registered: null }; // devtools.js listens once it's loaded
|
|
11
|
+
|
|
12
|
+
// Elements from before the reading side loads: it watches their writes when it does. WeakRefs,
|
|
13
|
+
// so a removed element isn't kept alive, and pruned now and then on a page that never connects.
|
|
14
|
+
export const early = [];
|
|
15
|
+
|
|
16
|
+
/** An element finished setting up (see devtools.js for what the record holds). */
|
|
17
|
+
export function register(host, record) {
|
|
18
|
+
elements.set(host, record);
|
|
19
|
+
if (hooks.registered) return hooks.registered(record);
|
|
20
|
+
if (early.length >= 1000) early.splice(0, Infinity, ...early.filter((ref) => ref.deref()));
|
|
21
|
+
early.push(new WeakRef(record));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A listener part was bound to `node` (effects carry their part; listeners aren't effects). */
|
|
25
|
+
export function listened(part, node) {
|
|
26
|
+
let parts = listeners.get(node);
|
|
27
|
+
if (!parts) listeners.set(node, (parts = []));
|
|
28
|
+
if (!parts.includes(part)) parts.push(part);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let loading = null;
|
|
32
|
+
if (!globalThis.__ZUBRA_DEVTOOLS__) {
|
|
33
|
+
globalThis.__ZUBRA_DEVTOOLS__ = {
|
|
34
|
+
version: 1,
|
|
35
|
+
/** Load the reading side; the extension calls this, then `inspect()` once it's there. */
|
|
36
|
+
connect() {
|
|
37
|
+
loading ??= import('./devtools.js');
|
|
38
|
+
return true;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/devtools.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// The DevTools reading side: what the browser extension (editors/devtools) shows for a node.
|
|
2
|
+
// devhook.js records from the page's start and loads this when the extension connects, so a
|
|
3
|
+
// page that never opens the pane doesn't carry it. Neither is in the production build
|
|
4
|
+
// (scripts/build-production.js).
|
|
5
|
+
//
|
|
6
|
+
// The extension reads the page through `globalThis.__ZUBRA_DEVTOOLS__` and nothing else: plain
|
|
7
|
+
// JSON out, so the runtime's internals stay free to change. `version` says which shape.
|
|
8
|
+
//
|
|
9
|
+
// What it knows comes from a `--devtools` build (which @zubra/unplugin makes in Vite's dev
|
|
10
|
+
// server): each part's line and column in the `.zb` file, the file itself, and the script's
|
|
11
|
+
// signals by name. Without that build, an element still shows its tag, props and bindings,
|
|
12
|
+
// unlocated and with fewer names.
|
|
13
|
+
//
|
|
14
|
+
// The dependencies are read through `Signal.subtle.introspectSources`, the Signals proposal's
|
|
15
|
+
// own introspection, so the same view works on the polyfill and on a native `Signal`.
|
|
16
|
+
import { Signal, isNative } from './signal.js';
|
|
17
|
+
import { dev as effects } from './effect.js';
|
|
18
|
+
import { dev as deep, raw } from './deep.js';
|
|
19
|
+
import { elements, listeners, hooks, early } from './devhook.js';
|
|
20
|
+
|
|
21
|
+
const { introspectSources, untrack } = Signal.subtle;
|
|
22
|
+
const isComputed = (value) => value instanceof Signal.Computed;
|
|
23
|
+
const isState = (value) => value instanceof Signal.State;
|
|
24
|
+
|
|
25
|
+
// The most recent flush since connecting: the effects it re-ran, and the signals written since
|
|
26
|
+
// the one before.
|
|
27
|
+
let writes = new Set();
|
|
28
|
+
let last = { ran: new Set(), written: new Set() };
|
|
29
|
+
let updates = 0;
|
|
30
|
+
|
|
31
|
+
effects.flushed = (ran) => {
|
|
32
|
+
last = { ran: new Set(ran), written: writes };
|
|
33
|
+
writes = new Set();
|
|
34
|
+
updates++;
|
|
35
|
+
};
|
|
36
|
+
const wrote = (signal) => {
|
|
37
|
+
// A write nothing reads never flushes: don't let those pile up.
|
|
38
|
+
if (writes.size > 1000) writes = new Set();
|
|
39
|
+
writes.add(signal);
|
|
40
|
+
};
|
|
41
|
+
deep.wrote = wrote;
|
|
42
|
+
|
|
43
|
+
/** Watch writes to a signal (a prop's, or a named state's). */
|
|
44
|
+
function watchWrites(signal) {
|
|
45
|
+
if (signal.__zubraWatched) return;
|
|
46
|
+
const set = signal.set;
|
|
47
|
+
signal.set = function (value) {
|
|
48
|
+
wrote(signal);
|
|
49
|
+
return set.call(this, value);
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(signal, '__zubraWatched', { value: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Watch a component's props and named state (once; elements from before connecting too). */
|
|
55
|
+
function watch(record) {
|
|
56
|
+
if (record.watched) return;
|
|
57
|
+
record.watched = true;
|
|
58
|
+
for (const signal of Object.values(record.props)) watchWrites(signal);
|
|
59
|
+
for (const signal of Object.values(record.named ?? {})) {
|
|
60
|
+
if (isState(signal) || typeof signal?.set === 'function') watchWrites(signal);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
hooks.registered = watch;
|
|
64
|
+
for (const ref of early.splice(0)) {
|
|
65
|
+
const record = ref.deref();
|
|
66
|
+
if (record) watch(record);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- reading -------------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** The nearest registered component at or above `node`, crossing shadow roots. */
|
|
72
|
+
function hostOf(node, skipSelf = false) {
|
|
73
|
+
for (let at = skipSelf ? up(node) : node; at; at = up(at)) if (elements.has(at)) return at;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const up = (node) => node.parentNode ?? node.host ?? null;
|
|
77
|
+
|
|
78
|
+
/** Every effect `owner` owns, depth first (a component's own: child components have their own roots). */
|
|
79
|
+
function effectsOf(owner, out = []) {
|
|
80
|
+
for (const child of owner.children) {
|
|
81
|
+
if (child.disposed) continue;
|
|
82
|
+
if (child.computed) out.push(child);
|
|
83
|
+
effectsOf(child, out);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Form controls' live state, which the runtime sets as a property (element.js, bindAttribute).
|
|
89
|
+
const LIVE = new Set(['input.value', 'textarea.value', 'select.value', 'input.checked', 'option.selected']);
|
|
90
|
+
|
|
91
|
+
/** The DOM operation a part performs on `node`, in the platform's terms. */
|
|
92
|
+
export function operation(part, node) {
|
|
93
|
+
if (part.text) return 'textContent = …';
|
|
94
|
+
if (part.anchor) return 'Text.data = …';
|
|
95
|
+
if (part.global) return `${part.global}.addEventListener('${part.on}', …)`;
|
|
96
|
+
if (part.on) return `addEventListener('${part.on}', …)`;
|
|
97
|
+
if (part.classes) return 'classList.add/remove(…)';
|
|
98
|
+
if (part.toggle) return `classList.toggle('${part.toggle}', …)`;
|
|
99
|
+
if (part.style) return `style.setProperty('${part.style}', …)`;
|
|
100
|
+
if (part.ref) return 'ref(element)';
|
|
101
|
+
if (part.property) return `.${part.property} = …`;
|
|
102
|
+
if (part.attribute) {
|
|
103
|
+
if (node && LIVE.has(`${node.localName}.${part.attribute}`)) return `.${part.attribute} = …`;
|
|
104
|
+
if (part.boolean) return `toggleAttribute('${part.attribute}', …)`;
|
|
105
|
+
if (part.prop) return `.${part.prop} = … (or setAttribute('${part.attribute}', …))`;
|
|
106
|
+
return `setAttribute('${part.attribute}', …)`;
|
|
107
|
+
}
|
|
108
|
+
if (part.when) return 'zb-if: insert or remove a block';
|
|
109
|
+
if (part.each) return 'zb-each: add, move or remove rows';
|
|
110
|
+
if (part.await) return 'zb-await: pending, then or catch';
|
|
111
|
+
if (part.slot) return `slot "${part.slot}"`;
|
|
112
|
+
if (part.spread) return 'spread: attributes and properties';
|
|
113
|
+
return 'binding';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A short name for a node: `<button.on>`, or its parent's for a text anchor or region marker. */
|
|
117
|
+
function describe(node) {
|
|
118
|
+
if (!node) return '?';
|
|
119
|
+
if (node.nodeType === 1) {
|
|
120
|
+
const id = node.id ? `#${node.id}` : '';
|
|
121
|
+
const classes = [...(node.classList ?? [])].slice(0, 2).map((name) => `.${name}`).join('');
|
|
122
|
+
return `<${node.localName}${id}${classes}>`;
|
|
123
|
+
}
|
|
124
|
+
return node.parentNode?.nodeType === 1 ? `${describe(node.parentNode)} content` : node.nodeName;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A value as a short string: the pane shows values, it doesn't hold them. */
|
|
128
|
+
export function preview(value, depth = 0) {
|
|
129
|
+
value = raw(value);
|
|
130
|
+
if (value === undefined) return 'undefined';
|
|
131
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
132
|
+
if (typeof value === 'string') return JSON.stringify(value.length > 80 ? `${value.slice(0, 80)}…` : value);
|
|
133
|
+
if (typeof value === 'bigint') return `${value}n`;
|
|
134
|
+
if (typeof value === 'symbol') return value.toString();
|
|
135
|
+
if (typeof value === 'function') return `ƒ ${value.name || 'anonymous'}()`;
|
|
136
|
+
if (isComputed(value) || isState(value)) return preview(untrack(() => value.get()), depth);
|
|
137
|
+
if (typeof Node !== 'undefined' && value instanceof Node) return describe(value);
|
|
138
|
+
if (value instanceof Promise) return 'Promise';
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
if (depth > 1) return `Array(${value.length})`;
|
|
141
|
+
const shown = value.slice(0, 5).map((item) => preview(item, depth + 1));
|
|
142
|
+
return `[${shown.join(', ')}${value.length > 5 ? `, … ${value.length - 5} more` : ''}]`;
|
|
143
|
+
}
|
|
144
|
+
const name = value.constructor && value.constructor !== Object ? value.constructor.name : '';
|
|
145
|
+
if (depth > 1) return name || '{…}';
|
|
146
|
+
const keys = Object.keys(value);
|
|
147
|
+
const shown = keys.slice(0, 5).map((key) => `${key}: ${preview(value[key], depth + 1)}`);
|
|
148
|
+
return `${name ? `${name} ` : ''}{${shown.join(', ')}${keys.length > 5 ? ', …' : ''}}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Names for the signals a component can read: its props and named script signals, and the
|
|
153
|
+
* property signals of the arrays and objects they hold (`items[0].done`, `items.length`).
|
|
154
|
+
*/
|
|
155
|
+
function namer(record) {
|
|
156
|
+
const names = new Map();
|
|
157
|
+
for (const [name, signal] of Object.entries(record.props)) names.set(signal, name);
|
|
158
|
+
for (const [name, signal] of Object.entries(record.named ?? {})) if (signal && typeof signal === 'object') names.set(signal, name);
|
|
159
|
+
const paths = new Map(); // raw object → path
|
|
160
|
+
const walk = (value, path, depth) => {
|
|
161
|
+
value = raw(value);
|
|
162
|
+
if (!value || typeof value !== 'object' || paths.has(value) || depth > 3) return;
|
|
163
|
+
paths.set(value, path);
|
|
164
|
+
if (Array.isArray(value)) value.slice(0, 200).forEach((item, i) => walk(item, `${path}[${i}]`, depth + 1));
|
|
165
|
+
else if (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) {
|
|
166
|
+
for (const key of Object.keys(value).slice(0, 50)) walk(value[key], `${path}.${key}`, depth + 1);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
for (const [signal, name] of names) {
|
|
170
|
+
try { walk(untrack(() => signal.get()), name, 0); } catch {}
|
|
171
|
+
}
|
|
172
|
+
return (signal) => {
|
|
173
|
+
if (names.has(signal)) return names.get(signal);
|
|
174
|
+
const property = deep.propertyOf.get(signal);
|
|
175
|
+
if (!property) return null;
|
|
176
|
+
const path = paths.get(property.target);
|
|
177
|
+
if (!path) return null;
|
|
178
|
+
const { key } = property;
|
|
179
|
+
if (typeof key === 'symbol') return path; // its keys
|
|
180
|
+
return /^\d+$/.test(key) ? `${path}[${key}]` : `${path}.${key}`;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The named signals `sink` reads, looking through unnamed computeds to the names behind them. */
|
|
185
|
+
function dependencies(sink, name) {
|
|
186
|
+
const found = [];
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
const visit = (node, depth) => {
|
|
189
|
+
let sources;
|
|
190
|
+
try { sources = introspectSources(node); } catch { return; }
|
|
191
|
+
for (const source of sources) {
|
|
192
|
+
if (seen.has(source)) continue;
|
|
193
|
+
seen.add(source);
|
|
194
|
+
const label = name(source);
|
|
195
|
+
if (label) {
|
|
196
|
+
if (!found.includes(label)) found.push(label);
|
|
197
|
+
} else if (isComputed(source) && depth < 6) visit(source, depth + 1);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
visit(sink, 0);
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const location = (part) => (Array.isArray(part?.at) ? { line: part.at[0], column: part.at[1] } : null);
|
|
205
|
+
|
|
206
|
+
/** One binding as the pane shows it. */
|
|
207
|
+
function binding(part, node, effect, name, selected) {
|
|
208
|
+
return {
|
|
209
|
+
operation: operation(part, node),
|
|
210
|
+
target: describe(node),
|
|
211
|
+
at: location(part),
|
|
212
|
+
dependsOn: effect ? dependencies(effect.computed, name) : [],
|
|
213
|
+
ranLast: !!effect && last.ran.has(effect),
|
|
214
|
+
here: node === selected || (node?.nodeType !== 1 && node?.parentNode === selected),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** What the pane shows for `node`: its component, and that component's state and bindings. */
|
|
219
|
+
export function inspect(node) {
|
|
220
|
+
if (!node) return { version: 1, component: null };
|
|
221
|
+
const host = hostOf(node);
|
|
222
|
+
if (!host) return { version: 1, component: null, native: isNative };
|
|
223
|
+
const record = elements.get(host);
|
|
224
|
+
watch(record);
|
|
225
|
+
const name = namer(record);
|
|
226
|
+
const effects = record.scope ? effectsOf(record.scope) : [];
|
|
227
|
+
const bindings = effects
|
|
228
|
+
.filter((effect) => effect.label)
|
|
229
|
+
.map((effect) => binding(effect.label.part, effect.label.node, effect, name, node));
|
|
230
|
+
// Listeners on the selected node (on the host itself, they're its parent's: see `setBy`).
|
|
231
|
+
if (node !== host) for (const part of listeners.get(node) ?? []) bindings.unshift(binding(part, node, null, name, node));
|
|
232
|
+
// The selected node is itself a component: what its parent binds onto it (its props, its events).
|
|
233
|
+
let setBy = null;
|
|
234
|
+
const parent = node === host ? hostOf(host, true) : null;
|
|
235
|
+
if (parent) {
|
|
236
|
+
const parentRecord = elements.get(parent);
|
|
237
|
+
const parentName = namer(parentRecord);
|
|
238
|
+
const onto = (parentRecord.scope ? effectsOf(parentRecord.scope) : [])
|
|
239
|
+
.filter((effect) => effect.label?.node === host)
|
|
240
|
+
.map((effect) => binding(effect.label.part, host, effect, parentName, host));
|
|
241
|
+
for (const part of listeners.get(host) ?? []) onto.push(binding(part, host, null, parentName, host));
|
|
242
|
+
if (onto.length) setBy = { tag: parentRecord.tag, file: parentRecord.definition.source?.file ?? null, bindings: onto };
|
|
243
|
+
}
|
|
244
|
+
const values = (entries) => entries.map(([key, signal]) => ({ name: key, value: preview(signal) }));
|
|
245
|
+
const written = [...last.written].map(name).filter(Boolean);
|
|
246
|
+
return {
|
|
247
|
+
version: 1,
|
|
248
|
+
native: isNative,
|
|
249
|
+
update: updates,
|
|
250
|
+
component: {
|
|
251
|
+
tag: record.tag,
|
|
252
|
+
file: record.definition.source?.file ?? null,
|
|
253
|
+
at: location(record.definition.source),
|
|
254
|
+
located: !!record.definition.source,
|
|
255
|
+
},
|
|
256
|
+
props: values(Object.entries(record.props)),
|
|
257
|
+
state: values(Object.entries(record.named ?? {}).filter(([, signal]) => signal && typeof signal === 'object')),
|
|
258
|
+
bindings,
|
|
259
|
+
setBy,
|
|
260
|
+
lastUpdate: {
|
|
261
|
+
number: updates,
|
|
262
|
+
written: [...new Set(written)],
|
|
263
|
+
ran: bindings.filter((item) => item.ranLast).length,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Open `file` at `line` and `column` in the editor, through Vite's dev server (or the page). */
|
|
269
|
+
export function open(file, line = 1, column = 1) {
|
|
270
|
+
// A page can show the place itself (the playground selects the line in its editor): it
|
|
271
|
+
// cancels this event. Otherwise Vite's dev server opens the editor.
|
|
272
|
+
const event = new CustomEvent('zubra:open', { detail: { file, line, column }, cancelable: true });
|
|
273
|
+
if (!globalThis.dispatchEvent(event)) return Promise.resolve(true);
|
|
274
|
+
return fetch(`/__open-in-editor?file=${encodeURIComponent(`${file}:${line}:${column}`)}`).then((response) => response.ok, () => false);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// (defineProperties, not Object.assign, which would copy the getter's value once.)
|
|
278
|
+
Object.defineProperties(globalThis.__ZUBRA_DEVTOOLS__, {
|
|
279
|
+
ready: { value: true, enumerable: true },
|
|
280
|
+
inspect: { value: inspect, enumerable: true },
|
|
281
|
+
open: { value: open, enumerable: true },
|
|
282
|
+
/** Counts flushes since connecting: the pane re-reads when it changes. */
|
|
283
|
+
updates: { get: () => updates, enumerable: true },
|
|
284
|
+
});
|
package/src/effect.js
CHANGED
|
@@ -28,6 +28,15 @@ let scheduled = null; // promise for the pending microtask flush
|
|
|
28
28
|
let flushing = false;
|
|
29
29
|
const effectOf = new WeakMap();
|
|
30
30
|
|
|
31
|
+
// @dev
|
|
32
|
+
/**
|
|
33
|
+
* For the DevTools hook (devtools.js), in development only: `label` is what the effect being
|
|
34
|
+
* created binds (element.js sets it around each binding), and `flushed` hears which effects each
|
|
35
|
+
* flush re-ran.
|
|
36
|
+
*/
|
|
37
|
+
export const dev = { label: null, flushed: null };
|
|
38
|
+
// @dev-end
|
|
39
|
+
|
|
31
40
|
const watcher = new Signal.subtle.Watcher(() => {
|
|
32
41
|
scheduled ??= new Promise((resolve) => queueMicrotask(() => {
|
|
33
42
|
try { flush(); } finally { scheduled = null; resolve(); }
|
|
@@ -113,6 +122,9 @@ class Effect extends Owner {
|
|
|
113
122
|
super(parent);
|
|
114
123
|
this.id = ++nextId;
|
|
115
124
|
this.cleanup = undefined;
|
|
125
|
+
// @dev
|
|
126
|
+
this.label = dev.label;
|
|
127
|
+
// @dev-end
|
|
116
128
|
this.computed = new Signal.Computed(() => {
|
|
117
129
|
this.disposeChildren();
|
|
118
130
|
this.runCleanup();
|
|
@@ -209,11 +221,19 @@ export function root(fn) {
|
|
|
209
221
|
export function flush() {
|
|
210
222
|
if (flushing) return;
|
|
211
223
|
flushing = true;
|
|
224
|
+
// @dev
|
|
225
|
+
const ran = [];
|
|
226
|
+
// @dev-end
|
|
212
227
|
try {
|
|
213
228
|
for (let round = 0; ; round++) {
|
|
214
229
|
const pending = watcher.getPending();
|
|
215
230
|
watcher.watch(); // re-arm before running, so writes made by effects schedule another round
|
|
216
|
-
if (pending.length === 0)
|
|
231
|
+
if (pending.length === 0) {
|
|
232
|
+
// @dev
|
|
233
|
+
if (ran.length) dev.flushed?.(ran);
|
|
234
|
+
// @dev-end
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
217
237
|
if (round >= MAX_ROUNDS) {
|
|
218
238
|
report(new Error('zubra: effects keep re-triggering each other; stopped after 100 rounds'));
|
|
219
239
|
return;
|
|
@@ -222,6 +242,9 @@ export function flush() {
|
|
|
222
242
|
for (const e of effects) {
|
|
223
243
|
if (e.disposed || e.paused) continue;
|
|
224
244
|
try { e.run(); } catch (error) { report(error); }
|
|
245
|
+
// @dev
|
|
246
|
+
ran.push(e);
|
|
247
|
+
// @dev-end
|
|
225
248
|
}
|
|
226
249
|
}
|
|
227
250
|
} finally {
|
package/src/element.js
CHANGED
|
@@ -12,6 +12,10 @@ import { Signal } from './signal.js';
|
|
|
12
12
|
import { wrap, raw } from './deep.js';
|
|
13
13
|
import { effect, scope, root as rootScope, tick, batch } from './effect.js';
|
|
14
14
|
import { RESOURCE } from './resource.js';
|
|
15
|
+
// @dev (left out of the production build: scripts/build-production.js)
|
|
16
|
+
import { dev as devEffects } from './effect.js';
|
|
17
|
+
import { register as devRegister, listened as devListened } from './devhook.js';
|
|
18
|
+
// @dev-end
|
|
15
19
|
|
|
16
20
|
const { untrack } = Signal.subtle;
|
|
17
21
|
const TEXT = 3;
|
|
@@ -690,6 +694,12 @@ function listen(part, node) {
|
|
|
690
694
|
}
|
|
691
695
|
|
|
692
696
|
function bind(part, node, hydrating, context) {
|
|
697
|
+
// @dev
|
|
698
|
+
// The effects created here are this part's (for the DevTools hook, devtools.js).
|
|
699
|
+
const outerLabel = devEffects.label;
|
|
700
|
+
devEffects.label = { part, node };
|
|
701
|
+
if (part.on && !part.global) devListened(part, node);
|
|
702
|
+
// @dev-end
|
|
693
703
|
if (part.text) {
|
|
694
704
|
effect(() => {
|
|
695
705
|
const text = part.text();
|
|
@@ -764,6 +774,9 @@ function bind(part, node, hydrating, context) {
|
|
|
764
774
|
} else if (part.slot) {
|
|
765
775
|
bindSlot(part, node, hydrating, context);
|
|
766
776
|
}
|
|
777
|
+
// @dev
|
|
778
|
+
devEffects.label = outerLabel;
|
|
779
|
+
// @dev-end
|
|
767
780
|
}
|
|
768
781
|
|
|
769
782
|
/** The signal saying whether slot `name` has content (shared by hasSlot() and the slot). */
|
|
@@ -1738,6 +1751,9 @@ function defineElement(tag, definition) {
|
|
|
1738
1751
|
mount: [], unmount: [], host: this, props: $p, instance: ++instances, ids: 0, filled: new Map(), states: [], kept, hydrated: new Signal.State(false),
|
|
1739
1752
|
internals: this.#internals, formReset: [], formRestore: [], formDisabled: [],
|
|
1740
1753
|
};
|
|
1754
|
+
// @dev
|
|
1755
|
+
let named = null; // the script's signals by name (a --devtools build)
|
|
1756
|
+
// @dev-end
|
|
1741
1757
|
// The element's own root: an effect that inserted it (a list row) doesn't own it, so
|
|
1742
1758
|
// re-running that effect can't dispose the element's effects.
|
|
1743
1759
|
this.#scope = rootScope(() => {
|
|
@@ -1749,7 +1765,17 @@ function defineElement(tag, definition) {
|
|
|
1749
1765
|
const initial = (name, compute) => {
|
|
1750
1766
|
if (!this.#given.has(name)) this.#signals[name].set(compute());
|
|
1751
1767
|
};
|
|
1752
|
-
|
|
1768
|
+
// @dev
|
|
1769
|
+
// A --devtools build hands over its script's signals by name, for the DevTools hook.
|
|
1770
|
+
const dev = (names) => { named = names; };
|
|
1771
|
+
// @dev-end
|
|
1772
|
+
try {
|
|
1773
|
+
parts = setup ? setup($p, (name, detail) => this.#emit(name, detail), initial
|
|
1774
|
+
// @dev
|
|
1775
|
+
, dev
|
|
1776
|
+
// @dev-end
|
|
1777
|
+
) : [];
|
|
1778
|
+
} finally { current = previous; }
|
|
1753
1779
|
// A form control with one two-way prop: that's what it submits, and what a form reset
|
|
1754
1780
|
// restores, unless the script says otherwise.
|
|
1755
1781
|
const models = Object.keys(props).filter((name) => props[name].model);
|
|
@@ -1829,6 +1855,9 @@ function defineElement(tag, definition) {
|
|
|
1829
1855
|
// always calls formDisabledCallback after connectedCallback, so this never runs there (the
|
|
1830
1856
|
// browser tests confirm the behaviour without it); it guards engines that order the
|
|
1831
1857
|
// callbacks the other way, which the test suite can't currently exercise.
|
|
1858
|
+
// @dev
|
|
1859
|
+
devRegister(this, { tag, definition, props: this.#signals, named, scope: this.#scope });
|
|
1860
|
+
// @dev-end
|
|
1832
1861
|
if (this.#disabled) for (const fn of this.#hooks.formDisabled) fn(true);
|
|
1833
1862
|
}
|
|
1834
1863
|
}
|
package/src/remark.js
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
// - an zb-if region holds the branch the current values choose, and an zb-each region one row
|
|
10
10
|
// per item, each with the shape of its block (checked element by element);
|
|
11
11
|
// - a light-DOM slot region is the rest of its range, when nothing follows it there; its
|
|
12
|
-
// content is the fallback if it has the fallback's shape, and supplied content otherwise
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// content is the fallback if it has the fallback's shape, and supplied content otherwise;
|
|
13
|
+
// - an zb-await region holds its zb-then block, for a value known at build time, or its pending
|
|
14
|
+
// block, which the element renders afresh anyway (so only where the region ends matters).
|
|
15
|
+
// Anything else (content projected into a child component, a region the server left to the
|
|
16
|
+
// browser, other than one left empty or a pending zb-await) can't be placed for sure, and the
|
|
17
|
+
// element renders afresh as before.
|
|
16
18
|
|
|
17
19
|
const TEXT = 3;
|
|
18
20
|
const COMMENT = 8;
|
|
@@ -55,7 +57,12 @@ export function remark(root, template, parts, { blueprint, isOpen, isClose, read
|
|
|
55
57
|
let index = 0;
|
|
56
58
|
for (; t; t = t.nextSibling) {
|
|
57
59
|
if (t.nodeType === TEXT) {
|
|
58
|
-
|
|
60
|
+
// Static text (if kept): the same words as the template's. Text that isn't (a page's own
|
|
61
|
+
// `<x-a>Save</x-a>` where the template has only whitespace) isn't server markup.
|
|
62
|
+
if (cursor?.nodeType === TEXT) {
|
|
63
|
+
if (cursor.data.trim() !== t.data.trim()) throw fail;
|
|
64
|
+
cursor = cursor.nextSibling;
|
|
65
|
+
}
|
|
59
66
|
continue;
|
|
60
67
|
}
|
|
61
68
|
const path = [...prefix, index];
|
|
@@ -146,8 +153,40 @@ export function remark(root, template, parts, { blueprint, isOpen, isClose, read
|
|
|
146
153
|
insert(parent, null, ']');
|
|
147
154
|
}
|
|
148
155
|
cursor = null;
|
|
156
|
+
} else if (part.await) {
|
|
157
|
+
// zb-await: the server rendered the zb-then block for a value it knew at build time (a
|
|
158
|
+
// plain value here too), or the pending block in a region it left to the element, which
|
|
159
|
+
// the element renders afresh anyway (`[?`): only its extent has to be found, by the
|
|
160
|
+
// pending block's shape. Failing both, an empty region left to the element, as above.
|
|
161
|
+
const saved = edits.length;
|
|
162
|
+
const start = cursor;
|
|
163
|
+
const value = untrack(() => $r(part.await()));
|
|
164
|
+
const thenable = (item) => item != null && typeof $r(item)?.then === 'function';
|
|
165
|
+
const settled = !(Array.isArray(value) ? value.some(thenable) : thenable(value));
|
|
166
|
+
const attempts = [];
|
|
167
|
+
if (settled && part.blocks[1]) attempts.push(['[', part.blocks[1], [new State(value), new State(() => {})]]);
|
|
168
|
+
if (part.blocks[0]) attempts.push(['[?', part.blocks[0], []]);
|
|
169
|
+
let placed = false;
|
|
170
|
+
for (const [open, chosen, args] of attempts) {
|
|
171
|
+
try {
|
|
172
|
+
insert(parent, cursor, open);
|
|
173
|
+
cursor = block(chosen, args);
|
|
174
|
+
insert(parent, cursor, ']');
|
|
175
|
+
placed = true;
|
|
176
|
+
break;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error !== fail) throw error;
|
|
179
|
+
edits.length = saved;
|
|
180
|
+
cursor = start;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!placed) {
|
|
184
|
+
if (!followsRegion(t, skipText(cursor))) throw fail;
|
|
185
|
+
insert(parent, cursor, '[?');
|
|
186
|
+
insert(parent, cursor, ']');
|
|
187
|
+
}
|
|
149
188
|
} else {
|
|
150
|
-
throw fail;
|
|
189
|
+
throw fail;
|
|
151
190
|
}
|
|
152
191
|
// Past the template's matching close.
|
|
153
192
|
let depth = 0;
|