solid-tag-runtime 0.0.11 → 0.0.13

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/README.md CHANGED
@@ -1,45 +1,34 @@
1
1
  # solid-tag-runtime
2
2
 
3
- `solid-tag-runtime` is a small runtime module system for applications that want to load, compose, and execute Solid JSX modules dynamically in the browser.
4
-
5
- It builds on [`solid-tag`](https://www.npmjs.com/package/solid-tag):
3
+ `solid-tag-runtime` is a runtime module system for dynamically defined Solid JSX and JavaScript modules.
6
4
 
7
5
  ```text
8
- JSX source modules
9
- ↓
10
- solid-tag
11
- ↓
12
- JavaScript containing html``
13
- ↓
14
- solid-tag-runtime module linker
15
- ↓
16
- @solidjs/html + the host application's Solid runtime
6
+ source module
7
+ ↓
8
+ solid-tag compiler
9
+ ↓
10
+ pre-link compiled artifact
11
+ ↓
12
+ solid-tag-runtime linker
13
+ ↓
14
+ @solidjs/html + host Solid runtime
17
15
  ```
18
16
 
19
- The package is intentionally runtime-oriented. It is not a build plugin and does not require a traditional application compilation workflow for the dynamic modules it executes.
17
+ The core package is module-first and DOM-independent. Browser discovery, ownership, declarative rendering, and `<solid-render>` live in the `solid-tag-runtime/html` adapter.
20
18
 
21
- ## Installation
19
+ ## Install
22
20
 
23
21
  ```bash
24
22
  npm install solid-tag-runtime solid-tag
25
23
  ```
26
24
 
27
- `solid-tag-runtime` does **not** install its own copy of Solid. A host application can expose its already-loaded Solid modules with `defineModule()` so dynamically loaded components share the same reactive runtime.
28
-
29
- ## Create a runtime
30
-
31
- ```ts
32
- import { createRuntime } from "solid-tag-runtime";
33
-
34
- const runtime = createRuntime();
35
- ```
36
-
37
- For a real Solid application, register the host runtime:
25
+ ## Quick start
38
26
 
39
27
  ```ts
40
28
  import * as Solid from "solid-js";
41
29
  import * as SolidWeb from "@solidjs/web";
42
30
  import html from "@solidjs/html";
31
+ import { createRuntime } from "solid-tag-runtime";
43
32
 
44
33
  const runtime = createRuntime({
45
34
  modules: {
@@ -48,121 +37,31 @@ const runtime = createRuntime({
48
37
  "@solidjs/html": { default: html },
49
38
  },
50
39
  });
51
- ```
52
40
 
53
- This is important: dynamic modules now use the **same** `solid-js`, `@solidjs/web`, and `@solidjs/html` instances as the host application.
54
-
55
- ## Define and import runtime modules
56
-
57
- ```ts
58
- runtime.define(
59
- "/ui/Button.jsx",
60
- `
61
- export function Button(props) {
62
- return (
63
- <button onClick={props.onClick}>
64
- {props.children}
65
- </button>
66
- );
67
- }
68
- `,
69
- );
70
- ```
71
-
72
- A second module can import the first one normally:
73
-
74
- ```ts
75
- runtime.define(
76
- "/features/Counter.jsx",
77
- `
78
- import { createSignal } from "solid-js";
79
- import { Button } from "../ui/Button.jsx";
80
-
81
- export function Counter() {
82
- const [count, setCount] = createSignal(0);
83
-
84
- return (
85
- <Button onClick={() => setCount(value => value + 1)}>
86
- Count: {count()}
87
- </Button>
88
- );
89
- }
90
- `,
91
- );
92
-
93
- const counterModule = await runtime.import("/features/Counter.jsx");
94
-
95
- counterModule.Counter;
96
- ```
97
-
98
- The runtime compiles and links the dependency graph automatically.
99
-
100
-
101
- ## Declarative HTML modules
102
-
103
- `solid-tag-runtime/html` is the browser/DOM adapter. The core runtime remains DOM-independent.
104
-
105
- 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.11",
111
- "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.11/html"
41
+ runtime.define("/Greeting.jsx", `
42
+ export default function Greeting() {
43
+ return <p>Hello from runtime JSX</p>;
112
44
  }
113
- }
114
- ```
115
-
116
- For simple single-runtime use, the original helpers remain available:
117
-
118
- ```ts
119
- import {
120
- registerHTML,
121
- observeHTML,
122
- } from "solid-tag-runtime/html";
45
+ `);
123
46
 
124
- await registerHTML(runtime);
47
+ const module = await runtime.import("/Greeting.jsx");
48
+ module.default;
125
49
  ```
126
50
 
127
- For applications that may have multiple runtimes or that create runtime scripts programmatically, `0.0.3` added a persistent HTML runtime controller:
51
+ ## Declarative browser modules
128
52
 
129
53
  ```ts
130
54
  import { createHTMLRuntime } from "solid-tag-runtime/html";
131
55
 
132
- const html = createHTMLRuntime(runtime, {
56
+ const htmlRuntime = createHTMLRuntime(runtime, {
133
57
  scope: "main",
134
58
  root: document,
135
59
  });
136
60
 
137
- await html.register();
138
- await html.observe({ registerExisting: false });
61
+ await htmlRuntime.register();
139
62
  ```
140
63
 
141
-
142
- ## Render runtime UI
143
-
144
- `solid-tag-runtime/html` can now mount component exports as an HTML-adapter concern while the core runtime remains DOM-independent.
145
-
146
- There are two complementary forms:
147
-
148
- ```text
149
- <script module>
150
- define a runtime module
151
-
152
- <script module render>
153
- define a module + mount one instance
154
-
155
- <solid-render module>
156
- reference an existing module + mount one instance
157
- ```
158
-
159
- ### Define and render in one declaration
160
-
161
- Render the default export into a normal Solid container:
162
-
163
64
  ```html
164
- <div id="app"></div>
165
-
166
65
  <script
167
66
  type="solid-jsx"
168
67
  data-solid-runtime="main"
@@ -170,810 +69,102 @@ Render the default export into a normal Solid container:
170
69
  render="#app"
171
70
  >
172
71
  export default function App() {
173
- return <h1>Hello from runtime JSX</h1>;
174
- }
175
- </script>
176
- ```
177
-
178
- `render` defaults to `module.default`. Select a named export with `component`:
179
-
180
- ```html
181
- <script
182
- type="solid-jsx"
183
- module="/widgets.jsx"
184
- render="#app"
185
- component="Counter"
186
- >
187
- export function Counter() {
188
- return <button>Counter</button>;
72
+ return <h1>Hello</h1>;
189
73
  }
190
74
  </script>
191
75
  ```
192
76
 
193
- Bare `render` mounts at the declaration's exact sibling position without adding a wrapper:
77
+ Bare `render` mounts in place without adding a wrapper:
194
78
 
195
79
  ```html
196
- <p>Before</p>
197
-
198
80
  <script type="solid-jsx" module="/Message.jsx" render>
199
81
  export default function Message() {
200
82
  return <strong>Hello</strong>;
201
83
  }
202
84
  </script>
203
-
204
- <p>After</p>
205
- ```
206
-
207
- The adapter replaces the declaration with an owned start/end marker range and inserts the component between the markers. The module definition and mount lifecycle remain separate: removing the declaration through `html.removeElement()` disposes the Solid owner while module removal follows the existing lifecycle options.
208
-
209
- `entry` and `render` are intentionally mutually exclusive on one declaration. Use separate declarations when a side-effect entry and a mounted component are both needed.
210
-
211
- All matching modules in one scan/observer batch are defined before any `entry` or `render` action executes, so a render declaration may import a dependency declared later in the same batch.
212
-
213
- ### Reuse an existing module with `<solid-render>`
214
-
215
- `<solid-render>` references an already-defined module and remains as the light-DOM mount container:
216
-
217
- ```html
218
- <solid-render
219
- data-solid-runtime="main"
220
- module="/Counter.jsx"
221
- ></solid-render>
222
- ```
223
-
224
- Named export:
225
-
226
- ```html
227
- <solid-render
228
- data-solid-runtime="main"
229
- module="/widgets.jsx"
230
- component="Counter"
231
- ></solid-render>
232
- ```
233
-
234
- Component props use an explicit namespace so renderer configuration can never collide with component prop names:
235
-
236
- ```html
237
- <solid-render
238
- module="/UserCard.jsx"
239
- prop:name="Alice"
240
- prop:module="billing"
241
- prop:compact
242
- ></solid-render>
243
- ```
244
-
245
- Declarative values are conservative:
246
-
247
- ```text
248
- prop:name="Alice" → "Alice"
249
- prop:compact → true
250
- prop:count="42" → "42"
251
- ```
252
-
253
- Kebab-case prop names normalize to camelCase (`prop:user-id` → `userId`). Arbitrary JavaScript references use the `.props` property:
254
-
255
- ```ts
256
- const renderer = document.querySelector("solid-render");
257
-
258
- renderer.props = {
259
- user,
260
- onSave,
261
- service,
262
- };
263
- ```
264
-
265
- Programmatic props override `prop:*` attributes. Prop changes update the existing component instance reactively and preserve local Solid state. Changes to `module`, `component`, or `data-solid-runtime` dispose the current component and mount a new identity.
266
-
267
- Initial light-DOM children become `props.children`:
268
-
269
- ```html
270
- <solid-render module="/Card.jsx" prop:title="Profile">
271
- <p>Hello from HTML.</p>
272
- <solid-render module="/SaveButton.jsx"></solid-render>
273
- </solid-render>
274
- ```
275
-
276
- The first implementation deliberately has no named-slot system and no Shadow DOM. Nested `<solid-render>` elements work through normal custom-element connection when captured children are instantiated.
277
-
278
- `createHTMLRuntime()` ensures the custom element is registered globally once after the controller has entered the shared runtime registry. `registerHTML()` / `observeHTML()` keep the same idempotent guarantee. Most applications therefore do not need to call a registration helper directly.
279
-
280
- For custom registries, tests, or explicit registration, the helper remains available:
281
-
282
- ```ts
283
- import { registerSolidRenderElement } from "solid-tag-runtime/html";
284
-
285
- registerSolidRenderElement();
286
- ```
287
-
288
- Multiple `<solid-render>` elements may share one cached runtime module namespace while each mounted component owns independent Solid state. Async module changes are generation-guarded so a stale import can never replace a newer `module`/`component`/runtime selection.
289
-
290
- `<solid-render>` uses the same runtime-routing rules as declarative scripts. An explicit `data-solid-runtime` selects a controller with that scope **whose root contains the element**. Without an explicit scope, any containing controller that accepts unscoped declarations (`acceptUnscoped: true`) may handle the element. If more than one equally specific controller can handle it, add `data-solid-runtime` to disambiguate. Controllers outside the element's DOM root are never selected as a fallback.
291
-
292
- Both of these reference the same registered virtual module when used with the correct runtime:
293
-
294
- ```html
295
- <solid-render module="/ui/Button.jsx"></solid-render>
296
- <solid-render module="./ui/Button.jsx"></solid-render>
297
85
  ```
298
86
 
299
- Top-level relative module references are normalized as virtual paths; they are not browser URL fetches.
300
-
301
- A scope is written to owned script elements as:
87
+ ## Reusable `<solid-render>` instances
302
88
 
303
89
  ```html
304
- <script data-solid-runtime="main" ...></script>
305
- ```
306
-
307
- When multiple runtimes observe the same document, give each one a unique scope and normally set `acceptUnscoped: false`.
308
-
309
- A scoped declaration that only defines a module may be ignored by non-matching controllers without noise. A scoped declaration with active `render` semantics is different: rendering requests immediate UI work. If no registered HTML runtime with that scope can handle the declaration's DOM root, the adapter emits one deduplicated console warning and an `html-warning` lifecycle event with `code: "unresolved-runtime-scope"`. `executeRenders: false` suppresses this diagnostic because rendering was explicitly disabled.
310
-
311
- ```ts
312
- html.subscribe("html-warning", event => {
313
- if (event.code === "unresolved-runtime-scope") {
314
- console.warn(event.requestedScope, event.moduleId);
315
- }
316
- });
317
- ```
318
-
319
- `<solid-render>` keeps stronger semantics: when it actively connects and cannot resolve its selected runtime, that is a render error rather than only a warning.
320
-
321
- ### Declarative modules
322
-
323
- ```html
324
- <script
325
- type="solid-jsx"
326
- data-solid-runtime="main"
327
- module="/ui/Button.jsx">
328
- export function Button(props) {
329
- return (
330
- <button onClick={props.onClick}>
331
- {props.children}
332
- </button>
333
- );
90
+ <script type="solid-jsx" module="/Counter.jsx">
91
+ export default function Counter() {
92
+ return <button>Counter</button>;
334
93
  }
335
94
  </script>
336
- ```
337
-
338
- Runtime modules import one another normally:
339
-
340
- ```tsx
341
- import { Button } from "./ui/Button.jsx";
342
- ```
343
-
344
- All newly discovered scripts in one batch are defined before any newly discovered `entry` module executes, so HTML document order does not require dependencies to appear first.
345
-
346
- ### External source
347
-
348
- ```html
349
- <script
350
- type="solid-jsx"
351
- data-solid-runtime="main"
352
- module="/ui/Button.jsx"
353
- src="./components/Button.jsx">
354
- </script>
355
- ```
356
-
357
- `src` answers **where source is loaded from**. `module` answers **what identity the source has in the runtime graph**.
358
-
359
- ### User-created script elements
360
-
361
- If application code creates a script itself, use the controller to associate it with the correct runtime:
362
95
 
363
- ```ts
364
- const script = document.createElement("script");
365
- script.type = "solid-jsx";
366
- script.setAttribute("module", "/dynamic/Greeting.jsx");
367
- script.textContent = `
368
- export function Greeting(props) {
369
- return <p>Hello {props.name}</p>;
370
- }
371
- `;
372
-
373
- await html.append(script);
96
+ <solid-render module="/Counter.jsx"></solid-render>
97
+ <solid-render module="/Counter.jsx"></solid-render>
374
98
  ```
375
99
 
376
- `append()` claims the element **before** inserting it in the DOM, stamps the controller scope, then registers it directly. If a `MutationObserver` is active, the later mutation callback sees that the element is already owned and does not compile it twice.
377
-
378
- For an element that is already in the DOM:
379
-
380
- ```ts
381
- await html.registerElement(script);
382
- ```
100
+ **In HTML, always use the explicit closing tag.** Do not write `<solid-render ... />`; custom elements are not HTML void elements and the self-closing slash is ignored by the HTML parser.
383
101
 
384
- Same-controller repeated registration is idempotent. Trying to register an element already owned by another HTML controller throws `HTMLModuleOwnershipError`.
102
+ ## Persistent compile cache
385
103
 
386
- ### Let the controller create the script
104
+ `0.0.13` adds an opt-in persistent cache for **pre-link compiler artifacts**. Runtime-specific linked URLs and evaluated module namespaces are never persisted.
387
105
 
388
106
  ```ts
389
- await html.addModule({
390
- id: "/dynamic/Badge.jsx",
391
- source: `
392
- export function Badge(props) {
393
- return <span>{props.children}</span>;
394
- }
395
- `,
396
- });
397
- ```
398
-
399
- `addModule()` creates the `<script>` element, associates it with the controller, appends it, and registers it.
400
-
401
- ### Observe scripts added by other code
107
+ import {
108
+ createRuntime,
109
+ createIndexedDBCompileCache,
110
+ } from "solid-tag-runtime";
402
111
 
403
- ```ts
404
- await html.observe({
405
- registerExisting: false,
406
- onError(error) {
407
- console.error(error);
112
+ const runtime = createRuntime({
113
+ compileCache: {
114
+ store: createIndexedDBCompileCache({
115
+ database: "my-app-runtime",
116
+ }),
117
+ namespace: "main",
118
+ version: "1",
408
119
  },
409
120
  });
410
-
411
- // Some unrelated code mutates the DOM directly.
412
- const script = document.createElement("script");
413
- script.type = "solid-jsx";
414
- script.setAttribute("data-solid-runtime", "main");
415
- script.setAttribute("module", "/observed/Thing.jsx");
416
- script.textContent = `export const value = 42;`;
417
- document.body.append(script);
418
-
419
- await html.flush();
420
- const module = await runtime.import("/observed/Thing.jsx");
421
- ```
422
-
423
- The observer is for DOM changes that happen **outside** the controller API. Prefer `html.append()` / `html.addModule()` when your code controls insertion because those operations are deterministic and do not depend on observer timing.
424
-
425
- ### Ownership and deduplication
426
-
427
- All HTML controllers share an internal `WeakMap` of script element → ownership/registration record.
428
-
429
- That controller model gives these guarantees:
430
-
431
- - one script element has at most one HTML-runtime owner;
432
- - manual registration and observer registration cannot compile the same element twice;
433
- - the same controller can register the same element repeatedly without redefining it;
434
- - two distinct elements cannot silently define the same HTML-owned module ID;
435
- - an HTML script cannot silently replace a module already defined outside that HTML controller;
436
- - `data-solid-runtime` is declarative metadata while the in-memory owner record is authoritative.
437
-
438
- The controller also exposes lightweight introspection:
439
-
440
- ```ts
441
- html.owns(script);
442
- html.getModuleId(script);
443
- html.getElement("/dynamic/Greeting.jsx");
444
- ```
445
-
446
- ### Script formats
447
-
448
- ```text
449
- solid-jsx / text/solid-jsx → JSX transformation
450
- solid-js / text/solid-js → plain JavaScript module
451
- solid-module → infer from module/src extension
452
- ```
453
-
454
- `language="js"` or `language="jsx"` may override inference.
455
-
456
-
457
- ### Change the observed root
458
-
459
- The HTML controller can move to a different discovery root without creating a new runtime:
460
-
461
- ```ts
462
- const first = document.querySelector("#first-runtime")!;
463
- const second = document.querySelector("#second-runtime")!;
464
-
465
- const html = createHTMLRuntime(runtime, {
466
- root: first,
467
- appendTo: first,
468
- });
469
-
470
- await html.observe({ registerExisting: false });
471
-
472
- // Rebind a live observer to a different node. Because the observer is already
473
- // connected, matching scripts already in the new root are registered by default.
474
- await html.setRoot(second);
475
121
  ```
476
122
 
477
- If the controller is disconnected, `setRoot()` only changes configuration unless `registerExisting: true` is passed:
123
+ The cache key is content-based and includes the source hash, compiler fingerprint, artifact ABI, format, and compile-affecting context. Dependency versions and resolver state are deliberately excluded, so unchanged importers can reuse compiler work and relink against changed dependencies.
478
124
 
479
125
  ```ts
480
- await html.setRoot(second, {
481
- registerExisting: true,
482
- });
483
- ```
484
-
485
- Use `registerExisting: false` when switching a live observer but intentionally ignoring scripts already present in the new root.
486
-
487
- 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.
126
+ await runtime.compile("/App.jsx", { cache: "use" });
127
+ await runtime.compile("/App.jsx", { cache: "refresh" });
128
+ await runtime.compile("/App.jsx", { cache: "bypass" });
488
129
 
489
- ### Move root and append target together
490
-
491
- When the runtime module area itself moves to another container, `moveTo()` changes both boundaries:
492
-
493
- ```ts
494
- await html.moveTo(nextContainer, {
495
- registerExisting: true,
496
- });
130
+ await runtime.compileCache.invalidate("/App.jsx");
131
+ await runtime.compileCache.clear();
497
132
  ```
498
133
 
499
- 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.
134
+ ## Documentation
500
135
 
501
- ### Change only the append target
136
+ The detailed documentation now lives under [`docs/`](./docs/README.md):
502
137
 
503
- Observation and insertion can have different boundaries:
138
+ - [Getting started](./docs/getting-started.md)
139
+ - [Runtime modules and resolution](./docs/modules.md)
140
+ - [HTML runtime and ownership](./docs/html-runtime.md)
141
+ - [Declarative rendering](./docs/rendering.md)
142
+ - [`<solid-render>`](./docs/solid-render.md)
143
+ - [Lifecycle events](./docs/lifecycle-events.md)
144
+ - [Persistent compile cache](./docs/compile-cache.md)
145
+ - [Core runtime API](./docs/api/runtime.md)
146
+ - [HTML runtime API](./docs/api/html.md)
147
+ - [Compile-cache API](./docs/api/compile-cache.md)
504
148
 
505
- ```ts
506
- html.setAppendTarget(document.head);
507
- ```
149
+ For engineering invariants and design decisions, see [`ARCHITECTURE.md`](./ARCHITECTURE.md).
508
150
 
509
- This does not change the currently observed root. The controller exposes the active values for diagnostics:
151
+ ## Browser import maps
510
152
 
511
- ```ts
512
- html.root;
513
- html.appendTarget;
514
- ```
515
-
516
- 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.
517
-
518
- ### Addition-only observation
519
-
520
- Observation remains addition-oriented.
521
-
522
- 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.
153
+ When loading from an import map, map both entries to the same version:
523
154
 
524
- The HTML adapter does **not** reinterpret arbitrary document HTML as JSX and does not currently assign component semantics to `<template>`.
525
-
526
- ## `toModule()`
527
-
528
- For one-off source strings:
529
-
530
- ```ts
531
- const module = await runtime.toModule(`
532
- export function MyComponent(props) {
533
- return <>{props.message} {props.name}</>;
534
- }
535
- `);
536
-
537
- const MyComponent = module.MyComponent;
538
- ```
539
-
540
- `toModule()` creates an anonymous runtime module, compiles it with `solid-tag`, evaluates it as a real ES module, and returns the module namespace.
541
-
542
- It is asynchronous because the runtime preserves ES-module-style evaluation rather than using `new Function()`.
543
-
544
- ## `toComponent()`
545
-
546
- For the common single-component case:
547
-
548
- ```ts
549
- const MyComponent = await runtime.toComponent(
550
- `
551
- export function MyComponent(props) {
552
- return <div>{props.message}</div>;
553
- }
554
- `,
555
- { exportName: "MyComponent" },
556
- );
557
- ```
558
-
559
- For a default export:
560
-
561
- ```ts
562
- const Counter = await runtime.toComponent(`
563
- export default function Counter() {
564
- return <button>Count</button>;
155
+ ```json
156
+ {
157
+ "imports": {
158
+ "solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.13",
159
+ "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.13/html"
565
160
  }
566
- `);
567
- ```
568
-
569
- ## Inject application values as modules
570
-
571
- The runtime deliberately does not rewrite unresolved identifiers into a magical scope object.
572
-
573
- Instead, application scope can be expressed as a normal module:
574
-
575
- ```ts
576
- const [count, setCount] = createSignal(0);
577
-
578
- runtime.defineModule("@app/state", {
579
- count,
580
- setCount,
581
- });
582
- ```
583
-
584
- Runtime source imports those references normally:
585
-
586
- ```tsx
587
- import { count, setCount } from "@app/state";
588
-
589
- export function Counter() {
590
- return (
591
- <button onClick={() => setCount(value => value + 1)}>
592
- {count()}
593
- </button>
594
- );
595
161
  }
596
162
  ```
597
163
 
598
- This works for:
599
-
600
- - Solid signals and setters
601
- - stores
602
- - components
603
- - callbacks
604
- - services
605
- - data objects
606
- - application utilities
607
-
608
- The values cross the boundary as actual JavaScript references; they are not serialized.
609
-
610
- ## Expose existing components
611
-
612
- ```ts
613
- runtime.defineModule("@app/components", {
614
- Button,
615
- Dialog,
616
- Card,
617
- });
618
- ```
619
-
620
- Then runtime JSX can use them like any other module:
621
-
622
- ```tsx
623
- import { Button } from "@app/components";
624
-
625
- export function SaveButton() {
626
- return <Button>Save</Button>;
627
- }
628
- ```
629
-
630
- ## URL modules
631
-
632
- A package or external library can be mapped to a native ESM URL:
633
-
634
- ```ts
635
- runtime.defineUrl(
636
- "some-library",
637
- "https://esm.sh/some-library@1.2.3",
638
- );
639
- ```
640
-
641
- Then dynamic modules can write:
642
-
643
- ```js
644
- import something from "some-library";
645
- ```
646
-
647
- ## Plain JavaScript modules
648
-
649
- JSX transformation can be disabled per module:
650
-
651
- ```ts
652
- runtime.define(
653
- "/config.js",
654
- `export const value = 42;`,
655
- { format: "js" },
656
- );
657
- ```
658
-
659
- This is useful when the same runtime graph contains both JSX-backed modules and ordinary JavaScript modules.
660
-
661
- ## Compilation without execution
662
-
663
- ```ts
664
- runtime.define("/App.jsx", source);
665
-
666
- const compiled = await runtime.compile("/App.jsx");
667
-
668
- console.log(compiled.code);
669
- console.log(compiled.dependencies);
670
- console.log(compiled.diagnostics);
671
- ```
672
-
673
- This is useful for editors, debugging tools, and inspecting the exact `html\`\`` output generated by `solid-tag`.
674
-
675
- ## Module graph introspection
676
-
677
- ```ts
678
- runtime.modules();
679
- runtime.dependencies("/App.jsx");
680
- runtime.dependents("/ui/Button.jsx");
681
- runtime.getModuleInfo("/App.jsx");
682
- ```
683
-
684
- ## Updating modules
685
-
686
- ```ts
687
- runtime.update("/ui/Button.jsx", newButtonSource);
688
-
689
- const app = await runtime.import("/App.jsx");
690
- ```
691
-
692
- Updating a module invalidates its compiled URL and its dependent runtime modules. Re-importing produces a newly linked graph.
693
-
694
- Existing references to an older module namespace/component are not mutated. Applications that implement live editing should import the updated entry module again.
695
-
696
- ### Define several source modules together
697
-
698
- ```ts
699
- runtime.defineMany([
700
- {
701
- id: "/ui/Button.jsx",
702
- source: buttonSource,
703
- },
704
- {
705
- id: "/App.jsx",
706
- source: appSource,
707
- },
708
- ]);
709
- ```
710
-
711
- `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.
712
-
713
- ### Remove modules
714
-
715
- ```ts
716
- runtime.remove("/ui/Button.jsx");
717
- ```
718
-
719
- Removal invalidates transitive dependents by default. A later import of a dependent will fail resolution until the missing module is defined again.
720
-
721
- If you intentionally want to leave currently linked dependents untouched:
722
-
723
- ```ts
724
- runtime.remove("/ui/Button.jsx", {
725
- invalidateDependents: false,
726
- });
727
- ```
728
-
729
- ### Clear a runtime
730
-
731
- ```ts
732
- runtime.clear();
733
- ```
734
-
735
- 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.
736
-
737
- ```ts
738
- runtime.clear({
739
- preserveHostModules: false,
740
- });
741
- ```
742
-
743
- removes everything. The returned array contains the IDs that were removed.
744
-
745
- ### Lifecycle events
746
-
747
- `0.0.8` expands lifecycle subscriptions into a typed event stream with independent join/leave semantics.
748
-
749
- Subscribe to everything:
750
-
751
- ```ts
752
- const unsubscribe = runtime.subscribe(event => {
753
- console.log(event.type, event);
754
- });
755
- ```
756
-
757
- Subscribe to one event type:
758
-
759
- ```ts
760
- const leaveErrors = runtime.subscribe(
761
- "module-error",
762
- event => {
763
- console.error(event.phase, event.error);
764
- },
765
- );
766
- ```
767
-
768
- Subscribe to several event types:
769
-
770
- ```ts
771
- const leaveEvaluation = runtime.subscribe(
772
- ["module-evaluating", "module-evaluated"],
773
- event => {
774
- console.log(event.type, event.id);
775
- },
776
- );
777
- ```
778
-
779
- Each call owns an independent subscription:
780
-
781
- ```ts
782
- leaveEvaluation();
783
- // the error subscription remains active
784
- ```
785
-
786
- Core events include:
787
-
788
- ```text
789
- module-defined
790
- module-updated
791
- modules-defined
792
-
793
- module-resolving
794
- module-resolved
795
-
796
- module-linking
797
- module-linked
798
-
799
- module-invalidated
800
- module-compiled
801
- module-evaluating
802
- module-evaluated
803
-
804
- module-removed
805
- module-error
806
-
807
- runtime-cleared
808
- runtime-disposed
809
- ```
810
-
811
- `module-resolved` reports how a specifier was resolved (`relative`, `registered`, `custom`, `native`, and so on). `module-linked` exposes the logical dependency mapping used to link a source module. `modules-defined` marks the completion of a `defineMany()` batch after all definitions have been installed.
812
-
813
- `module-error.phase` is one of the runtime pipeline phases such as `resolve`, `analyze`, `compile`, `link`, `url-create`, `evaluate`, or `host-bridge`.
814
-
815
- Subscribers are observational only: they cannot cancel or modify runtime operations, and subscriber exceptions are isolated from runtime execution. Behavioral extension remains separate through the resolver, compiler adapter, and module URL backend.
816
-
817
- ### HTML lifecycle events
818
-
819
- The HTML controller has its own event stream because DOM ownership/observation is intentionally separate from the core module engine:
820
-
821
- ```ts
822
- const leaveHTML = html.subscribe(event => {
823
- console.log(event.type, event);
824
- });
825
- ```
826
-
827
- The same selective forms are supported:
828
-
829
- ```ts
830
- const leaveOwnership = html.subscribe(
831
- ["element-claimed", "element-registered", "element-removed"],
832
- event => {
833
- console.log(event.type, event.moduleId);
834
- },
835
- );
836
- ```
837
-
838
- HTML events include:
839
-
840
- ```text
841
- element-discovered
842
- element-claimed
843
- element-loading
844
- element-loaded
845
- element-registered
846
-
847
- element-updating
848
- element-updated
849
- element-removing
850
- element-released
851
- element-removed
852
-
853
- observer-connected
854
- observer-disconnected
855
- observer-batch
856
-
857
- root-changing
858
- root-changed
859
- append-target-changed
860
-
861
- entry-executing
862
- entry-executed
863
-
864
- render-mounting
865
- render-mounted
866
- render-disposing
867
- render-disposed
868
-
869
- html-warning
870
- html-error
871
- ```
872
-
873
- Ownership-related events include an `origin` describing how the element entered the controller (`observer`, `append`, `register-element`, `add-module`, and related internal scan origins). This is useful for tracing observer/manual-registration races and proving that an explicit `append()` was not processed a second time by the observer.
874
-
875
- `html-warning` currently reports non-fatal adapter diagnostics. `unresolved-runtime-scope` is emitted once per render declaration/scope when `data-solid-runtime` requests immediate rendering but no registered controller can handle that scoped declaration. The event includes `requestedScope`, `registeredScopes`, `moduleId` when available, and whether a matching scope exists outside the declaration's root.
876
-
877
- `observer-batch` summarizes a DOM discovery pass instead of exposing noisy raw `MutationRecord` objects. `element-loading` / `element-loaded` are emitted for `src`-backed modules, and entry events distinguish evaluation caused by an HTML `entry` declaration from an ordinary `runtime.import()`. Render events cover both `<script render>` and `<solid-render>`; `source` identifies `script-render` versus `solid-render`, and `mode` identifies selector, in-place, or container mounting.
878
-
879
- Like core runtime subscribers, HTML subscribers are observational and exception-isolated.
880
-
881
- ## HTML-owned module updates and removal
882
-
883
- When a script element is already owned by an HTML runtime controller, update the runtime module from the current element contents with:
884
-
885
- ```ts
886
- script.textContent = `
887
- export function Card() {
888
- return <div>updated</div>;
889
- }
890
- `;
891
-
892
- await html.updateElement(script);
893
- ```
894
-
895
- `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.
896
-
897
- Release an owned element explicitly with:
898
-
899
- ```ts
900
- await html.removeElement(script);
901
- ```
902
-
903
- By default this:
904
-
905
- 1. removes the runtime module,
906
- 2. invalidates its dependents,
907
- 3. releases HTML ownership, and
908
- 4. removes the script element from the DOM.
909
-
910
- The two lifecycles can be controlled independently:
911
-
912
- ```ts
913
- await html.removeElement(script, {
914
- removeModule: false,
915
- removeFromDOM: true,
916
- });
917
- ```
918
-
919
- This is intentionally explicit: ordinary DOM removal does not silently delete a runtime module.
920
-
921
- ## Custom resolution
922
-
923
- ```ts
924
- const runtime = createRuntime({
925
- resolve(specifier, importer) {
926
- if (specifier.startsWith("@ui/")) {
927
- return `/ui/${specifier.slice(4)}`;
928
- }
929
- },
930
- });
931
- ```
932
-
933
- Default runtime resolution supports:
934
-
935
- - exact virtual module IDs
936
- - relative virtual imports
937
- - absolute virtual paths
938
- - registered URL modules
939
- - native/bare imports when `allowNativeImports` is enabled
940
-
941
- ## Native imports
942
-
943
- `allowNativeImports` defaults to `true`.
944
-
945
- This means an unresolved **bare** import can be left for the browser's native module resolver/import map:
946
-
947
- ```ts
948
- const runtime = createRuntime({
949
- allowNativeImports: true,
950
- });
951
- ```
952
-
953
- Relative and absolute virtual paths are different. If `/ui/Button.jsx` or `./ui/Button.jsx` does not resolve to a registered runtime module, `runtime.import()` throws `ModuleResolutionError`; it does not turn the path into a browser/native network import. Use `defineUrl()` or an actual absolute URL when native URL loading is intended.
954
-
955
- Set `allowNativeImports` to `false` when even unresolved bare imports must be explicitly registered with the runtime.
956
-
957
- ## Runtime lifecycle
958
-
959
- ```ts
960
- runtime.invalidate("/App.jsx");
961
- runtime.remove("/Unused.jsx");
962
- runtime.clear();
963
- runtime.dispose();
964
- ```
965
-
966
- `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.
967
-
968
164
  ## Current limitations
969
165
 
970
- The first version deliberately keeps module semantics constrained:
971
-
972
- - circular dependencies between runtime-defined source modules are not supported yet
973
- - generated modules normally use Blob URLs in browsers, so CSP must permit the chosen execution strategy
974
- - host-module export bridges expose the export values present when the bridge module is created; redefining the host module invalidates dependents
975
- - module updates create new module identities; already-held namespaces/components remain the old version
976
- - TypeScript/TSX parsing depends on what the underlying `solid-tag` compiler supports
977
- - this package is **not a security sandbox**; loaded source executes with the privileges of the page
978
-
979
- See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the detailed design and decision log.
166
+ - runtime-defined source-module cycles are not supported yet
167
+ - generated browser modules require a compatible CSP for the selected module URL backend
168
+ - persistent compile caching is an optimization, not a security sandbox
169
+ - already-held namespace/component references are not mutated by module updates
170
+ - TypeScript/TSX support depends on the configured compiler