solid-tag-runtime 0.0.15 → 0.0.18

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/ARCHITECTURE.md CHANGED
@@ -1344,11 +1344,11 @@ The shared render adapter lazily resolves the host application's own `solid-js`
1344
1344
  3. `data-solid-runtime` selects a matching HTML controller whose configured root contains the element. Without the attribute, controller selection follows the same rule as unscoped script discovery: a containing scoped controller may participate when `acceptUnscoped:true`, while an unscoped controller naturally accepts it. Equally specific matches are ambiguous and require explicit scope. A controller outside the element's root is never selected merely because it is the only candidate.
1345
1345
  4. The element remains in the DOM and is the light-DOM mount container. Wrapperless rendering belongs to bare `<script render>`.
1346
1346
  5. Renderer configuration (`module`, `component`, `data-solid-runtime`) is never forwarded as component props.
1347
- 6. Declarative props use `prop:*`; kebab names normalize to camelCase. Empty/presence values are boolean `true`, other values remain strings.
1348
- 7. `.props` carries arbitrary JavaScript references and overrides declarative props.
1347
+ 6. Declarative prop layers are bulk `props`, individual `prop:*`, then programmatic `.props`. Kebab `prop:*` names normalize to camelCase. Empty/presence values are boolean `true`; plain values remain strings; one outer `{...}` pair opts an individual prop into safe typed-data parsing.
1348
+ 7. Bulk `props` uses a safe JSON5-style object parser. Declarative parsing never evaluates JavaScript. `.props` carries arbitrary JavaScript references and overrides both declarative layers.
1349
1349
  8. Prop changes update a reactive prop facade and do not remount the component, preserving local state.
1350
1350
  9. Module/component/runtime identity changes increment a generation token, dispose the current owner, and remount. Stale async import successes and failures are ignored; they cannot mount or report an error against a newer identity.
1351
- 10. Initial light-DOM child nodes are captured once and exposed as reusable `props.children`; named slots and dynamic child recapture are not part of this phase.
1351
+ 10. Initial light-DOM child nodes are captured once and exposed as reusable `props.children`. From 0.0.18 the element is hidden while pending by default so those children do not flash before activation; `show-until-ready` intentionally exposes a cloned fallback while pending. Named slots and dynamic child recapture remain out of scope.
1352
1352
  11. Multiple elements may share one evaluated module namespace while owning independent Solid component roots.
1353
1353
  12. Disconnect disposes the mounted root; reconnect mounts a fresh component instance from the retained declaration inputs.
1354
1354
  13. Custom-element registration is global/idempotent per `CustomElementRegistry`, but controller creation does **not** eagerly upgrade existing `solid-render` elements. Initial `register()` / `observe()` first install the complete declarative script batch and then ensure the custom element is registered. `registerSolidRenderElement()` remains available for explicit/custom-registry use.
@@ -1356,7 +1356,7 @@ The shared render adapter lazily resolves the host application's own `solid-js`
1356
1356
 
1357
1357
  Rendering lifecycle joins the existing HTML event stream through `render-mounting`, `render-mounted`, `render-disposing`, and `render-disposed`; failures are also reported through `html-error` with `render`/`solid-render` operations. Lifecycle subscriptions remain observational and cannot alter rendering.
1358
1358
 
1359
- The first `solid-render` phase intentionally excludes named slots, Shadow DOM, automatic numeric/JSON coercion, arbitrary unprefixed prop forwarding, loading/fallback templates, and expression evaluation inside attributes.
1359
+ The initial `solid-render` phase excluded named slots, Shadow DOM, typed declarative data, loading/fallback visibility policy, and expression evaluation. `0.0.18` adds safe typed data plus pending/fallback visibility while continuing to exclude named slots, Shadow DOM, arbitrary unprefixed prop forwarding, and JavaScript expression evaluation.
1360
1360
 
1361
1361
  ### 0.0.10 — unresolved scoped render diagnostics
1362
1362
 
@@ -1476,3 +1476,130 @@ Reason: HTML lifecycle subscribers, devtools, status panels, and normal applicat
1476
1476
  Invariant: unrelated DOM mutations must not enqueue runtime discovery or emit `observer-batch`. Observing `document` is therefore robust against diagnostic/application UI churn, while matching declarations and mounted-render cleanup remain live.
1477
1477
 
1478
1478
  Regression coverage: package tests verify unrelated child-list additions enqueue no observer batch, matching runtime scripts still register normally, an `observer-batch` subscriber may render unrelated DOM without recursively triggering additional scans, and relevant declaration removals still dispose mounted render ownership. The browser playground test suite carries the same feedback-loop regression against the native `MutationObserver`.
1479
+
1480
+
1481
+ ### 0.0.16 — incremental mutation-driven observation
1482
+
1483
+ Decision: keep the 0.0.15 declaration-driven relevance filter, but stop turning every relevant `MutationObserver` callback back into `enqueueObserverScan()`. Normal observer callbacks now derive a bounded work set directly from `MutationRecord[]`.
1484
+
1485
+ Pipeline:
1486
+
1487
+ ```text
1488
+ MutationRecord[]
1489
+ ↓
1490
+ collectObserverMutationBatch()
1491
+ ├── exact added declaration elements
1492
+ ├── exact added <solid-render> retry candidates
1493
+ ├── tracked mounted records affected by removal
1494
+ └── unsafe/unknown host mutation → conservative full scan
1495
+ ↓
1496
+ enqueueObserverBatch()
1497
+ ↓
1498
+ process exact candidates once
1499
+ ```
1500
+
1501
+ Rules:
1502
+
1503
+ 1. An added element is checked directly and only its added subtree is queried for matching declarations.
1504
+ 2. Candidate identity is deduplicated across mutation records before registration.
1505
+ 3. Elements already owned by the same controller are dropped before observer work is queued, so `append()`/`addModule()` do not rediscover their own DOM mutations.
1506
+ 4. Relevant removals carry the exact mounted ownership records forward and dispose only records that are actually detached after the mutation batch settles.
1507
+ 5. `register()`, initial `observe({ registerExisting:true })`, explicit `flush()`, and root-change registration retain full-root scan semantics.
1508
+ 6. Unknown/custom DOM hosts that cannot be classified safely fall back to the previous full-root scan path.
1509
+ 7. Define-before-entry/render ordering is preserved because all exact declaration candidates in one observer callback are still claimed and defined as one batch before entries/renders execute.
1510
+
1511
+ Invariant: live observation is mutation-driven and incremental. Relevant mutation cost should scale with the changed subtree or tracked removal set, not with total root size. Full-root discovery is reserved for explicit scan operations and conservative fallback.
1512
+
1513
+ Regression coverage adds direct-addition and nested-subtree cases that assert zero root selector calls, duplicate-record deduplication, and explicit `append()` under active observation producing no observer batch. `bench/observer.mjs` provides a synthetic scaling check comparing incremental addition with forced `flush()`.
1514
+
1515
+
1516
+ ### 0.0.17 — explicit observation lifecycle strategies
1517
+
1518
+ Decision: observation is a compatibility mechanism for DOM changes the runtime does not already know about, not the primary module lifecycle mechanism. Keep `observe()` continuous by default for compatibility, but add an opt-in bootstrap lifecycle for applications that only need automatic discovery during initial page/framework startup.
1519
+
1520
+ Public modes:
1521
+
1522
+ ```text
1523
+ manual / no observer
1524
+ html.register() + explicit APIs + optional html.flush()
1525
+
1526
+ bootstrap / temporary observer
1527
+ html.observe({ mode: "bootstrap", idleMs })
1528
+
1529
+ continuous / long-lived observer
1530
+ html.observe()
1531
+ html.observe({ mode: "continuous" })
1532
+ ```
1533
+
1534
+ Bootstrap semantics:
1535
+
1536
+ 1. connect the normal incremental observer;
1537
+ 2. optionally register existing declarations using the existing `registerExisting` behavior;
1538
+ 3. remain connected through DOM readiness;
1539
+ 4. only relevant observer mutations increment bootstrap activity; unrelated UI mutations do not extend observation;
1540
+ 5. after `idleMs` with no new relevant activity, wait for the controller queue to drain;
1541
+ 6. if activity occurred during the wait, repeat the quiet-period check;
1542
+ 7. otherwise disconnect with lifecycle reason `bootstrap-idle`;
1543
+ 8. a manual `disconnect()` cancels bootstrap settling;
1544
+ 9. root rebinding preserves bootstrap state and resets relevant activity rather than creating a second observer;
1545
+ 10. after bootstrap completes, a later `observe()` may start a fresh continuous session.
1546
+
1547
+ Default `idleMs` is 50 ms. Invalid modes and negative/non-finite idle values are rejected.
1548
+
1549
+ Invariant: use continuous observation only when runtime declarations may enter the DOM through code outside the runtime's explicit APIs. Controlled dynamic module loading should remain observer-free after initial registration.
1550
+
1551
+ Regression coverage verifies continuous compatibility, bootstrap initial registration, late startup additions, unrelated mutation filtering, automatic disconnect, option validation, and re-observation after bootstrap.
1552
+
1553
+ ### 0.0.18 — `<solid-render>` pending visibility and safe typed declarative props
1554
+
1555
+ Decision: make reusable `<solid-render>` instances visually stable during asynchronous activation and make declarative prop data expressive without turning HTML attributes into executable JavaScript.
1556
+
1557
+ Visibility lifecycle:
1558
+
1559
+ ```text
1560
+ connected / identity changed
1561
+ ↓
1562
+ pending
1563
+ ↓
1564
+ module import + component mount
1565
+ ↓
1566
+ ready
1567
+
1568
+ error path: pending → error
1569
+ ```
1570
+
1571
+ Rules:
1572
+
1573
+ 1. `<solid-render>` is hidden while `data-solid-render-state="pending"` by default.
1574
+ 2. The HTML runtime installs one document-level visibility rule automatically; application CSS is not required. The rule also covers `solid-render:not(:defined)` once the HTML runtime has initialized, preventing the custom-element upgrade window from exposing fallback content.
1575
+ 3. `show-until-ready` opts out of hiding and displays a clone of captured initial light-DOM children while pending.
1576
+ 4. Error state is visible and restores captured initial content as fallback when no component mount exists.
1577
+ 5. `renderState` exposes `pending | ready | error`; `hideUntilReady` is the programmatic visibility-policy property.
1578
+ 6. Identity changes immediately re-enter `pending`; existing generation guards remain authoritative, so stale imports cannot reveal or mark ready a newer generation.
1579
+ 7. Visibility management does not change light-DOM ownership, `props.children` capture, runtime routing, or the independent Solid owner for each instance.
1580
+
1581
+ Declarative prop layers:
1582
+
1583
+ ```text
1584
+ props="{ ... }"
1585
+ ↓ overridden by
1586
+ prop:*
1587
+ ↓ overridden by
1588
+ element.props
1589
+ ```
1590
+
1591
+ Parsing rules:
1592
+
1593
+ 1. Bulk `props` must parse to a top-level object.
1594
+ 2. The built-in parser accepts a safe JSON5-style data subset: unquoted keys, single/double quoted strings, arrays/objects, booleans, null, finite numbers, comments, and trailing commas.
1595
+ 3. Plain individual `prop:*` attribute values remain HTML strings; empty/presence values remain boolean `true`.
1596
+ 4. One outer `{...}` pair marks an individual prop as typed data. Examples: `{3}`, `{true}`, `{[1,2,3]}`, and `{{a:123}}`.
1597
+ 5. Declarative parsing never uses `eval`, `new Function`, variable lookup, calls, member access, functions, constructors, or other executable JavaScript semantics. Unsupported expression syntax fails as a render/data error.
1598
+ 6. Prop-only changes update the existing reactive prop facade and do not remount. Invalid later declarative updates preserve the existing component mount rather than partially applying data.
1599
+ 7. An optional `parseProps(source, context)` controller hook may replace only declarative data decoding. `context.kind` distinguishes bulk `props` from one typed `prop:*`; module resolution, controller routing, and render ownership are unaffected.
1600
+ 8. Programmatic `.props` remains the path for functions, signals/accessors, services, DOM nodes, class instances, Maps/Sets, and other identity-sensitive JavaScript values.
1601
+
1602
+ Implementation boundary: the safe parser lives under `src/html/` and has no external runtime dependency, preserving direct-browser/zero-build package usage. The core runtime remains completely unaware of these HTML/data semantics.
1603
+
1604
+ Regression coverage verifies pending/ready/error state, default hiding, visible fallback opt-out, fallback restoration on error, typed individual values, structured bulk values, precedence, reactive updates without remount, custom parser behavior, non-execution of JavaScript-like input, and automatic visibility-style installation.
1605
+
package/README.md CHANGED
@@ -16,6 +16,48 @@ host Solid runtime
16
16
 
17
17
  The core package is module-first and DOM-independent. Browser discovery, ownership, declarative rendering, and `<solid-render>` live in `solid-tag-runtime/html`. Optional Solid setup/provider helpers live in `solid-tag-runtime/solid`.
18
18
 
19
+ ## 0.0.18 `<solid-render>` lifecycle and typed declarative props
20
+
21
+ `0.0.18` hides `<solid-render>` light-DOM content until the selected component is ready by default, exposes `pending` / `ready` / `error` through `data-solid-render-state`, and provides `show-until-ready` for intentional fallback content. The HTML runtime installs the pending-visibility rule automatically; applications do not need their own CSS.
22
+
23
+ Structured component data can now be written declaratively:
24
+
25
+ ```html
26
+ <solid-render
27
+ module="/Counter.jsx"
28
+ props="{ initial: 100, options: { theme: 'dark' } }"
29
+ prop:step="{5}"
30
+ prop:items="{[1, 2, 3]}"
31
+ ></solid-render>
32
+ ```
33
+
34
+ Plain `prop:*` values remain HTML strings; one outer `{...}` pair opts an individual prop into safe typed-data parsing. Bulk `props` uses a safe JSON5-style object syntax. Neither form evaluates JavaScript. Precedence is `props < prop:* < element.props`, and prop-only changes remain reactive without remounting.
35
+
36
+ ## 0.0.17 observation strategies
37
+
38
+ `0.0.17` adds explicit observation lifecycle strategies while keeping existing behavior backward compatible. `observe()` is still continuous by default. Applications that only need DOM discovery during startup can use `observe({ mode: "bootstrap" })`; it observes through DOM readiness and disconnects after a configurable quiet period of relevant runtime mutations.
39
+
40
+ ```ts
41
+ // Lowest ongoing overhead: initial scan, then explicit runtime APIs.
42
+ await html.register();
43
+
44
+ // Mostly declarative startup: temporary observer.
45
+ await html.observe({ mode: "bootstrap", idleMs: 50 });
46
+
47
+ // Unknown external DOM mutations throughout application lifetime.
48
+ await html.observe({ mode: "continuous" });
49
+ ```
50
+
51
+ Only relevant declaration/removal activity extends bootstrap lifetime. Ordinary application UI mutations do not.
52
+
53
+ ## 0.0.16 incremental observer processing
54
+
55
+ `0.0.16` keeps the `0.0.15` declaration-driven filter but removes the normal relevant-mutation root rescan. `MutationRecord[]` are converted into exact declaration, `<solid-render>`, and mounted-removal candidates; added subtrees are queried locally and duplicate candidates are coalesced before one observer batch.
56
+
57
+ Explicit APIs such as `append()` and `addModule()` claim ownership before DOM insertion, so an active observer now ignores that already-known work entirely. Full-root discovery remains available for initial registration, `flush()`, root-change registration, and conservative fallback when a host DOM cannot be classified safely.
58
+
59
+ The key performance invariant is that relevant observer work scales with the changed subtree rather than total root size.
60
+
19
61
  ## 0.0.15 observer hardening
20
62
 
21
63
  `0.0.15` filters `MutationObserver` records before scheduling HTML discovery. Ordinary UI DOM changes no longer enqueue a full runtime scan or emit an `observer-batch`; matching runtime declarations, `<solid-render>` retry boundaries, and removals that can detach mounted declarative renders still trigger observer work. Explicit `html.flush()` remains a deterministic forced scan.
@@ -167,6 +209,8 @@ See [Wrapperless delegation](./docs/wrapperless-delegation.md).
167
209
 
168
210
  **In HTML, always use the explicit closing tag.** Do not write `<solid-render ... />`; custom elements are not HTML void elements and the self-closing slash is ignored by the HTML parser.
169
211
 
212
+ Initial light-DOM content is hidden until the component is ready by default. Use `show-until-ready` when those children are intentional loading/fallback content. See the [`<solid-render>` guide](./docs/solid-render.md) for structured `props`, typed `prop:*` literals, precedence, and render-state details.
213
+
170
214
  ## Persistent compile cache
171
215
 
172
216
  The opt-in compile cache persists **pre-link compiler artifacts**. Runtime-specific linked URLs and evaluated module namespaces are never persisted.
@@ -217,9 +261,9 @@ When loading from an import map, map every used package subpath to the same rele
217
261
  ```json
218
262
  {
219
263
  "imports": {
220
- "solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.15",
221
- "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.15/html",
222
- "solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.15/solid"
264
+ "solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.18",
265
+ "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.18/html",
266
+ "solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.18/solid"
223
267
  }
224
268
  }
225
269
  ```
@@ -0,0 +1,103 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { createRuntime } from "../src/runtime.js";
3
+ import { createHTMLRuntime } from "../src/html.js";
4
+
5
+ class BenchMutationObserver {
6
+ static instances = [];
7
+ constructor(callback) { this.callback = callback; BenchMutationObserver.instances.push(this); }
8
+ observe(target) { this.target = target; this.connected = true; }
9
+ disconnect() { this.connected = false; }
10
+ trigger(records) { if (this.connected) this.callback(records, this); }
11
+ }
12
+
13
+ function script(id) {
14
+ const attrs = { type: "solid-js", module: id };
15
+ return {
16
+ nodeType: 1,
17
+ textContent: "export const value = 1;",
18
+ parentNode: null,
19
+ getAttribute(name) { return attrs[name] ?? null; },
20
+ hasAttribute(name) { return Object.prototype.hasOwnProperty.call(attrs, name); },
21
+ setAttribute(name, value) { attrs[name] = String(value); },
22
+ matches(selector) { return String(selector).includes('script[type="solid-js"]'); },
23
+ querySelectorAll() { return []; },
24
+ contains(target) { return target === this; },
25
+ };
26
+ }
27
+
28
+ function makeRoot(size) {
29
+ const filler = Array.from({ length: size }, () => ({ nodeType: 1 }));
30
+ const scripts = [];
31
+ let visited = 0;
32
+ return {
33
+ nodes: filler,
34
+ scripts,
35
+ querySelectorAll(selector) {
36
+ visited += this.nodes.length + this.scripts.length;
37
+ if (selector === "solid-render") return [];
38
+ return [...this.scripts];
39
+ },
40
+ contains(node) { return this.nodes.includes(node) || this.scripts.includes(node); },
41
+ resetVisited() { visited = 0; },
42
+ getVisited() { return visited; },
43
+ };
44
+ }
45
+
46
+ function makeRuntime() {
47
+ return createRuntime({
48
+ compiler: {
49
+ fingerprint: "observer-bench-v1",
50
+ analyze() { return { imports: [] }; },
51
+ transform(source) { return { code: source, diagnostics: [] }; },
52
+ },
53
+ });
54
+ }
55
+
56
+ async function waitTask() { await new Promise(resolve => setTimeout(resolve, 0)); }
57
+
58
+ async function measureIncremental(size) {
59
+ BenchMutationObserver.instances.length = 0;
60
+ const runtime = makeRuntime();
61
+ const root = makeRoot(size);
62
+ const html = createHTMLRuntime(runtime, { root, MutationObserver: BenchMutationObserver });
63
+ await html.observe({ registerExisting: false });
64
+ root.resetVisited();
65
+ const element = script(`/incremental-${size}.js`);
66
+ element.parentNode = root;
67
+ root.scripts.push(element);
68
+ const t0 = performance.now();
69
+ BenchMutationObserver.instances[0].trigger([{ type: "childList", addedNodes: [element], removedNodes: [] }]);
70
+ await waitTask();
71
+ const ms = performance.now() - t0;
72
+ const visited = root.getVisited();
73
+ html.disconnect();
74
+ runtime.dispose();
75
+ return { ms, visited };
76
+ }
77
+
78
+ async function measureForcedScan(size) {
79
+ BenchMutationObserver.instances.length = 0;
80
+ const runtime = makeRuntime();
81
+ const root = makeRoot(size);
82
+ const html = createHTMLRuntime(runtime, { root, MutationObserver: BenchMutationObserver });
83
+ await html.observe({ registerExisting: false });
84
+ const element = script(`/scan-${size}.js`);
85
+ element.parentNode = root;
86
+ root.scripts.push(element);
87
+ root.resetVisited();
88
+ const t0 = performance.now();
89
+ await html.flush();
90
+ const ms = performance.now() - t0;
91
+ const visited = root.getVisited();
92
+ html.disconnect();
93
+ runtime.dispose();
94
+ return { ms, visited };
95
+ }
96
+
97
+ console.log("observer benchmark (synthetic root; lower visited count is the main invariant)");
98
+ console.log("nodes\tincremental ms\tincremental visited\tflush ms\tflush visited");
99
+ for (const size of [1_000, 10_000, 50_000]) {
100
+ const incremental = await measureIncremental(size);
101
+ const scan = await measureForcedScan(size);
102
+ console.log(`${size}\t${incremental.ms.toFixed(3)}\t${incremental.visited}\t${scan.ms.toFixed(3)}\t${scan.visited}`);
103
+ }
package/docs/api/html.md CHANGED
@@ -25,17 +25,24 @@ Common options:
25
25
  - `executeEntries`
26
26
  - `executeRenders`
27
27
  - `onError`
28
+ - `parseProps(source, context)` for optional custom `<solid-render>` declarative-data parsing
28
29
 
29
30
  ## Discovery and observation
30
31
 
31
32
  ```ts
32
33
  await html.register(options?);
33
- await html.observe(options?);
34
+ await html.observe({
35
+ mode: "continuous" | "bootstrap",
36
+ idleMs: 50,
37
+ ...options,
38
+ });
34
39
  await html.flush(options?);
35
40
  html.disconnect();
36
41
  ```
37
42
 
38
- `observe()` filters child-list mutation records and schedules discovery only for mutations that can affect runtime declarations, `<solid-render>` retry work, or mounted declarative-render cleanup. `flush()` bypasses that filter and forces a scan.
43
+ `observe()` is continuous by default. `mode: "bootstrap"` temporarily observes startup DOM activity and resolves after DOM readiness plus `idleMs` with no relevant runtime mutations, at which point the observer is disconnected. `mode: "continuous"` retains long-lived observation.
44
+
45
+ `observe()` processes relevant child-list mutation records incrementally. Added declarations are discovered from the added node/subtree itself, removals consult tracked mounted ownership, and explicit API-owned elements are skipped. A full-root scan is reserved for initial registration, `flush()`, root-change registration, or conservative fallback. `flush()` always forces a full scan.
39
46
 
40
47
  ## Explicit ownership
41
48
 
@@ -89,3 +96,27 @@ For rendering semantics see [Declarative rendering](../rendering.md) and [`<soli
89
96
  For bare `<script render>` ranges, automatic Solid delegated-event setup is available when the underlying runtime was created with `createSolidRuntime()` from `solid-tag-runtime/solid`.
90
97
 
91
98
  The HTML adapter asks the runtime's private Solid integration to establish document-level delegation lazily, then keeps the declaration in its existing independent `createRoot() + insert()` lifecycle. Selector renders and `<solid-render>` retain their existing container semantics.
99
+
100
+ ## `<solid-render>` state and props
101
+
102
+ `0.0.18` exposes these element properties/state surfaces:
103
+
104
+ ```ts
105
+ renderer.renderState; // "pending" | "ready" | "error"
106
+ renderer.hideUntilReady; // true by default
107
+ renderer.props = { ... }; // strongest prop layer
108
+ ```
109
+
110
+ The DOM mirrors render state through `data-solid-render-state`. Pending content is hidden automatically unless `show-until-ready` is present.
111
+
112
+ Declarative data layers are:
113
+
114
+ ```text
115
+ props="{ initial: 100 }"
116
+ < prop:step="{5}"
117
+ < renderer.props
118
+ ```
119
+
120
+ Plain `prop:*` values are strings. An outer `{...}` marker parses one individual value with the same safe JSON5-style data parser used by bulk `props`. No declarative form evaluates JavaScript.
121
+
122
+ A custom parser receives `(source, context)` where `context.kind` is `"props"` or `"prop"`; bulk `props` must return a top-level object.
@@ -97,6 +97,21 @@ Declarative source:
97
97
 
98
98
  See [HTML runtime](./html-runtime.md), [declarative rendering](./rendering.md), and [`<solid-render>`](./solid-render.md).
99
99
 
100
+
101
+ ## Reusable component instance
102
+
103
+ After the HTML controller registers declarations, an existing runtime module can be instantiated directly from HTML:
104
+
105
+ ```html
106
+ <solid-render
107
+ module="/Counter.jsx"
108
+ props="{ initial: 100 }"
109
+ prop:step="{5}"
110
+ ></solid-render>
111
+ ```
112
+
113
+ `<solid-render>` is hidden while pending by default, so initial child content does not flash before the Solid mount is ready. Add `show-until-ready` when the initial children are intentional loading content. Plain `prop:*` values are strings; `{...}` opts an individual prop into safe typed-data parsing.
114
+
100
115
  ## Browser import maps
101
116
 
102
117
  Map every used package subpath explicitly:
@@ -104,9 +119,9 @@ Map every used package subpath explicitly:
104
119
  ```json
105
120
  {
106
121
  "imports": {
107
- "solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.15",
108
- "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.15/html",
109
- "solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.15/solid"
122
+ "solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.18",
123
+ "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.18/html",
124
+ "solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.18/solid"
110
125
  }
111
126
  }
112
127
  ```
@@ -20,18 +20,48 @@ const html = createHTMLRuntime(runtime, {
20
20
  await html.register();
21
21
  ```
22
22
 
23
- ## Observe later declarations
23
+ ## Choose an observation strategy
24
+
25
+ For controlled applications, prefer one initial scan plus explicit runtime APIs:
24
26
 
25
27
  ```ts
26
- await html.observe({ registerExisting: false });
28
+ await html.register();
29
+
30
+ await html.addModule(...);
31
+ await html.append(script);
32
+ ```
33
+
34
+ If raw DOM insertion is known and occasional, keep observation disabled and synchronize explicitly:
27
35
 
28
- // after external DOM mutations
36
+ ```ts
37
+ root.insertAdjacentHTML("beforeend", markup);
29
38
  await html.flush();
30
39
  ```
31
40
 
32
- The live observer is declaration-driven rather than mutation-driven. It inspects each `MutationRecord` before scheduling discovery and ignores ordinary UI child-list changes that cannot add a matching runtime declaration or require mounted-render cleanup. This means a document-scoped controller can coexist with application/devtools DOM updates without repeatedly rescanning the document.
41
+ For mostly declarative startup, use a temporary bootstrap observer:
42
+
43
+ ```ts
44
+ await html.observe({
45
+ mode: "bootstrap",
46
+ idleMs: 50,
47
+ });
48
+ ```
49
+
50
+ Bootstrap mode observes through DOM readiness, waits until there have been no **relevant runtime mutations** for `idleMs`, drains queued registration work, and disconnects. Ordinary UI mutations do not reset the quiet period.
51
+
52
+ For pages where declarations can arrive through uncontrolled DOM code throughout the application lifetime, use continuous observation:
53
+
54
+ ```ts
55
+ await html.observe({ mode: "continuous" });
56
+ ```
57
+
58
+ `observe()` with no `mode` remains equivalent to continuous mode for backward compatibility.
33
59
 
34
- `flush()` is intentionally different: it remains an explicit forced scan and can be used as a deterministic synchronization point even when no relevant observer mutation was seen.
60
+ The live observer is declaration-driven and incremental. It inspects each `MutationRecord`, ignores ordinary UI child-list changes, and derives exact runtime declaration/removal candidates from relevant added or removed subtrees. Normal relevant mutations do not rescan the configured root.
61
+
62
+ Added subtrees are queried only within the subtree that changed, duplicate candidates are coalesced, and elements already claimed through explicit APIs are skipped before observer work is queued. This means observer cost scales primarily with changed runtime-relevant DOM rather than total page size.
63
+
64
+ `flush()` is intentionally different: it remains an explicit forced full-root scan and can be used as a deterministic synchronization point after known raw DOM insertion.
35
65
 
36
66
  ## Script declarations
37
67
 
@@ -122,3 +152,9 @@ A low-level `createRuntime()` controller behaves exactly as before and does not
122
152
  When the runtime comes from `createSolidRuntime()` in `solid-tag-runtime/solid`, a bare `<script render>` can lazily request the document-level delegated-event setup required by Solid 2. Each marker-range declaration still owns an independent reactive root and disposer; the delegation host is infrastructure only and is not a shared reactive owner.
123
153
 
124
154
  See [Solid runtime setup](./solid-runtime-setup.md) and [Wrapperless delegation](./wrapperless-delegation.md).
155
+
156
+ ## `<solid-render>` declarative data and pending visibility
157
+
158
+ The HTML controller also owns the declarative-data parser configuration used by `<solid-render>`. The default parser treats bulk `props` as a safe JSON5-style object and treats `prop:name="{...}"` as an explicitly typed individual value. Plain individual attributes remain strings. Applications may override this data parser with `parseProps`, but parsing never changes module/runtime ownership.
159
+
160
+ When a controller is created in a browser document it installs one internal visibility rule for `<solid-render>`. Pending elements are hidden by default, including the pre-upgrade `:not(:defined)` window after the runtime has initialized. `show-until-ready` opts into visible fallback content. The custom element itself drives `data-solid-render-state="pending|ready|error"`.
@@ -74,6 +74,11 @@ html.subscribe("element-registered", event => {
74
74
 
75
75
  HTML events cover ownership, observation, roots, entry execution, render mounting/disposal, warnings, and errors.
76
76
 
77
- `observer-batch` is emitted only when observer work is actually scheduled. Unrelated child-list mutations are filtered before discovery, so a lifecycle subscriber may render diagnostics/status UI inside an observed document without causing a self-sustaining observer/event loop. Explicit `html.flush()` still performs and reports a forced scan.
77
+ `observer-batch` is emitted only when semantic observer work is scheduled. Unrelated child-list mutations are filtered before discovery, and relevant additions are processed from their exact changed subtree instead of rescanning the whole root. A lifecycle subscriber may therefore render diagnostics/status UI inside an observed document without causing a feedback loop. Explicit `html.flush()` still performs and reports a forced full-root scan.
78
78
 
79
79
  Listener exceptions are isolated from runtime/HTML operations and are reported to the console rather than aborting the underlying action.
80
+
81
+
82
+ ## Bootstrap observation lifecycle
83
+
84
+ `observe({ mode: "bootstrap" })` emits the normal `observer-connected` event and later an `observer-disconnected` event with reason `bootstrap-idle` when its relevant-mutation quiet period completes. Unrelated UI DOM mutations neither emit `observer-batch` nor extend bootstrap lifetime.
package/docs/rendering.md CHANGED
@@ -69,3 +69,7 @@ Combining `entry` and `render` on one declaration is rejected.
69
69
  ## Runtime scope diagnostics
70
70
 
71
71
  A scoped render declaration is immediate work. If `data-solid-runtime="main"` cannot be handled by any registered controller covering that DOM root, the HTML adapter emits `html-warning` with code `unresolved-runtime-scope` and logs one warning.
72
+
73
+ ## Reusable `<solid-render>` lifecycle
74
+
75
+ Reusable `<solid-render>` instances use container rendering rather than the marker-range wrapperless path. From `0.0.18`, they are hidden while pending by default and expose `data-solid-render-state="pending|ready|error"`. `show-until-ready` keeps captured initial children visible as fallback content. Structured `props` and typed `prop:*="{...}"` values are data-only and do not evaluate JavaScript. See [`<solid-render>`](./solid-render.md).