@tschk/moonshine-solid 0.4.0 → 0.4.4

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,34 +1,270 @@
1
1
  # `@tschk/moonshine-solid`
2
2
 
3
- Solid renderer and signal bridge for Moonshine. Solid keeps its own native
4
- rendering model; Moonshine does not share a vnode with React.
3
+ Independent replacements for the supported Solid APIs, built on
4
+ [`@tschk/moonshine-dom`](../dom/README.md). This package imports no Solid runtime
5
+ and has no Solid dependency or peer dependency. The default Moonshine export
6
+ remains signal-only.
5
7
 
6
- This is a **hosting** adapter: `solid-js` is a real peer dependency, re-exported
7
- on subpaths, and Moonshine renders its output. It is not a reimplementation —
8
- unlike `@tschk/moonshine-next`, `-react-router`, `-tanstack` and `-waku`, you
9
- cannot alias `solid-js` onto this package and drop Solid. Solid's component
10
- format is compiled, so there is nothing to alias.
8
+ Recompile supported authored JSX/TSX with Crepuscularity. This package does not
9
+ implement Solid's compiled DOM instructions and cannot be aliased over arbitrary
10
+ `solid-js/web` output. Crepus lowers its admitted named `solid-js` primitives to
11
+ the native graph; unsupported APIs reject. Moonshine rejects generated native
12
+ modules retaining a Solid runtime import.
13
+
14
+ - `/solid`: native signals, memos, effects, roots, lifecycle, batching, `on`,
15
+ context providers, memoized `children`, `mergeProps`/`splitProps`, client resources, `catchError`, DOM
16
+ `ErrorBoundary`, and getter-based `For`, `Index`, `Show`, `Match`, `Switch`.
17
+ - `/store`: independent `createStore`, `produce`, `reconcile` on the same graph.
18
+ - `/h`: the native browser `h` factory, exported as default. Use explicit tag
19
+ names and props; Solid hyperscript selector shorthand is unsupported.
20
+ - `/portal`: opt-in `Portal` and browser `h` with logical delegated events.
21
+ - `/reactive`, `/control`, `/dom`: direct native APIs. `/control` takes explicit
22
+ accessors, while `/solid` adapts Solid-style prop getters.
23
+ - `/ssr`: escaped server factories, suppressed effects/mount callbacks and
24
+ explicit server IR rendering. Author server modules against this entry.
25
+ - `/framework-native`: the bundled native framework renderer, requiring the
26
+ optional framework peer.
27
+
28
+ `catchError` owns an attached scope and handles component, memo and effect
29
+ callback failures without changing dependency tracking. DOM `ErrorBoundary`
30
+ keeps its fallback until reset, cleans the failed branch before rendering the
31
+ fallback, and tracks fallback getters while invoking fallback callbacks untracked.
32
+ Use a children getter to construct protected children. Event callbacks, rejected
33
+ promises, cleanup failures and later lazy function/array children are outside
34
+ this boundary, matching the tested Solid behavior. Native signal commits route
35
+ errors before returning; writes through foreign core signals defer routing to a
36
+ microtask. Disposed queued handlers are canceled, and handler failures route
37
+ directly to the nearest outer handler; these ownership choices differ from some
38
+ observed host callbacks. SSR component use rejects explicitly; serialization
39
+ boundaries and compiler admission remain separate work.
11
40
 
12
41
  ```ts
13
- import { createSignal as solidSignal, For } from "@tschk/moonshine-solid/solid";
14
- import { parseCrepus } from "@tschk/crepus-moonshine";
15
- import { renderCrepusIrSolid } from "@tschk/moonshine-solid";
16
- import { createMoonshineSignal } from "@tschk/moonshine-solid";
42
+ import h from "@tschk/moonshine-solid/h";
43
+ import { createSignal, Show } from "@tschk/moonshine-solid/solid";
44
+ import { render } from "@tschk/moonshine-solid/dom";
45
+
46
+ const dispose = render(() => {
47
+ const [count, setCount] = createSignal(0);
48
+ return h(
49
+ "main",
50
+ null,
51
+ h("button", { onClick: () => setCount(count() + 1) }, count),
52
+ Show({
53
+ get when() {
54
+ return count();
55
+ },
56
+ children: (value) => () => "count " + value(),
57
+ }),
58
+ );
59
+ }, document.getElementById("app")!);
60
+ ```
61
+
62
+ `mergeProps` applies rightmost defined values, retaining getter reactivity and
63
+ undefined fallback. `splitProps` partitions props without evaluating getters.
64
+ Plain sources copy own string property descriptors/names; symbols and inherited
65
+ properties are omitted, matching Solid's plain-object path. Function sources
66
+ are memoized in the native owner and produce live readonly views: writes/deletes
67
+ are ignored, dynamic string keys update, and symbol reads remain available.
68
+ Wrap a native store in `() => store` when live key replacement is needed; these
69
+ helpers do not implement Solid's private proxy marker ABI. Plain copied data
70
+ properties remain writable. No component compiler instructions are introduced.
71
+
72
+ Use lazy getters or callbacks for branch contents so they are created in the
73
+ selected branch's owner. List children receive `(item, indexAccessor)` for `For`
74
+ and `(itemAccessor, index)` for `Index`. Empty lists can supply `fallback`.
75
+ `Switch` selects the first truthy `Match`, retaining it while it stays selected.
76
+ With `keyed: true`, `Show` and `Match` pass the raw truthy value to render
77
+ callbacks and replace owned branches when its identity changes. Without `keyed`,
78
+ callbacks receive a live accessor and retain the branch across truthy changes.
79
+ Old branch cleanup runs before replacement setup. Control descriptors are
80
+ source-level helpers, not a Solid compiler ABI.
81
+
82
+ `children(() => props.children)` memoizes the source and resolved contents separately,
83
+ flattens nested arrays and calls zero-argument child accessors. Parameterized render
84
+ callbacks retain their identity. `toArray()` preserves arrays, wraps scalar values
85
+ (including `false`, `0`, and empty strings), and returns `[]` for nullish values.
86
+ Create the helper under an owner so child computations and cleanup follow the
87
+ wrapper's lifetime; ordinary native SSR factories remain opaque children.
88
+
89
+ `createContext` and `useContext` use native owner scopes. Provider values are
90
+ captured once without tracking; pass an accessor or store for reactive values.
91
+ Children must be lazy getters/callbacks so descendants inherit their provider.
92
+ Undefined provider values use the context default. Render providers through the
93
+ chosen DOM or SSR `h` factory; deferred SSR descendants retain the provider owner
94
+ through serialization, with request-local cleanup. Browser contexts do not load
95
+ the SSR serializer. This does not implement existing Solid compiler instructions.
96
+
97
+ The root `renderCrepusNodeSolid` and `renderCrepusIrSolid` create native DOM.
98
+ The same names on `/ssr` create server HTML. They preserve static View IR kind,
99
+ class and root mappings; `if` and `forEach` remain descriptive placeholders and
100
+ must be lowered by Crepus to execute expressions. IR types use the optional
101
+ Crepus/WASM peer; rendering a supplied IR object does not load a parser.
102
+
103
+ `fromMoonshineSignal` and `createBridgedSignal` now return native pairs. Their
104
+ subscriptions dispose with the native owner. Root kernel exports such as
105
+ `createMsSignal`, `createMemo`, `createResource`, `batch` and `untrack` retain
106
+ their original kernel signatures; use `/solid` for the native pair/resource API.
107
+ Do not mix independent observers with a separately installed Solid graph.
108
+
109
+ `solidRenderer` retains its compatibility name and renders native synchronous
110
+ SSR/static components and fresh SPA shells using the native document renderer.
111
+ It returns buffered HTML, with escaped data and owned per-request cleanup.
112
+ Existing `renderer: "solid"` configurations must port server modules to `/ssr`;
113
+ new `moonshine new --solid` apps use the full native build and preview path.
114
+ `hydrateSolid` throws an actionable unsupported error; use `render` for a fresh
115
+ mount. Hydration, islands, progressive streaming, async SSR components, Suspense
116
+ and full Solid resource options are unsupported.
117
+
118
+ See [native API behavior and limits](../dom/README.md) and the
119
+ [compiled TSX example](../../examples/native-template/README.md). Benchmarks in
120
+ `scripts/benchmark-reactive.ts`, `benchmark-dom.ts` and `benchmark-ssr.ts` use
121
+ Solid only as a development comparison. They do not establish universal speed
122
+ or framework equivalence.
123
+
124
+ ### Client component loading
125
+
126
+ `/solid` exports `lazy(loader)`. The loader resolves a module whose `default` is a
127
+ native component function. Call the returned component under a live native owner;
128
+ it returns an accessor, initially empty while loading, then initializes a separate
129
+ owned component per instance with live props and context. Disposing one pending
130
+ instance does not prevent other instances from loading.
131
+
132
+ `Lazy.preload()` may run without an owner on the client and returns the validated
133
+ module promise. Calls share that cached promise; its identity need not equal the
134
+ loader's original promise. Completed preloads permit synchronous setup. Async
135
+ rejections remain cached; synchronous loader throws can be retried. Malformed
136
+ modules reject with `TypeError` rather than silently rendering empty. Internal
137
+ promise observation prevents orphan rejections, while the returned promise still
138
+ rejects for callers. Mounted failures use native owner error handling.
17
139
 
18
- const view = renderCrepusIrSolid(parseCrepus(source), { rootTag: "main" });
140
+ Invocation and preload reject during synchronous SSR before running the loader,
141
+ including native SSR component initialization. This entry supplies no Suspense,
142
+ hydration, streaming or asynchronous SSR behavior.
143
+
144
+ ### Opt-in portals
145
+
146
+ Import `{ h, Portal }` from `@tschk/moonshine-solid/portal` for the whole logical
147
+ subtree. Its standard delegated events follow component ancestry across nested
148
+ portals; `on:custom`, `oncapture:click` and listener objects use physical DOM
149
+ listeners. Existing `/h` keeps native direct-event behavior. Handler objects retain
150
+ `capture`, `once`, `passive` and `signal` options captured at registration.
151
+
152
+ Use a children getter so the portal owns construction and cleanup. The default
153
+ mount is `document.body`; explicit same-origin document mounts are supported.
154
+ `isSVG` creates an SVG container. Head mounts insert an independently owned range
155
+ without a wrapper. `useShadow` uses an open shadow root when the container supports
156
+ it. Hydration is unsupported. This entry is not a `solid-js/web`
157
+ alias. For SSR import both `h` and `Portal` from `/ssr`: it emits nothing
158
+ and does not evaluate mount or children.
159
+
160
+ Foreign documents acquire all 22 delegated event types while a portal is alive;
161
+ portals sharing a document share those listeners, and the last disposal removes
162
+ them. Unlike the tested Solid iframe behavior, normal delegated handlers and
163
+ logical ancestors work in same-origin iframes. Browser security rules still block
164
+ access to cross-origin documents. Disposing an owner after iframe navigation
165
+ cleans its old document and does not affect the new document.
166
+
167
+ ```ts
168
+ import { h, Portal } from "@tschk/moonshine-solid/portal";
169
+ import { render } from "@tschk/moonshine-solid/dom";
170
+ const close = render(
171
+ () =>
172
+ h(Portal, {
173
+ get children() {
174
+ return h("button", { onClick: () => close() }, "close");
175
+ },
176
+ }),
177
+ document.querySelector("#app")!,
178
+ );
19
179
  ```
20
180
 
21
- | Subpath | Provides |
22
- | --------- | ----------------------------------------- |
23
- | `.` | Solid renderer, Crepus IR, signal bridges |
24
- | `./solid` | `solid-js` re-exports |
25
- | `./h` | `solid-js/h` factory |
26
- | `./store` | Solid store helpers |
181
+ `/dynamic` provides native `Dynamic` and `createDynamic` for choosing a component
182
+ function or intrinsic tag. `Dynamic` forwards live prop getters and omits its
183
+ `component` prop. Component changes dispose the previous branch; prop changes
184
+ retain its nodes. Intrinsic props use ordinary Solid values/getters, rather than
185
+ native `h` accessor-valued attributes. Ref callbacks run once; reactive events
186
+ reuse the adapter's event policy. Undefined selection renders nothing.
187
+
188
+ Import the server counterparts from `/ssr`: selection runs once per factory call
189
+ and component work belongs to the render request. The browser entry rejects SSR.
190
+ These entries use native DOM/HTML factories and their existing attribute, style,
191
+ raw-HTML, and form constraints; they do not provide Solid's compiled web ABI or
192
+ hydration. Root and `/solid` imports do not include the optional browser factory.
193
+
194
+ Common SVG tags such as `circle`, `path`, `foreignObject`, and `svg` select their
195
+ native namespace automatically. Pass `xmlns: "http://www.w3.org/2000/svg"` for
196
+ ambiguous dynamic SVG tags such as `a` and `title`, including when mounted inside
197
+ an SVG parent: parent namespace context alone does not persist across memo
198
+ updates. This explicit namespace behavior differs from Solid's Dynamic strings,
199
+ which create `a` and `title` in the HTML namespace.
200
+
201
+ A production Dynamic application comparison against Solid 1.9.14 measured
202
+ 6,883 gzip bytes for Moonshine and 7,521 for Solid, including the same harness.
203
+ In Chromium, nine alternating samples of 20,000 operations measured median
204
+ prop updates at 8.3/8.1 ms, component replacements at 47.2/28.3 ms, and reactive
205
+ event updates with clicks at 46.9/37.8 ms (Moonshine/Solid). This workload is
206
+ smaller with Moonshine but replacements and event updates are slower. Updating
207
+ delegated handlers in place reduced event time from 55.7 to 46.9 ms in the same
208
+ run, at a cost of 79 gzip bytes; ordinary prop/replacement timings overlapped
209
+ the previous implementation. Startup timings were below useful precision.
210
+ These are native factory measurements; they do not establish compiled
211
+ component or hydration performance.
212
+
213
+ Run `bun scripts/benchmark-dynamic.ts` from the repository root to reproduce
214
+ the checked workload. It uses the installed Solid reference and Python
215
+ Playwright/Chromium, prints both application sizes and every timing sample,
216
+ and rejects runtime source changes during measurement.
217
+
218
+ The local Crepus source compiler accepts explicit type-only Solid `JSX` imports
219
+ for `JSX.Element`, mapped to native DOM or SSR child types. Aliases work; other
220
+ namespace members and runtime namespace uses diagnose. Ordinary authored TSX
221
+ component calls preserve named/imported interface props, correlated generic
222
+ callbacks and cached child types. Wrong/missing props and incompatible children
223
+ remain errors in the generated graph. This does not implement the full Solid
224
+ JSX namespace or precompiled runtime ABI. Svelte's separate generated descriptor
225
+ path retains its existing prop-call typing limit.
226
+
227
+ Run the persistent authored-source checks with
228
+ `bun run check:components --compiler /path/to/crepus`. The gate copies and
229
+ fingerprints the compiler, checks generated DOM/SSR graphs with both TypeScript
230
+ lanes, rejects invalid authored calls, and exercises two independent browser
231
+ instances, live props, focus/identity, disposal and fresh escaped SSR. Omitting
232
+ `--compiler` keeps the existing runtime-only component gate.
233
+ Native sibling cleanup order and SSR cleanup timing still differ from Solid;
234
+ these source checks do not establish lifecycle parity.
235
+
236
+ The optional handwritten `/dom` and `/h` function factories share the current
237
+ Solid owner. `/solid` roots select reverse child/callback cleanup, and Show/Switch
238
+ keep their branch ownership. Cleanup callbacks see connected old DOM; resources
239
+ stop immediately, then outer roots detach while retaining their descendant tree.
240
+ Dynamic and lazy use this factory. Failed cleanup drains peers and preserves the
241
+ first error, retaining native safety rather than Solid's skipped peer cleanup.
242
+
243
+ Known native/Svelte/Vue component boundaries reset this policy. Unmarked generic
244
+ roots inherit active policy; wrap foreign components with their independent
245
+ native factory. SSR uses deterministic synchronous FIFO cleanup rather than
246
+ Solid's deferred server cleanup. Generated source still uses native factories,
247
+ so the handwritten policy does not establish source lifecycle parity. Dynamic/createDynamic require a live owner before selector reads or setup. This
248
+ intentionally rejects pinned Solid's ownerless setup without a disposal handle.
249
+
250
+ ### Explicit authored Solid source
27
251
 
28
- View IR types come from `@tschk/crepuscularity-wasm`, the same source of truth the React
29
- renderer uses. Template class tokens (`style.classes`) are emitted verbatim as
30
- `class`; View IR style hints are never converted to inline CSS.
252
+ A complete leading `/** @jsxImportSource solid-js */` directive selects bounded
253
+ native Solid lowering in Crepuscularity. Local stable synchronous component
254
+ factories preserve the tested owner, mount and reverse cleanup behavior; native
255
+ accessor controls keep their distinct compiler ABI. Recompile authored source;
256
+ this does not execute existing Solid compiler instructions.
31
257
 
32
- Peer: `solid-js` >= 1.8.
258
+ `bun run check:sfc --compiler /path/to/crepus` now includes six generated Solid
259
+ modules alongside all previous Svelte/Vue cases: **734 generated modules / 728
260
+ emitted TypeScript modules**, frozen compiler `82e517ca`. Both strict checkers,
261
+ five pinned-reference browser phases, focus/node retention, connected cleanup,
262
+ fresh escaped SSR and selector/root/publication guards pass. The native browser
263
+ and SSR bundles contain no Solid runtime or parser WASM. Strict legacy IR export
264
+ types still resolve the adapter's declared optional Crepus WASM type peer.
33
265
 
34
- See [Moonshine README](../../README.md) and [DESIGN.md](../../DESIGN.md).
266
+ This requires explicit manual native bootstrap. Existing tsconfig selection,
267
+ normal raw-TSX project builds, imported/foreign component dispatch, hydration and
268
+ the full compiler ABI remain unfinished. Native SSR cleanup remains synchronous
269
+ FIFO; no full server cleanup timing parity is claimed. Source acceptance adds no
270
+ Moonshine runtime changes or new performance claim.
package/package.json CHANGED
@@ -1,28 +1,34 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-solid",
3
- "version": "0.4.0",
4
- "description": "Solid adapter — moonshine IR bridge + solid-js library re-exports",
3
+ "version": "0.4.4",
4
+ "description": "Independent Moonshine replacement for supported Solid APIs and Crepus IR",
5
5
  "license": "ISC",
6
6
  "type": "module",
7
7
  "main": "./src/index.ts",
8
8
  "types": "./src/index.ts",
9
9
  "exports": {
10
10
  ".": "./src/index.ts",
11
+ "./reactive": "./src/reactive.ts",
12
+ "./dom": "./src/dom.ts",
13
+ "./control": "./src/control.ts",
14
+ "./ssr": "./src/ssr.ts",
15
+ "./framework-native": "./src/framework-native.ts",
11
16
  "./solid": "./src/solid.ts",
12
17
  "./store": "./src/store.ts",
13
- "./h": "./src/h.ts"
18
+ "./h": "./src/h.ts",
19
+ "./portal": "./src/portal.ts",
20
+ "./dynamic": "./src/dynamic.ts"
14
21
  },
15
22
  "dependencies": {
16
- "@tschk/crepuscularity-wasm": "^0.1.3",
17
- "@tschk/moonshine": "^0.3.6",
18
- "@tschk/moonshine-framework": "^0.3.6",
19
- "@tschk/crepus-moonshine": "^0.3.6"
23
+ "@tschk/moonshine": "^0.4.4",
24
+ "@tschk/moonshine-dom": "^0.4.4"
20
25
  },
21
26
  "devDependencies": {
22
27
  "typescript": "7.0.2"
23
28
  },
24
29
  "peerDependencies": {
25
- "solid-js": ">=1.8"
30
+ "@tschk/crepuscularity-wasm": "^0.1.3",
31
+ "@tschk/moonshine-framework": "^0.4.4"
26
32
  },
27
33
  "files": [
28
34
  "src",
@@ -38,5 +44,13 @@
38
44
  "lint": "bunx oxlint .",
39
45
  "typecheck": "bunx tsc --noEmit",
40
46
  "test": "bun test"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@tschk/crepuscularity-wasm": {
50
+ "optional": true
51
+ },
52
+ "@tschk/moonshine-framework": {
53
+ "optional": true
54
+ }
41
55
  }
42
56
  }
@@ -0,0 +1,30 @@
1
+ /** Memoized native child normalization; no JSX compiler instruction ABI. */
2
+ import { createMemo, type Accessor } from "@tschk/moonshine-dom/reactive";
3
+
4
+ export type ResolvedChildren<T> = T extends (...args: infer P) => infer R
5
+ ? P extends []
6
+ ? ResolvedChildren<R>
7
+ : T
8
+ : T extends readonly (infer U)[]
9
+ ? Array<ChildValue<ResolvedChildren<U>>>
10
+ : T;
11
+ export type ChildValue<T> = T extends readonly (infer U)[] ? ChildValue<U> : T;
12
+
13
+ function resolve(value: any): any {
14
+ while (typeof value === "function" && value.length === 0) value = value();
15
+ return Array.isArray(value)
16
+ ? value.flatMap((child) => resolve(child))
17
+ : value;
18
+ }
19
+ export function children<T>(fn: Accessor<T>): Accessor<ResolvedChildren<T>> & {
20
+ toArray(): Array<ChildValue<ResolvedChildren<T>>>;
21
+ } {
22
+ const source = createMemo(fn);
23
+ const resolved = createMemo(() => resolve(source()));
24
+ return Object.assign(resolved, {
25
+ toArray: () => {
26
+ const value = resolved();
27
+ return Array.isArray(value) ? value : value == null ? [] : [value];
28
+ },
29
+ });
30
+ }
@@ -0,0 +1,91 @@
1
+ import {
2
+ cleanupOrderKey,
3
+ getOwner,
4
+ onCleanup,
5
+ cleanupTransactionActive,
6
+ requestCleanupFinalizerFlush,
7
+ } from "@tschk/moonshine-dom/reactive";
8
+ import type { CleanupPolicy } from "@tschk/moonshine-dom/reactive";
9
+ export function retainRoot(value: unknown): void {
10
+ if (Array.isArray(value)) {
11
+ for (const child of value) retainRoot(child);
12
+ return;
13
+ }
14
+ if (
15
+ getOwner()?.context?.get(cleanupOrderKey) === solidCleanupPolicy &&
16
+ typeof Node !== "undefined" &&
17
+ value instanceof Node
18
+ )
19
+ onCleanup(() => retainCleanupRoot(value));
20
+ }
21
+ let roots: Set<Node> | undefined;
22
+ export function retainCleanupRoot(node: Node): void {
23
+ (roots ??= new Set()).add(node);
24
+ requestCleanupFinalizerFlush(flush);
25
+ }
26
+ let finalizers: Array<() => void> | undefined,
27
+ draining = false;
28
+ export function queueCleanupFinalizer(fn: () => void): void {
29
+ if (!cleanupTransactionActive() && !draining) {
30
+ fn();
31
+ return;
32
+ }
33
+ (finalizers ??= []).push(fn);
34
+ requestCleanupFinalizerFlush(flush);
35
+ }
36
+ function flush() {
37
+ if (cleanupTransactionActive() || draining || (!finalizers && !roots)) return;
38
+ draining = true;
39
+ let first: unknown,
40
+ failed = false;
41
+ let waves = 0;
42
+ try {
43
+ while (finalizers) {
44
+ if (++waves > 1000) {
45
+ finalizers = undefined;
46
+ if (!failed)
47
+ first = new Error("Solid cleanup finalizer update limit exceeded");
48
+ failed = true;
49
+ break;
50
+ }
51
+ const callbacks = finalizers;
52
+ finalizers = undefined;
53
+ for (const callback of callbacks)
54
+ try {
55
+ callback();
56
+ } catch (error) {
57
+ if (!failed) first = error;
58
+ failed = true;
59
+ }
60
+ }
61
+ } finally {
62
+ finalizers = undefined;
63
+ roots = undefined;
64
+ draining = false;
65
+ }
66
+ if (failed) throw first;
67
+ }
68
+ export const solidCleanupPolicy: CleanupPolicy = {
69
+ reverse: true,
70
+ deferRemoval(owned, anchor, retain, remove) {
71
+ const oldParent = anchor.parentNode;
72
+ queueCleanupFinalizer(() =>
73
+ remove(owned, undefined, (node) => {
74
+ const adopted =
75
+ node !== anchor &&
76
+ (node.parentNode !== oldParent ||
77
+ anchor.parentNode !== oldParent ||
78
+ !(
79
+ node.compareDocumentPosition(anchor) &
80
+ Node.DOCUMENT_POSITION_FOLLOWING
81
+ ));
82
+ let ancestor = node.parentNode;
83
+ while (ancestor) {
84
+ if (roots?.has(ancestor)) return true;
85
+ ancestor = ancestor.parentNode;
86
+ }
87
+ return adopted || !!retain?.(node);
88
+ }),
89
+ );
90
+ },
91
+ };
@@ -0,0 +1,39 @@
1
+ import { isSSRRendering } from "@tschk/moonshine-dom/ssr-owner";
2
+ import { peekComponentContext } from "@tschk/moonshine-dom/component";
3
+ import { retainRoot, solidCleanupPolicy } from "./cleanup-finalization";
4
+ import { cleanupOrderKey } from "@tschk/moonshine-dom/reactive";
5
+ import {
6
+ getOwner,
7
+ createRoot as nativeRoot,
8
+ } from "@tschk/moonshine-dom/reactive";
9
+ import {
10
+ render as nativeRender,
11
+ type DOMChild,
12
+ } from "@tschk/moonshine-dom/dom";
13
+ export function install() {
14
+ const owner = getOwner();
15
+ if (!owner || owner.disposed) throw Error("live owner");
16
+ if (isSSRRendering() || peekComponentContext()?.mode === "ssr") {
17
+ if (owner.context?.has(cleanupOrderKey)) {
18
+ owner.context = new Map(owner.context);
19
+ owner.context.delete(cleanupOrderKey);
20
+ }
21
+ return;
22
+ }
23
+ owner.context = new Map(owner.context);
24
+ owner.context.set(cleanupOrderKey, solidCleanupPolicy);
25
+ }
26
+ export function createRoot<T>(fn: (stop: () => void) => T): T {
27
+ return nativeRoot((stop) => {
28
+ install();
29
+ return fn(stop);
30
+ });
31
+ }
32
+ export function render(fn: () => DOMChild, node: Element | ShadowRoot) {
33
+ return nativeRender(() => {
34
+ install();
35
+ const value = fn();
36
+ retainRoot(value);
37
+ return value;
38
+ }, node);
39
+ }
@@ -0,0 +1,62 @@
1
+ /** Solid function components share their consumer owner; native/foreign components use native h. */
2
+ import { h as nativeH, type DOMChild } from "@tschk/moonshine-dom/dom";
3
+ import { getOwner, untrack } from "@tschk/moonshine-dom/reactive";
4
+ import {
5
+ peekComponentContext,
6
+ withComponentConsumer,
7
+ type ComponentContext,
8
+ } from "@tschk/moonshine-dom/component";
9
+ import { retainRoot } from "./cleanup-finalization";
10
+ export function createComponent<P, T>(fn: (props: P) => T, props: P): T {
11
+ const owner = getOwner();
12
+ if (!owner || owner.disposed)
13
+ throw Error("Solid components require a live native owner");
14
+ const frame: ComponentContext<P> = {
15
+ owner,
16
+ props,
17
+ mode: "dom",
18
+ contexts: new Map(peekComponentContext()?.contexts),
19
+ active: false,
20
+ };
21
+ const value = withComponentConsumer(frame, () => untrack(() => fn(props)));
22
+ let first: unknown,
23
+ failed = false;
24
+ for (const ready of frame.ready ?? []) {
25
+ if (owner.disposed) break;
26
+ try {
27
+ ready();
28
+ } catch (error) {
29
+ if (!failed) first = error;
30
+ failed = true;
31
+ }
32
+ }
33
+ if (failed) throw first;
34
+ retainRoot(value);
35
+ return value;
36
+ }
37
+ export const h: typeof nativeH = ((
38
+ tag: string | ((props: any) => DOMChild),
39
+ props: any = null,
40
+ ...children: DOMChild[]
41
+ ) => {
42
+ if (typeof tag !== "function") {
43
+ const node = nativeH(tag, props, ...children);
44
+ retainRoot(node);
45
+ return node;
46
+ }
47
+ const next = children.length
48
+ ? Object.defineProperties(
49
+ {},
50
+ {
51
+ ...Object.getOwnPropertyDescriptors(props ?? {}),
52
+ children: {
53
+ value: children,
54
+ enumerable: true,
55
+ writable: true,
56
+ configurable: true,
57
+ },
58
+ },
59
+ )
60
+ : (props ?? {});
61
+ return createComponent(tag, next);
62
+ }) as typeof nativeH;
package/src/context.ts ADDED
@@ -0,0 +1,53 @@
1
+ /** Independent owner-scoped context; lazy children must be created under Provider. */
2
+ import {
3
+ createComponentScope,
4
+ getOwner,
5
+ untrack,
6
+ } from "@tschk/moonshine-dom/reactive";
7
+
8
+ import { peekComponentContext } from "@tschk/moonshine-dom/component";
9
+ import { scopeSSRChild } from "@tschk/moonshine-dom/ssr-owner";
10
+
11
+ import { children } from "./children";
12
+
13
+ export interface Context<T> {
14
+ id: symbol;
15
+ Provider: (props: { value: T; children?: any }) => any;
16
+ defaultValue: T;
17
+ }
18
+ export function createContext<T>(
19
+ defaultValue?: undefined,
20
+ options?: { name?: string },
21
+ ): Context<T | undefined>;
22
+ export function createContext<T>(
23
+ defaultValue: T,
24
+ options?: { name?: string },
25
+ ): Context<T>;
26
+ export function createContext<T>(
27
+ defaultValue?: T,
28
+ _options?: { name?: string },
29
+ ): Context<T | undefined> {
30
+ const id = Symbol("context");
31
+ return {
32
+ id,
33
+ defaultValue,
34
+ Provider(props) {
35
+ return createComponentScope(() => {
36
+ const owner = getOwner()!;
37
+ owner.context = new Map(owner.context);
38
+ owner.context.set(
39
+ id,
40
+ untrack(() => props.value),
41
+ );
42
+ const resolved = children(() => props.children);
43
+ return peekComponentContext()?.mode === "ssr"
44
+ ? scopeSSRChild(resolved)
45
+ : resolved;
46
+ });
47
+ },
48
+ };
49
+ }
50
+ export function useContext<T>(context: Context<T>): T {
51
+ const value = getOwner()?.context?.get(context.id);
52
+ return value === undefined ? context.defaultValue : (value as T);
53
+ }
package/src/control.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Native accessor-shaped compiler controls; public /solid controls keep their distinct props ABI. */
2
+ export * from "@tschk/moonshine-dom/control";
3
+ import { Show as nativeShow } from "@tschk/moonshine-dom/control";
4
+ import { createComponentScope, untrack } from "@tschk/moonshine-dom/reactive";
5
+ export const createSolidShow: typeof nativeShow = (props, schedule) =>
6
+ createComponentScope(() => untrack(() => nativeShow(props, schedule)));
package/src/dom.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "@tschk/moonshine-dom/dom";
2
+ export { render } from "./cleanup-policy";
3
+ export { h } from "./component-factory";