solid-tag-runtime 0.0.17 → 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
 
@@ -1549,3 +1549,57 @@ Default `idleMs` is 50 ms. Invalid modes and negative/non-finite idle values are
1549
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
1550
 
1551
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,23 @@ 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
+
19
36
  ## 0.0.17 observation strategies
20
37
 
21
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.
@@ -192,6 +209,8 @@ See [Wrapperless delegation](./docs/wrapperless-delegation.md).
192
209
 
193
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.
194
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
+
195
214
  ## Persistent compile cache
196
215
 
197
216
  The opt-in compile cache persists **pre-link compiler artifacts**. Runtime-specific linked URLs and evaluated module namespaces are never persisted.
@@ -242,9 +261,9 @@ When loading from an import map, map every used package subpath to the same rele
242
261
  ```json
243
262
  {
244
263
  "imports": {
245
- "solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.17",
246
- "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.17/html",
247
- "solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.17/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"
248
267
  }
249
268
  }
250
269
  ```
package/docs/api/html.md CHANGED
@@ -25,6 +25,7 @@ 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
 
@@ -95,3 +96,27 @@ For rendering semantics see [Declarative rendering](../rendering.md) and [`<soli
95
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`.
96
97
 
97
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.17",
108
- "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.17/html",
109
- "solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.17/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
  ```
@@ -152,3 +152,9 @@ A low-level `createRuntime()` controller behaves exactly as before and does not
152
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.
153
153
 
154
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"`.
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).
@@ -1,20 +1,21 @@
1
1
  # `<solid-render>`
2
2
 
3
- `<solid-render>` references an already-defined runtime module and mounts a component instance into its own light DOM.
3
+ `<solid-render>` references an already-defined runtime module and mounts one component instance into its own light DOM.
4
4
 
5
5
  ```html
6
6
  <script type="solid-jsx" module="/Counter.jsx">
7
- export default function Counter() {
8
- return <button>Counter</button>;
7
+ export default function Counter(props) {
8
+ return <button>{props.initial}</button>;
9
9
  }
10
10
  </script>
11
11
 
12
- <solid-render module="/Counter.jsx"></solid-render>
12
+ <solid-render
13
+ module="/Counter.jsx"
14
+ props="{ initial: 100 }"
15
+ ></solid-render>
13
16
  ```
14
17
 
15
- ## Important HTML syntax rule
16
-
17
- **Always use an explicit closing tag in HTML.**
18
+ ## Always use an explicit closing tag
18
19
 
19
20
  Correct:
20
21
 
@@ -28,7 +29,68 @@ Do not write:
28
29
  <solid-render module="/Counter.jsx" />
29
30
  ```
30
31
 
31
- Custom elements are not HTML void elements. The HTML parser ignores the XML-style self-closing slash, so following siblings may become children of `<solid-render>`. Because initial child content is captured as `props.children`, this can make later page content appear to disappear.
32
+ Custom elements are not HTML void elements. The HTML parser ignores the XML-style self-closing slash, so following siblings may accidentally become children of `<solid-render>`.
33
+
34
+ ## Hidden until ready by default
35
+
36
+ `0.0.18` prevents initial light-DOM content from flashing before the requested component is ready.
37
+
38
+ ```html
39
+ <solid-render module="/Counter.jsx">
40
+ this text is captured, but does not flash before the component mounts
41
+ </solid-render>
42
+ ```
43
+
44
+ The runtime installs the required visibility rule automatically; application CSS is not required.
45
+
46
+ The element exposes its current state:
47
+
48
+ ```text
49
+ data-solid-render-state="pending"
50
+ data-solid-render-state="ready"
51
+ data-solid-render-state="error"
52
+ ```
53
+
54
+ The normal lifecycle is:
55
+
56
+ ```text
57
+ pending
58
+ ↓
59
+ module import / component mount
60
+ ↓
61
+ ready
62
+ ```
63
+
64
+ On failure:
65
+
66
+ ```text
67
+ pending → error
68
+ ```
69
+
70
+ Error state is visible, and captured initial content is restored as fallback content.
71
+
72
+ The visibility rule is installed as soon as the HTML runtime is created. For pages that need to avoid any paint before the runtime bootstrap itself executes, load/bootstrap the HTML runtime early in the document.
73
+
74
+ ### Show fallback content while pending
75
+
76
+ If initial children are intentional loading content, opt out of hiding:
77
+
78
+ ```html
79
+ <solid-render
80
+ module="/Account.jsx"
81
+ show-until-ready
82
+ >
83
+ Loading account…
84
+ </solid-render>
85
+ ```
86
+
87
+ Programmatically:
88
+
89
+ ```ts
90
+ renderer.hideUntilReady = false;
91
+ ```
92
+
93
+ The state attribute still transitions through `pending`, `ready`, and `error`; only the visibility policy changes.
32
94
 
33
95
  ## Named component
34
96
 
@@ -39,6 +101,8 @@ Custom elements are not HTML void elements. The HTML parser ignores the XML-styl
39
101
  ></solid-render>
40
102
  ```
41
103
 
104
+ Without `component`, `module.default` is used.
105
+
42
106
  ## Runtime selection
43
107
 
44
108
  ```html
@@ -50,48 +114,180 @@ Custom elements are not HTML void elements. The HTML parser ignores the XML-styl
50
114
 
51
115
  The selected controller must match scope rules and contain the element within its configured root.
52
116
 
53
- ## Declarative props
117
+ ## Structured declarative props
118
+
119
+ Use `props` for a structured object:
54
120
 
55
121
  ```html
56
122
  <solid-render
57
123
  module="/UserCard.jsx"
58
- prop:name="Alice"
59
- prop:user-id="42"
60
- prop:compact
124
+ props="{
125
+ user: {
126
+ name: 'Alice',
127
+ age: 32,
128
+ },
129
+ compact: true,
130
+ tags: ['admin', 'active'],
131
+ }"
61
132
  ></solid-render>
62
133
  ```
63
134
 
64
- Rules:
135
+ The built-in parser accepts safe JSON5-style data conveniences:
65
136
 
66
- - `prop:user-id` becomes `userId`
67
- - a present empty prop becomes `true`
68
- - other attribute values remain strings
69
- - renderer configuration (`module`, `component`, `data-solid-runtime`) is not forwarded
70
- - `prop:module` is a normal component prop and does not collide with renderer `module`
137
+ - unquoted object keys
138
+ - single- or double-quoted strings
139
+ - arrays and nested objects
140
+ - numbers, booleans, and `null`
141
+ - trailing commas
142
+ - line/block comments
71
143
 
72
- ## Programmatic props
144
+ Declarative props are **data only**. The parser never uses `eval()` or `new Function()` and does not resolve JavaScript variables, member access, calls, functions, or constructors.
73
145
 
74
- ```ts
75
- const element = document.querySelector("solid-render");
146
+ This is rejected rather than executed:
147
+
148
+ ```html
149
+ <solid-render props="{ value: getValue() }"></solid-render>
150
+ ```
151
+
152
+ The top-level `props` value must be an object.
153
+
154
+ ## Individual `prop:*` values
155
+
156
+ Plain HTML attribute values remain strings:
157
+
158
+ ```html
159
+ <solid-render
160
+ prop:first="123"
161
+ prop:second=123
162
+ ></solid-render>
163
+ ```
164
+
165
+ Both values are the string `"123"`.
166
+
167
+ A present empty prop remains boolean `true`:
168
+
169
+ ```html
170
+ <solid-render prop:compact></solid-render>
171
+ ```
172
+
173
+ To opt one prop into typed data parsing, wrap it in one outer `{...}` pair:
174
+
175
+ ```html
176
+ <solid-render
177
+ prop:count="{3}"
178
+ prop:enabled="{true}"
179
+ prop:missing="{null}"
180
+ prop:label="{'3'}"
181
+ prop:items="{[1, 2, 3]}"
182
+ prop:options="{{ theme: 'dark', step: 5 }}"
183
+ ></solid-render>
184
+ ```
185
+
186
+ Effective values:
187
+
188
+ ```js
189
+ {
190
+ count: 3,
191
+ enabled: true,
192
+ missing: null,
193
+ label: "3",
194
+ items: [1, 2, 3],
195
+ options: { theme: "dark", step: 5 },
196
+ }
197
+ ```
198
+
199
+ The outer braces are a typed-data marker, not JavaScript expression syntax. Object values naturally use double braces because the inner braces belong to the object literal.
200
+
201
+ `prop:user-id` still normalizes to `userId`.
202
+
203
+ ## Prop precedence
204
+
205
+ The effective prop layers are:
206
+
207
+ ```text
208
+ props="..."
209
+ ↓ overridden by
210
+ prop:*
211
+ ↓ overridden by
212
+ element.props
213
+ ```
214
+
215
+ Example:
216
+
217
+ ```html
218
+ <solid-render
219
+ module="/Counter.jsx"
220
+ props="{ initial: 100, step: 5 }"
221
+ prop:step="{10}"
222
+ ></solid-render>
223
+ ```
76
224
 
77
- element.props = {
78
- user,
225
+ Then:
226
+
227
+ ```ts
228
+ renderer.props = {
229
+ step: 20,
79
230
  onSave,
80
- service,
81
231
  };
82
232
  ```
83
233
 
84
- Programmatic values may contain arbitrary JavaScript references and override declarative `prop:*` values.
234
+ The effective `step` is `20`. Programmatic `.props` can contain arbitrary JavaScript values such as functions, signals, services, class instances, Maps/Sets, or identity-sensitive objects.
235
+
236
+ ## Reactive prop updates
237
+
238
+ Changing any of these updates the existing mounted component without remounting:
239
+
240
+ ```text
241
+ props attribute
242
+ prop:* attributes
243
+ element.props
244
+ ```
245
+
246
+ For example:
247
+
248
+ ```ts
249
+ renderer.setAttribute(
250
+ "props",
251
+ "{ initial: 200, options: { theme: 'light' } }",
252
+ );
85
253
 
86
- ## Reactive updates
254
+ renderer.setAttribute("prop:step", "{20}");
255
+ ```
87
256
 
88
- Changing `prop:*` or `.props` updates the existing component instance without remounting.
257
+ Local component state is preserved.
89
258
 
90
259
  Changing `module`, `component`, or `data-solid-runtime` changes render identity and therefore disposes/remounts.
91
260
 
261
+ Invalid declarative data produces a `solid-render` error; it is never partially executed as JavaScript.
262
+
263
+ ## Custom declarative parser
264
+
265
+ Applications with a domain-specific data syntax may override declarative parsing at the HTML-controller boundary:
266
+
267
+ ```ts
268
+ const html = createHTMLRuntime(runtime, {
269
+ parseProps(source, context) {
270
+ if (context.kind === "props") {
271
+ return myObjectParser(source);
272
+ }
273
+
274
+ return myValueParser(source);
275
+ },
276
+ });
277
+ ```
278
+
279
+ Context distinguishes:
280
+
281
+ ```text
282
+ kind: "props" bulk props attribute
283
+ kind: "prop" typed prop:* value
284
+ ```
285
+
286
+ A custom bulk parser must still return an object. Parser customization changes data decoding only; it does not change runtime selection, module resolution, or component ownership.
287
+
92
288
  ## Children
93
289
 
94
- Initial child DOM becomes `props.children`:
290
+ Initial child DOM is captured once and becomes `props.children`:
95
291
 
96
292
  ```html
97
293
  <solid-render module="/Card.jsx">
@@ -99,7 +295,9 @@ Initial child DOM becomes `props.children`:
99
295
  </solid-render>
100
296
  ```
101
297
 
102
- Initial children are captured once. Named slots and dynamic child recapture are not part of the current API.
298
+ By default those children are hidden while the element is pending, then instantiated through `props.children` when the component mounts. With `show-until-ready`, a clone is also displayed as fallback content while pending.
299
+
300
+ Named slots and dynamic child recapture are not part of the current API.
103
301
 
104
302
  ## Multiple instances
105
303
 
@@ -126,7 +126,7 @@ When provider fallback is needed and there is no versioned Solid anchor, this re
126
126
  import { TESTED_SOLID_VERSION } from "solid-tag-runtime/solid";
127
127
  ```
128
128
 
129
- For `0.0.14` through `0.0.17`, the tested fallback line is Solid `2.0.0-rc.13`.
129
+ For `0.0.14` through `0.0.18`, the tested fallback line is Solid `2.0.0-rc.13`.
130
130
 
131
131
  If an existing Solid mapping is explicitly versioned, that version becomes the family anchor for generated siblings. An explicitly unversioned existing mapping remains unversioned; the loader does not pretend it is pinned.
132
132
 
@@ -37,14 +37,19 @@
37
37
  <solid-render
38
38
  data-solid-runtime="main"
39
39
  module="/Counter.jsx"
40
- prop:label="Counter A"
40
+ props="{ label: 'Counter A' }"
41
+ prop:initial="{100}"
41
42
  ></solid-render>
42
43
 
43
44
  <solid-render
44
45
  data-solid-runtime="main"
45
46
  module="/Counter.jsx"
46
47
  prop:label="Counter B"
47
- ></solid-render>
48
+ prop:initial="{200}"
49
+ show-until-ready
50
+ >
51
+ Loading Counter B…
52
+ </solid-render>
48
53
 
49
54
  <!--
50
55
  This bootstrap is ordinary JavaScript. The surrounding application/import
package/html.d.ts CHANGED
@@ -18,8 +18,13 @@ export interface HTMLAttributeElementLike {
18
18
 
19
19
  export interface HTMLModuleScriptElement extends HTMLAttributeElementLike {}
20
20
 
21
+ export type SolidRenderState = "pending" | "ready" | "error";
22
+
21
23
  export interface SolidRenderElement extends HTMLAttributeElementLike, HTMLAppendTarget {
22
24
  props: Record<string, unknown>;
25
+ readonly renderState: SolidRenderState;
26
+ /** Default true. Set false to expose captured fallback children while pending. */
27
+ hideUntilReady: boolean;
23
28
  childNodes?: ArrayLike<any> | Iterable<any>;
24
29
  attributes?: ArrayLike<{ name: string; value: string }> | Iterable<{ name: string; value: string }>;
25
30
  replaceChildren?(...nodes: any[]): void;
@@ -32,6 +37,7 @@ export interface HTMLAppendTarget {
32
37
  }
33
38
 
34
39
  export interface HTMLDocumentLike extends HTMLModuleRoot, HTMLAppendTarget {
40
+ head?: HTMLAppendTarget;
35
41
  body?: HTMLAppendTarget;
36
42
  documentElement?: HTMLAppendTarget;
37
43
  createElement?(tagName: string): any;
@@ -159,6 +165,8 @@ export interface RemoveHTMLElementResult {
159
165
 
160
166
  export interface RegisterHTMLOptions extends DefineScriptOptions {
161
167
  selector?: string;
168
+ /** Override safe declarative data parsing for `<solid-render props>` and typed `prop:*="{...}"`. */
169
+ parseProps?: HTMLDeclarativePropsParser;
162
170
  executeEntries?: boolean;
163
171
  /** Execute declarative render actions. Default: true. */
164
172
  executeRenders?: boolean;
@@ -257,6 +265,19 @@ export interface AddHTMLModuleOptions extends DefineScriptOptions {
257
265
  createElement?: (tagName: string) => HTMLModuleScriptElement;
258
266
  }
259
267
 
268
+ export interface HTMLDeclarativePropsParseContext {
269
+ element: SolidRenderElement;
270
+ /** `props` parses a top-level object; `prop` parses one typed prop value. */
271
+ kind: "props" | "prop";
272
+ attribute: string;
273
+ key?: string;
274
+ }
275
+
276
+ export type HTMLDeclarativePropsParser = (
277
+ source: string,
278
+ context: HTMLDeclarativePropsParseContext,
279
+ ) => unknown;
280
+
260
281
  export interface HTMLRuntimeOptions extends ObserveHTMLOptions {
261
282
  appendTo?: HTMLAppendTarget;
262
283
  createElement?: (tagName: string) => HTMLModuleScriptElement;
@@ -323,6 +344,8 @@ export interface CustomElementRegistryLike {
323
344
  export interface RegisterSolidRenderElementOptions {
324
345
  customElements?: CustomElementRegistryLike;
325
346
  HTMLElement?: new (...args: any[]) => any;
347
+ /** Document that receives the built-in pending-visibility rule. Defaults to global document. */
348
+ document?: HTMLDocumentLike;
326
349
  }
327
350
 
328
351
  export type HTMLObserverController = HTMLRuntimeController;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-tag-runtime",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Runtime module system for JSX modules compiled with solid-tag and executed through @solidjs/html",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Parse a safe JSON5-style data literal without evaluating JavaScript.
3
+ *
4
+ * Supported conveniences intentionally cover declarative HTML data:
5
+ * - single/double quoted strings
6
+ * - unquoted object keys
7
+ * - line/block comments
8
+ * - trailing commas
9
+ * - arrays/objects
10
+ * - booleans/null/numbers
11
+ *
12
+ * Identifiers are accepted only as object keys or the literals true/false/null.
13
+ * Calls, property access, functions, constructors, variables, and expressions
14
+ * are rejected because this parser never executes JavaScript.
15
+ */
16
+ export function parseHTMLDataLiteral(source) {
17
+ const input = String(source ?? "");
18
+ let index = 0;
19
+
20
+ const error = message => {
21
+ const position = index;
22
+ const context = input.slice(Math.max(0, position - 16), Math.min(input.length, position + 16));
23
+ const failure = new SyntaxError(`${message} at position ${position}${context ? ` near ${JSON.stringify(context)}` : ""}.`);
24
+ failure.position = position;
25
+ throw failure;
26
+ };
27
+
28
+ const peek = () => input[index];
29
+ const consume = expected => {
30
+ if (input[index] !== expected) error(`Expected ${JSON.stringify(expected)}`);
31
+ index += 1;
32
+ };
33
+
34
+ function skipSpaceAndComments() {
35
+ while (index < input.length) {
36
+ const char = input[index];
37
+ if (/\s/.test(char)) {
38
+ index += 1;
39
+ continue;
40
+ }
41
+ if (char === "/" && input[index + 1] === "/") {
42
+ index += 2;
43
+ while (index < input.length && input[index] !== "\n" && input[index] !== "\r") index += 1;
44
+ continue;
45
+ }
46
+ if (char === "/" && input[index + 1] === "*") {
47
+ index += 2;
48
+ const end = input.indexOf("*/", index);
49
+ if (end < 0) error("Unterminated block comment");
50
+ index = end + 2;
51
+ continue;
52
+ }
53
+ break;
54
+ }
55
+ }
56
+
57
+ function parseString() {
58
+ const quote = input[index++];
59
+ let result = "";
60
+ while (index < input.length) {
61
+ const char = input[index++];
62
+ if (char === quote) return result;
63
+ if (char === "\n" || char === "\r") error("Unterminated string literal");
64
+ if (char !== "\\") {
65
+ result += char;
66
+ continue;
67
+ }
68
+ if (index >= input.length) error("Unterminated escape sequence");
69
+ const escape = input[index++];
70
+ switch (escape) {
71
+ case "'": result += "'"; break;
72
+ case '"': result += '"'; break;
73
+ case "\\": result += "\\"; break;
74
+ case "/": result += "/"; break;
75
+ case "b": result += "\b"; break;
76
+ case "f": result += "\f"; break;
77
+ case "n": result += "\n"; break;
78
+ case "r": result += "\r"; break;
79
+ case "t": result += "\t"; break;
80
+ case "v": result += "\v"; break;
81
+ case "0": {
82
+ if (/[0-9]/.test(input[index] ?? "")) error("Legacy octal escapes are not supported");
83
+ result += "\0";
84
+ break;
85
+ }
86
+ case "x": {
87
+ const hex = input.slice(index, index + 2);
88
+ if (!/^[0-9a-fA-F]{2}$/.test(hex)) error("Invalid hexadecimal escape");
89
+ result += String.fromCharCode(Number.parseInt(hex, 16));
90
+ index += 2;
91
+ break;
92
+ }
93
+ case "u": {
94
+ if (input[index] === "{") {
95
+ index += 1;
96
+ const close = input.indexOf("}", index);
97
+ if (close < 0) error("Invalid Unicode escape");
98
+ const hex = input.slice(index, close);
99
+ if (!/^[0-9a-fA-F]{1,6}$/.test(hex)) error("Invalid Unicode escape");
100
+ result += String.fromCodePoint(Number.parseInt(hex, 16));
101
+ index = close + 1;
102
+ } else {
103
+ const hex = input.slice(index, index + 4);
104
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) error("Invalid Unicode escape");
105
+ result += String.fromCharCode(Number.parseInt(hex, 16));
106
+ index += 4;
107
+ }
108
+ break;
109
+ }
110
+ case "\n": break;
111
+ case "\r":
112
+ if (input[index] === "\n") index += 1;
113
+ break;
114
+ default:
115
+ result += escape;
116
+ }
117
+ }
118
+ error("Unterminated string literal");
119
+ }
120
+
121
+ function parseIdentifier() {
122
+ const start = index;
123
+ if (!/[A-Za-z_$]/.test(input[index] ?? "")) error("Expected identifier");
124
+ index += 1;
125
+ while (/[A-Za-z0-9_$]/.test(input[index] ?? "")) index += 1;
126
+ return input.slice(start, index);
127
+ }
128
+
129
+ function parseNumber() {
130
+ const start = index;
131
+ if (input[index] === "+" || input[index] === "-") index += 1;
132
+
133
+ if (input[index] === "0" && /[xX]/.test(input[index + 1] ?? "")) {
134
+ index += 2;
135
+ const hexStart = index;
136
+ while (/[0-9a-fA-F]/.test(input[index] ?? "")) index += 1;
137
+ if (index === hexStart) error("Invalid hexadecimal number");
138
+ const raw = input.slice(start, index);
139
+ const sign = raw.startsWith("-") ? -1 : 1;
140
+ const normalized = raw.replace(/^[+-]?0[xX]/, "");
141
+ return sign * Number.parseInt(normalized, 16);
142
+ }
143
+
144
+ let digits = 0;
145
+ while (/[0-9]/.test(input[index] ?? "")) {
146
+ digits += 1;
147
+ index += 1;
148
+ }
149
+ if (input[index] === ".") {
150
+ index += 1;
151
+ while (/[0-9]/.test(input[index] ?? "")) {
152
+ digits += 1;
153
+ index += 1;
154
+ }
155
+ }
156
+ if (digits === 0) error("Invalid number");
157
+ if (/[eE]/.test(input[index] ?? "")) {
158
+ index += 1;
159
+ if (input[index] === "+" || input[index] === "-") index += 1;
160
+ const exponentStart = index;
161
+ while (/[0-9]/.test(input[index] ?? "")) index += 1;
162
+ if (index === exponentStart) error("Invalid exponent");
163
+ }
164
+ const raw = input.slice(start, index);
165
+ const value = Number(raw);
166
+ if (!Number.isFinite(value)) error("Invalid finite number");
167
+ return value;
168
+ }
169
+
170
+ function defineDataProperty(target, key, value) {
171
+ Object.defineProperty(target, key, {
172
+ configurable: true,
173
+ enumerable: true,
174
+ writable: true,
175
+ value,
176
+ });
177
+ }
178
+
179
+ function parseObject() {
180
+ consume("{");
181
+ skipSpaceAndComments();
182
+ const object = {};
183
+ if (peek() === "}") {
184
+ index += 1;
185
+ return object;
186
+ }
187
+ while (index < input.length) {
188
+ skipSpaceAndComments();
189
+ let key;
190
+ const char = peek();
191
+ if (char === "'" || char === '"') key = parseString();
192
+ else if (/[A-Za-z_$]/.test(char ?? "")) key = parseIdentifier();
193
+ else if (/[+\-.0-9]/.test(char ?? "")) key = String(parseNumber());
194
+ else error("Expected object key");
195
+
196
+ skipSpaceAndComments();
197
+ consume(":");
198
+ skipSpaceAndComments();
199
+ defineDataProperty(object, key, parseValue());
200
+ skipSpaceAndComments();
201
+ if (peek() === "}") {
202
+ index += 1;
203
+ return object;
204
+ }
205
+ consume(",");
206
+ skipSpaceAndComments();
207
+ if (peek() === "}") {
208
+ index += 1;
209
+ return object;
210
+ }
211
+ }
212
+ error("Unterminated object literal");
213
+ }
214
+
215
+ function parseArray() {
216
+ consume("[");
217
+ skipSpaceAndComments();
218
+ const array = [];
219
+ if (peek() === "]") {
220
+ index += 1;
221
+ return array;
222
+ }
223
+ while (index < input.length) {
224
+ array.push(parseValue());
225
+ skipSpaceAndComments();
226
+ if (peek() === "]") {
227
+ index += 1;
228
+ return array;
229
+ }
230
+ consume(",");
231
+ skipSpaceAndComments();
232
+ if (peek() === "]") {
233
+ index += 1;
234
+ return array;
235
+ }
236
+ }
237
+ error("Unterminated array literal");
238
+ }
239
+
240
+ function parseValue() {
241
+ skipSpaceAndComments();
242
+ const char = peek();
243
+ if (char === "{") return parseObject();
244
+ if (char === "[") return parseArray();
245
+ if (char === "'" || char === '"') return parseString();
246
+ if (/[+\-.0-9]/.test(char ?? "")) return parseNumber();
247
+ if (/[A-Za-z_$]/.test(char ?? "")) {
248
+ const identifier = parseIdentifier();
249
+ if (identifier === "true") return true;
250
+ if (identifier === "false") return false;
251
+ if (identifier === "null") return null;
252
+ error(`Unsupported identifier ${JSON.stringify(identifier)}; declarative values cannot execute JavaScript`);
253
+ }
254
+ error("Expected data literal");
255
+ }
256
+
257
+ skipSpaceAndComments();
258
+ const value = parseValue();
259
+ skipSpaceAndComments();
260
+ if (index !== input.length) error("Unexpected trailing input");
261
+ return value;
262
+ }
263
+
264
+ export function parseHTMLPropsObject(source) {
265
+ const value = parseHTMLDataLiteral(source);
266
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
267
+ throw new TypeError("The `props` attribute must contain a top-level object literal.");
268
+ }
269
+ return value;
270
+ }
package/src/html.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ModuleResolutionError, SolidTagRuntimeError } from "./errors.js";
2
2
  import { createEventDispatcher } from "./events.js";
3
3
  import { HTMLRenderError, createDefaultHTMLRenderAdapter, resolveRenderComponent } from "./render.js";
4
+ import { parseHTMLDataLiteral, parseHTMLPropsObject } from "./html/data-literal.js";
4
5
 
5
6
  const DEFAULT_SELECTOR = [
6
7
  'script[type="solid-jsx"]',
@@ -12,6 +13,10 @@ const DEFAULT_SELECTOR = [
12
13
  ].join(",");
13
14
 
14
15
  const RUNTIME_SCOPE_ATTRIBUTE = "data-solid-runtime";
16
+ const SOLID_RENDER_STATE_ATTRIBUTE = "data-solid-render-state";
17
+ const SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE = "show-until-ready";
18
+ const SOLID_RENDER_VISIBILITY_STYLE_ATTRIBUTE = "data-solid-tag-render-visibility";
19
+ const solidRenderVisibilityDocuments = new WeakSet();
15
20
  const elementOwners = new WeakMap();
16
21
  const defaultControllers = new WeakMap();
17
22
  const activeControllers = new Set();
@@ -73,6 +78,7 @@ export function createHTMLRuntime(runtime, options = {}) {
73
78
  createElement: options.createElement,
74
79
  appendTo: options.appendTo,
75
80
  renderAdapter: options.renderAdapter ?? createDefaultHTMLRenderAdapter(runtime),
81
+ parseProps: typeof options.parseProps === "function" ? options.parseProps : undefined,
76
82
  bindings: new Map(),
77
83
  renderElements: new Set(),
78
84
  disposed: false,
@@ -134,6 +140,7 @@ export function createHTMLRuntime(runtime, options = {}) {
134
140
 
135
141
  controller.api = api;
136
142
  activeControllers.add(controller);
143
+ ensureSolidRenderVisibilityStyle(resolveControllerDocument(controller));
137
144
  // Do not define/upgrade <solid-render> here. Existing declarative script
138
145
  // modules must be installed first; otherwise connectedCallback() can import
139
146
  // a module before register()/observe() has defined the preceding <script>.
@@ -667,6 +674,7 @@ export const htmlRuntimeScopeAttribute = RUNTIME_SCOPE_ATTRIBUTE;
667
674
  * browser; programmatic-only applications may call it explicitly.
668
675
  */
669
676
  export function registerSolidRenderElement(options = {}) {
677
+ ensureSolidRenderVisibilityStyle(options.document ?? globalThis.document);
670
678
  const registry = options.customElements ?? globalThis.customElements;
671
679
  const HTMLElementBase = options.HTMLElement ?? globalThis.HTMLElement;
672
680
  if (!registry || typeof registry.define !== "function" || typeof registry.get !== "function" || typeof HTMLElementBase !== "function") {
@@ -680,7 +688,7 @@ export function registerSolidRenderElement(options = {}) {
680
688
 
681
689
  class SolidRenderElement extends HTMLElementBase {
682
690
  static get observedAttributes() {
683
- return ["module", "component", RUNTIME_SCOPE_ATTRIBUTE];
691
+ return ["module", "component", RUNTIME_SCOPE_ATTRIBUTE, SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE];
684
692
  }
685
693
 
686
694
  constructor() {
@@ -696,13 +704,16 @@ export function registerSolidRenderElement(options = {}) {
696
704
  propObserver: undefined,
697
705
  pendingModuleResolution: undefined,
698
706
  pendingCheckGeneration: undefined,
707
+ renderState: "pending",
699
708
  });
700
709
  }
701
710
 
702
711
  connectedCallback() {
703
712
  const state = getSolidRenderState(this);
704
713
  state.connected = true;
714
+ setSolidRenderState(this, state, "pending");
705
715
  captureSolidRenderChildren(this, state);
716
+ syncSolidRenderPendingContent(this, state);
706
717
  startSolidRenderPropObserver(this, state);
707
718
  void remountSolidRender(this, "connect");
708
719
  }
@@ -720,9 +731,26 @@ export function registerSolidRenderElement(options = {}) {
720
731
  if (oldValue === newValue) return;
721
732
  const state = getSolidRenderState(this);
722
733
  if (!state.connected) return;
734
+ if (name === SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE) {
735
+ syncSolidRenderPendingContent(this, state);
736
+ return;
737
+ }
723
738
  void remountSolidRender(this, `identity:${name}`);
724
739
  }
725
740
 
741
+ get renderState() {
742
+ return getSolidRenderState(this).renderState;
743
+ }
744
+
745
+ get hideUntilReady() {
746
+ return !this.hasAttribute?.(SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE);
747
+ }
748
+
749
+ set hideUntilReady(value) {
750
+ if (value === false) this.setAttribute?.(SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE, "");
751
+ else this.removeAttribute?.(SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE);
752
+ }
753
+
726
754
  get props() {
727
755
  return getSolidRenderState(this).programmaticProps;
728
756
  }
@@ -753,12 +781,78 @@ function getSolidRenderState(element) {
753
781
  propObserver: undefined,
754
782
  pendingModuleResolution: undefined,
755
783
  pendingCheckGeneration: undefined,
784
+ renderState: "pending",
756
785
  };
757
786
  solidRenderInternals.set(element, state);
758
787
  }
759
788
  return state;
760
789
  }
761
790
 
791
+
792
+ function resolveControllerDocument(controller) {
793
+ const root = controller?.root ?? globalThis.document;
794
+ if (root && typeof root.createElement === "function" && (root.head || root.documentElement || root.body)) return root;
795
+ return root?.ownerDocument ?? globalThis.document;
796
+ }
797
+
798
+ function ensureSolidRenderVisibilityStyle(document) {
799
+ if (!document || (typeof document !== "object" && typeof document !== "function")) return;
800
+ if (solidRenderVisibilityDocuments.has(document)) return;
801
+
802
+ try {
803
+ if (document.querySelector?.(`style[${SOLID_RENDER_VISIBILITY_STYLE_ATTRIBUTE}]`)) {
804
+ solidRenderVisibilityDocuments.add(document);
805
+ return;
806
+ }
807
+ if (typeof document.createElement !== "function") return;
808
+ const style = document.createElement("style");
809
+ style.setAttribute?.(SOLID_RENDER_VISIBILITY_STYLE_ATTRIBUTE, "");
810
+ style.textContent = [
811
+ `solid-render:not([${SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE}]):not(:defined),`,
812
+ `solid-render:not([${SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE}])[${SOLID_RENDER_STATE_ATTRIBUTE}="pending"] {`,
813
+ " visibility: hidden !important;",
814
+ "}",
815
+ ].join("\n");
816
+ const target = document.head ?? document.documentElement ?? document.body;
817
+ if (typeof target?.append === "function") target.append(style);
818
+ else if (typeof target?.appendChild === "function") target.appendChild(style);
819
+ else return;
820
+ solidRenderVisibilityDocuments.add(document);
821
+ } catch {
822
+ // Styling is a progressive enhancement for pre-upgrade visibility. The
823
+ // custom element still manages pending/ready/error state in JavaScript.
824
+ }
825
+ }
826
+
827
+ function setSolidRenderState(element, state, nextState) {
828
+ state.renderState = nextState;
829
+ element.setAttribute?.(SOLID_RENDER_STATE_ATTRIBUTE, nextState);
830
+ }
831
+
832
+ function cloneSolidRenderFallbackNodes(state) {
833
+ const clone = state.childrenTemplate?.cloneNode?.(true);
834
+ if (!clone) return [];
835
+ if (clone.childNodes) return Array.from(clone.childNodes);
836
+ return [clone];
837
+ }
838
+
839
+ function restoreSolidRenderFallback(element, state) {
840
+ if (!state.childrenTemplate) {
841
+ element.replaceChildren?.();
842
+ return;
843
+ }
844
+ element.replaceChildren?.(...cloneSolidRenderFallbackNodes(state));
845
+ }
846
+
847
+ function syncSolidRenderPendingContent(element, state) {
848
+ if (state.renderState !== "pending" || state.mount) return;
849
+ if (element.hasAttribute?.(SOLID_RENDER_SHOW_UNTIL_READY_ATTRIBUTE)) {
850
+ restoreSolidRenderFallback(element, state);
851
+ } else {
852
+ element.replaceChildren?.();
853
+ }
854
+ }
855
+
762
856
  function captureSolidRenderChildren(element, state) {
763
857
  if (state.captured) return;
764
858
  state.captured = true;
@@ -784,7 +878,11 @@ function startSolidRenderPropObserver(element, state) {
784
878
  if (typeof MutationObserverImpl !== "function") return;
785
879
 
786
880
  const observer = new MutationObserverImpl(records => {
787
- if (!Array.from(records ?? []).some(record => String(record?.attributeName ?? "").startsWith("prop:"))) return;
881
+ const relevant = Array.from(records ?? []).some(record => {
882
+ const name = String(record?.attributeName ?? "").toLowerCase();
883
+ return name === "props" || name.startsWith("prop:");
884
+ });
885
+ if (!relevant) return;
788
886
  updateSolidRenderProps(element, state);
789
887
  });
790
888
  observer.observe(element, { attributes: true });
@@ -797,8 +895,10 @@ async function remountSolidRender(element, reason, options = {}) {
797
895
  const generation = ++state.generation;
798
896
  state.pendingModuleResolution = undefined;
799
897
  state.pendingCheckGeneration = undefined;
898
+ setSolidRenderState(element, state, "pending");
800
899
  await disposeSolidRenderMount(element, state, reason === "connect" ? "reconnect" : reason);
801
900
  if (!state.connected || generation !== state.generation) return;
901
+ syncSolidRenderPendingContent(element, state);
802
902
 
803
903
  let controller;
804
904
  try {
@@ -812,7 +912,7 @@ async function remountSolidRender(element, reason, options = {}) {
812
912
  }
813
913
  const componentName = nonEmpty(element.getAttribute?.("component"));
814
914
  const exportName = componentName ?? "default";
815
- const props = buildSolidRenderProps(element, state);
915
+ const props = buildSolidRenderProps(element, state, controller);
816
916
 
817
917
  emitHTML(controller, {
818
918
  type: "render-mounting",
@@ -849,6 +949,7 @@ async function remountSolidRender(element, reason, options = {}) {
849
949
  state.mount = mount;
850
950
  state.pendingModuleResolution = undefined;
851
951
  state.pendingCheckGeneration = undefined;
952
+ setSolidRenderState(element, state, "ready");
852
953
  emitHTML(controller, {
853
954
  type: "render-mounted",
854
955
  element,
@@ -884,6 +985,8 @@ async function remountSolidRender(element, reason, options = {}) {
884
985
  }
885
986
  }
886
987
 
988
+ setSolidRenderState(element, state, "error");
989
+ if (!state.mount) restoreSolidRenderFallback(element, state);
887
990
  if (controller) {
888
991
  emitHTMLError(controller, "solid-render", error, {
889
992
  element,
@@ -944,20 +1047,44 @@ async function disposeSolidRenderMount(element, state, reason = "dispose") {
944
1047
 
945
1048
  function updateSolidRenderProps(element, state) {
946
1049
  if (!state.mount?.updateProps) return;
947
- state.mount.updateProps(buildSolidRenderProps(element, state));
1050
+ let controller = state.controller;
1051
+ try {
1052
+ controller ??= resolveHTMLControllerForElement(element);
1053
+ const props = buildSolidRenderProps(element, state, controller);
1054
+ state.mount.updateProps(props);
1055
+ setSolidRenderState(element, state, "ready");
1056
+ } catch (error) {
1057
+ setSolidRenderState(element, state, "error");
1058
+ if (controller) {
1059
+ emitHTMLError(controller, "solid-render", error, {
1060
+ element,
1061
+ moduleId: nonEmpty(element.getAttribute?.("module")),
1062
+ origin: "solid-render",
1063
+ });
1064
+ }
1065
+ dispatchSolidRenderError(element, error);
1066
+ }
948
1067
  }
949
1068
 
950
- function buildSolidRenderProps(element, state) {
1069
+ function buildSolidRenderProps(element, state, controller) {
951
1070
  const declarative = {};
1071
+ const bulk = parseSolidRenderBulkProps(element, controller);
1072
+ copyPropRecord(declarative, bulk);
1073
+
952
1074
  for (const attribute of Array.from(element.attributes ?? [])) {
953
- const name = String(attribute?.name ?? "").toLowerCase();
1075
+ const rawName = String(attribute?.name ?? "");
1076
+ const name = rawName.toLowerCase();
954
1077
  if (!name.startsWith("prop:")) continue;
955
1078
  const key = kebabToCamel(name.slice(5));
956
1079
  if (!key) continue;
957
- declarative[key] = attribute.value === "" ? true : attribute.value;
1080
+ declarative[key] = parseSolidRenderAttributeProp(element, controller, key, attribute.value);
958
1081
  }
959
1082
 
960
- if (state.childrenTemplate && !("children" in (state.programmaticProps ?? {}))) {
1083
+ if (
1084
+ state.childrenTemplate
1085
+ && !("children" in declarative)
1086
+ && !("children" in (state.programmaticProps ?? {}))
1087
+ ) {
961
1088
  Object.defineProperty(declarative, "children", {
962
1089
  configurable: true,
963
1090
  enumerable: true,
@@ -968,14 +1095,69 @@ function buildSolidRenderProps(element, state) {
968
1095
  }
969
1096
 
970
1097
  const merged = Object.create(null);
971
- for (const key of Reflect.ownKeys(declarative)) {
972
- const descriptor = Object.getOwnPropertyDescriptor(declarative, key);
973
- if (descriptor) Object.defineProperty(merged, key, descriptor);
1098
+ copyPropRecord(merged, declarative);
1099
+ copyPropRecord(merged, state.programmaticProps ?? {});
1100
+ return merged;
1101
+ }
1102
+
1103
+ function copyPropRecord(target, source) {
1104
+ for (const key of Reflect.ownKeys(source ?? {})) {
1105
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
1106
+ if (descriptor) Object.defineProperty(target, key, descriptor);
1107
+ }
1108
+ return target;
1109
+ }
1110
+
1111
+ function parseSolidRenderBulkProps(element, controller) {
1112
+ const source = element.getAttribute?.("props");
1113
+ if (source == null || String(source).trim() === "") return {};
1114
+ const value = parseSolidRenderData(controller, String(source), {
1115
+ element,
1116
+ kind: "props",
1117
+ attribute: "props",
1118
+ });
1119
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1120
+ throw new HTMLRenderError("<solid-render> `props` must parse to a top-level object.", { element });
974
1121
  }
975
- for (const key of Reflect.ownKeys(state.programmaticProps ?? {})) {
976
- merged[key] = state.programmaticProps[key];
1122
+ return value;
1123
+ }
1124
+
1125
+ function parseSolidRenderAttributeProp(element, controller, key, source) {
1126
+ if (source === "") return true;
1127
+ const raw = String(source);
1128
+ const trimmed = raw.trim();
1129
+ if (!(trimmed.startsWith("{") && trimmed.endsWith("}"))) return raw;
1130
+
1131
+ const inner = trimmed.slice(1, -1).trim();
1132
+ if (!inner) {
1133
+ throw new HTMLRenderError(
1134
+ `<solid-render> prop:${key} uses an outer brace pair for typed data; the inner value cannot be empty.`,
1135
+ { element },
1136
+ );
1137
+ }
1138
+ return parseSolidRenderData(controller, inner, {
1139
+ element,
1140
+ kind: "prop",
1141
+ attribute: `prop:${key}`,
1142
+ key,
1143
+ });
1144
+ }
1145
+
1146
+ function parseSolidRenderData(controller, source, context) {
1147
+ try {
1148
+ if (typeof controller?.parseProps === "function") {
1149
+ return controller.parseProps(source, context);
1150
+ }
1151
+ return context.kind === "props"
1152
+ ? parseHTMLPropsObject(source)
1153
+ : parseHTMLDataLiteral(source);
1154
+ } catch (cause) {
1155
+ const label = context.kind === "props" ? "props" : context.attribute;
1156
+ throw new HTMLRenderError(
1157
+ `<solid-render> could not parse ${label} as declarative data: ${cause?.message ?? cause}`,
1158
+ { element: context.element, cause },
1159
+ );
977
1160
  }
978
- return merged;
979
1161
  }
980
1162
 
981
1163
  function kebabToCamel(value) {