solid-tag-runtime 0.0.1

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.
@@ -0,0 +1,608 @@
1
+ # solid-tag-runtime Architecture
2
+
3
+ This file is the engineering handoff and design record for `solid-tag-runtime`.
4
+
5
+ **Maintenance rule:** update this document whenever we change the public runtime API, module record model, module resolution/linking semantics, execution backend, compiler adapter contract, caching/invalidation behavior, supported source formats, host-module behavior, or an important architectural invariant.
6
+
7
+ ## 1. Purpose
8
+
9
+ `solid-tag-runtime` provides a runtime ES-module-like environment for source modules that may contain ordinary Solid JSX.
10
+
11
+ It is intentionally separate from `solid-tag`:
12
+
13
+ ```text
14
+ solid-tag
15
+ syntax transformation
16
+ JSX → @solidjs/html tagged templates
17
+
18
+ solid-tag-runtime
19
+ module definition
20
+ dependency resolution
21
+ linking
22
+ execution
23
+ caching
24
+ invalidation
25
+ host-module injection
26
+ ```
27
+
28
+ The runtime's primary abstraction is a **module**, not a component. Components are normal exports of modules.
29
+
30
+ ## 2. Goals
31
+
32
+ The runtime should allow applications to:
33
+
34
+ 1. define JSX source modules from strings
35
+ 2. import those modules and receive module namespace objects
36
+ 3. allow one runtime module to import another runtime module
37
+ 4. expose the host application's existing Solid runtime to dynamic source
38
+ 5. expose host components, signals, callbacks, services, and data as modules
39
+ 6. reuse compiled/evaluated modules through caching
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
44
+
45
+ ## 3. Non-goals for the first version
46
+
47
+ The initial package does not attempt to provide:
48
+
49
+ - a Vite/Rollup/esbuild integration
50
+ - a security sandbox
51
+ - Node package resolution
52
+ - automatic npm installation
53
+ - automatic unresolved-variable scope rewriting
54
+ - circular runtime source-module support
55
+ - transparent live replacement of already-held component references
56
+ - full HMR propagation semantics
57
+ - script-tag discovery/registration
58
+
59
+ These may be added later without changing the module-first public model.
60
+
61
+ ## 4. Package relationship
62
+
63
+ ```text
64
+ Application
65
+ │
66
+ ├── host Solid runtime
67
+ │ ├── solid-js
68
+ │ ├── @solidjs/web
69
+ │ └── @solidjs/html
70
+ │
71
+ └── solid-tag-runtime
72
+ │
73
+ ├── solid-tag
74
+ │ └── JSX → html``
75
+ │
76
+ └── runtime module graph
77
+ ```
78
+
79
+ `solid-tag-runtime` depends on `solid-tag` but deliberately does not depend on or bundle its own copy of Solid.
80
+
81
+ The host application can register its existing Solid namespaces:
82
+
83
+ ```ts
84
+ runtime.defineModule("solid-js", Solid);
85
+ runtime.defineModule("@solidjs/web", SolidWeb);
86
+ runtime.defineModule("@solidjs/html", { default: html });
87
+ ```
88
+
89
+ This avoids duplicate Solid runtimes.
90
+
91
+ ## 5. Public API model
92
+
93
+ The first API surface is centered on `createRuntime()`:
94
+
95
+ ```ts
96
+ const runtime = createRuntime(options);
97
+ ```
98
+
99
+ ### Source modules
100
+
101
+ ```ts
102
+ runtime.define(id, source, options?);
103
+ runtime.update(id, source, options?);
104
+ ```
105
+
106
+ ### Host namespace modules
107
+
108
+ ```ts
109
+ runtime.defineModule(id, namespace);
110
+ ```
111
+
112
+ ### URL/native modules
113
+
114
+ ```ts
115
+ runtime.defineUrl(id, url);
116
+ ```
117
+
118
+ ### Loading
119
+
120
+ ```ts
121
+ await runtime.import(id);
122
+ await runtime.toModule(source, options?);
123
+ await runtime.toComponent(source, options?);
124
+ ```
125
+
126
+ ### Tooling/introspection
127
+
128
+ ```ts
129
+ await runtime.compile(id);
130
+ runtime.modules();
131
+ runtime.dependencies(id);
132
+ runtime.dependents(id);
133
+ runtime.getModuleInfo(id);
134
+ ```
135
+
136
+ ### Lifecycle
137
+
138
+ ```ts
139
+ runtime.invalidate(id);
140
+ runtime.dispose();
141
+ ```
142
+
143
+ ## 6. Module kinds
144
+
145
+ The runtime currently has three module record kinds.
146
+
147
+ ### 6.1 Source module
148
+
149
+ A string containing JavaScript with optional JSX:
150
+
151
+ ```ts
152
+ runtime.define("/Button.jsx", source);
153
+ ```
154
+
155
+ Source modules are analyzed, linked, optionally transformed with `solid-tag`, converted to a module URL, and evaluated by the native module loader.
156
+
157
+ ### 6.2 Host module
158
+
159
+ An already-existing namespace/object from the application:
160
+
161
+ ```ts
162
+ runtime.defineModule("@app/components", {
163
+ Button,
164
+ Card,
165
+ });
166
+ ```
167
+
168
+ The runtime generates a tiny bridge ES module which reads those values from a runtime-specific registry on `globalThis` and re-exports them.
169
+
170
+ The bridge lets native dynamic modules import application-owned JavaScript references without serializing them.
171
+
172
+ ### 6.3 URL module
173
+
174
+ A module ID mapped directly to a URL:
175
+
176
+ ```ts
177
+ runtime.defineUrl("lib", "https://example.test/lib.js");
178
+ ```
179
+
180
+ The runtime rewrites imports of `lib` to that URL and lets the native module loader handle it.
181
+
182
+ ## 7. Source formats
183
+
184
+ ### `jsx`
185
+
186
+ Default.
187
+
188
+ The runtime links imports, then passes source through the compiler adapter. The default compiler adapter is `solid-tag` and injects `@solidjs/html` when JSX is transformed.
189
+
190
+ ### `js`
191
+
192
+ No JSX transformation. Imports are still resolved and rewritten through the runtime module graph.
193
+
194
+ This permits ordinary JavaScript and JSX modules to coexist in one graph.
195
+
196
+ ## 8. Compiler adapter boundary
197
+
198
+ The runtime engine itself does not depend on Acorn or the `solid-tag` AST.
199
+
200
+ It requires a compiler adapter with two operations:
201
+
202
+ ```ts
203
+ interface RuntimeCompiler {
204
+ analyze(source, context): {
205
+ imports: ImportReference[];
206
+ needsHtmlRuntime?: boolean;
207
+ };
208
+
209
+ transform(source, context): {
210
+ code: string;
211
+ diagnostics?: unknown[];
212
+ };
213
+ }
214
+ ```
215
+
216
+ The default adapter lives in `src/compiler.js` and uses:
217
+
218
+ ```text
219
+ solid-tag.parseJSX()
220
+ solid-tag.transformModule()
221
+ ```
222
+
223
+ ### Why this boundary exists
224
+
225
+ It allows future compiler implementations without rewriting the runtime module system.
226
+
227
+ Possible future adapters include:
228
+
229
+ - a TSX-capable `solid-tag` backend
230
+ - another JSX parser
231
+ - pre-tagged source processing
232
+ - reverse/tagged transformations for tooling
233
+
234
+ The module graph must remain independent of parser details.
235
+
236
+ ## 9. Linking pipeline
237
+
238
+ For a source module `/features/Counter.jsx`:
239
+
240
+ ```text
241
+ source
242
+ ↓
243
+ compiler.analyze()
244
+ ↓
245
+ collect static imports / re-exports / literal dynamic imports
246
+ ↓
247
+ resolve each specifier
248
+ ↓
249
+ ensure virtual dependencies have module URLs
250
+ ↓
251
+ rewrite import string literals to linked URLs
252
+ ↓
253
+ solid-tag transformation (for JSX format)
254
+ ↓
255
+ create native module URL
256
+ ↓
257
+ import(moduleUrl)
258
+ ```
259
+
260
+ Example graph:
261
+
262
+ ```text
263
+ /features/Counter.jsx
264
+ ├── solid-js → host bridge module
265
+ ├── @solidjs/html → host bridge module
266
+ └── ../ui/Button.jsx → compiled virtual source URL
267
+ ```
268
+
269
+ ## 10. Resolution rules
270
+
271
+ Resolution order is conceptually:
272
+
273
+ 1. custom `resolve()` callback
274
+ 2. exact registered module ID
275
+ 3. absolute URL
276
+ 4. absolute virtual path
277
+ 5. relative path resolved against the importing virtual module
278
+ 6. unresolved bare import left native when `allowNativeImports === true`
279
+ 7. otherwise resolution error
280
+
281
+ Relative virtual resolution uses POSIX-style paths regardless of the host operating system.
282
+
283
+ This keeps module IDs stable in browsers and on Windows hosts.
284
+
285
+ ## 11. Native ESM execution backend
286
+
287
+ The first execution backend deliberately relies on the JavaScript engine's native ESM evaluator rather than `new Function()`.
288
+
289
+ Browser default:
290
+
291
+ ```text
292
+ compiled source
293
+ ↓
294
+ Blob
295
+ ↓
296
+ blob: URL
297
+ ↓
298
+ import(blobUrl)
299
+ ```
300
+
301
+ Node/test default:
302
+
303
+ ```text
304
+ compiled source
305
+ ↓
306
+ data:text/javascript URL
307
+ ↓
308
+ import(dataUrl)
309
+ ```
310
+
311
+ The URL creation mechanism is abstracted by `ModuleUrlBackend` so another execution strategy can be supplied later.
312
+
313
+ ### Why native modules
314
+
315
+ Benefits:
316
+
317
+ - real module namespace objects
318
+ - native `import` / `export` behavior for the supported graph
319
+ - top-level code is evaluated as a module
320
+ - no `eval` / `new Function()` runtime wrapper
321
+ - clean `toModule()` semantics
322
+
323
+ ## 12. Circular dependency limitation
324
+
325
+ Virtual source module cycles are explicitly rejected in v0.0.1.
326
+
327
+ 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
+
329
+ Example currently rejected:
330
+
331
+ ```text
332
+ /A.jsx → /B.jsx → /A.jsx
333
+ ```
334
+
335
+ Future options:
336
+
337
+ 1. runtime linker/backend instead of native URL rewriting
338
+ 2. service-worker-backed stable virtual URLs
339
+ 3. an internal module server
340
+ 4. another backend capable of stable preallocated module identities
341
+
342
+ Do not add ad-hoc cycle handling that breaks module semantics; change the backend deliberately.
343
+
344
+ ## 13. Host module bridges
345
+
346
+ `defineModule()` stores the namespace in a runtime-specific registry:
347
+
348
+ ```text
349
+ globalThis.__solidTagRuntimeHostModules__
350
+ ↓
351
+ runtimeId
352
+ ↓
353
+ moduleId
354
+ ↓
355
+ namespace object
356
+ ```
357
+
358
+ A generated bridge module re-exports identifier-safe properties.
359
+
360
+ ### Important invariant
361
+
362
+ Host values are passed as **references**, not serialized copies.
363
+
364
+ This is what allows dynamic source to import:
365
+
366
+ - signal accessor functions
367
+ - setters
368
+ - stores
369
+ - components
370
+ - callbacks
371
+ - objects/services
372
+
373
+ ### Current limitation
374
+
375
+ The ES module bridge captures each exported property value when the bridge module evaluates. Mutating the namespace object later does not create ESM live bindings.
376
+
377
+ To replace exports, call `defineModule()` again. This invalidates dependent runtime modules.
378
+
379
+ ## 14. Scope injection design choice
380
+
381
+ The runtime does not automatically turn unknown identifiers into scope lookups.
382
+
383
+ Rejected initial design:
384
+
385
+ ```tsx
386
+ <div>{hostVariable}</div>
387
+ ```
388
+
389
+ with compiler magic rewriting `hostVariable` to `scope.hostVariable`.
390
+
391
+ Chosen design:
392
+
393
+ ```ts
394
+ runtime.defineModule("@app/scope", {
395
+ hostVariable,
396
+ });
397
+ ```
398
+
399
+ and runtime source explicitly imports it:
400
+
401
+ ```ts
402
+ import { hostVariable } from "@app/scope";
403
+ ```
404
+
405
+ Reasons:
406
+
407
+ - explicit dependencies
408
+ - no lexical-scope guessing
409
+ - easier module graph introspection
410
+ - actual JavaScript references cross the boundary
411
+ - components and state use the same mechanism
412
+ - fewer compiler responsibilities
413
+
414
+ A future convenience API may create scope modules automatically, but it should still compile down to module semantics.
415
+
416
+ ## 15. Dependency graph
417
+
418
+ Each record tracks:
419
+
420
+ ```text
421
+ record.dependencies
422
+ record.dependents
423
+ ```
424
+
425
+ When linking `/App.jsx` imports `/Button.jsx`:
426
+
427
+ ```text
428
+ /App.jsx.dependencies includes /Button.jsx
429
+ /Button.jsx.dependents includes /App.jsx
430
+ ```
431
+
432
+ These edges power invalidation and tooling.
433
+
434
+ ## 16. Caching
435
+
436
+ A source module caches:
437
+
438
+ - analyzed/linked compiled source
439
+ - generated module URL
440
+ - import promise
441
+ - loaded namespace when directly imported through the runtime
442
+
443
+ Repeated imports use the same module URL/import promise until invalidated.
444
+
445
+ Native ESM also caches imports by URL.
446
+
447
+ ## 17. Invalidation and updates
448
+
449
+ Updating a module invalidates:
450
+
451
+ 1. that module's generated URL/import cache
452
+ 2. dependent runtime modules recursively
453
+
454
+ The next `runtime.import(entry)` rebuilds the affected graph with new URLs.
455
+
456
+ ### Important invariant
457
+
458
+ Already-held module namespaces/components are not mutated.
459
+
460
+ Example:
461
+
462
+ ```ts
463
+ const old = await runtime.import("/App.jsx");
464
+ runtime.update("/Button.jsx", newSource);
465
+ const fresh = await runtime.import("/App.jsx");
466
+ ```
467
+
468
+ `old.App` remains the old module identity. `fresh.App` comes from the rebuilt graph.
469
+
470
+ This is deliberate and matches the immutable identity of native evaluated modules.
471
+
472
+ ## 18. `toModule()` semantics
473
+
474
+ `toModule(source)` is sugar for:
475
+
476
+ ```text
477
+ generate anonymous module ID
478
+ ↓
479
+ runtime.define(id, source)
480
+ ↓
481
+ runtime.import(id)
482
+ ↓
483
+ return module namespace
484
+ ```
485
+
486
+ It is asynchronous by design.
487
+
488
+ ## 19. `toComponent()` semantics
489
+
490
+ `toComponent()` calls `toModule()` and returns a selected callable export.
491
+
492
+ Default:
493
+
494
+ ```text
495
+ exportName = "default"
496
+ ```
497
+
498
+ Named exports require an explicit `exportName`.
499
+
500
+ The runtime does not guess between multiple component exports.
501
+
502
+ ## 20. Security model
503
+
504
+ `solid-tag-runtime` is **not a sandbox**.
505
+
506
+ Dynamic source executes as JavaScript in the page/module environment and has whatever capabilities its imports/globals provide.
507
+
508
+ 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
+
510
+ ## 21. CSP considerations
511
+
512
+ The browser backend currently uses Blob module URLs by default.
513
+
514
+ Applications with strict Content Security Policy may need:
515
+
516
+ - an allowed `blob:` module policy, or
517
+ - a custom `ModuleUrlBackend`, or
518
+ - a future stable virtual-module backend
519
+
520
+ The public module API should not depend on Blob URLs so this backend can change later.
521
+
522
+ ## 22. Future directions
523
+
524
+ Potential additions, in rough architectural order:
525
+
526
+ ### Stable/cycle-capable module backend
527
+
528
+ Needed before claiming full ESM graph compatibility.
529
+
530
+ ### URL fetching/loading
531
+
532
+ Possible API:
533
+
534
+ ```ts
535
+ await runtime.load("/App.jsx");
536
+ ```
537
+
538
+ with configurable fetch/source providers.
539
+
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
+ ### Runtime update/HMR helpers
551
+
552
+ Graph invalidation already exists. Future APIs can add lifecycle hooks and entry re-rendering without changing module identity rules.
553
+
554
+ ### Tagged-source modules
555
+
556
+ A format for source that already contains `html\`\`` templates and only needs module linking/execution.
557
+
558
+ ### TSX compiler adapter
559
+
560
+ The runtime compiler boundary allows a future TSX-capable transformer without coupling module linking to TypeScript or another parser.
561
+
562
+ ### Bidirectional JSX/tagged tooling
563
+
564
+ If `solid-tag` later gains tagged-template → JSX transformation, runtime/editor tooling can expose source views without changing module execution semantics.
565
+
566
+ ## 23. Decision log
567
+
568
+ ### 2026-10-02 — Separate runtime package
569
+
570
+ Decision: create `solid-tag-runtime` instead of adding module execution to `solid-tag`.
571
+
572
+ Reason: keep syntax transformation small and reusable while allowing the runtime package to grow a module graph, execution backend, caching, and lifecycle APIs.
573
+
574
+ ### 2026-10-02 — Modules are the primary abstraction
575
+
576
+ Decision: components are module exports; there is no separate component registry at the linker level.
577
+
578
+ Reason: normal imports/exports naturally solve component composition and also work for utilities, state, and services.
579
+
580
+ ### 2026-10-02 — Host Solid is injected as modules
581
+
582
+ Decision: do not bundle a private Solid runtime in `solid-tag-runtime`.
583
+
584
+ Reason: dynamically compiled components must share the host application's reactive ownership/runtime graph.
585
+
586
+ ### 2026-10-02 — Scope is represented as modules
587
+
588
+ Decision: prefer `defineModule("@app/scope", values)` plus explicit imports instead of compiler rewriting of unresolved identifiers.
589
+
590
+ Reason: explicit, inspectable dependency semantics and fewer compiler responsibilities.
591
+
592
+ ### 2026-10-02 — Native ESM URL backend first
593
+
594
+ Decision: use Blob URLs in browsers and data URLs in Node/tests.
595
+
596
+ Reason: preserve real module namespaces and avoid `new Function()` while the API remains backend-independent.
597
+
598
+ ### 2026-10-02 — Circular virtual dependencies deferred
599
+
600
+ Decision: reject cycles rather than emulate them incorrectly.
601
+
602
+ Reason: the current immutable URL linker cannot assign stable module identities before the full cyclic graph is linked.
603
+
604
+ ### 2026-10-02 — Compiler adapter boundary
605
+
606
+ Decision: the runtime engine consumes `analyze()` and `transform()` rather than importing parser internals directly.
607
+
608
+ Reason: preserve the option to add TSX, tagged-source, or alternative transformer backends later.