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 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. inspect dependencies and compiled source
42
- 9. support normal JavaScript modules in the same graph
43
- 10. keep the JSX compiler replaceable behind a narrow adapter
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-only in `0.0.6`.
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. Use `runtime.update()` / `runtime.invalidate()` for explicit module lifecycle changes.
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
- `dispose()` revokes generated module URLs and clears the host-module registry for that runtime instance.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-tag-runtime",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Runtime module system for JSX modules compiled with solid-tag and executed through @solidjs/html",
5
5
  "type": "module",
6
6
  "exports": {
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
- records.set(normalized, {
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
- records.set(normalized, {
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
- throw new ModuleCompileError(record.id, error);
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
- throw new ModuleCompileError(record.id, error);
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,