@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 +259 -23
- package/package.json +22 -8
- package/src/children.ts +30 -0
- package/src/cleanup-finalization.ts +91 -0
- package/src/cleanup-policy.ts +39 -0
- package/src/component-factory.ts +62 -0
- package/src/context.ts +53 -0
- package/src/control.ts +6 -0
- package/src/dom.ts +3 -0
- package/src/dynamic.ts +90 -0
- package/src/events.ts +185 -0
- package/src/framework-native.ts +1 -0
- package/src/framework.ts +32 -115
- package/src/h.ts +2 -4
- package/src/index.ts +20 -246
- package/src/ir.ts +213 -0
- package/src/lazy.ts +62 -0
- package/src/portal-h.ts +42 -0
- package/src/portal.ts +56 -0
- package/src/props.ts +152 -0
- package/src/reactive.ts +1 -0
- package/src/solid.ts +198 -16
- package/src/ssr.ts +41 -0
- package/src/store.ts +7 -4
package/README.md
CHANGED
|
@@ -1,34 +1,270 @@
|
|
|
1
1
|
# `@tschk/moonshine-solid`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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/
|
|
17
|
-
"@tschk/moonshine": "^0.
|
|
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
|
-
"
|
|
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
|
}
|
package/src/children.ts
ADDED
|
@@ -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