solid-tag-runtime 0.0.1 → 0.0.2

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
@@ -42,7 +42,7 @@ The runtime should allow applications to:
42
42
  9. support normal JavaScript modules in the same graph
43
43
  10. keep the JSX compiler replaceable behind a narrow adapter
44
44
 
45
- ## 3. Non-goals for the first version
45
+ ## 3. Current non-goals
46
46
 
47
47
  The initial package does not attempt to provide:
48
48
 
@@ -54,9 +54,8 @@ The initial package does not attempt to provide:
54
54
  - circular runtime source-module support
55
55
  - transparent live replacement of already-held component references
56
56
  - full HMR propagation semantics
57
- - script-tag discovery/registration
58
57
 
59
- These may be added later without changing the module-first public model.
58
+ These may be added later without changing the module-first public model. HTML script discovery was added in `0.0.2` as a separate browser adapter rather than as a responsibility of the core module engine.
60
59
 
61
60
  ## 4. Package relationship
62
61
 
@@ -193,7 +192,119 @@ No JSX transformation. Imports are still resolved and rewritten through the runt
193
192
 
194
193
  This permits ordinary JavaScript and JSX modules to coexist in one graph.
195
194
 
196
- ## 8. Compiler adapter boundary
195
+
196
+ ## 8. HTML/browser adapter
197
+
198
+ `0.0.2` adds `solid-tag-runtime/html` as an opt-in browser-facing adapter.
199
+
200
+ Important architectural boundary:
201
+
202
+ ```text
203
+ DOM / HTML document
204
+ ↓
205
+ solid-tag-runtime/html
206
+ ↓
207
+ runtime.define(...)
208
+ ↓
209
+ core module graph
210
+ ```
211
+
212
+ The core runtime remains DOM-independent. It does not scan documents, fetch `<script>` elements, or know about HTML attributes.
213
+
214
+ ### Public HTML API
215
+
216
+ ```ts
217
+ await registerHTML(runtime, options?);
218
+ await defineScript(runtime, element, options?);
219
+ const observer = await observeHTML(runtime, options?);
220
+ ```
221
+
222
+ `registerHTML()` discovers matching script elements, defines the complete discovered graph, and only then executes modules marked `entry` unless `executeEntries: false` is supplied.
223
+
224
+ `observeHTML()` is the live-discovery adapter. It uses `MutationObserver` to discover scripts added after registration. Existing scripts are registered by default; `registerExisting: false` supports the pattern where `registerHTML()` already performed the initial scan.
225
+
226
+ Every observer scan preserves the same graph invariant as `registerHTML()`: all newly discovered modules in that batch are defined before any newly discovered entry is executed. The observer tracks element identity so an already-seen element is not repeatedly redefined on unrelated DOM mutations.
227
+
228
+ The returned controller exposes:
229
+
230
+ ```ts
231
+ observer.initial;
232
+ observer.connected;
233
+ await observer.flush();
234
+ observer.disconnect();
235
+ ```
236
+
237
+ `flush()` waits for queued observer work and performs another scan, making it useful to synchronize application code with asynchronous `src` fetching and module evaluation. Background observer failures may be handled with `onError`.
238
+
239
+ Observation in `0.0.2` is intentionally addition-only: changes to attributes/text of an already registered script and removal of that script do not implicitly update or remove its runtime module. Those semantics require explicit runtime updates today and may become a separate future feature.
240
+
241
+ This ordering is an invariant: document order must not require dependencies to appear before entries.
242
+
243
+ ### Supported script declarations
244
+
245
+ ```html
246
+ <script type="solid-jsx" module="/App.jsx">...</script>
247
+ <script type="solid-js" module="/utils.js">...</script>
248
+ <script type="solid-module" module="/App.jsx">...</script>
249
+ ```
250
+
251
+ `text/solid-jsx`, `text/solid-js`, and `text/solid-module` aliases are also accepted.
252
+
253
+ Format rules:
254
+
255
+ - `solid-jsx` → `jsx`
256
+ - `solid-js` → `js`
257
+ - `solid-module` → infer from `module`/`src` extension
258
+ - `language="js"` or `language="jsx"` overrides inference
259
+
260
+ ### `src` versus `module`
261
+
262
+ These attributes deliberately have different roles:
263
+
264
+ ```text
265
+ src = source provider / fetch location
266
+ module = identity in the runtime module graph
267
+ ```
268
+
269
+ Example:
270
+
271
+ ```html
272
+ <script
273
+ type="solid-jsx"
274
+ src="./source/Button.jsx"
275
+ module="/ui/Button.jsx">
276
+ </script>
277
+ ```
278
+
279
+ The source may move without changing imports of `/ui/Button.jsx`.
280
+
281
+ If a `src` script omits `module`, its resolved source URL becomes the module ID. This allows multiple URL-backed source scripts to use normal relative imports.
282
+
283
+ Inline non-entry scripts require an explicit `module` identity. Inline entries may omit it and receive an anonymous runtime ID.
284
+
285
+ ### Entry execution
286
+
287
+ `entry` is an adapter concern, not a new runtime module kind.
288
+
289
+ ```html
290
+ <script type="solid-jsx" module="/App.jsx" entry>...</script>
291
+ ```
292
+
293
+ After all discovered modules are defined, the adapter performs:
294
+
295
+ ```ts
296
+ await runtime.import("/App.jsx");
297
+ ```
298
+
299
+ The entry module is expected to perform whatever side effect starts the application, such as calling `render()`.
300
+
301
+ ### Explicitly not HTML-as-JSX
302
+
303
+ The adapter does not transform arbitrary document HTML into JSX and does not currently assign semantics to `<template>` elements. JSX remains inside JavaScript module source.
304
+
305
+ This avoids introducing a second template language, implicit lexical scope rules, or ambiguous ownership of already-parsed DOM.
306
+
307
+ ## 9. Compiler adapter boundary
197
308
 
198
309
  The runtime engine itself does not depend on Acorn or the `solid-tag` AST.
199
310
 
@@ -233,7 +344,7 @@ Possible future adapters include:
233
344
 
234
345
  The module graph must remain independent of parser details.
235
346
 
236
- ## 9. Linking pipeline
347
+ ## 10. Linking pipeline
237
348
 
238
349
  For a source module `/features/Counter.jsx`:
239
350
 
@@ -266,7 +377,7 @@ Example graph:
266
377
  └── ../ui/Button.jsx → compiled virtual source URL
267
378
  ```
268
379
 
269
- ## 10. Resolution rules
380
+ ## 11. Resolution rules
270
381
 
271
382
  Resolution order is conceptually:
272
383
 
@@ -282,7 +393,7 @@ Relative virtual resolution uses POSIX-style paths regardless of the host operat
282
393
 
283
394
  This keeps module IDs stable in browsers and on Windows hosts.
284
395
 
285
- ## 11. Native ESM execution backend
396
+ ## 12. Native ESM execution backend
286
397
 
287
398
  The first execution backend deliberately relies on the JavaScript engine's native ESM evaluator rather than `new Function()`.
288
399
 
@@ -320,9 +431,9 @@ Benefits:
320
431
  - no `eval` / `new Function()` runtime wrapper
321
432
  - clean `toModule()` semantics
322
433
 
323
- ## 12. Circular dependency limitation
434
+ ## 13. Circular dependency limitation
324
435
 
325
- Virtual source module cycles are explicitly rejected in v0.0.1.
436
+ Virtual source module cycles are explicitly rejected through v0.0.2.
326
437
 
327
438
  Reason: the current linker must know the final URL of dependencies before it can create a module's immutable Blob/data URL. A cycle requires module identities to exist before final source URLs can be constructed.
328
439
 
@@ -341,7 +452,7 @@ Future options:
341
452
 
342
453
  Do not add ad-hoc cycle handling that breaks module semantics; change the backend deliberately.
343
454
 
344
- ## 13. Host module bridges
455
+ ## 14. Host module bridges
345
456
 
346
457
  `defineModule()` stores the namespace in a runtime-specific registry:
347
458
 
@@ -376,7 +487,7 @@ The ES module bridge captures each exported property value when the bridge modul
376
487
 
377
488
  To replace exports, call `defineModule()` again. This invalidates dependent runtime modules.
378
489
 
379
- ## 14. Scope injection design choice
490
+ ## 15. Scope injection design choice
380
491
 
381
492
  The runtime does not automatically turn unknown identifiers into scope lookups.
382
493
 
@@ -413,7 +524,7 @@ Reasons:
413
524
 
414
525
  A future convenience API may create scope modules automatically, but it should still compile down to module semantics.
415
526
 
416
- ## 15. Dependency graph
527
+ ## 16. Dependency graph
417
528
 
418
529
  Each record tracks:
419
530
 
@@ -431,7 +542,7 @@ When linking `/App.jsx` imports `/Button.jsx`:
431
542
 
432
543
  These edges power invalidation and tooling.
433
544
 
434
- ## 16. Caching
545
+ ## 17. Caching
435
546
 
436
547
  A source module caches:
437
548
 
@@ -444,7 +555,7 @@ Repeated imports use the same module URL/import promise until invalidated.
444
555
 
445
556
  Native ESM also caches imports by URL.
446
557
 
447
- ## 17. Invalidation and updates
558
+ ## 18. Invalidation and updates
448
559
 
449
560
  Updating a module invalidates:
450
561
 
@@ -469,7 +580,7 @@ const fresh = await runtime.import("/App.jsx");
469
580
 
470
581
  This is deliberate and matches the immutable identity of native evaluated modules.
471
582
 
472
- ## 18. `toModule()` semantics
583
+ ## 19. `toModule()` semantics
473
584
 
474
585
  `toModule(source)` is sugar for:
475
586
 
@@ -485,7 +596,7 @@ return module namespace
485
596
 
486
597
  It is asynchronous by design.
487
598
 
488
- ## 19. `toComponent()` semantics
599
+ ## 20. `toComponent()` semantics
489
600
 
490
601
  `toComponent()` calls `toModule()` and returns a selected callable export.
491
602
 
@@ -499,7 +610,7 @@ Named exports require an explicit `exportName`.
499
610
 
500
611
  The runtime does not guess between multiple component exports.
501
612
 
502
- ## 20. Security model
613
+ ## 21. Security model
503
614
 
504
615
  `solid-tag-runtime` is **not a sandbox**.
505
616
 
@@ -507,7 +618,7 @@ Dynamic source executes as JavaScript in the page/module environment and has wha
507
618
 
508
619
  Do not use this package to execute untrusted hostile code without a separate isolation mechanism such as a suitably sandboxed iframe/worker/process boundary.
509
620
 
510
- ## 21. CSP considerations
621
+ ## 22. CSP considerations
511
622
 
512
623
  The browser backend currently uses Blob module URLs by default.
513
624
 
@@ -519,7 +630,7 @@ Applications with strict Content Security Policy may need:
519
630
 
520
631
  The public module API should not depend on Blob URLs so this backend can change later.
521
632
 
522
- ## 22. Future directions
633
+ ## 23. Future directions
523
634
 
524
635
  Potential additions, in rough architectural order:
525
636
 
@@ -537,16 +648,6 @@ await runtime.load("/App.jsx");
537
648
 
538
649
  with configurable fetch/source providers.
539
650
 
540
- ### Script-tag module registration
541
-
542
- Possible declarative zero-build API:
543
-
544
- ```html
545
- <script type="solid-jsx" module="/Button.jsx">...</script>
546
- ```
547
-
548
- implemented as a thin source-provider layer over the same runtime graph.
549
-
550
651
  ### Runtime update/HMR helpers
551
652
 
552
653
  Graph invalidation already exists. Future APIs can add lifecycle hooks and entry re-rendering without changing module identity rules.
@@ -563,7 +664,7 @@ The runtime compiler boundary allows a future TSX-capable transformer without co
563
664
 
564
665
  If `solid-tag` later gains tagged-template → JSX transformation, runtime/editor tooling can expose source views without changing module execution semantics.
565
666
 
566
- ## 23. Decision log
667
+ ## 24. Decision log
567
668
 
568
669
  ### 2026-10-02 — Separate runtime package
569
670
 
@@ -606,3 +707,33 @@ Reason: the current immutable URL linker cannot assign stable module identities
606
707
  Decision: the runtime engine consumes `analyze()` and `transform()` rather than importing parser internals directly.
607
708
 
608
709
  Reason: preserve the option to add TSX, tagged-source, or alternative transformer backends later.
710
+ ### 2026-10-02 — HTML integration is an adapter subpath
711
+
712
+ Decision: add declarative script registration in `0.0.2` as `solid-tag-runtime/html`, not as methods on the core runtime and not as a separate npm package.
713
+
714
+ Reason: HTML discovery, DOM access, and fetching are browser concerns. Keeping them behind an export subpath preserves a DOM-free core while avoiding premature package fragmentation.
715
+
716
+ ### 2026-10-02 — HTML declares modules; it is not itself JSX
717
+
718
+ Decision: support JSX/JS source inside runtime `<script>` modules, but do not reinterpret arbitrary document HTML or `<template>` content as JSX in `0.0.2`.
719
+
720
+ Reason: module source already has clear lexical/import semantics. Treating document HTML as executable JSX would introduce separate scope, parsing, hydration, and DOM-ownership semantics.
721
+
722
+ ### 2026-10-02 — `src` and `module` are separate concepts
723
+
724
+ Decision: `src` identifies where source is fetched, while `module` identifies the module inside the runtime graph. When `module` is omitted for an external script, the resolved source URL is used as its identity.
725
+
726
+ Reason: source location and logical module identity should be independently controllable while still making zero-config relative URL module graphs ergonomic.
727
+
728
+ ### 2026-10-02 — Define all HTML modules before running entries
729
+
730
+ Decision: `registerHTML()` completes discovery/fetch/definition for all matching scripts before importing any module marked `entry`.
731
+
732
+ Reason: dependency availability must not depend on HTML document order.
733
+
734
+ ### 2026-10-02 — Live HTML discovery uses an addition-only observer
735
+
736
+ Decision: add `observeHTML()` to the existing `solid-tag-runtime/html` subpath in `0.0.2`. It uses `MutationObserver`, registers existing scripts by default, tracks seen element identities, and applies the same define-before-entry invariant to every newly discovered batch.
737
+
738
+ Reason: applications that dynamically insert runtime modules should not need to manually rescan the document, while keeping DOM observation separate from the core runtime module engine. Addition-only semantics avoid silently redefining modules because of text/attribute churn or trying to infer deletion semantics before the runtime has an explicit module-removal API.
739
+
package/README.md CHANGED
@@ -97,6 +97,150 @@ counterModule.Counter;
97
97
 
98
98
  The runtime compiles and links the dependency graph automatically.
99
99
 
100
+
101
+ ## Declarative HTML modules
102
+
103
+ `0.0.2` adds a browser adapter at `solid-tag-runtime/html`. The core runtime remains DOM-independent; the HTML adapter only discovers source modules and feeds them into the same runtime module graph.
104
+
105
+ ```ts
106
+ import { registerHTML, observeHTML } from "solid-tag-runtime/html";
107
+
108
+ await registerHTML(runtime);
109
+ ```
110
+
111
+ The adapter recognizes runtime script modules such as:
112
+
113
+ ```html
114
+ <script type="solid-jsx" module="/ui/Button.jsx">
115
+ export function Button(props) {
116
+ return (
117
+ <button onClick={props.onClick}>
118
+ {props.children}
119
+ </button>
120
+ );
121
+ }
122
+ </script>
123
+
124
+ <script type="solid-jsx" module="/App.jsx" entry>
125
+ import { render } from "@solidjs/web";
126
+ import { Button } from "./ui/Button.jsx";
127
+
128
+ function App() {
129
+ return <Button>Hello from runtime JSX</Button>;
130
+ }
131
+
132
+ render(() => <App />, document.getElementById("app"));
133
+ </script>
134
+ ```
135
+
136
+ All matching scripts are **defined before any `entry` module is executed**. This means an entry may import a module that appears later in the HTML document.
137
+
138
+ External source is supported as well:
139
+
140
+ ```html
141
+ <script
142
+ type="solid-jsx"
143
+ module="/ui/Button.jsx"
144
+ src="./components/Button.jsx">
145
+ </script>
146
+
147
+ <script
148
+ type="solid-jsx"
149
+ module="/App.jsx"
150
+ src="./App.jsx"
151
+ entry>
152
+ </script>
153
+ ```
154
+
155
+ `src` answers **where the source is fetched from** while `module` answers **what its identity is in the runtime graph**. They are intentionally separate.
156
+
157
+ If `module` is omitted for a `src` script, the resolved source URL becomes the module ID. This makes relative imports between externally loaded modules work naturally. Inline non-entry scripts require a `module` attribute; an inline `entry` may omit it and receives an anonymous runtime module ID.
158
+
159
+ ### HTML script formats
160
+
161
+ Explicit script types:
162
+
163
+ ```text
164
+ solid-jsx / text/solid-jsx → JSX transformation
165
+ solid-js / text/solid-js → plain JavaScript module
166
+ solid-module → infer from module/src extension
167
+ ```
168
+
169
+ For `solid-module`, `.js`/`.mjs` are treated as JavaScript and other extensions currently default to JSX. A `language="js"` or `language="jsx"` attribute can override inference.
170
+
171
+ ### Register without executing entries
172
+
173
+ ```ts
174
+ const registration = await registerHTML(runtime, {
175
+ executeEntries: false,
176
+ });
177
+
178
+ console.log(registration.modules);
179
+ console.log(registration.entries);
180
+
181
+ await runtime.import(registration.entries[0]);
182
+ ```
183
+
184
+ ### Register one script manually
185
+
186
+ ```ts
187
+ import { defineScript } from "solid-tag-runtime/html";
188
+
189
+ const definition = await defineScript(
190
+ runtime,
191
+ document.querySelector("#runtime-component"),
192
+ );
193
+
194
+ const namespace = await runtime.import(definition.id);
195
+ ```
196
+
197
+ ### Observe scripts added later
198
+
199
+ `observeHTML()` uses `MutationObserver` to register runtime module scripts added after startup. It performs an initial scan by default and preserves the same batch invariant as `registerHTML()`: all newly discovered modules are defined before any newly discovered `entry` executes.
200
+
201
+ ```ts
202
+ import { observeHTML } from "solid-tag-runtime/html";
203
+
204
+ const observer = await observeHTML(runtime, {
205
+ onError(error) {
206
+ console.error("runtime module registration failed", error);
207
+ },
208
+ });
209
+
210
+ // Later, code may append a new runtime module script.
211
+ const script = document.createElement("script");
212
+ script.type = "solid-jsx";
213
+ script.setAttribute("module", "/dynamic/Greeting.jsx");
214
+ script.textContent = `
215
+ export function Greeting(props) {
216
+ return <p>Hello {props.name}</p>;
217
+ }
218
+ `;
219
+ document.body.append(script);
220
+
221
+ // Normally MutationObserver handles this automatically. flush() is useful when
222
+ // code needs to wait until queued observation work has completed.
223
+ await observer.flush();
224
+
225
+ const { Greeting } = await runtime.import("/dynamic/Greeting.jsx");
226
+
227
+ observer.disconnect();
228
+ ```
229
+
230
+ If existing scripts were already registered separately, start observation without re-registering them:
231
+
232
+ ```ts
233
+ await registerHTML(runtime);
234
+
235
+ const observer = await observeHTML(runtime, {
236
+ registerExisting: false,
237
+ });
238
+ ```
239
+
240
+ In `0.0.2`, observation is intentionally **addition-only**. Editing attributes/text of an already registered script or removing that script does not automatically update/remove the corresponding runtime module. Use `runtime.update()` / `runtime.invalidate()` explicitly when needed.
241
+
242
+ The HTML adapter does **not** reinterpret ordinary document HTML as JSX and does not currently add `<template>` component semantics. JSX remains source-code syntax inside runtime modules.
243
+
100
244
  ## `toModule()`
101
245
 
102
246
  For one-off source strings:
package/html.d.ts ADDED
@@ -0,0 +1,114 @@
1
+ import { SolidTagRuntimeError } from "./index.js";
2
+ import type { ModuleFormat, ModuleNamespaceLike, SolidTagRuntime } from "./index.js";
3
+
4
+ export interface HTMLModuleScriptElement {
5
+ textContent?: string | null;
6
+ baseURI?: string;
7
+ getAttribute(name: string): string | null;
8
+ hasAttribute?(name: string): boolean;
9
+ }
10
+
11
+ export interface HTMLModuleRoot {
12
+ baseURI?: string;
13
+ querySelectorAll(selector: string): Iterable<HTMLModuleScriptElement> | ArrayLike<HTMLModuleScriptElement>;
14
+ }
15
+
16
+ export interface HTMLFetchResponse {
17
+ ok?: boolean;
18
+ status?: number;
19
+ statusText?: string;
20
+ text(): Promise<string>;
21
+ }
22
+
23
+ export type HTMLFetch = (url: string) => Promise<HTMLFetchResponse>;
24
+
25
+ export interface HTMLMutationObserverLike {
26
+ observe(
27
+ target: unknown,
28
+ options: {
29
+ childList: boolean;
30
+ subtree: boolean;
31
+ },
32
+ ): void;
33
+ disconnect(): void;
34
+ }
35
+
36
+ export interface HTMLMutationObserverConstructor {
37
+ new (
38
+ callback: (...args: unknown[]) => void,
39
+ ): HTMLMutationObserverLike;
40
+ }
41
+
42
+ export interface DefineScriptOptions {
43
+ root?: HTMLModuleRoot;
44
+ baseUrl?: string;
45
+ format?: ModuleFormat | "javascript";
46
+ fetch?: HTMLFetch;
47
+ }
48
+
49
+ export interface HTMLModuleDefinition {
50
+ id: string;
51
+ format: ModuleFormat;
52
+ entry: boolean;
53
+ sourceUrl?: string;
54
+ inline: boolean;
55
+ element: HTMLModuleScriptElement;
56
+ }
57
+
58
+ export interface RegisterHTMLOptions extends DefineScriptOptions {
59
+ selector?: string;
60
+ executeEntries?: boolean;
61
+ }
62
+
63
+ export interface RegisterHTMLResult {
64
+ modules: HTMLModuleDefinition[];
65
+ entries: string[];
66
+ executed: Array<{
67
+ id: string;
68
+ namespace: ModuleNamespaceLike;
69
+ }>;
70
+ }
71
+
72
+ export interface ObserveHTMLOptions extends RegisterHTMLOptions {
73
+ /** Register scripts already present when observation starts. Default: true. */
74
+ registerExisting?: boolean;
75
+ /** Observe descendant additions as well as direct children. Default: true. */
76
+ subtree?: boolean;
77
+ /** Override the platform MutationObserver constructor, useful for DOM-like hosts/tests. */
78
+ MutationObserver?: HTMLMutationObserverConstructor;
79
+ /** Called for asynchronous observer registration/import failures. */
80
+ onError?: (error: unknown) => void;
81
+ }
82
+
83
+ export interface HTMLObserverController {
84
+ /** Result of the initial scan. Empty when registerExisting is false. */
85
+ initial: RegisterHTMLResult;
86
+ readonly connected: boolean;
87
+ /** Wait for queued observer work and synchronously scan for any unseen scripts. */
88
+ flush(): Promise<RegisterHTMLResult>;
89
+ disconnect(): void;
90
+ }
91
+
92
+ export declare class HTMLModuleError extends SolidTagRuntimeError {
93
+ element?: HTMLModuleScriptElement;
94
+ moduleId?: string;
95
+ sourceUrl?: string;
96
+ }
97
+
98
+ export declare const htmlModuleSelector: string;
99
+
100
+ export declare function defineScript(
101
+ runtime: SolidTagRuntime,
102
+ element: HTMLModuleScriptElement,
103
+ options?: DefineScriptOptions,
104
+ ): Promise<HTMLModuleDefinition>;
105
+
106
+ export declare function registerHTML(
107
+ runtime: SolidTagRuntime,
108
+ options?: RegisterHTMLOptions,
109
+ ): Promise<RegisterHTMLResult>;
110
+
111
+ export declare function observeHTML(
112
+ runtime: SolidTagRuntime,
113
+ options?: ObserveHTMLOptions,
114
+ ): Promise<HTMLObserverController>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-tag-runtime",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
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": {
@@ -18,6 +18,11 @@
18
18
  "types": "./index.d.ts",
19
19
  "import": "./src/runtime.js",
20
20
  "default": "./src/runtime.js"
21
+ },
22
+ "./html": {
23
+ "types": "./html.d.ts",
24
+ "import": "./src/html.js",
25
+ "default": "./src/html.js"
21
26
  }
22
27
  },
23
28
  "main": "./src/index.js",
@@ -26,6 +31,7 @@
26
31
  "files": [
27
32
  "src",
28
33
  "index.d.ts",
34
+ "html.d.ts",
29
35
  "README.md",
30
36
  "ARCHITECTURE.md"
31
37
  ],
package/src/html.js ADDED
@@ -0,0 +1,362 @@
1
+ import { SolidTagRuntimeError } from "./errors.js";
2
+
3
+ const DEFAULT_SELECTOR = [
4
+ 'script[type="solid-jsx"]',
5
+ 'script[type="text/solid-jsx"]',
6
+ 'script[type="solid-js"]',
7
+ 'script[type="text/solid-js"]',
8
+ 'script[type="solid-module"]',
9
+ 'script[type="text/solid-module"]',
10
+ ].join(",");
11
+
12
+ let anonymousSequence = 0;
13
+
14
+ export class HTMLModuleError extends SolidTagRuntimeError {
15
+ constructor(message, options = {}) {
16
+ super(message, options);
17
+ this.name = "HTMLModuleError";
18
+ this.element = options.element;
19
+ this.moduleId = options.moduleId;
20
+ this.sourceUrl = options.sourceUrl;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Discover runtime module scripts under a Document/Element-like root, define
26
+ * all of them first, then optionally execute scripts marked with `entry`.
27
+ */
28
+ export async function registerHTML(runtime, options = {}) {
29
+ assertRuntime(runtime);
30
+
31
+ const root = getRoot(options, "registerHTML");
32
+ const elements = Array.from(root.querySelectorAll(options.selector ?? DEFAULT_SELECTOR));
33
+
34
+ return registerElements(runtime, elements, {
35
+ ...options,
36
+ root,
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Observe a DOM root for newly-added runtime module scripts.
42
+ *
43
+ * Existing scripts are registered by default. Every discovered mutation batch
44
+ * preserves the same invariant as registerHTML(): all modules in the batch are
45
+ * defined before any entry module from that batch executes.
46
+ *
47
+ * v0.0.2 intentionally observes additions only. Mutating or removing a script
48
+ * that was already registered does not update/remove its runtime module.
49
+ */
50
+ export async function observeHTML(runtime, options = {}) {
51
+ assertRuntime(runtime);
52
+
53
+ const root = getRoot(options, "observeHTML");
54
+ const MutationObserverImpl = options.MutationObserver ?? globalThis.MutationObserver;
55
+
56
+ if (typeof MutationObserverImpl !== "function") {
57
+ throw new TypeError(
58
+ "observeHTML() requires MutationObserver support or a MutationObserver option.",
59
+ );
60
+ }
61
+
62
+ const selector = options.selector ?? DEFAULT_SELECTOR;
63
+ const seen = new WeakSet();
64
+ let connected = true;
65
+ let lastError;
66
+ let queue = Promise.resolve();
67
+
68
+ if (options.registerExisting === false) {
69
+ for (const element of Array.from(root.querySelectorAll(selector))) {
70
+ seen.add(element);
71
+ }
72
+ }
73
+
74
+ const scanAndRegister = async () => {
75
+ const elements = Array.from(root.querySelectorAll(selector)).filter(
76
+ element => !seen.has(element),
77
+ );
78
+
79
+ if (elements.length === 0) return emptyRegistrationResult();
80
+
81
+ return registerElements(
82
+ runtime,
83
+ elements,
84
+ {
85
+ ...options,
86
+ root,
87
+ },
88
+ element => seen.add(element),
89
+ );
90
+ };
91
+
92
+ const reportError = error => {
93
+ lastError = error;
94
+
95
+ if (typeof options.onError === "function") {
96
+ options.onError(error);
97
+ return;
98
+ }
99
+
100
+ // MutationObserver callbacks cannot be awaited by the DOM. Keep the error
101
+ // available for flush(), but also make background failures visible.
102
+ globalThis.console?.error?.("solid-tag-runtime/html observer error", error);
103
+ };
104
+
105
+ const enqueue = () => {
106
+ const task = queue.then(scanAndRegister);
107
+ queue = task.then(
108
+ () => undefined,
109
+ error => {
110
+ reportError(error);
111
+ },
112
+ );
113
+ return task;
114
+ };
115
+
116
+ const observer = new MutationObserverImpl(() => {
117
+ // The settled queue owns background error reporting. Attach a rejection
118
+ // handler here as well so the callback never creates an unhandled promise.
119
+ void enqueue().catch(() => {});
120
+ });
121
+
122
+ observer.observe(root, {
123
+ childList: true,
124
+ subtree: options.subtree !== false,
125
+ });
126
+
127
+ let initial = emptyRegistrationResult();
128
+
129
+ try {
130
+ if (options.registerExisting !== false) {
131
+ initial = await enqueue();
132
+ }
133
+ } catch (error) {
134
+ connected = false;
135
+ observer.disconnect();
136
+ throw error;
137
+ }
138
+
139
+ return {
140
+ initial,
141
+
142
+ get connected() {
143
+ return connected;
144
+ },
145
+
146
+ async flush() {
147
+ await queue;
148
+
149
+ if (lastError) {
150
+ const error = lastError;
151
+ lastError = undefined;
152
+ throw error;
153
+ }
154
+
155
+ return enqueue();
156
+ },
157
+
158
+ disconnect() {
159
+ if (!connected) return;
160
+ connected = false;
161
+ observer.disconnect();
162
+ },
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Define one <script> element as a runtime module without executing it.
168
+ */
169
+ export async function defineScript(runtime, element, options = {}) {
170
+ assertRuntime(runtime);
171
+
172
+ if (!element || typeof element.getAttribute !== "function") {
173
+ throw new TypeError("defineScript() expects a script element-like object.");
174
+ }
175
+
176
+ const type = normalizeType(element.getAttribute("type"));
177
+ const entry = hasAttribute(element, "entry");
178
+ const srcAttribute = nonEmpty(element.getAttribute("src"));
179
+ const explicitId = nonEmpty(element.getAttribute("module"));
180
+ const sourceUrl = srcAttribute
181
+ ? resolveSourceUrl(srcAttribute, options.baseUrl ?? element.baseURI ?? options.root?.baseURI)
182
+ : undefined;
183
+
184
+ const format = inferFormat({
185
+ explicitFormat: options.format,
186
+ language: nonEmpty(element.getAttribute("language")),
187
+ type,
188
+ moduleId: explicitId,
189
+ sourceUrl,
190
+ });
191
+
192
+ const id = explicitId ?? sourceUrl ?? (entry ? createAnonymousId(format) : undefined);
193
+
194
+ if (!id) {
195
+ throw new HTMLModuleError(
196
+ "Inline solid runtime module scripts require a `module` attribute unless they are marked `entry`.",
197
+ { element },
198
+ );
199
+ }
200
+
201
+ let source;
202
+ if (sourceUrl) {
203
+ source = await fetchSource(sourceUrl, options.fetch ?? globalThis.fetch, element, id);
204
+ } else {
205
+ source = element.textContent ?? "";
206
+ }
207
+
208
+ runtime.define(id, source, { format });
209
+
210
+ return {
211
+ id: runtime.resolve(id),
212
+ format,
213
+ entry,
214
+ sourceUrl,
215
+ inline: !sourceUrl,
216
+ element,
217
+ };
218
+ }
219
+
220
+ export const htmlModuleSelector = DEFAULT_SELECTOR;
221
+
222
+ async function registerElements(runtime, elements, options, onDefined) {
223
+ const modules = [];
224
+
225
+ // Define the complete discovered batch before evaluating any entry. This
226
+ // allows an entry to import another module discovered later in the same scan.
227
+ for (const element of elements) {
228
+ const definition = await defineScript(runtime, element, options);
229
+ modules.push(definition);
230
+ onDefined?.(element, definition);
231
+ }
232
+
233
+ const entries = modules.filter(module => module.entry);
234
+ const executed = [];
235
+
236
+ if (options.executeEntries !== false) {
237
+ for (const entry of entries) {
238
+ executed.push({
239
+ id: entry.id,
240
+ namespace: await runtime.import(entry.id),
241
+ });
242
+ }
243
+ }
244
+
245
+ return {
246
+ modules,
247
+ entries: entries.map(entry => entry.id),
248
+ executed,
249
+ };
250
+ }
251
+
252
+ function emptyRegistrationResult() {
253
+ return {
254
+ modules: [],
255
+ entries: [],
256
+ executed: [],
257
+ };
258
+ }
259
+
260
+ function getRoot(options, caller) {
261
+ const root = options.root ?? globalThis.document;
262
+ if (!root || typeof root.querySelectorAll !== "function") {
263
+ throw new TypeError(`${caller}() requires a DOM root with querySelectorAll().`);
264
+ }
265
+ return root;
266
+ }
267
+
268
+ function assertRuntime(runtime) {
269
+ if (!runtime || typeof runtime.define !== "function" || typeof runtime.import !== "function") {
270
+ throw new TypeError("Expected a solid-tag-runtime instance.");
271
+ }
272
+ }
273
+
274
+ function normalizeType(value) {
275
+ return String(value ?? "").trim().toLowerCase();
276
+ }
277
+
278
+ function inferFormat({ explicitFormat, language, type, moduleId, sourceUrl }) {
279
+ if (explicitFormat != null) return normalizeFormat(explicitFormat);
280
+ if (language != null) return normalizeFormat(language);
281
+
282
+ if (type === "solid-js" || type === "text/solid-js") return "js";
283
+ if (type === "solid-jsx" || type === "text/solid-jsx") return "jsx";
284
+
285
+ const target = moduleId ?? sourceUrl ?? "";
286
+ const pathname = stripQueryAndHash(target).toLowerCase();
287
+ if (pathname.endsWith(".js") || pathname.endsWith(".mjs")) return "js";
288
+ return "jsx";
289
+ }
290
+
291
+ function normalizeFormat(value) {
292
+ const format = String(value).trim().toLowerCase();
293
+ if (format === "js" || format === "javascript") return "js";
294
+ if (format === "jsx") return "jsx";
295
+ throw new HTMLModuleError(`Unsupported HTML runtime module format ${JSON.stringify(value)}.`);
296
+ }
297
+
298
+ async function fetchSource(url, fetchImpl, element, moduleId) {
299
+ if (typeof fetchImpl !== "function") {
300
+ throw new HTMLModuleError(
301
+ `Cannot load ${JSON.stringify(url)} because no fetch implementation is available.`,
302
+ { element, moduleId, sourceUrl: url },
303
+ );
304
+ }
305
+
306
+ let response;
307
+ try {
308
+ response = await fetchImpl(url);
309
+ } catch (cause) {
310
+ throw new HTMLModuleError(
311
+ `Failed to fetch runtime module ${JSON.stringify(url)}.`,
312
+ { cause, element, moduleId, sourceUrl: url },
313
+ );
314
+ }
315
+
316
+ if (!response || response.ok === false) {
317
+ const status = response?.status ? ` (${response.status}${response.statusText ? ` ${response.statusText}` : ""})` : "";
318
+ throw new HTMLModuleError(
319
+ `Failed to fetch runtime module ${JSON.stringify(url)}${status}.`,
320
+ { element, moduleId, sourceUrl: url },
321
+ );
322
+ }
323
+
324
+ try {
325
+ return await response.text();
326
+ } catch (cause) {
327
+ throw new HTMLModuleError(
328
+ `Failed to read runtime module ${JSON.stringify(url)} as text.`,
329
+ { cause, element, moduleId, sourceUrl: url },
330
+ );
331
+ }
332
+ }
333
+
334
+ function resolveSourceUrl(src, baseUrl) {
335
+ try {
336
+ if (baseUrl) return new URL(src, baseUrl).href;
337
+ return new URL(src).href;
338
+ } catch {
339
+ // A DOM element normally supplies baseURI. Keeping the original string is
340
+ // still useful for custom DOM-like environments and injected fetchers.
341
+ return src;
342
+ }
343
+ }
344
+
345
+ function createAnonymousId(format) {
346
+ const extension = format === "js" ? "js" : "jsx";
347
+ return `/__solid_tag_runtime__/html-entry-${++anonymousSequence}.${extension}`;
348
+ }
349
+
350
+ function stripQueryAndHash(value) {
351
+ return String(value).split(/[?#]/, 1)[0];
352
+ }
353
+
354
+ function nonEmpty(value) {
355
+ const text = String(value ?? "").trim();
356
+ return text || undefined;
357
+ }
358
+
359
+ function hasAttribute(element, name) {
360
+ if (typeof element.hasAttribute === "function") return element.hasAttribute(name);
361
+ return element.getAttribute(name) != null;
362
+ }