solid-tag-runtime 0.0.6 → 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 +54 -3
- package/README.md +130 -3
- package/html.d.ts +30 -1
- package/index.d.ts +30 -0
- package/package.json +1 -1
- package/src/html.js +123 -1
- 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
|
|
|
@@ -980,3 +982,52 @@ Reason: applications can reparent an existing observed root without intervention
|
|
|
980
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.
|
|
981
983
|
|
|
982
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
|
|
@@ -334,9 +345,9 @@ The `root` may be a `Document`, normal `Element`, `DocumentFragment`, or `Shadow
|
|
|
334
345
|
|
|
335
346
|
### Addition-only observation
|
|
336
347
|
|
|
337
|
-
Observation remains addition-
|
|
348
|
+
Observation remains addition-oriented.
|
|
338
349
|
|
|
339
|
-
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.
|
|
340
351
|
|
|
341
352
|
The HTML adapter does **not** reinterpret arbitrary document HTML as JSX and does not currently assign component semantics to `<template>`.
|
|
342
353
|
|
|
@@ -510,6 +521,120 @@ Updating a module invalidates its compiled URL and its dependent runtime modules
|
|
|
510
521
|
|
|
511
522
|
Existing references to an older module namespace/component are not mutated. Applications that implement live editing should import the updated entry module again.
|
|
512
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
|
+
|
|
513
638
|
## Custom resolution
|
|
514
639
|
|
|
515
640
|
```ts
|
|
@@ -548,10 +673,12 @@ Set it to `false` when all module dependencies must be explicitly registered wit
|
|
|
548
673
|
|
|
549
674
|
```ts
|
|
550
675
|
runtime.invalidate("/App.jsx");
|
|
676
|
+
runtime.remove("/Unused.jsx");
|
|
677
|
+
runtime.clear();
|
|
551
678
|
runtime.dispose();
|
|
552
679
|
```
|
|
553
680
|
|
|
554
|
-
`
|
|
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.
|
|
555
682
|
|
|
556
683
|
## Current limitations
|
|
557
684
|
|
package/html.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export interface HTMLModuleScriptElement {
|
|
|
5
5
|
textContent?: string | null;
|
|
6
6
|
baseURI?: string;
|
|
7
7
|
ownerDocument?: HTMLDocumentLike | null;
|
|
8
|
-
parentNode?: unknown;
|
|
8
|
+
parentNode?: { removeChild?(element: any): unknown } | null;
|
|
9
|
+
remove?(): void;
|
|
9
10
|
getAttribute(name: string): string | null;
|
|
10
11
|
hasAttribute?(name: string): boolean;
|
|
11
12
|
setAttribute?(name: string, value: string): void;
|
|
@@ -76,6 +77,22 @@ export interface HTMLModuleDefinition {
|
|
|
76
77
|
element: HTMLModuleScriptElement;
|
|
77
78
|
}
|
|
78
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
|
+
|
|
79
96
|
export interface RegisterHTMLOptions extends DefineScriptOptions {
|
|
80
97
|
selector?: string;
|
|
81
98
|
executeEntries?: boolean;
|
|
@@ -181,6 +198,18 @@ export interface HTMLRuntimeController {
|
|
|
181
198
|
options?: RegisterHTMLOptions,
|
|
182
199
|
): Promise<HTMLModuleDefinition>;
|
|
183
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
|
+
|
|
184
213
|
/** Claim first, append a user-created element, then register it directly. */
|
|
185
214
|
append(
|
|
186
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
|
@@ -97,6 +97,8 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
97
97
|
setAppendTarget,
|
|
98
98
|
defineElement,
|
|
99
99
|
registerElement,
|
|
100
|
+
updateElement,
|
|
101
|
+
removeElement,
|
|
100
102
|
append,
|
|
101
103
|
addModule,
|
|
102
104
|
owns,
|
|
@@ -259,6 +261,97 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
259
261
|
return result.modules[0] ?? definitionFromOwnedElement(controller, element);
|
|
260
262
|
}
|
|
261
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
|
+
|
|
262
355
|
/**
|
|
263
356
|
* Claim a user-created script before DOM insertion, append it to this
|
|
264
357
|
* controller's target, then register it directly. The observer will see the
|
|
@@ -649,7 +742,7 @@ function inspectElement(controller, element, options = {}) {
|
|
|
649
742
|
sourceUrl,
|
|
650
743
|
});
|
|
651
744
|
|
|
652
|
-
const id = explicitId ?? sourceUrl ?? (entry ? createAnonymousId(format) : undefined);
|
|
745
|
+
const id = explicitId ?? sourceUrl ?? (entry ? options.existingId ?? createAnonymousId(format) : undefined);
|
|
653
746
|
if (!id) {
|
|
654
747
|
throw new HTMLModuleError(
|
|
655
748
|
"Inline solid runtime module scripts require a `module` attribute unless they are marked `entry`.",
|
|
@@ -660,6 +753,35 @@ function inspectElement(controller, element, options = {}) {
|
|
|
660
753
|
return { id, format, entry, sourceUrl };
|
|
661
754
|
}
|
|
662
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
|
+
|
|
663
785
|
function definitionFromRecord(record) {
|
|
664
786
|
return {
|
|
665
787
|
id: record.moduleId,
|
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,
|