solid-tag-runtime 0.0.5 → 0.0.7
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 +108 -3
- package/README.md +192 -3
- package/html.d.ts +62 -0
- package/index.d.ts +30 -0
- package/package.json +1 -1
- package/src/html.js +270 -45
- package/src/runtime.js +221 -11
package/ARCHITECTURE.md
CHANGED
|
@@ -38,9 +38,11 @@ The runtime should allow applications to:
|
|
|
38
38
|
5. expose host components, signals, callbacks, services, and data as modules
|
|
39
39
|
6. reuse compiled/evaluated modules through caching
|
|
40
40
|
7. invalidate and rebuild a module graph when source changes
|
|
41
|
-
8.
|
|
42
|
-
9.
|
|
43
|
-
10.
|
|
41
|
+
8. explicitly remove and clear module definitions
|
|
42
|
+
9. observe runtime lifecycle transitions for tooling/tracing
|
|
43
|
+
10. inspect dependencies and compiled source
|
|
44
|
+
11. support normal JavaScript modules in the same graph
|
|
45
|
+
12. keep the JSX compiler replaceable behind a narrow adapter
|
|
44
46
|
|
|
45
47
|
## 3. Current non-goals
|
|
46
48
|
|
|
@@ -235,6 +237,10 @@ html.observe(options?);
|
|
|
235
237
|
html.flush(options?);
|
|
236
238
|
html.disconnect();
|
|
237
239
|
|
|
240
|
+
await html.setRoot(root, options?);
|
|
241
|
+
await html.moveTo(root, options?);
|
|
242
|
+
html.setAppendTarget(target);
|
|
243
|
+
|
|
238
244
|
html.defineElement(element, options?);
|
|
239
245
|
html.registerElement(element, options?);
|
|
240
246
|
html.append(element, options?);
|
|
@@ -321,6 +327,45 @@ A scoped controller accepts matching scripts. It may also claim unscoped scripts
|
|
|
321
327
|
|
|
322
328
|
An unscoped controller does not silently claim a script carrying `data-solid-runtime` for another runtime.
|
|
323
329
|
|
|
330
|
+
|
|
331
|
+
### Mutable root and append-target semantics
|
|
332
|
+
|
|
333
|
+
`0.0.6` makes the HTML controller's DOM boundaries mutable without replacing the controller or core runtime.
|
|
334
|
+
|
|
335
|
+
The discovery root may be any DOM-like container that supports `querySelectorAll()` and can be observed by `MutationObserver`, including browser `Document`, `Element`, `DocumentFragment`, and `ShadowRoot`.
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
await html.setRoot(nextRoot, {
|
|
339
|
+
registerExisting: true,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await html.moveTo(nextRoot);
|
|
343
|
+
html.setAppendTarget(target);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The two boundaries remain distinct:
|
|
347
|
+
|
|
348
|
+
```text
|
|
349
|
+
root
|
|
350
|
+
one-shot discovery + MutationObserver target
|
|
351
|
+
|
|
352
|
+
appendTarget
|
|
353
|
+
default destination for append()/addModule()
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Important invariants:
|
|
357
|
+
|
|
358
|
+
1. Reparenting the exact same observed root node does not require rebinding; MutationObserver follows node identity rather than the node's former parent.
|
|
359
|
+
2. Replacing that root with a different node requires `setRoot()`/`moveTo()` for the controller to observe the new subtree.
|
|
360
|
+
3. A connected controller stays connected when `setRoot()` changes the node: pending work against the old root is drained, the old observer target is disconnected, and observation is rebound to the new root.
|
|
361
|
+
4. Connected root changes register matching scripts already present in the new root by default; `registerExisting: false` opts out.
|
|
362
|
+
5. Disconnected `setRoot()` is configuration-only by default; `registerExisting: true` explicitly performs a one-shot scan.
|
|
363
|
+
6. `moveTo()` changes both discovery root and append target. For a `Document`, its body/documentElement is chosen as the append target rather than appending directly beside the document element.
|
|
364
|
+
7. `setAppendTarget()` never changes observation.
|
|
365
|
+
8. Root rebinding does not reset module ownership or the runtime graph. Elements already owned remain owned; module IDs remain reserved by the same controller.
|
|
366
|
+
|
|
367
|
+
Each HTML runtime controller owns its own MutationObserver instance. The observer is attached directly to the selected root; the implementation does not observe the entire document and then perform containment filtering. This keeps unrelated DOM mutation traffic out of the controller and correctly supports isolated `ShadowRoot` trees.
|
|
368
|
+
|
|
324
369
|
### User-created script elements
|
|
325
370
|
|
|
326
371
|
Applications may create the element themselves and explicitly associate it with one runtime:
|
|
@@ -926,3 +971,63 @@ Invariant: invalidation must result in a fresh executable module identity on the
|
|
|
926
971
|
|
|
927
972
|
Regression coverage: host-module replacement must update already-imported dependents, and explicit `runtime.invalidate()` must cause a side-effecting module to evaluate again on the next import.
|
|
928
973
|
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
### 0.0.6 — mutable HTML runtime roots and append targets
|
|
977
|
+
|
|
978
|
+
Decision: HTML runtime controllers now expose `setRoot()`, `moveTo()`, and `setAppendTarget()`, plus `root` and `appendTarget` introspection. A connected observer is rebound directly to the new root rather than keeping a document-wide observer and filtering by containment.
|
|
979
|
+
|
|
980
|
+
Reason: applications can reparent an existing observed root without intervention, but replacing a mount/container with a different DOM node would otherwise leave the observer and append destination attached to stale nodes. Explicit mutable boundaries preserve one controller/module graph while allowing UI containers, previews, micro-frontends, and shadow roots to move during application lifetime.
|
|
981
|
+
|
|
982
|
+
Invariant: a root change must not create a second observer, lose ownership records, or race pending scans from the old root. Pending observer work is drained before rebinding. A live root change registers existing scripts in the new root by default; a disconnected root change does not perform discovery unless requested.
|
|
983
|
+
|
|
984
|
+
Regression coverage: the suite verifies same-node reparenting, live root rebinding, skipping existing scripts, disconnected root changes, `moveTo()` synchronization of root/append target, and independent append-target changes.
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
### 0.0.7 — explicit module lifecycle, batching, and HTML-owned replacement
|
|
988
|
+
|
|
989
|
+
Decision: add `defineMany()`, `remove()`, `clear()`, and `subscribe()` to the core runtime, and `updateElement()` / `removeElement()` to `solid-tag-runtime/html`.
|
|
990
|
+
|
|
991
|
+
Reason: long-lived runtime environments need an explicit lifecycle beyond define/update/invalidate. Editors, previews, CMSs, and plugin hosts must be able to install related modules together, remove stale definitions, reset runtime state while preserving host modules, trace module transitions, and keep HTML ownership synchronized with explicit module replacement/removal.
|
|
992
|
+
|
|
993
|
+
Core lifecycle semantics:
|
|
994
|
+
|
|
995
|
+
- `defineMany()` validates the definition list before applying it and rejects duplicate IDs inside one batch. No module evaluation happens during definition, so the full batch is available before an import starts linking. Lifecycle events produced while applying the batch are buffered and published only after every definition has been installed, preventing a subscriber from observing or importing a partially installed batch.
|
|
996
|
+
- `remove(id)` deletes one module record, revokes generated URLs, removes host-registry state when applicable, and invalidates transitive dependents by default.
|
|
997
|
+
- `clear()` removes source/URL modules while preserving host modules by default; `preserveHostModules:false` removes every record.
|
|
998
|
+
- `subscribe(listener)` emits synchronous lifecycle events but isolates subscriber exceptions from runtime execution.
|
|
999
|
+
- public `resolve()` remains the canonical tooling hook for asking how a specifier resolves without evaluating it.
|
|
1000
|
+
|
|
1001
|
+
Lifecycle event contract:
|
|
1002
|
+
|
|
1003
|
+
```text
|
|
1004
|
+
module-defined
|
|
1005
|
+
module-invalidated
|
|
1006
|
+
module-compiled
|
|
1007
|
+
module-evaluating
|
|
1008
|
+
module-evaluated
|
|
1009
|
+
module-removed
|
|
1010
|
+
module-error
|
|
1011
|
+
runtime-cleared
|
|
1012
|
+
runtime-disposed
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
HTML lifecycle semantics:
|
|
1016
|
+
|
|
1017
|
+
- `updateElement(element)` only operates on an element already owned by that controller. It re-reads/fetches source and calls `runtime.update()` while preserving logical module identity. Changing `module` identity requires explicit remove + register.
|
|
1018
|
+
- `removeElement(element)` releases the shared WeakMap ownership and per-controller binding. By default it also removes the runtime module and DOM node. `removeModule` and `removeFromDOM` can be controlled independently.
|
|
1019
|
+
- DOM removal by itself still does not imply module removal. Module lifecycle remains explicit.
|
|
1020
|
+
- If ownership is released while the element remains under an active observed root, the controller marks that element ignored so the observer does not immediately reclaim it. An explicit `registerElement()` may claim it again.
|
|
1021
|
+
|
|
1022
|
+
Invariants:
|
|
1023
|
+
|
|
1024
|
+
1. Removing a module must revoke its executable URL/namespace cache and clean outbound graph edges.
|
|
1025
|
+
2. Dependents are invalidated before dependency removal unless explicitly disabled.
|
|
1026
|
+
3. `defineMany()` must not publish lifecycle events until the whole batch has been installed.
|
|
1027
|
+
4. `clear()` must not silently discard host environment modules under its default configuration.
|
|
1028
|
+
5. Lifecycle listeners are observational; listener failures cannot alter runtime execution.
|
|
1029
|
+
6. HTML element ownership and runtime module existence are related but separate lifecycles.
|
|
1030
|
+
7. `updateElement()` never changes the module identity associated with an owned element.
|
|
1031
|
+
8. `removeElement()` is the canonical operation for releasing shared WeakMap ownership.
|
|
1032
|
+
|
|
1033
|
+
Regression coverage: package tests cover batch definition and duplicate rejection, dependency removal/invalidation, clear-with-host-preservation, lifecycle event delivery/unsubscribe, owned element updates, identity-change rejection, ownership release, DOM removal, and preserving a runtime module while releasing its HTML element.
|
package/README.md
CHANGED
|
@@ -102,6 +102,17 @@ The runtime compiles and links the dependency graph automatically.
|
|
|
102
102
|
|
|
103
103
|
`solid-tag-runtime/html` is the browser/DOM adapter. The core runtime remains DOM-independent.
|
|
104
104
|
|
|
105
|
+
Browser import maps must map the subpath explicitly as well as the package root:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"imports": {
|
|
110
|
+
"solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.7",
|
|
111
|
+
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.7/html"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
105
116
|
For simple single-runtime use, the original helpers remain available:
|
|
106
117
|
|
|
107
118
|
```ts
|
|
@@ -270,11 +281,73 @@ solid-module → infer from module/src extension
|
|
|
270
281
|
|
|
271
282
|
`language="js"` or `language="jsx"` may override inference.
|
|
272
283
|
|
|
284
|
+
|
|
285
|
+
### Change the observed root
|
|
286
|
+
|
|
287
|
+
The HTML controller can move to a different discovery root without creating a new runtime:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const first = document.querySelector("#first-runtime")!;
|
|
291
|
+
const second = document.querySelector("#second-runtime")!;
|
|
292
|
+
|
|
293
|
+
const html = createHTMLRuntime(runtime, {
|
|
294
|
+
root: first,
|
|
295
|
+
appendTo: first,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await html.observe({ registerExisting: false });
|
|
299
|
+
|
|
300
|
+
// Rebind a live observer to a different node. Because the observer is already
|
|
301
|
+
// connected, matching scripts already in the new root are registered by default.
|
|
302
|
+
await html.setRoot(second);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
If the controller is disconnected, `setRoot()` only changes configuration unless `registerExisting: true` is passed:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
await html.setRoot(second, {
|
|
309
|
+
registerExisting: true,
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Use `registerExisting: false` when switching a live observer but intentionally ignoring scripts already present in the new root.
|
|
314
|
+
|
|
315
|
+
Moving the **same root node** elsewhere in the DOM does not require `setRoot()`. `MutationObserver` remains attached to that node object even when it is reparented.
|
|
316
|
+
|
|
317
|
+
### Move root and append target together
|
|
318
|
+
|
|
319
|
+
When the runtime module area itself moves to another container, `moveTo()` changes both boundaries:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
await html.moveTo(nextContainer, {
|
|
323
|
+
registerExisting: true,
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
For an `Element`, `DocumentFragment`, or `ShadowRoot`, future `append()` / `addModule()` calls insert into that root. For a `Document`, the controller chooses the document body (or document element fallback) as the append target.
|
|
328
|
+
|
|
329
|
+
### Change only the append target
|
|
330
|
+
|
|
331
|
+
Observation and insertion can have different boundaries:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
html.setAppendTarget(document.head);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
This does not change the currently observed root. The controller exposes the active values for diagnostics:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
html.root;
|
|
341
|
+
html.appendTarget;
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The `root` may be a `Document`, normal `Element`, `DocumentFragment`, or `ShadowRoot` (which is a `DocumentFragment`). A controller observes the selected root directly rather than observing the whole document and filtering mutations afterward.
|
|
345
|
+
|
|
273
346
|
### Addition-only observation
|
|
274
347
|
|
|
275
|
-
Observation remains addition-
|
|
348
|
+
Observation remains addition-oriented.
|
|
276
349
|
|
|
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.
|
|
350
|
+
Changing the source/attributes of an already owned script or removing it from the DOM does not implicitly update/delete the corresponding runtime module. In `0.0.7`, use `html.updateElement()` and `html.removeElement()` when the lifecycle should follow an owned script explicitly, or use the core `runtime.update()` / `runtime.remove()` APIs directly.
|
|
278
351
|
|
|
279
352
|
The HTML adapter does **not** reinterpret arbitrary document HTML as JSX and does not currently assign component semantics to `<template>`.
|
|
280
353
|
|
|
@@ -448,6 +521,120 @@ Updating a module invalidates its compiled URL and its dependent runtime modules
|
|
|
448
521
|
|
|
449
522
|
Existing references to an older module namespace/component are not mutated. Applications that implement live editing should import the updated entry module again.
|
|
450
523
|
|
|
524
|
+
### Define several source modules together
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
runtime.defineMany([
|
|
528
|
+
{
|
|
529
|
+
id: "/ui/Button.jsx",
|
|
530
|
+
source: buttonSource,
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
id: "/App.jsx",
|
|
534
|
+
source: appSource,
|
|
535
|
+
},
|
|
536
|
+
]);
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
`defineMany()` validates the whole definition list before applying it and rejects duplicate module IDs inside the batch. It is useful when a group of modules should become available before anything is imported. Lifecycle notifications from the batch are published only after every definition has been installed.
|
|
540
|
+
|
|
541
|
+
### Remove modules
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
runtime.remove("/ui/Button.jsx");
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Removal invalidates transitive dependents by default. A later import of a dependent will fail resolution until the missing module is defined again.
|
|
548
|
+
|
|
549
|
+
If you intentionally want to leave currently linked dependents untouched:
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
runtime.remove("/ui/Button.jsx", {
|
|
553
|
+
invalidateDependents: false,
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Clear a runtime
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
runtime.clear();
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
By default `clear()` removes source and URL modules while preserving host modules registered with `defineModule()`. This is convenient for editor/preview resets where the host environment should remain installed.
|
|
564
|
+
|
|
565
|
+
```ts
|
|
566
|
+
runtime.clear({
|
|
567
|
+
preserveHostModules: false,
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
removes everything. The returned array contains the IDs that were removed.
|
|
572
|
+
|
|
573
|
+
### Lifecycle events
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
const unsubscribe = runtime.subscribe(event => {
|
|
577
|
+
console.log(event.type, event);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// later
|
|
581
|
+
unsubscribe();
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Events currently include:
|
|
585
|
+
|
|
586
|
+
- `module-defined`
|
|
587
|
+
- `module-invalidated`
|
|
588
|
+
- `module-compiled`
|
|
589
|
+
- `module-evaluating`
|
|
590
|
+
- `module-evaluated`
|
|
591
|
+
- `module-removed`
|
|
592
|
+
- `module-error`
|
|
593
|
+
- `runtime-cleared`
|
|
594
|
+
- `runtime-disposed`
|
|
595
|
+
|
|
596
|
+
Subscriber exceptions are isolated from runtime execution. This makes `subscribe()` suitable for tracing, developer tools, and editor diagnostics.
|
|
597
|
+
|
|
598
|
+
## HTML-owned module updates and removal
|
|
599
|
+
|
|
600
|
+
When a script element is already owned by an HTML runtime controller, update the runtime module from the current element contents with:
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
script.textContent = `
|
|
604
|
+
export function Card() {
|
|
605
|
+
return <div>updated</div>;
|
|
606
|
+
}
|
|
607
|
+
`;
|
|
608
|
+
|
|
609
|
+
await html.updateElement(script);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
`updateElement()` keeps the existing logical module ID and calls `runtime.update()` underneath. Changing the element's logical `module` identity is intentionally rejected; remove and register it again instead.
|
|
613
|
+
|
|
614
|
+
Release an owned element explicitly with:
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
await html.removeElement(script);
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
By default this:
|
|
621
|
+
|
|
622
|
+
1. removes the runtime module,
|
|
623
|
+
2. invalidates its dependents,
|
|
624
|
+
3. releases HTML ownership, and
|
|
625
|
+
4. removes the script element from the DOM.
|
|
626
|
+
|
|
627
|
+
The two lifecycles can be controlled independently:
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
await html.removeElement(script, {
|
|
631
|
+
removeModule: false,
|
|
632
|
+
removeFromDOM: true,
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
This is intentionally explicit: ordinary DOM removal does not silently delete a runtime module.
|
|
637
|
+
|
|
451
638
|
## Custom resolution
|
|
452
639
|
|
|
453
640
|
```ts
|
|
@@ -486,10 +673,12 @@ Set it to `false` when all module dependencies must be explicitly registered wit
|
|
|
486
673
|
|
|
487
674
|
```ts
|
|
488
675
|
runtime.invalidate("/App.jsx");
|
|
676
|
+
runtime.remove("/Unused.jsx");
|
|
677
|
+
runtime.clear();
|
|
489
678
|
runtime.dispose();
|
|
490
679
|
```
|
|
491
680
|
|
|
492
|
-
`
|
|
681
|
+
`invalidate()` forces fresh compilation/evaluation without deleting the module definition. `remove()` deletes one definition, `clear()` resets a group of definitions, and `dispose()` permanently tears down the runtime instance.
|
|
493
682
|
|
|
494
683
|
## Current limitations
|
|
495
684
|
|
package/html.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface HTMLModuleScriptElement {
|
|
|
5
5
|
textContent?: string | null;
|
|
6
6
|
baseURI?: string;
|
|
7
7
|
ownerDocument?: HTMLDocumentLike | null;
|
|
8
|
+
parentNode?: { removeChild?(element: any): unknown } | null;
|
|
9
|
+
remove?(): void;
|
|
8
10
|
getAttribute(name: string): string | null;
|
|
9
11
|
hasAttribute?(name: string): boolean;
|
|
10
12
|
setAttribute?(name: string, value: string): void;
|
|
@@ -75,6 +77,22 @@ export interface HTMLModuleDefinition {
|
|
|
75
77
|
element: HTMLModuleScriptElement;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
export interface RemoveHTMLElementOptions {
|
|
81
|
+
/** Remove the runtime module as well as releasing HTML ownership. Default: true. */
|
|
82
|
+
removeModule?: boolean;
|
|
83
|
+
/** Remove the script element from the DOM. Default: true. */
|
|
84
|
+
removeFromDOM?: boolean;
|
|
85
|
+
/** Invalidate runtime dependents when removing the module. Default: true. */
|
|
86
|
+
invalidateDependents?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RemoveHTMLElementResult {
|
|
90
|
+
id: string;
|
|
91
|
+
element: HTMLModuleScriptElement;
|
|
92
|
+
moduleRemoved: boolean;
|
|
93
|
+
elementRemoved: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
export interface RegisterHTMLOptions extends DefineScriptOptions {
|
|
79
97
|
selector?: string;
|
|
80
98
|
executeEntries?: boolean;
|
|
@@ -119,10 +137,24 @@ export interface HTMLRuntimeOptions extends ObserveHTMLOptions {
|
|
|
119
137
|
createElement?: (tagName: string) => HTMLModuleScriptElement;
|
|
120
138
|
}
|
|
121
139
|
|
|
140
|
+
export interface SetHTMLRootOptions extends RegisterHTMLOptions {
|
|
141
|
+
/**
|
|
142
|
+
* Register matching scripts already present in the new root.
|
|
143
|
+
* Defaults to true when rebinding a connected observer and false otherwise.
|
|
144
|
+
*/
|
|
145
|
+
registerExisting?: boolean;
|
|
146
|
+
/** Optionally update the controller's default append target at the same time. */
|
|
147
|
+
appendTo?: HTMLAppendTarget;
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
export interface HTMLRuntimeController {
|
|
123
151
|
readonly runtime: SolidTagRuntime;
|
|
124
152
|
readonly scope?: string;
|
|
125
153
|
readonly connected: boolean;
|
|
154
|
+
/** Current configured/effective discovery root when available. */
|
|
155
|
+
readonly root?: HTMLModuleRoot;
|
|
156
|
+
/** Current effective target used by append()/addModule() when available. */
|
|
157
|
+
readonly appendTarget?: HTMLAppendTarget;
|
|
126
158
|
/** Result of the observer's initial scan. */
|
|
127
159
|
readonly initial: RegisterHTMLResult;
|
|
128
160
|
|
|
@@ -136,6 +168,24 @@ export interface HTMLRuntimeController {
|
|
|
136
168
|
flush(options?: RegisterHTMLOptions): Promise<RegisterHTMLResult>;
|
|
137
169
|
disconnect(): void;
|
|
138
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Change the discovery/observation root. A connected observer is rebound to
|
|
173
|
+
* the new node automatically.
|
|
174
|
+
*/
|
|
175
|
+
setRoot(
|
|
176
|
+
root: HTMLModuleRoot,
|
|
177
|
+
options?: SetHTMLRootOptions,
|
|
178
|
+
): Promise<RegisterHTMLResult>;
|
|
179
|
+
|
|
180
|
+
/** Change both the discovery root and append target to the selected root. */
|
|
181
|
+
moveTo(
|
|
182
|
+
root: HTMLModuleRoot,
|
|
183
|
+
options?: SetHTMLRootOptions,
|
|
184
|
+
): Promise<RegisterHTMLResult>;
|
|
185
|
+
|
|
186
|
+
/** Change only where append()/addModule() insert future elements. */
|
|
187
|
+
setAppendTarget(target: HTMLAppendTarget): HTMLRuntimeController;
|
|
188
|
+
|
|
139
189
|
/** Define one element without auto-running an entry. */
|
|
140
190
|
defineElement(
|
|
141
191
|
element: HTMLModuleScriptElement,
|
|
@@ -148,6 +198,18 @@ export interface HTMLRuntimeController {
|
|
|
148
198
|
options?: RegisterHTMLOptions,
|
|
149
199
|
): Promise<HTMLModuleDefinition>;
|
|
150
200
|
|
|
201
|
+
/** Re-read and replace source for an element already owned by this controller. */
|
|
202
|
+
updateElement(
|
|
203
|
+
element: HTMLModuleScriptElement,
|
|
204
|
+
options?: RegisterHTMLOptions,
|
|
205
|
+
): Promise<HTMLModuleDefinition>;
|
|
206
|
+
|
|
207
|
+
/** Release an owned element and optionally remove its module and DOM node. */
|
|
208
|
+
removeElement(
|
|
209
|
+
element: HTMLModuleScriptElement,
|
|
210
|
+
options?: RemoveHTMLElementOptions,
|
|
211
|
+
): Promise<RemoveHTMLElementResult>;
|
|
212
|
+
|
|
151
213
|
/** Claim first, append a user-created element, then register it directly. */
|
|
152
214
|
append(
|
|
153
215
|
element: HTMLModuleScriptElement,
|
package/index.d.ts
CHANGED
|
@@ -76,8 +76,35 @@ export interface CompileResult {
|
|
|
76
76
|
diagnostics: unknown[];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export interface SourceModuleDefinition extends DefineOptions {
|
|
80
|
+
id: string;
|
|
81
|
+
source: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RemoveModuleOptions {
|
|
85
|
+
/** Invalidate transitive dependents before removal. Default: true. */
|
|
86
|
+
invalidateDependents?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ClearRuntimeOptions {
|
|
90
|
+
/** Keep host namespace modules registered with defineModule(). Default: true. */
|
|
91
|
+
preserveHostModules?: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type RuntimeLifecycleEvent =
|
|
95
|
+
| { runtimeId: string; timestamp: number; type: "module-defined"; id: string; kind: ModuleInfo["kind"]; version: number; operation: "define" | "defineMany" | "update" | "defineModule" | "defineUrl"; replaced: boolean; module: ModuleInfo }
|
|
96
|
+
| { runtimeId: string; timestamp: number; type: "module-invalidated"; id: string; kind: ModuleInfo["kind"]; version: number; reason: string }
|
|
97
|
+
| { runtimeId: string; timestamp: number; type: "module-compiled"; id: string; kind: ModuleInfo["kind"]; version: number; module: ModuleInfo }
|
|
98
|
+
| { runtimeId: string; timestamp: number; type: "module-evaluating"; id: string; kind: ModuleInfo["kind"]; version: number }
|
|
99
|
+
| { runtimeId: string; timestamp: number; type: "module-evaluated"; id: string; kind: ModuleInfo["kind"]; version: number; module: ModuleInfo }
|
|
100
|
+
| { runtimeId: string; timestamp: number; type: "module-removed"; id: string; kind: ModuleInfo["kind"]; version: number; invalidateDependents: boolean }
|
|
101
|
+
| { runtimeId: string; timestamp: number; type: "module-error"; id?: string; kind?: ModuleInfo["kind"]; version?: number; phase: string; error: unknown }
|
|
102
|
+
| { runtimeId: string; timestamp: number; type: "runtime-cleared"; ids: string[]; preserveHostModules: boolean }
|
|
103
|
+
| { runtimeId: string; timestamp: number; type: "runtime-disposed" };
|
|
104
|
+
|
|
79
105
|
export interface SolidTagRuntime {
|
|
80
106
|
define(id: string, source: string, options?: DefineOptions): string;
|
|
107
|
+
defineMany(definitions: SourceModuleDefinition[]): string[];
|
|
81
108
|
update(id: string, source: string, options?: DefineOptions): string;
|
|
82
109
|
defineModule(id: string, namespace: ModuleNamespaceLike | Function): string;
|
|
83
110
|
defineUrl(id: string, url: string): string;
|
|
@@ -87,6 +114,9 @@ export interface SolidTagRuntime {
|
|
|
87
114
|
toComponent<T extends Function = Function>(source: string, options?: ToComponentOptions): Promise<T>;
|
|
88
115
|
has(id: string): boolean;
|
|
89
116
|
invalidate(id: string, options?: { dependents?: boolean }): string[];
|
|
117
|
+
remove(id: string, options?: RemoveModuleOptions): boolean;
|
|
118
|
+
clear(options?: ClearRuntimeOptions): string[];
|
|
119
|
+
subscribe(listener: (event: RuntimeLifecycleEvent) => void): () => boolean;
|
|
90
120
|
modules(): ModuleInfo[];
|
|
91
121
|
dependencies(id: string): string[];
|
|
92
122
|
dependents(id: string): string[];
|
package/package.json
CHANGED
package/src/html.js
CHANGED
|
@@ -61,6 +61,7 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
61
61
|
bindings: new Map(),
|
|
62
62
|
observer: undefined,
|
|
63
63
|
observerRoot: undefined,
|
|
64
|
+
observerOptions: undefined,
|
|
64
65
|
ignoredExisting: new WeakSet(),
|
|
65
66
|
queue: Promise.resolve(),
|
|
66
67
|
lastError: undefined,
|
|
@@ -81,12 +82,23 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
81
82
|
get initial() {
|
|
82
83
|
return controller.initial;
|
|
83
84
|
},
|
|
85
|
+
get root() {
|
|
86
|
+
return controller.root ?? globalThis.document;
|
|
87
|
+
},
|
|
88
|
+
get appendTarget() {
|
|
89
|
+
return getAppendTarget(controller);
|
|
90
|
+
},
|
|
84
91
|
register,
|
|
85
92
|
observe,
|
|
86
93
|
flush,
|
|
87
94
|
disconnect,
|
|
95
|
+
setRoot,
|
|
96
|
+
moveTo,
|
|
97
|
+
setAppendTarget,
|
|
88
98
|
defineElement,
|
|
89
99
|
registerElement,
|
|
100
|
+
updateElement,
|
|
101
|
+
removeElement,
|
|
90
102
|
append,
|
|
91
103
|
addModule,
|
|
92
104
|
owns,
|
|
@@ -120,47 +132,14 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
120
132
|
async function observe(callOptions = {}) {
|
|
121
133
|
if (controller.connected) return api;
|
|
122
134
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
const MutationObserverImpl = callOptions.MutationObserver ?? controller.MutationObserver ?? globalThis.MutationObserver;
|
|
126
|
-
|
|
127
|
-
if (typeof MutationObserverImpl !== "function") {
|
|
128
|
-
throw new TypeError(
|
|
129
|
-
"observe() requires MutationObserver support or a MutationObserver option.",
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
controller.observerRoot = root;
|
|
134
|
-
controller.connected = true;
|
|
135
|
-
controller.lastError = undefined;
|
|
136
|
-
controller.ignoredExisting = new WeakSet();
|
|
137
|
-
|
|
138
|
-
if (callOptions.registerExisting === false) {
|
|
139
|
-
for (const element of Array.from(root.querySelectorAll(callOptions.selector ?? controller.selector))) {
|
|
140
|
-
if (acceptsDiscoveredElement(controller, element, callOptions)) {
|
|
141
|
-
controller.ignoredExisting.add(element);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
controller.initial = emptyRegistrationResult();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const observer = new MutationObserverImpl(() => {
|
|
148
|
-
void enqueueObserverScan(controller, callOptions).catch(() => {});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
controller.observer = observer;
|
|
152
|
-
observer.observe(root, {
|
|
153
|
-
childList: true,
|
|
154
|
-
subtree: callOptions.subtree ?? controller.subtree,
|
|
155
|
-
});
|
|
135
|
+
const root = getRoot(mergeOptions(controller, callOptions), "HTML runtime observe");
|
|
136
|
+
controller.observerOptions = { ...callOptions };
|
|
156
137
|
|
|
157
138
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
});
|
|
163
|
-
}
|
|
139
|
+
controller.initial = await connectObserver(controller, root, callOptions, {
|
|
140
|
+
registerExisting: callOptions.registerExisting !== false,
|
|
141
|
+
initial: true,
|
|
142
|
+
});
|
|
164
143
|
} catch (error) {
|
|
165
144
|
disconnect();
|
|
166
145
|
throw error;
|
|
@@ -184,11 +163,80 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
184
163
|
}
|
|
185
164
|
|
|
186
165
|
function disconnect() {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
stopObserver(controller);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Change the discovery/observation root. If observation is active, it is
|
|
171
|
+
* rebound to the new root and existing matching scripts are registered by
|
|
172
|
+
* default. When disconnected, changing the root is configuration-only unless
|
|
173
|
+
* registerExisting:true is requested.
|
|
174
|
+
*/
|
|
175
|
+
async function setRoot(root, callOptions = {}) {
|
|
176
|
+
const nextRoot = getRoot({ root }, "HTML runtime setRoot");
|
|
177
|
+
|
|
178
|
+
// Finish work scheduled against the old root before rebinding observer state.
|
|
179
|
+
await controller.queue;
|
|
180
|
+
if (controller.lastError) {
|
|
181
|
+
const error = controller.lastError;
|
|
182
|
+
controller.lastError = undefined;
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const wasConnected = controller.connected;
|
|
187
|
+
const previousObserverOptions = controller.observerOptions ?? {};
|
|
188
|
+
|
|
189
|
+
if (wasConnected) stopObserver(controller, { preserveOptions: true });
|
|
190
|
+
|
|
191
|
+
controller.root = nextRoot;
|
|
192
|
+
if (Object.prototype.hasOwnProperty.call(callOptions, "appendTo")) {
|
|
193
|
+
controller.appendTo = callOptions.appendTo;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const registerExisting = callOptions.registerExisting ?? wasConnected;
|
|
197
|
+
|
|
198
|
+
if (wasConnected) {
|
|
199
|
+
const observeOptions = {
|
|
200
|
+
...previousObserverOptions,
|
|
201
|
+
...callOptions,
|
|
202
|
+
root: nextRoot,
|
|
203
|
+
registerExisting,
|
|
204
|
+
};
|
|
205
|
+
controller.observerOptions = { ...observeOptions };
|
|
206
|
+
try {
|
|
207
|
+
return await connectObserver(controller, nextRoot, observeOptions, {
|
|
208
|
+
registerExisting,
|
|
209
|
+
initial: false,
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
stopObserver(controller, { preserveOptions: true });
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (registerExisting) {
|
|
218
|
+
return register({ ...callOptions, root: nextRoot });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return emptyRegistrationResult();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Change both observation root and default append target to the same node. */
|
|
225
|
+
async function moveTo(root, callOptions = {}) {
|
|
226
|
+
const nextRoot = getRoot({ root }, "HTML runtime moveTo");
|
|
227
|
+
return setRoot(nextRoot, {
|
|
228
|
+
...callOptions,
|
|
229
|
+
appendTo: callOptions.appendTo ?? defaultAppendTargetForRoot(nextRoot),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Change only where append()/addModule() insert future script elements. */
|
|
234
|
+
function setAppendTarget(target) {
|
|
235
|
+
if (!target || (typeof target.append !== "function" && typeof target.appendChild !== "function")) {
|
|
236
|
+
throw new TypeError("setAppendTarget() requires a target with append() or appendChild().");
|
|
237
|
+
}
|
|
238
|
+
controller.appendTo = target;
|
|
239
|
+
return api;
|
|
192
240
|
}
|
|
193
241
|
|
|
194
242
|
/** Define one element without automatically executing an entry module. */
|
|
@@ -213,6 +261,97 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
213
261
|
return result.modules[0] ?? definitionFromOwnedElement(controller, element);
|
|
214
262
|
}
|
|
215
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Re-read an owned script element and replace its runtime module source. The
|
|
266
|
+
* logical module id is stable; changing `module`/src-derived identity requires
|
|
267
|
+
* removeElement() + registerElement() instead.
|
|
268
|
+
*/
|
|
269
|
+
async function updateElement(element, callOptions = {}) {
|
|
270
|
+
const record = requireOwnedRecord(controller, element, "updateElement");
|
|
271
|
+
const metadata = inspectElement(controller, element, {
|
|
272
|
+
...callOptions,
|
|
273
|
+
existingId: record.moduleId,
|
|
274
|
+
});
|
|
275
|
+
const nextModuleId = normalizeModuleId(runtime, metadata.id);
|
|
276
|
+
|
|
277
|
+
if (nextModuleId !== record.moduleId) {
|
|
278
|
+
throw new HTMLModuleError(
|
|
279
|
+
`updateElement() cannot change module identity from ${JSON.stringify(record.moduleId)} to ${JSON.stringify(nextModuleId)}. Remove and register the element again instead.`,
|
|
280
|
+
{ element, moduleId: record.moduleId, sourceUrl: metadata.sourceUrl },
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const candidate = {
|
|
285
|
+
...record,
|
|
286
|
+
format: metadata.format,
|
|
287
|
+
entry: metadata.entry,
|
|
288
|
+
sourceUrl: metadata.sourceUrl,
|
|
289
|
+
inline: !metadata.sourceUrl,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const { source } = await prepareRecordSource(controller, candidate, callOptions);
|
|
293
|
+
runtime.update(record.moduleId, source, { format: candidate.format });
|
|
294
|
+
|
|
295
|
+
record.format = candidate.format;
|
|
296
|
+
record.entry = candidate.entry;
|
|
297
|
+
record.sourceUrl = candidate.sourceUrl;
|
|
298
|
+
record.inline = candidate.inline;
|
|
299
|
+
record.state = "defined";
|
|
300
|
+
record.error = undefined;
|
|
301
|
+
record.definition = definitionFromRecord(record);
|
|
302
|
+
|
|
303
|
+
if (record.entry && (callOptions.executeEntries ?? controller.executeEntries) !== false) {
|
|
304
|
+
record.state = "executing";
|
|
305
|
+
try {
|
|
306
|
+
await runtime.import(record.moduleId);
|
|
307
|
+
record.state = "ready";
|
|
308
|
+
} catch (error) {
|
|
309
|
+
record.state = "error";
|
|
310
|
+
record.error = error;
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return record.definition;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Release an element from this HTML controller. By default both the runtime
|
|
320
|
+
* module and DOM element are removed. DOM removal and module removal can be
|
|
321
|
+
* controlled independently.
|
|
322
|
+
*/
|
|
323
|
+
async function removeElement(element, callOptions = {}) {
|
|
324
|
+
const record = requireOwnedRecord(controller, element, "removeElement");
|
|
325
|
+
const removeModule = callOptions.removeModule !== false;
|
|
326
|
+
const removeFromDOM = callOptions.removeFromDOM !== false;
|
|
327
|
+
const invalidateDependents = callOptions.invalidateDependents !== false;
|
|
328
|
+
|
|
329
|
+
let moduleRemoved = false;
|
|
330
|
+
if (removeModule) {
|
|
331
|
+
moduleRemoved = runtime.remove(record.moduleId, { invalidateDependents });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
controller.bindings.delete(record.moduleId);
|
|
335
|
+
elementOwners.delete(element);
|
|
336
|
+
record.state = "removed";
|
|
337
|
+
|
|
338
|
+
let elementRemoved = false;
|
|
339
|
+
if (removeFromDOM) {
|
|
340
|
+
elementRemoved = removeDOMElement(element);
|
|
341
|
+
} else {
|
|
342
|
+
// Keep a released element in the observed tree from being immediately
|
|
343
|
+
// rediscovered. Explicit registerElement() can still claim it again.
|
|
344
|
+
controller.ignoredExisting.add(element);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
id: record.moduleId,
|
|
349
|
+
element,
|
|
350
|
+
moduleRemoved,
|
|
351
|
+
elementRemoved,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
216
355
|
/**
|
|
217
356
|
* Claim a user-created script before DOM insertion, append it to this
|
|
218
357
|
* controller's target, then register it directly. The observer will see the
|
|
@@ -326,6 +465,59 @@ export async function defineScript(runtime, element, options = {}) {
|
|
|
326
465
|
export const htmlModuleSelector = DEFAULT_SELECTOR;
|
|
327
466
|
export const htmlRuntimeScopeAttribute = RUNTIME_SCOPE_ATTRIBUTE;
|
|
328
467
|
|
|
468
|
+
async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
469
|
+
const MutationObserverImpl = callOptions.MutationObserver ?? controller.MutationObserver ?? globalThis.MutationObserver;
|
|
470
|
+
|
|
471
|
+
if (typeof MutationObserverImpl !== "function") {
|
|
472
|
+
throw new TypeError(
|
|
473
|
+
"observe() requires MutationObserver support or a MutationObserver option.",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
controller.observerRoot = root;
|
|
478
|
+
controller.connected = true;
|
|
479
|
+
controller.lastError = undefined;
|
|
480
|
+
controller.ignoredExisting = new WeakSet();
|
|
481
|
+
|
|
482
|
+
const registerExisting = mode.registerExisting !== false;
|
|
483
|
+
if (!registerExisting) {
|
|
484
|
+
for (const element of Array.from(root.querySelectorAll(callOptions.selector ?? controller.selector))) {
|
|
485
|
+
if (acceptsDiscoveredElement(controller, element, callOptions)) {
|
|
486
|
+
controller.ignoredExisting.add(element);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const observer = new MutationObserverImpl(() => {
|
|
492
|
+
void enqueueObserverScan(controller, callOptions).catch(() => {});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
controller.observer = observer;
|
|
496
|
+
observer.observe(root, {
|
|
497
|
+
childList: true,
|
|
498
|
+
subtree: callOptions.subtree ?? controller.subtree,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const result = registerExisting
|
|
502
|
+
? await enqueueObserverScan(controller, {
|
|
503
|
+
...callOptions,
|
|
504
|
+
origin: mode.initial ? "observer-initial" : "observer-root-change",
|
|
505
|
+
})
|
|
506
|
+
: emptyRegistrationResult();
|
|
507
|
+
|
|
508
|
+
if (mode.initial) controller.initial = result;
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function stopObserver(controller, options = {}) {
|
|
513
|
+
if (!controller.connected && !controller.observer) return;
|
|
514
|
+
controller.connected = false;
|
|
515
|
+
controller.observer?.disconnect();
|
|
516
|
+
controller.observer = undefined;
|
|
517
|
+
controller.observerRoot = undefined;
|
|
518
|
+
if (!options.preserveOptions) controller.observerOptions = undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
329
521
|
async function enqueueObserverScan(controller, callOptions = {}) {
|
|
330
522
|
const task = controller.queue.then(async () => {
|
|
331
523
|
if (!controller.connected) return emptyRegistrationResult();
|
|
@@ -550,7 +742,7 @@ function inspectElement(controller, element, options = {}) {
|
|
|
550
742
|
sourceUrl,
|
|
551
743
|
});
|
|
552
744
|
|
|
553
|
-
const id = explicitId ?? sourceUrl ?? (entry ? createAnonymousId(format) : undefined);
|
|
745
|
+
const id = explicitId ?? sourceUrl ?? (entry ? options.existingId ?? createAnonymousId(format) : undefined);
|
|
554
746
|
if (!id) {
|
|
555
747
|
throw new HTMLModuleError(
|
|
556
748
|
"Inline solid runtime module scripts require a `module` attribute unless they are marked `entry`.",
|
|
@@ -561,6 +753,35 @@ function inspectElement(controller, element, options = {}) {
|
|
|
561
753
|
return { id, format, entry, sourceUrl };
|
|
562
754
|
}
|
|
563
755
|
|
|
756
|
+
function requireOwnedRecord(controller, element, caller) {
|
|
757
|
+
assertElement(element, caller);
|
|
758
|
+
const record = elementOwners.get(element);
|
|
759
|
+
if (!record) {
|
|
760
|
+
throw new HTMLModuleOwnershipError(
|
|
761
|
+
`${caller}() requires an element already owned by this HTML runtime.`,
|
|
762
|
+
{ element, requestedScope: controller.scope },
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (record.owner !== controller) throw ownershipError(controller, element, record);
|
|
766
|
+
return record;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function removeDOMElement(element) {
|
|
770
|
+
if (typeof element?.remove === "function") {
|
|
771
|
+
const wasConnected = Boolean(element.parentNode);
|
|
772
|
+
element.remove();
|
|
773
|
+
return wasConnected || !element.parentNode;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const parent = element?.parentNode;
|
|
777
|
+
if (parent && typeof parent.removeChild === "function") {
|
|
778
|
+
parent.removeChild(element);
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
|
|
564
785
|
function definitionFromRecord(record) {
|
|
565
786
|
return {
|
|
566
787
|
id: record.moduleId,
|
|
@@ -669,6 +890,10 @@ function getAppendTarget(controller, options = {}) {
|
|
|
669
890
|
if (explicit) return explicit;
|
|
670
891
|
|
|
671
892
|
const root = options.root ?? controller.root ?? globalThis.document;
|
|
893
|
+
return defaultAppendTargetForRoot(root);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function defaultAppendTargetForRoot(root) {
|
|
672
897
|
if (root?.body) return root.body;
|
|
673
898
|
if (root?.documentElement && root.documentElement !== root) return root.documentElement;
|
|
674
899
|
return root;
|
package/src/runtime.js
CHANGED
|
@@ -27,6 +27,9 @@ export function createRuntime(options = {}) {
|
|
|
27
27
|
const records = new Map();
|
|
28
28
|
let anonymousSequence = 0;
|
|
29
29
|
let disposed = false;
|
|
30
|
+
const subscribers = new Set();
|
|
31
|
+
let eventBufferDepth = 0;
|
|
32
|
+
const eventBuffer = [];
|
|
30
33
|
|
|
31
34
|
const globalRegistry = getHostRegistry();
|
|
32
35
|
const hostNamespaces = new Map();
|
|
@@ -34,6 +37,7 @@ export function createRuntime(options = {}) {
|
|
|
34
37
|
|
|
35
38
|
const api = {
|
|
36
39
|
define,
|
|
40
|
+
defineMany,
|
|
37
41
|
defineModule,
|
|
38
42
|
defineUrl,
|
|
39
43
|
update,
|
|
@@ -43,6 +47,9 @@ export function createRuntime(options = {}) {
|
|
|
43
47
|
toComponent,
|
|
44
48
|
has,
|
|
45
49
|
invalidate,
|
|
50
|
+
remove,
|
|
51
|
+
clear,
|
|
52
|
+
subscribe,
|
|
46
53
|
modules: listModules,
|
|
47
54
|
dependencies,
|
|
48
55
|
dependents,
|
|
@@ -61,11 +68,57 @@ export function createRuntime(options = {}) {
|
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
function define(id, source, defineOptions = {}) {
|
|
71
|
+
return defineSource(id, source, defineOptions, "define");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function defineMany(definitions) {
|
|
75
|
+
assertActive();
|
|
76
|
+
if (!Array.isArray(definitions)) {
|
|
77
|
+
throw new TypeError("defineMany(definitions) expects an array of source module definitions.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const prepared = definitions.map((definition, index) => {
|
|
81
|
+
if (!definition || typeof definition !== "object") {
|
|
82
|
+
throw new TypeError(`defineMany() item ${index} must be an object with id and source.`);
|
|
83
|
+
}
|
|
84
|
+
if (definition.id == null) {
|
|
85
|
+
throw new TypeError(`defineMany() item ${index} is missing id.`);
|
|
86
|
+
}
|
|
87
|
+
const id = normalizeDefinedId(definition.id);
|
|
88
|
+
return {
|
|
89
|
+
id,
|
|
90
|
+
source: String(definition.source ?? ""),
|
|
91
|
+
format: definition.format ?? "jsx",
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
for (const item of prepared) {
|
|
97
|
+
if (seen.has(item.id)) {
|
|
98
|
+
throw new SolidTagRuntimeError(`defineMany() contains duplicate module id ${JSON.stringify(item.id)}.`);
|
|
99
|
+
}
|
|
100
|
+
seen.add(item.id);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
eventBufferDepth += 1;
|
|
104
|
+
try {
|
|
105
|
+
return prepared.map(item => defineSource(item.id, item.source, { format: item.format }, "defineMany"));
|
|
106
|
+
} finally {
|
|
107
|
+
eventBufferDepth -= 1;
|
|
108
|
+
if (eventBufferDepth === 0) flushEventBuffer();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function update(id, source, updateOptions = {}) {
|
|
113
|
+
return defineSource(id, source, updateOptions, "update");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function defineSource(id, source, defineOptions = {}, operation = "define") {
|
|
64
117
|
assertActive();
|
|
65
118
|
const normalized = normalizeDefinedId(id);
|
|
66
119
|
const existing = records.get(normalized);
|
|
67
120
|
|
|
68
|
-
if (existing) invalidate(normalized, { dependents: true });
|
|
121
|
+
if (existing) invalidate(normalized, { dependents: true, reason: operation });
|
|
69
122
|
|
|
70
123
|
const record = {
|
|
71
124
|
id: normalized,
|
|
@@ -84,18 +137,23 @@ export function createRuntime(options = {}) {
|
|
|
84
137
|
};
|
|
85
138
|
|
|
86
139
|
records.set(normalized, record);
|
|
140
|
+
emit({
|
|
141
|
+
type: "module-defined",
|
|
142
|
+
id: normalized,
|
|
143
|
+
kind: record.kind,
|
|
144
|
+
version: record.version,
|
|
145
|
+
operation,
|
|
146
|
+
replaced: Boolean(existing),
|
|
147
|
+
module: toModuleInfo(record),
|
|
148
|
+
});
|
|
87
149
|
return normalized;
|
|
88
150
|
}
|
|
89
151
|
|
|
90
|
-
function update(id, source, updateOptions = {}) {
|
|
91
|
-
return define(id, source, updateOptions);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
152
|
function defineModule(id, namespace) {
|
|
95
153
|
assertActive();
|
|
96
154
|
const normalized = normalizeDefinedId(id);
|
|
97
155
|
const existing = records.get(normalized);
|
|
98
|
-
if (existing) invalidate(normalized, { dependents: true });
|
|
156
|
+
if (existing) invalidate(normalized, { dependents: true, reason: "defineModule" });
|
|
99
157
|
|
|
100
158
|
if ((typeof namespace !== "object" || namespace === null) && typeof namespace !== "function") {
|
|
101
159
|
throw new TypeError("defineModule(id, namespace) expects an object, function, or ES module namespace-like value.");
|
|
@@ -103,7 +161,7 @@ export function createRuntime(options = {}) {
|
|
|
103
161
|
|
|
104
162
|
hostNamespaces.set(normalized, namespace);
|
|
105
163
|
|
|
106
|
-
|
|
164
|
+
const record = {
|
|
107
165
|
id: normalized,
|
|
108
166
|
kind: "host",
|
|
109
167
|
namespace,
|
|
@@ -112,6 +170,16 @@ export function createRuntime(options = {}) {
|
|
|
112
170
|
dependencies: new Set(),
|
|
113
171
|
dependents: existing?.dependents ?? new Set(),
|
|
114
172
|
url: undefined,
|
|
173
|
+
};
|
|
174
|
+
records.set(normalized, record);
|
|
175
|
+
emit({
|
|
176
|
+
type: "module-defined",
|
|
177
|
+
id: normalized,
|
|
178
|
+
kind: record.kind,
|
|
179
|
+
version: record.version,
|
|
180
|
+
operation: "defineModule",
|
|
181
|
+
replaced: Boolean(existing),
|
|
182
|
+
module: toModuleInfo(record),
|
|
115
183
|
});
|
|
116
184
|
|
|
117
185
|
return normalized;
|
|
@@ -121,10 +189,10 @@ export function createRuntime(options = {}) {
|
|
|
121
189
|
assertActive();
|
|
122
190
|
const normalized = normalizeDefinedId(id);
|
|
123
191
|
const existing = records.get(normalized);
|
|
124
|
-
if (existing) invalidate(normalized, { dependents: true });
|
|
192
|
+
if (existing) invalidate(normalized, { dependents: true, reason: "defineUrl" });
|
|
125
193
|
|
|
126
194
|
const href = String(url);
|
|
127
|
-
|
|
195
|
+
const record = {
|
|
128
196
|
id: normalized,
|
|
129
197
|
kind: "url",
|
|
130
198
|
externalUrl: href,
|
|
@@ -134,6 +202,16 @@ export function createRuntime(options = {}) {
|
|
|
134
202
|
dependents: existing?.dependents ?? new Set(),
|
|
135
203
|
importPromise: undefined,
|
|
136
204
|
namespace: undefined,
|
|
205
|
+
};
|
|
206
|
+
records.set(normalized, record);
|
|
207
|
+
emit({
|
|
208
|
+
type: "module-defined",
|
|
209
|
+
id: normalized,
|
|
210
|
+
kind: record.kind,
|
|
211
|
+
version: record.version,
|
|
212
|
+
operation: "defineUrl",
|
|
213
|
+
replaced: Boolean(existing),
|
|
214
|
+
module: toModuleInfo(record),
|
|
137
215
|
});
|
|
138
216
|
|
|
139
217
|
return normalized;
|
|
@@ -155,23 +233,36 @@ export function createRuntime(options = {}) {
|
|
|
155
233
|
|
|
156
234
|
if (record.kind === "url") {
|
|
157
235
|
if (!record.importPromise) {
|
|
236
|
+
record.state = "evaluating";
|
|
237
|
+
emit({ type: "module-evaluating", id: record.id, kind: record.kind, version: record.version });
|
|
158
238
|
record.importPromise = import(record.externalUrl).then(namespace => {
|
|
159
239
|
record.namespace = namespace;
|
|
160
240
|
record.state = "ready";
|
|
241
|
+
emit({ type: "module-evaluated", id: record.id, kind: record.kind, version: record.version, module: toModuleInfo(record) });
|
|
161
242
|
return namespace;
|
|
243
|
+
}).catch(error => {
|
|
244
|
+
record.importPromise = undefined;
|
|
245
|
+
record.state = "error";
|
|
246
|
+
emitError(record, "evaluate", error);
|
|
247
|
+
throw error;
|
|
162
248
|
});
|
|
163
249
|
}
|
|
164
250
|
return record.importPromise;
|
|
165
251
|
}
|
|
166
252
|
|
|
167
253
|
if (!record.importPromise) {
|
|
254
|
+
record.state = "evaluating";
|
|
255
|
+
emit({ type: "module-evaluating", id: record.id, kind: record.kind, version: record.version });
|
|
168
256
|
record.importPromise = ensureSourceUrl(record, []).then(url => import(url)).then(namespace => {
|
|
169
257
|
record.namespace = namespace;
|
|
170
258
|
record.state = "ready";
|
|
259
|
+
emit({ type: "module-evaluated", id: record.id, kind: record.kind, version: record.version, module: toModuleInfo(record) });
|
|
171
260
|
return namespace;
|
|
172
261
|
}).catch(error => {
|
|
173
262
|
record.importPromise = undefined;
|
|
263
|
+
const alreadyReported = record.state === "error";
|
|
174
264
|
record.state = "error";
|
|
265
|
+
if (!alreadyReported) emitError(record, "evaluate", error);
|
|
175
266
|
throw error;
|
|
176
267
|
});
|
|
177
268
|
}
|
|
@@ -241,6 +332,7 @@ export function createRuntime(options = {}) {
|
|
|
241
332
|
assertActive();
|
|
242
333
|
const normalized = normalizeLookupId(id);
|
|
243
334
|
const includeDependents = invalidateOptions.dependents ?? true;
|
|
335
|
+
const reason = invalidateOptions.reason ?? "explicit";
|
|
244
336
|
const seen = new Set();
|
|
245
337
|
|
|
246
338
|
invalidateOne(normalized);
|
|
@@ -271,7 +363,71 @@ export function createRuntime(options = {}) {
|
|
|
271
363
|
record.namespace = undefined;
|
|
272
364
|
record.state = "defined";
|
|
273
365
|
}
|
|
366
|
+
|
|
367
|
+
emit({
|
|
368
|
+
type: "module-invalidated",
|
|
369
|
+
id: record.id,
|
|
370
|
+
kind: record.kind,
|
|
371
|
+
version: record.version,
|
|
372
|
+
reason,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function remove(id, removeOptions = {}) {
|
|
378
|
+
assertActive();
|
|
379
|
+
const normalized = normalizeLookupId(id);
|
|
380
|
+
const record = records.get(normalized);
|
|
381
|
+
if (!record) return false;
|
|
382
|
+
|
|
383
|
+
if (removeOptions.invalidateDependents !== false) {
|
|
384
|
+
for (const dependent of [...record.dependents]) {
|
|
385
|
+
invalidate(dependent, { dependents: true, reason: `dependency-removed:${normalized}` });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const dependency of record.dependencies) {
|
|
390
|
+
records.get(dependency)?.dependents.delete(normalized);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
revokeRecordUrl(record);
|
|
394
|
+
if (record.kind === "host") hostNamespaces.delete(normalized);
|
|
395
|
+
records.delete(normalized);
|
|
396
|
+
|
|
397
|
+
emit({
|
|
398
|
+
type: "module-removed",
|
|
399
|
+
id: normalized,
|
|
400
|
+
kind: record.kind,
|
|
401
|
+
version: record.version,
|
|
402
|
+
invalidateDependents: removeOptions.invalidateDependents !== false,
|
|
403
|
+
});
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function clear(clearOptions = {}) {
|
|
408
|
+
assertActive();
|
|
409
|
+
const preserveHostModules = clearOptions.preserveHostModules !== false;
|
|
410
|
+
const ids = [...records.values()]
|
|
411
|
+
.filter(record => !(preserveHostModules && record.kind === "host"))
|
|
412
|
+
.map(record => record.id);
|
|
413
|
+
|
|
414
|
+
for (const id of ids) remove(id, { invalidateDependents: false });
|
|
415
|
+
|
|
416
|
+
emit({
|
|
417
|
+
type: "runtime-cleared",
|
|
418
|
+
ids: [...ids],
|
|
419
|
+
preserveHostModules,
|
|
420
|
+
});
|
|
421
|
+
return ids;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function subscribe(listener) {
|
|
425
|
+
assertActive();
|
|
426
|
+
if (typeof listener !== "function") {
|
|
427
|
+
throw new TypeError("subscribe(listener) expects a function.");
|
|
274
428
|
}
|
|
429
|
+
subscribers.add(listener);
|
|
430
|
+
return () => subscribers.delete(listener);
|
|
275
431
|
}
|
|
276
432
|
|
|
277
433
|
function listModules() {
|
|
@@ -296,11 +452,13 @@ export function createRuntime(options = {}) {
|
|
|
296
452
|
function dispose() {
|
|
297
453
|
if (disposed) return;
|
|
298
454
|
disposed = true;
|
|
455
|
+
emit({ type: "runtime-disposed" });
|
|
299
456
|
|
|
300
457
|
for (const record of records.values()) revokeRecordUrl(record);
|
|
301
458
|
records.clear();
|
|
302
459
|
hostNamespaces.clear();
|
|
303
460
|
globalRegistry.delete(runtimeId);
|
|
461
|
+
subscribers.clear();
|
|
304
462
|
}
|
|
305
463
|
|
|
306
464
|
function resolvePublic(specifier, importer) {
|
|
@@ -323,7 +481,9 @@ export function createRuntime(options = {}) {
|
|
|
323
481
|
analysis = compiler.analyze(record.source, { id: record.id, format: record.format });
|
|
324
482
|
} catch (error) {
|
|
325
483
|
record.state = "error";
|
|
326
|
-
|
|
484
|
+
const wrapped = new ModuleCompileError(record.id, error);
|
|
485
|
+
emitError(record, "analyze", wrapped);
|
|
486
|
+
throw wrapped;
|
|
327
487
|
}
|
|
328
488
|
|
|
329
489
|
const resolvedImports = [];
|
|
@@ -394,11 +554,20 @@ export function createRuntime(options = {}) {
|
|
|
394
554
|
}
|
|
395
555
|
} catch (error) {
|
|
396
556
|
record.state = "error";
|
|
397
|
-
|
|
557
|
+
const wrapped = new ModuleCompileError(record.id, error);
|
|
558
|
+
emitError(record, "transform", wrapped);
|
|
559
|
+
throw wrapped;
|
|
398
560
|
}
|
|
399
561
|
|
|
400
562
|
record.url = moduleUrlBackend.create(appendSourceUrl(record.compiledCode, record.id));
|
|
401
563
|
record.state = "compiled";
|
|
564
|
+
emit({
|
|
565
|
+
type: "module-compiled",
|
|
566
|
+
id: record.id,
|
|
567
|
+
kind: record.kind,
|
|
568
|
+
version: record.version,
|
|
569
|
+
module: toModuleInfo(record),
|
|
570
|
+
});
|
|
402
571
|
return record.url;
|
|
403
572
|
}
|
|
404
573
|
|
|
@@ -472,6 +641,47 @@ export function createRuntime(options = {}) {
|
|
|
472
641
|
}
|
|
473
642
|
}
|
|
474
643
|
|
|
644
|
+
function emit(event) {
|
|
645
|
+
if (subscribers.size === 0) return;
|
|
646
|
+
const payload = {
|
|
647
|
+
runtimeId,
|
|
648
|
+
timestamp: Date.now(),
|
|
649
|
+
...event,
|
|
650
|
+
};
|
|
651
|
+
if (eventBufferDepth > 0) {
|
|
652
|
+
eventBuffer.push(payload);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
dispatchEvent(payload);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function flushEventBuffer() {
|
|
659
|
+
if (eventBuffer.length === 0) return;
|
|
660
|
+
const pending = eventBuffer.splice(0, eventBuffer.length);
|
|
661
|
+
for (const event of pending) dispatchEvent(event);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function dispatchEvent(payload) {
|
|
665
|
+
for (const listener of [...subscribers]) {
|
|
666
|
+
try {
|
|
667
|
+
listener(payload);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
globalThis.console?.error?.("solid-tag-runtime lifecycle subscriber error", error);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function emitError(record, phase, error) {
|
|
675
|
+
emit({
|
|
676
|
+
type: "module-error",
|
|
677
|
+
id: record?.id,
|
|
678
|
+
kind: record?.kind,
|
|
679
|
+
version: record?.version,
|
|
680
|
+
phase,
|
|
681
|
+
error,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
475
685
|
function toModuleInfo(record) {
|
|
476
686
|
return {
|
|
477
687
|
id: record.id,
|