solid-tag-runtime 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +336 -30
- package/README.md +181 -0
- package/html.d.ts +201 -0
- package/package.json +7 -1
- package/src/html.js +832 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -42,7 +42,7 @@ The runtime should allow applications to:
|
|
|
42
42
|
9. support normal JavaScript modules in the same graph
|
|
43
43
|
10. keep the JSX compiler replaceable behind a narrow adapter
|
|
44
44
|
|
|
45
|
-
## 3.
|
|
45
|
+
## 3. Current non-goals
|
|
46
46
|
|
|
47
47
|
The initial package does not attempt to provide:
|
|
48
48
|
|
|
@@ -54,9 +54,8 @@ The initial package does not attempt to provide:
|
|
|
54
54
|
- circular runtime source-module support
|
|
55
55
|
- transparent live replacement of already-held component references
|
|
56
56
|
- full HMR propagation semantics
|
|
57
|
-
- script-tag discovery/registration
|
|
58
57
|
|
|
59
|
-
These may be added later without changing the module-first public model.
|
|
58
|
+
These may be added later without changing the module-first public model. HTML script discovery was added in `0.0.2` as a separate browser adapter rather than as a responsibility of the core module engine.
|
|
60
59
|
|
|
61
60
|
## 4. Package relationship
|
|
62
61
|
|
|
@@ -193,7 +192,270 @@ No JSX transformation. Imports are still resolved and rewritten through the runt
|
|
|
193
192
|
|
|
194
193
|
This permits ordinary JavaScript and JSX modules to coexist in one graph.
|
|
195
194
|
|
|
196
|
-
|
|
195
|
+
|
|
196
|
+
## 8. HTML/browser adapter
|
|
197
|
+
|
|
198
|
+
`solid-tag-runtime/html` is an opt-in browser-facing adapter over the core module runtime.
|
|
199
|
+
|
|
200
|
+
`0.0.3` makes the adapter runtime-aware rather than treating document scripts as a single global pool.
|
|
201
|
+
|
|
202
|
+
Important architectural boundary:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
DOM / HTML document
|
|
206
|
+
↓
|
|
207
|
+
HTML runtime controller
|
|
208
|
+
↓
|
|
209
|
+
runtime.define(...)
|
|
210
|
+
↓
|
|
211
|
+
core module graph
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The core runtime remains DOM-independent. It does not scan documents, own HTML elements, run `MutationObserver`, or know about HTML attributes.
|
|
215
|
+
|
|
216
|
+
### Public HTML API
|
|
217
|
+
|
|
218
|
+
The preferred API for non-trivial usage is a persistent controller:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const html = createHTMLRuntime(runtime, {
|
|
222
|
+
scope: "preview",
|
|
223
|
+
root: document,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await html.register();
|
|
227
|
+
await html.observe({ registerExisting: false });
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The controller exposes:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
html.register(options?);
|
|
234
|
+
html.observe(options?);
|
|
235
|
+
html.flush(options?);
|
|
236
|
+
html.disconnect();
|
|
237
|
+
|
|
238
|
+
html.defineElement(element, options?);
|
|
239
|
+
html.registerElement(element, options?);
|
|
240
|
+
html.append(element, options?);
|
|
241
|
+
html.addModule(options);
|
|
242
|
+
|
|
243
|
+
html.owns(element);
|
|
244
|
+
html.getModuleId(element);
|
|
245
|
+
html.getElement(moduleId);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The original convenience functions remain supported and are implemented on top of a cached default controller for each runtime:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
await registerHTML(runtime, options?);
|
|
252
|
+
await defineScript(runtime, element, options?);
|
|
253
|
+
const observer = await observeHTML(runtime, options?);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
This keeps the simple single-runtime API while giving multi-runtime applications an explicit ownership object.
|
|
257
|
+
|
|
258
|
+
### Element ownership model
|
|
259
|
+
|
|
260
|
+
A module-level `WeakMap` is the authoritative in-memory ownership registry for runtime script elements.
|
|
261
|
+
|
|
262
|
+
Conceptually:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
WeakMap<HTMLScriptElement, ElementRecord>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
An element record stores at least:
|
|
269
|
+
|
|
270
|
+
```text
|
|
271
|
+
owner controller
|
|
272
|
+
module ID
|
|
273
|
+
format
|
|
274
|
+
entry flag
|
|
275
|
+
source URL
|
|
276
|
+
registration state
|
|
277
|
+
pending registration promise
|
|
278
|
+
result/error
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
This registry is shared by all HTML runtime controllers created by this package instance.
|
|
282
|
+
|
|
283
|
+
Important invariants:
|
|
284
|
+
|
|
285
|
+
1. A runtime script element has at most one HTML-runtime owner.
|
|
286
|
+
2. Ownership is claimed synchronously before asynchronous fetch/compile work.
|
|
287
|
+
3. `append()` claims ownership before inserting the node into the DOM.
|
|
288
|
+
4. Observer discovery, scans, explicit registration, and append/add helpers all use the same ownership record.
|
|
289
|
+
5. Re-registering an element through the same controller is idempotent and waits on the same pending registration work.
|
|
290
|
+
6. A different controller attempting to claim an owned element receives `HTMLModuleOwnershipError`.
|
|
291
|
+
7. The observer must never redefine a module already being handled through `append()` or `registerElement()`.
|
|
292
|
+
8. Distinct HTML elements cannot silently claim the same module ID in one HTML controller.
|
|
293
|
+
9. Existing non-HTML runtime modules are not silently replaced by HTML registration.
|
|
294
|
+
10. DOM removal does not implicitly delete a runtime module.
|
|
295
|
+
|
|
296
|
+
A `WeakSet` alone is insufficient because the adapter needs to distinguish ownership, pending work, success, and failure. The shared `WeakMap` also avoids retaining detached elements after all external references disappear.
|
|
297
|
+
|
|
298
|
+
### Controller scope and multiple runtimes
|
|
299
|
+
|
|
300
|
+
A controller may declare a scope:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
const previewHTML = createHTMLRuntime(previewRuntime, {
|
|
304
|
+
scope: "preview",
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Owned elements are marked declaratively:
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<script
|
|
312
|
+
type="solid-jsx"
|
|
313
|
+
data-solid-runtime="preview"
|
|
314
|
+
module="/App.jsx">
|
|
315
|
+
</script>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`data-solid-runtime` is useful for declarative selection and DevTools visibility. It is not the authoritative ownership record; the `WeakMap` points to the actual controller object and prevents two controllers with the same string scope from owning the same element.
|
|
319
|
+
|
|
320
|
+
A scoped controller accepts matching scripts. It may also claim unscoped scripts when `acceptUnscoped` is enabled (default `true`). Setting `acceptUnscoped: false` is recommended when multiple scoped runtimes observe the same DOM root.
|
|
321
|
+
|
|
322
|
+
An unscoped controller does not silently claim a script carrying `data-solid-runtime` for another runtime.
|
|
323
|
+
|
|
324
|
+
### User-created script elements
|
|
325
|
+
|
|
326
|
+
Applications may create the element themselves and explicitly associate it with one runtime:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const script = document.createElement("script");
|
|
330
|
+
script.type = "solid-jsx";
|
|
331
|
+
script.setAttribute("module", "/Preview.jsx");
|
|
332
|
+
script.textContent = source;
|
|
333
|
+
|
|
334
|
+
await previewHTML.append(script);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`append()` performs this ordering:
|
|
338
|
+
|
|
339
|
+
```text
|
|
340
|
+
claim element
|
|
341
|
+
↓
|
|
342
|
+
stamp data-solid-runtime (when scoped)
|
|
343
|
+
↓
|
|
344
|
+
append to DOM
|
|
345
|
+
↓
|
|
346
|
+
load/define module directly
|
|
347
|
+
↓
|
|
348
|
+
observer later sees mutation and skips duplicate work
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
This is deliberately not implemented as “append and wait for MutationObserver”. Explicit APIs are deterministic even when observation is disabled.
|
|
352
|
+
|
|
353
|
+
For an element already in the DOM:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
await previewHTML.registerElement(script);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Controller-created script elements
|
|
360
|
+
|
|
361
|
+
`addModule()` creates and owns the script element for the caller:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
await previewHTML.addModule({
|
|
365
|
+
id: "/dynamic/Greeting.jsx",
|
|
366
|
+
source: `
|
|
367
|
+
export function Greeting(props) {
|
|
368
|
+
return <p>Hello {props.name}</p>;
|
|
369
|
+
}
|
|
370
|
+
`,
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
It uses the same claim-before-append pipeline as `append()`.
|
|
375
|
+
|
|
376
|
+
### One-shot registration and live observation
|
|
377
|
+
|
|
378
|
+
`register()` / `registerHTML()` perform one-shot discovery.
|
|
379
|
+
|
|
380
|
+
`observe()` / `observeHTML()` use `MutationObserver` for newly added scripts. Existing scripts are registered by default; `registerExisting: false` supports the pattern where a one-shot registration already ran.
|
|
381
|
+
|
|
382
|
+
Every discovered batch preserves the graph invariant:
|
|
383
|
+
|
|
384
|
+
```text
|
|
385
|
+
discover complete batch
|
|
386
|
+
↓
|
|
387
|
+
claim applicable elements
|
|
388
|
+
↓
|
|
389
|
+
load all source
|
|
390
|
+
↓
|
|
391
|
+
define all modules
|
|
392
|
+
↓
|
|
393
|
+
execute newly discovered entries
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
An entry may therefore import a dependency that appears later in the same discovery batch.
|
|
397
|
+
|
|
398
|
+
`flush()` waits for queued observer work and scans again, providing a deterministic synchronization point after external DOM mutations.
|
|
399
|
+
|
|
400
|
+
Observation in `0.0.3` remains addition-only: changing attributes/text of an already owned script or removing it does not implicitly update/remove the runtime module. Module replacement remains explicit through the core runtime update/invalidation APIs.
|
|
401
|
+
|
|
402
|
+
### Supported script declarations
|
|
403
|
+
|
|
404
|
+
```html
|
|
405
|
+
<script type="solid-jsx" module="/App.jsx">...</script>
|
|
406
|
+
<script type="solid-js" module="/utils.js">...</script>
|
|
407
|
+
<script type="solid-module" module="/App.jsx">...</script>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`text/solid-jsx`, `text/solid-js`, and `text/solid-module` aliases are also accepted.
|
|
411
|
+
|
|
412
|
+
Format rules:
|
|
413
|
+
|
|
414
|
+
- `solid-jsx` → `jsx`
|
|
415
|
+
- `solid-js` → `js`
|
|
416
|
+
- `solid-module` → infer from `module`/`src` extension
|
|
417
|
+
- `language="js"` or `language="jsx"` overrides inference
|
|
418
|
+
|
|
419
|
+
### `src` versus `module`
|
|
420
|
+
|
|
421
|
+
These attributes deliberately have different roles:
|
|
422
|
+
|
|
423
|
+
```text
|
|
424
|
+
src = source provider / fetch location
|
|
425
|
+
module = identity in the runtime module graph
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Example:
|
|
429
|
+
|
|
430
|
+
```html
|
|
431
|
+
<script
|
|
432
|
+
type="solid-jsx"
|
|
433
|
+
src="./source/Button.jsx"
|
|
434
|
+
module="/ui/Button.jsx">
|
|
435
|
+
</script>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
The source may move without changing imports of `/ui/Button.jsx`.
|
|
439
|
+
|
|
440
|
+
If a `src` script omits `module`, its resolved source URL becomes the module ID. Inline non-entry scripts require an explicit `module` identity. Inline entries may omit it and receive an anonymous runtime ID.
|
|
441
|
+
|
|
442
|
+
### Entry execution
|
|
443
|
+
|
|
444
|
+
`entry` is an adapter concern, not a new runtime module kind.
|
|
445
|
+
|
|
446
|
+
```html
|
|
447
|
+
<script type="solid-jsx" module="/App.jsx" entry>...</script>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
After all newly discovered modules in the batch are defined, the adapter imports each entry unless `executeEntries: false` is supplied.
|
|
451
|
+
|
|
452
|
+
### Explicitly not HTML-as-JSX
|
|
453
|
+
|
|
454
|
+
The adapter does not transform arbitrary document HTML into JSX and does not currently assign semantics to `<template>` elements. JSX remains inside JavaScript module source.
|
|
455
|
+
|
|
456
|
+
This avoids introducing a second template language, implicit lexical scope rules, or ambiguous ownership of already-parsed DOM.
|
|
457
|
+
|
|
458
|
+
## 9. Compiler adapter boundary
|
|
197
459
|
|
|
198
460
|
The runtime engine itself does not depend on Acorn or the `solid-tag` AST.
|
|
199
461
|
|
|
@@ -233,7 +495,7 @@ Possible future adapters include:
|
|
|
233
495
|
|
|
234
496
|
The module graph must remain independent of parser details.
|
|
235
497
|
|
|
236
|
-
##
|
|
498
|
+
## 10. Linking pipeline
|
|
237
499
|
|
|
238
500
|
For a source module `/features/Counter.jsx`:
|
|
239
501
|
|
|
@@ -266,7 +528,7 @@ Example graph:
|
|
|
266
528
|
└── ../ui/Button.jsx → compiled virtual source URL
|
|
267
529
|
```
|
|
268
530
|
|
|
269
|
-
##
|
|
531
|
+
## 11. Resolution rules
|
|
270
532
|
|
|
271
533
|
Resolution order is conceptually:
|
|
272
534
|
|
|
@@ -282,7 +544,7 @@ Relative virtual resolution uses POSIX-style paths regardless of the host operat
|
|
|
282
544
|
|
|
283
545
|
This keeps module IDs stable in browsers and on Windows hosts.
|
|
284
546
|
|
|
285
|
-
##
|
|
547
|
+
## 12. Native ESM execution backend
|
|
286
548
|
|
|
287
549
|
The first execution backend deliberately relies on the JavaScript engine's native ESM evaluator rather than `new Function()`.
|
|
288
550
|
|
|
@@ -320,9 +582,9 @@ Benefits:
|
|
|
320
582
|
- no `eval` / `new Function()` runtime wrapper
|
|
321
583
|
- clean `toModule()` semantics
|
|
322
584
|
|
|
323
|
-
##
|
|
585
|
+
## 13. Circular dependency limitation
|
|
324
586
|
|
|
325
|
-
Virtual source module cycles are explicitly rejected
|
|
587
|
+
Virtual source module cycles are explicitly rejected through v0.0.2.
|
|
326
588
|
|
|
327
589
|
Reason: the current linker must know the final URL of dependencies before it can create a module's immutable Blob/data URL. A cycle requires module identities to exist before final source URLs can be constructed.
|
|
328
590
|
|
|
@@ -341,7 +603,7 @@ Future options:
|
|
|
341
603
|
|
|
342
604
|
Do not add ad-hoc cycle handling that breaks module semantics; change the backend deliberately.
|
|
343
605
|
|
|
344
|
-
##
|
|
606
|
+
## 14. Host module bridges
|
|
345
607
|
|
|
346
608
|
`defineModule()` stores the namespace in a runtime-specific registry:
|
|
347
609
|
|
|
@@ -376,7 +638,7 @@ The ES module bridge captures each exported property value when the bridge modul
|
|
|
376
638
|
|
|
377
639
|
To replace exports, call `defineModule()` again. This invalidates dependent runtime modules.
|
|
378
640
|
|
|
379
|
-
##
|
|
641
|
+
## 15. Scope injection design choice
|
|
380
642
|
|
|
381
643
|
The runtime does not automatically turn unknown identifiers into scope lookups.
|
|
382
644
|
|
|
@@ -413,7 +675,7 @@ Reasons:
|
|
|
413
675
|
|
|
414
676
|
A future convenience API may create scope modules automatically, but it should still compile down to module semantics.
|
|
415
677
|
|
|
416
|
-
##
|
|
678
|
+
## 16. Dependency graph
|
|
417
679
|
|
|
418
680
|
Each record tracks:
|
|
419
681
|
|
|
@@ -431,7 +693,7 @@ When linking `/App.jsx` imports `/Button.jsx`:
|
|
|
431
693
|
|
|
432
694
|
These edges power invalidation and tooling.
|
|
433
695
|
|
|
434
|
-
##
|
|
696
|
+
## 17. Caching
|
|
435
697
|
|
|
436
698
|
A source module caches:
|
|
437
699
|
|
|
@@ -444,7 +706,7 @@ Repeated imports use the same module URL/import promise until invalidated.
|
|
|
444
706
|
|
|
445
707
|
Native ESM also caches imports by URL.
|
|
446
708
|
|
|
447
|
-
##
|
|
709
|
+
## 18. Invalidation and updates
|
|
448
710
|
|
|
449
711
|
Updating a module invalidates:
|
|
450
712
|
|
|
@@ -469,7 +731,7 @@ const fresh = await runtime.import("/App.jsx");
|
|
|
469
731
|
|
|
470
732
|
This is deliberate and matches the immutable identity of native evaluated modules.
|
|
471
733
|
|
|
472
|
-
##
|
|
734
|
+
## 19. `toModule()` semantics
|
|
473
735
|
|
|
474
736
|
`toModule(source)` is sugar for:
|
|
475
737
|
|
|
@@ -485,7 +747,7 @@ return module namespace
|
|
|
485
747
|
|
|
486
748
|
It is asynchronous by design.
|
|
487
749
|
|
|
488
|
-
##
|
|
750
|
+
## 20. `toComponent()` semantics
|
|
489
751
|
|
|
490
752
|
`toComponent()` calls `toModule()` and returns a selected callable export.
|
|
491
753
|
|
|
@@ -499,7 +761,7 @@ Named exports require an explicit `exportName`.
|
|
|
499
761
|
|
|
500
762
|
The runtime does not guess between multiple component exports.
|
|
501
763
|
|
|
502
|
-
##
|
|
764
|
+
## 21. Security model
|
|
503
765
|
|
|
504
766
|
`solid-tag-runtime` is **not a sandbox**.
|
|
505
767
|
|
|
@@ -507,7 +769,7 @@ Dynamic source executes as JavaScript in the page/module environment and has wha
|
|
|
507
769
|
|
|
508
770
|
Do not use this package to execute untrusted hostile code without a separate isolation mechanism such as a suitably sandboxed iframe/worker/process boundary.
|
|
509
771
|
|
|
510
|
-
##
|
|
772
|
+
## 22. CSP considerations
|
|
511
773
|
|
|
512
774
|
The browser backend currently uses Blob module URLs by default.
|
|
513
775
|
|
|
@@ -519,7 +781,7 @@ Applications with strict Content Security Policy may need:
|
|
|
519
781
|
|
|
520
782
|
The public module API should not depend on Blob URLs so this backend can change later.
|
|
521
783
|
|
|
522
|
-
##
|
|
784
|
+
## 23. Future directions
|
|
523
785
|
|
|
524
786
|
Potential additions, in rough architectural order:
|
|
525
787
|
|
|
@@ -537,16 +799,6 @@ await runtime.load("/App.jsx");
|
|
|
537
799
|
|
|
538
800
|
with configurable fetch/source providers.
|
|
539
801
|
|
|
540
|
-
### Script-tag module registration
|
|
541
|
-
|
|
542
|
-
Possible declarative zero-build API:
|
|
543
|
-
|
|
544
|
-
```html
|
|
545
|
-
<script type="solid-jsx" module="/Button.jsx">...</script>
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
implemented as a thin source-provider layer over the same runtime graph.
|
|
549
|
-
|
|
550
802
|
### Runtime update/HMR helpers
|
|
551
803
|
|
|
552
804
|
Graph invalidation already exists. Future APIs can add lifecycle hooks and entry re-rendering without changing module identity rules.
|
|
@@ -563,7 +815,7 @@ The runtime compiler boundary allows a future TSX-capable transformer without co
|
|
|
563
815
|
|
|
564
816
|
If `solid-tag` later gains tagged-template → JSX transformation, runtime/editor tooling can expose source views without changing module execution semantics.
|
|
565
817
|
|
|
566
|
-
##
|
|
818
|
+
## 24. Decision log
|
|
567
819
|
|
|
568
820
|
### 2026-10-02 — Separate runtime package
|
|
569
821
|
|
|
@@ -606,3 +858,57 @@ Reason: the current immutable URL linker cannot assign stable module identities
|
|
|
606
858
|
Decision: the runtime engine consumes `analyze()` and `transform()` rather than importing parser internals directly.
|
|
607
859
|
|
|
608
860
|
Reason: preserve the option to add TSX, tagged-source, or alternative transformer backends later.
|
|
861
|
+
### 2026-10-02 — HTML integration is an adapter subpath
|
|
862
|
+
|
|
863
|
+
Decision: add declarative script registration in `0.0.2` as `solid-tag-runtime/html`, not as methods on the core runtime and not as a separate npm package.
|
|
864
|
+
|
|
865
|
+
Reason: HTML discovery, DOM access, and fetching are browser concerns. Keeping them behind an export subpath preserves a DOM-free core while avoiding premature package fragmentation.
|
|
866
|
+
|
|
867
|
+
### 2026-10-02 — HTML declares modules; it is not itself JSX
|
|
868
|
+
|
|
869
|
+
Decision: support JSX/JS source inside runtime `<script>` modules, but do not reinterpret arbitrary document HTML or `<template>` content as JSX in `0.0.2`.
|
|
870
|
+
|
|
871
|
+
Reason: module source already has clear lexical/import semantics. Treating document HTML as executable JSX would introduce separate scope, parsing, hydration, and DOM-ownership semantics.
|
|
872
|
+
|
|
873
|
+
### 2026-10-02 — `src` and `module` are separate concepts
|
|
874
|
+
|
|
875
|
+
Decision: `src` identifies where source is fetched, while `module` identifies the module inside the runtime graph. When `module` is omitted for an external script, the resolved source URL is used as its identity.
|
|
876
|
+
|
|
877
|
+
Reason: source location and logical module identity should be independently controllable while still making zero-config relative URL module graphs ergonomic.
|
|
878
|
+
|
|
879
|
+
### 2026-10-02 — Define all HTML modules before running entries
|
|
880
|
+
|
|
881
|
+
Decision: `registerHTML()` completes discovery/fetch/definition for all matching scripts before importing any module marked `entry`.
|
|
882
|
+
|
|
883
|
+
Reason: dependency availability must not depend on HTML document order.
|
|
884
|
+
|
|
885
|
+
### 2026-10-02 — Live HTML discovery uses an addition-only observer
|
|
886
|
+
|
|
887
|
+
Decision: add `observeHTML()` to the existing `solid-tag-runtime/html` subpath in `0.0.2`. It uses `MutationObserver`, registers existing scripts by default, tracks seen element identities, and applies the same define-before-entry invariant to every newly discovered batch.
|
|
888
|
+
|
|
889
|
+
Reason: applications that dynamically insert runtime modules should not need to manually rescan the document, while keeping DOM observation separate from the core runtime module engine. Addition-only semantics avoid silently redefining modules because of text/attribute churn or trying to infer deletion semantics before the runtime has an explicit module-removal API.
|
|
890
|
+
|
|
891
|
+
### 2026-10-02 — Persistent HTML runtime controllers own DOM/module bindings
|
|
892
|
+
|
|
893
|
+
Decision: `0.0.3` adds `createHTMLRuntime(runtime, options)` as the preferred advanced HTML API while keeping `registerHTML()`, `observeHTML()`, and `defineScript()` as compatibility conveniences.
|
|
894
|
+
|
|
895
|
+
Reason: once multiple runtimes, explicit append/register helpers, and observers coexist, ownership must be represented by a persistent object instead of implicit document-wide behavior.
|
|
896
|
+
|
|
897
|
+
### 2026-10-02 — Runtime script ownership is coordinated through a shared WeakMap
|
|
898
|
+
|
|
899
|
+
Decision: all HTML ingestion paths use one module-level `WeakMap` from script element to ownership/registration record. Per-controller module-ID maps supplement element ownership.
|
|
900
|
+
|
|
901
|
+
Reason: a simple observer-local `WeakSet` cannot prevent races between manual registration and MutationObserver, distinguish different owners, retain a pending registration promise, or reject cross-runtime claims deterministically.
|
|
902
|
+
|
|
903
|
+
### 2026-10-02 — Explicit append claims before DOM insertion
|
|
904
|
+
|
|
905
|
+
Decision: `html.append(element)` and `html.addModule()` claim ownership before mutating the DOM and register directly rather than relying on the observer callback.
|
|
906
|
+
|
|
907
|
+
Reason: ownership-before-insertion guarantees the observer can only observe already-owned work, eliminating duplicate compilation and making explicit APIs deterministic even when no observer is active.
|
|
908
|
+
|
|
909
|
+
### 2026-10-02 — HTML runtime scope is declarative metadata, not ownership authority
|
|
910
|
+
|
|
911
|
+
Decision: scoped controllers stamp/use `data-solid-runtime="<scope>"` for discovery and debugging, while the in-memory WeakMap remains authoritative.
|
|
912
|
+
|
|
913
|
+
Reason: attributes are useful across declarative HTML and DevTools but cannot uniquely identify a controller instance or safely coordinate concurrent registration work.
|
|
914
|
+
|
package/README.md
CHANGED
|
@@ -97,6 +97,187 @@ counterModule.Counter;
|
|
|
97
97
|
|
|
98
98
|
The runtime compiles and links the dependency graph automatically.
|
|
99
99
|
|
|
100
|
+
|
|
101
|
+
## Declarative HTML modules
|
|
102
|
+
|
|
103
|
+
`solid-tag-runtime/html` is the browser/DOM adapter. The core runtime remains DOM-independent.
|
|
104
|
+
|
|
105
|
+
For simple single-runtime use, the original helpers remain available:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import {
|
|
109
|
+
registerHTML,
|
|
110
|
+
observeHTML,
|
|
111
|
+
} from "solid-tag-runtime/html";
|
|
112
|
+
|
|
113
|
+
await registerHTML(runtime);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For applications that may have multiple runtimes or that create runtime scripts programmatically, `0.0.3` adds a persistent HTML runtime controller:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { createHTMLRuntime } from "solid-tag-runtime/html";
|
|
120
|
+
|
|
121
|
+
const html = createHTMLRuntime(runtime, {
|
|
122
|
+
scope: "main",
|
|
123
|
+
root: document,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await html.register();
|
|
127
|
+
await html.observe({ registerExisting: false });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A scope is written to owned script elements as:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<script data-solid-runtime="main" ...></script>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
When multiple runtimes observe the same document, give each one a unique scope and normally set `acceptUnscoped: false`.
|
|
137
|
+
|
|
138
|
+
### Declarative modules
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<script
|
|
142
|
+
type="solid-jsx"
|
|
143
|
+
data-solid-runtime="main"
|
|
144
|
+
module="/ui/Button.jsx">
|
|
145
|
+
export function Button(props) {
|
|
146
|
+
return (
|
|
147
|
+
<button onClick={props.onClick}>
|
|
148
|
+
{props.children}
|
|
149
|
+
</button>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
</script>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Runtime modules import one another normally:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { Button } from "./ui/Button.jsx";
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
All newly discovered scripts in one batch are defined before any newly discovered `entry` module executes, so HTML document order does not require dependencies to appear first.
|
|
162
|
+
|
|
163
|
+
### External source
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<script
|
|
167
|
+
type="solid-jsx"
|
|
168
|
+
data-solid-runtime="main"
|
|
169
|
+
module="/ui/Button.jsx"
|
|
170
|
+
src="./components/Button.jsx">
|
|
171
|
+
</script>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`src` answers **where source is loaded from**. `module` answers **what identity the source has in the runtime graph**.
|
|
175
|
+
|
|
176
|
+
### User-created script elements
|
|
177
|
+
|
|
178
|
+
If application code creates a script itself, use the controller to associate it with the correct runtime:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const script = document.createElement("script");
|
|
182
|
+
script.type = "solid-jsx";
|
|
183
|
+
script.setAttribute("module", "/dynamic/Greeting.jsx");
|
|
184
|
+
script.textContent = `
|
|
185
|
+
export function Greeting(props) {
|
|
186
|
+
return <p>Hello {props.name}</p>;
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
await html.append(script);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`append()` claims the element **before** inserting it in the DOM, stamps the controller scope, then registers it directly. If a `MutationObserver` is active, the later mutation callback sees that the element is already owned and does not compile it twice.
|
|
194
|
+
|
|
195
|
+
For an element that is already in the DOM:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
await html.registerElement(script);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Same-controller repeated registration is idempotent. Trying to register an element already owned by another HTML controller throws `HTMLModuleOwnershipError`.
|
|
202
|
+
|
|
203
|
+
### Let the controller create the script
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
await html.addModule({
|
|
207
|
+
id: "/dynamic/Badge.jsx",
|
|
208
|
+
source: `
|
|
209
|
+
export function Badge(props) {
|
|
210
|
+
return <span>{props.children}</span>;
|
|
211
|
+
}
|
|
212
|
+
`,
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`addModule()` creates the `<script>` element, associates it with the controller, appends it, and registers it.
|
|
217
|
+
|
|
218
|
+
### Observe scripts added by other code
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
await html.observe({
|
|
222
|
+
registerExisting: false,
|
|
223
|
+
onError(error) {
|
|
224
|
+
console.error(error);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Some unrelated code mutates the DOM directly.
|
|
229
|
+
const script = document.createElement("script");
|
|
230
|
+
script.type = "solid-jsx";
|
|
231
|
+
script.setAttribute("data-solid-runtime", "main");
|
|
232
|
+
script.setAttribute("module", "/observed/Thing.jsx");
|
|
233
|
+
script.textContent = `export const value = 42;`;
|
|
234
|
+
document.body.append(script);
|
|
235
|
+
|
|
236
|
+
await html.flush();
|
|
237
|
+
const module = await runtime.import("/observed/Thing.jsx");
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The observer is for DOM changes that happen **outside** the controller API. Prefer `html.append()` / `html.addModule()` when your code controls insertion because those operations are deterministic and do not depend on observer timing.
|
|
241
|
+
|
|
242
|
+
### Ownership and deduplication
|
|
243
|
+
|
|
244
|
+
All HTML controllers share an internal `WeakMap` of script element → ownership/registration record.
|
|
245
|
+
|
|
246
|
+
That gives `0.0.3` these guarantees:
|
|
247
|
+
|
|
248
|
+
- one script element has at most one HTML-runtime owner;
|
|
249
|
+
- manual registration and observer registration cannot compile the same element twice;
|
|
250
|
+
- the same controller can register the same element repeatedly without redefining it;
|
|
251
|
+
- two distinct elements cannot silently define the same HTML-owned module ID;
|
|
252
|
+
- an HTML script cannot silently replace a module already defined outside that HTML controller;
|
|
253
|
+
- `data-solid-runtime` is declarative metadata while the in-memory owner record is authoritative.
|
|
254
|
+
|
|
255
|
+
The controller also exposes lightweight introspection:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
html.owns(script);
|
|
259
|
+
html.getModuleId(script);
|
|
260
|
+
html.getElement("/dynamic/Greeting.jsx");
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Script formats
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
solid-jsx / text/solid-jsx → JSX transformation
|
|
267
|
+
solid-js / text/solid-js → plain JavaScript module
|
|
268
|
+
solid-module → infer from module/src extension
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`language="js"` or `language="jsx"` may override inference.
|
|
272
|
+
|
|
273
|
+
### Addition-only observation
|
|
274
|
+
|
|
275
|
+
Observation remains addition-only in `0.0.3`.
|
|
276
|
+
|
|
277
|
+
Changing the source/attributes of an already owned script or removing it from the DOM does not implicitly update/delete the corresponding runtime module. Use `runtime.update()` / `runtime.invalidate()` for explicit module lifecycle changes.
|
|
278
|
+
|
|
279
|
+
The HTML adapter does **not** reinterpret arbitrary document HTML as JSX and does not currently assign component semantics to `<template>`.
|
|
280
|
+
|
|
100
281
|
## `toModule()`
|
|
101
282
|
|
|
102
283
|
For one-off source strings:
|