@pyreon/svelte-compat 0.17.0 → 0.26.3
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/package.json +5 -10
- package/src/env.d.ts +0 -6
- package/src/index.ts +0 -538
- package/src/jsx-dev-runtime.ts +0 -1
- package/src/jsx-runtime.ts +0 -316
- package/src/store.ts +0 -26
- package/src/svelte-compat.browser.test.ts +0 -67
- package/src/tests/child-instance-leak-repro.test.ts +0 -123
- package/src/tests/lifecycle-cleanup-leak-repro.test.ts +0 -81
- package/src/tests/native-marker-bypass.test.tsx +0 -72
- package/src/tests/setup.ts +0 -3
- package/src/tests/store-entry.test.ts +0 -36
- package/src/tests/svelte-compat.test.ts +0 -547
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/svelte-compat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.3",
|
|
4
4
|
"description": "Svelte-compatible API shim for Pyreon — write Svelte-style stores / lifecycle code that runs on Pyreon's reactive engine",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/svelte-compat#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"lib",
|
|
17
|
-
"src",
|
|
18
17
|
"README.md",
|
|
19
18
|
"LICENSE"
|
|
20
19
|
],
|
|
@@ -25,22 +24,18 @@
|
|
|
25
24
|
"types": "./lib/types/index.d.ts",
|
|
26
25
|
"exports": {
|
|
27
26
|
".": {
|
|
28
|
-
"bun": "./src/index.ts",
|
|
29
27
|
"import": "./lib/index.js",
|
|
30
28
|
"types": "./lib/types/index.d.ts"
|
|
31
29
|
},
|
|
32
30
|
"./store": {
|
|
33
|
-
"bun": "./src/store.ts",
|
|
34
31
|
"import": "./lib/store.js",
|
|
35
32
|
"types": "./lib/types/store.d.ts"
|
|
36
33
|
},
|
|
37
34
|
"./jsx-runtime": {
|
|
38
|
-
"bun": "./src/jsx-runtime.ts",
|
|
39
35
|
"import": "./lib/jsx-runtime.js",
|
|
40
36
|
"types": "./lib/types/jsx-runtime.d.ts"
|
|
41
37
|
},
|
|
42
38
|
"./jsx-dev-runtime": {
|
|
43
|
-
"bun": "./src/jsx-runtime.ts",
|
|
44
39
|
"import": "./lib/jsx-runtime.js",
|
|
45
40
|
"types": "./lib/types/jsx-runtime.d.ts"
|
|
46
41
|
}
|
|
@@ -61,12 +56,12 @@
|
|
|
61
56
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
62
57
|
"@pyreon/test-utils": "^0.13.13",
|
|
63
58
|
"@pyreon/vitest-config": "0.13.1",
|
|
64
|
-
"@vitest/browser-playwright": "^4.1.
|
|
59
|
+
"@vitest/browser-playwright": "^4.1.8",
|
|
65
60
|
"happy-dom": "^20.9.0"
|
|
66
61
|
},
|
|
67
62
|
"peerDependencies": {
|
|
68
|
-
"@pyreon/core": "^0.26.
|
|
69
|
-
"@pyreon/reactivity": "^0.26.
|
|
70
|
-
"@pyreon/runtime-dom": "^0.26.
|
|
63
|
+
"@pyreon/core": "^0.26.3",
|
|
64
|
+
"@pyreon/reactivity": "^0.26.3",
|
|
65
|
+
"@pyreon/runtime-dom": "^0.26.3"
|
|
71
66
|
}
|
|
72
67
|
}
|
package/src/env.d.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @pyreon/svelte-compat
|
|
3
|
-
*
|
|
4
|
-
* Svelte-compatible **importable runtime API** powered by Pyreon's
|
|
5
|
-
* reactive engine. Mirrors the scope boundary of the sibling compat
|
|
6
|
-
* layers (react/preact/vue/solid-compat): it shims the APIs Svelte code
|
|
7
|
-
* actually `import`s —
|
|
8
|
-
*
|
|
9
|
-
* - `svelte/store` → `writable` `readable` `derived` `get` `readonly`
|
|
10
|
-
* - `svelte` → `onMount` `onDestroy` `beforeUpdate` `afterUpdate`
|
|
11
|
-
* `tick` `setContext` `getContext` `hasContext`
|
|
12
|
-
* `getAllContexts` `createEventDispatcher`
|
|
13
|
-
* `mount` `unmount` `flushSync`
|
|
14
|
-
*
|
|
15
|
-
* It does NOT implement the `.svelte` single-file-component compiler or
|
|
16
|
-
* the non-importable Svelte 5 rune *syntax* (`$state`/`$derived`/
|
|
17
|
-
* `$effect`) — those are compiler constructs, not runtime imports, the
|
|
18
|
-
* same boundary solid-compat draws around Solid's compiler. Components
|
|
19
|
-
* here are plain functions returning JSX that run on Pyreon via the
|
|
20
|
-
* shared compat JSX runtime (re-render on store change).
|
|
21
|
-
*
|
|
22
|
-
* Store model: a faithful Svelte store — a plain Set of subscribers
|
|
23
|
-
* notified synchronously on `set`/`update` (signal-free, exactly like
|
|
24
|
-
* Svelte's own `writable`; `derived` subscribes to its inputs
|
|
25
|
-
* explicitly). The store contract (`subscribe(run, invalidate?) →
|
|
26
|
-
* unsubscribe`, lazy `start(set, update?) → stop` notifier) matches
|
|
27
|
-
* Svelte exactly. Subscribing inside a compat component body re-renders
|
|
28
|
-
* it on store change (the faithful `$store` auto-subscription
|
|
29
|
-
* equivalent) without a disposable tracking effect.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'
|
|
33
|
-
import {
|
|
34
|
-
ErrorBoundary,
|
|
35
|
-
For,
|
|
36
|
-
Match,
|
|
37
|
-
nativeCompat,
|
|
38
|
-
createContext as pyreonCreateContext,
|
|
39
|
-
onMount as pyreonOnMount,
|
|
40
|
-
onUnmount as pyreonOnUnmount,
|
|
41
|
-
provide as pyreonProvide,
|
|
42
|
-
useContext as pyreonUseContext,
|
|
43
|
-
Show,
|
|
44
|
-
Suspense,
|
|
45
|
-
Switch,
|
|
46
|
-
} from '@pyreon/core'
|
|
47
|
-
import { mount as pyreonMount } from '@pyreon/runtime-dom'
|
|
48
|
-
import { getCurrentCtx, getHookIndex, jsx } from './jsx-runtime'
|
|
49
|
-
|
|
50
|
-
// Dev-mode counter sink — see packages/internals/perf-harness for contract.
|
|
51
|
-
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
52
|
-
|
|
53
|
-
// ─── Store types (Svelte API surface) ───────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
export type Subscriber<T> = (value: T) => void
|
|
56
|
-
export type Invalidator<T> = (value?: T) => void
|
|
57
|
-
export type Unsubscriber = () => void
|
|
58
|
-
export type Updater<T> = (value: T) => T
|
|
59
|
-
/** `(set, update?) => stop?` — lazy notifier, runs on first subscriber. */
|
|
60
|
-
export type StartStopNotifier<T> = (
|
|
61
|
-
set: (value: T) => void,
|
|
62
|
-
update: (fn: Updater<T>) => void,
|
|
63
|
-
) => Unsubscriber | void
|
|
64
|
-
|
|
65
|
-
export interface Readable<T> {
|
|
66
|
-
subscribe(run: Subscriber<T>, invalidate?: Invalidator<T>): Unsubscriber
|
|
67
|
-
}
|
|
68
|
-
export interface Writable<T> extends Readable<T> {
|
|
69
|
-
set(value: T): void
|
|
70
|
-
update(updater: Updater<T>): void
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const noop = () => {
|
|
74
|
-
/* noop */
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Internal subscriptions (`get()`, `derived`'s input wiring) must NOT take
|
|
78
|
-
// the render-aware hook-indexed path even when they happen to run during a
|
|
79
|
-
// component render — doing so would consume the component's hook indices and
|
|
80
|
-
// desync onMount/onDestroy. This depth counter suppresses the render-aware
|
|
81
|
-
// branch for the duration of an internal subscribe.
|
|
82
|
-
let _plainDepth = 0
|
|
83
|
-
function plainSubscribe<R>(fn: () => R): R {
|
|
84
|
-
_plainDepth++
|
|
85
|
-
try {
|
|
86
|
-
return fn()
|
|
87
|
-
} finally {
|
|
88
|
-
_plainDepth--
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ─── writable ────────────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Svelte's `safe_not_equal`: primitives dedup, objects/functions always
|
|
96
|
-
* notify (so in-place-mutated store objects still propagate).
|
|
97
|
-
*/
|
|
98
|
-
function safeNotEqual(a: unknown, b: unknown): boolean {
|
|
99
|
-
// eslint-disable-next-line no-self-compare
|
|
100
|
-
return a != a
|
|
101
|
-
? // eslint-disable-next-line no-self-compare
|
|
102
|
-
b == b
|
|
103
|
-
: a !== b || (a !== null && typeof a === 'object') || typeof a === 'function'
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
interface SubEntry<T> {
|
|
107
|
-
run: Subscriber<T>
|
|
108
|
-
invalidate: Invalidator<T>
|
|
109
|
-
/** Component re-render trigger — set only for render-aware subscriptions. */
|
|
110
|
-
rerender?: () => void
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Svelte-compatible `writable`. A faithful Svelte store: a Set of
|
|
115
|
-
* subscribers notified synchronously on `set`/`update`. NOT signal-
|
|
116
|
-
* backed — Svelte's own `writable` is signal-free, and `derived` here
|
|
117
|
-
* subscribes to its inputs explicitly, so no signal auto-tracking is
|
|
118
|
-
* needed.
|
|
119
|
-
*
|
|
120
|
-
* Subscribing inside a compat component body is the faithful equivalent
|
|
121
|
-
* of Svelte's `$store` auto-subscription: the subscriber carries the
|
|
122
|
-
* component's `scheduleRerender`, so a store write re-renders the
|
|
123
|
-
* component — the same "write drives re-render" model the sibling
|
|
124
|
-
* solid-compat layer uses. Crucially this is NOT a persistent tracking
|
|
125
|
-
* effect: such an effect, created inside the wrapper accessor's run, is
|
|
126
|
-
* collected as an inner effect and disposed on the NEXT re-render (the
|
|
127
|
-
* cached path never recreates it), so store changes stopped propagating
|
|
128
|
-
* after the first one. A plain subscriber Set has no such hazard — it
|
|
129
|
-
* lives until `unsub` (registered in `ctx.unmountCallbacks`).
|
|
130
|
-
*
|
|
131
|
-
* `start` runs when the subscriber count goes 0→1 and its returned
|
|
132
|
-
* `stop` runs at 1→0. Synchronous `set` calls inside `start` mutate the
|
|
133
|
-
* value but do NOT notify (the store isn't "ready" until `start`
|
|
134
|
-
* returns), so the first subscriber sees exactly the post-start value —
|
|
135
|
-
* matching Svelte's `derived` (one emission, no spurious initial).
|
|
136
|
-
*/
|
|
137
|
-
export function writable<T>(value?: T, start: StartStopNotifier<T> = noop): Writable<T> {
|
|
138
|
-
let v = value as T
|
|
139
|
-
let stop: Unsubscriber | void
|
|
140
|
-
const subs = new Set<SubEntry<T>>()
|
|
141
|
-
|
|
142
|
-
const setVal = (next: T): void => {
|
|
143
|
-
if (!safeNotEqual(v, next)) return
|
|
144
|
-
v = next
|
|
145
|
-
if (!stop) return // not "ready" — Svelte's gate (start hasn't returned)
|
|
146
|
-
for (const s of subs) s.invalidate(v)
|
|
147
|
-
for (const s of subs) {
|
|
148
|
-
s.run(v)
|
|
149
|
-
s.rerender?.()
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const set = (next: T): void => setVal(next)
|
|
153
|
-
const update = (fn: Updater<T>): void => setVal(fn(v))
|
|
154
|
-
|
|
155
|
-
const addSub = (entry: SubEntry<T>): Unsubscriber => {
|
|
156
|
-
subs.add(entry)
|
|
157
|
-
if (subs.size === 1) stop = start(set, update) || noop
|
|
158
|
-
entry.run(v) // Svelte: subscriber invoked immediately with current value
|
|
159
|
-
return () => {
|
|
160
|
-
subs.delete(entry)
|
|
161
|
-
if (subs.size === 0 && stop) {
|
|
162
|
-
stop()
|
|
163
|
-
stop = undefined
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
set,
|
|
170
|
-
update,
|
|
171
|
-
subscribe(run: Subscriber<T>, invalidate: Invalidator<T> = noop): Unsubscriber {
|
|
172
|
-
// Render-aware path: inside a compat component body (and not an
|
|
173
|
-
// internal `_plainDepth` subscription), carry the component's
|
|
174
|
-
// scheduleRerender so a store write re-renders it. Hook-indexed so
|
|
175
|
-
// the one live subscription is created exactly once across
|
|
176
|
-
// re-renders; the cached pass just refreshes the component-local.
|
|
177
|
-
const ctx = _plainDepth === 0 ? getCurrentCtx() : null
|
|
178
|
-
if (ctx) {
|
|
179
|
-
const idx = getHookIndex()
|
|
180
|
-
const cached = ctx.hooks[idx] as { unsub: Unsubscriber } | undefined
|
|
181
|
-
if (cached) {
|
|
182
|
-
run(v)
|
|
183
|
-
// Re-push the cached unsub into the (possibly-reset)
|
|
184
|
-
// unmountCallbacks array. When a parent re-renders and
|
|
185
|
-
// preserves the ChildInstance, the wrapper resets
|
|
186
|
-
// `ctx.unmountCallbacks = []` to drop stale cycle-N
|
|
187
|
-
// callbacks before cycle-N+1 begins (`jsx-runtime.ts:172`).
|
|
188
|
-
// Without this re-push the cached subscription's unsub is
|
|
189
|
-
// lost from the array and the subscription stays active on
|
|
190
|
-
// the store forever — one leaked subscriber per
|
|
191
|
-
// `writable.subscribe()` call per parent re-render cycle.
|
|
192
|
-
if (!ctx.unmountCallbacks.includes(cached.unsub)) {
|
|
193
|
-
ctx.unmountCallbacks.push(cached.unsub)
|
|
194
|
-
// Leak-class D diagnostic — emit per re-push that fires
|
|
195
|
-
// during the cached fast-path. Non-zero confirms parent
|
|
196
|
-
// re-renders are actually exercising the cached subscribe
|
|
197
|
-
// path (and the unsub stays bound to the live cleanup
|
|
198
|
-
// array). Zero on a render-heavy workload = either no
|
|
199
|
-
// cached subscriptions OR — bug — the includes() guard
|
|
200
|
-
// suppressed a valid re-push.
|
|
201
|
-
if (process.env.NODE_ENV !== 'production')
|
|
202
|
-
_countSink.__pyreon_count__?.('svelte-compat.subscribe.cachedRePush')
|
|
203
|
-
}
|
|
204
|
-
return cached.unsub
|
|
205
|
-
}
|
|
206
|
-
const entry: SubEntry<T> = {
|
|
207
|
-
run,
|
|
208
|
-
invalidate,
|
|
209
|
-
rerender: () => {
|
|
210
|
-
if (!ctx.unmounted) ctx.scheduleRerender()
|
|
211
|
-
},
|
|
212
|
-
}
|
|
213
|
-
const unsub = addSub(entry)
|
|
214
|
-
ctx.hooks[idx] = { unsub }
|
|
215
|
-
ctx.unmountCallbacks.push(unsub)
|
|
216
|
-
return unsub
|
|
217
|
-
}
|
|
218
|
-
return addSub({ run, invalidate })
|
|
219
|
-
},
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ─── readable ────────────────────────────────────────────────────────────────
|
|
224
|
-
|
|
225
|
-
/** Svelte-compatible `readable` — a `writable` with `set`/`update` hidden. */
|
|
226
|
-
export function readable<T>(value?: T, start?: StartStopNotifier<T>): Readable<T> {
|
|
227
|
-
const w = writable<T>(value, start)
|
|
228
|
-
return { subscribe: w.subscribe }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ─── readonly ────────────────────────────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
/** Svelte-compatible `readonly` — view of a store exposing only `subscribe`. */
|
|
234
|
-
export function readonly<T>(store: Readable<T>): Readable<T> {
|
|
235
|
-
return { subscribe: store.subscribe }
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ─── get ─────────────────────────────────────────────────────────────────────
|
|
239
|
-
|
|
240
|
-
/** Svelte-compatible `get` — read a store's value synchronously. */
|
|
241
|
-
export function get<T>(store: Readable<T>): T {
|
|
242
|
-
let value!: T
|
|
243
|
-
plainSubscribe(() => {
|
|
244
|
-
const unsub = store.subscribe((v) => {
|
|
245
|
-
value = v
|
|
246
|
-
})
|
|
247
|
-
unsub()
|
|
248
|
-
})
|
|
249
|
-
return value
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ─── derived ─────────────────────────────────────────────────────────────────
|
|
253
|
-
|
|
254
|
-
type Stores =
|
|
255
|
-
| Readable<unknown>
|
|
256
|
-
| [Readable<unknown>, ...Array<Readable<unknown>>]
|
|
257
|
-
| Array<Readable<unknown>>
|
|
258
|
-
type StoresValues<T> = T extends Readable<infer U>
|
|
259
|
-
? U
|
|
260
|
-
: { [K in keyof T]: T[K] extends Readable<infer U> ? U : never }
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Svelte-compatible `derived`. Supports both the sync form
|
|
264
|
-
* `(values) => result` and the async/cleanup form
|
|
265
|
-
* `(values, set, update?) => stop`.
|
|
266
|
-
*/
|
|
267
|
-
export function derived<S extends Stores, T>(
|
|
268
|
-
stores: S,
|
|
269
|
-
fn:
|
|
270
|
-
| ((values: StoresValues<S>) => T)
|
|
271
|
-
| ((
|
|
272
|
-
values: StoresValues<S>,
|
|
273
|
-
set: (value: T) => void,
|
|
274
|
-
update: (fn: Updater<T>) => void,
|
|
275
|
-
) => Unsubscriber | void),
|
|
276
|
-
initialValue?: T,
|
|
277
|
-
): Readable<T> {
|
|
278
|
-
const single = !Array.isArray(stores)
|
|
279
|
-
const storeArr = (single ? [stores] : stores) as Array<Readable<unknown>>
|
|
280
|
-
const isAsync = fn.length >= 2
|
|
281
|
-
|
|
282
|
-
const out = writable<T>(initialValue as T, (set, update) => {
|
|
283
|
-
let inited = false
|
|
284
|
-
const values: unknown[] = []
|
|
285
|
-
let cleanup: Unsubscriber | void
|
|
286
|
-
|
|
287
|
-
const sync = () => {
|
|
288
|
-
if (cleanup) {
|
|
289
|
-
cleanup()
|
|
290
|
-
cleanup = undefined
|
|
291
|
-
}
|
|
292
|
-
const input = (single ? values[0] : values) as StoresValues<S>
|
|
293
|
-
if (isAsync) {
|
|
294
|
-
cleanup = (
|
|
295
|
-
fn as (v: StoresValues<S>, s: (x: T) => void, u: (f: Updater<T>) => void) => Unsubscriber | void
|
|
296
|
-
)(input, set, update)
|
|
297
|
-
} else {
|
|
298
|
-
set((fn as (v: StoresValues<S>) => T)(input))
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const unsubs = plainSubscribe(() =>
|
|
303
|
-
storeArr.map((s, i) =>
|
|
304
|
-
s.subscribe((v) => {
|
|
305
|
-
values[i] = v
|
|
306
|
-
if (inited) sync()
|
|
307
|
-
}),
|
|
308
|
-
),
|
|
309
|
-
)
|
|
310
|
-
inited = true
|
|
311
|
-
sync()
|
|
312
|
-
|
|
313
|
-
return () => {
|
|
314
|
-
for (const u of unsubs) u()
|
|
315
|
-
if (cleanup) cleanup()
|
|
316
|
-
}
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
return { subscribe: out.subscribe }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ─── lifecycle (svelte) ──────────────────────────────────────────────────────
|
|
323
|
-
|
|
324
|
-
type CleanupFn = () => void
|
|
325
|
-
|
|
326
|
-
/** Svelte-compatible `onMount` — runs after the component's first render. */
|
|
327
|
-
export function onMount(fn: () => CleanupFn | void): void {
|
|
328
|
-
const ctx = getCurrentCtx()
|
|
329
|
-
if (ctx) {
|
|
330
|
-
const idx = getHookIndex()
|
|
331
|
-
if (idx >= ctx.hooks.length) {
|
|
332
|
-
// Svelte's onMount may return a cleanup that runs on destroy. The
|
|
333
|
-
// shared jsx-runtime schedules pendingEffects post-render but never
|
|
334
|
-
// invokes their stored cleanup on unmount, so wire it explicitly
|
|
335
|
-
// into unmountCallbacks (runs in the wrapper's onUnmount).
|
|
336
|
-
let cleanup: CleanupFn | undefined
|
|
337
|
-
const unmountCb = () => {
|
|
338
|
-
if (cleanup) cleanup()
|
|
339
|
-
}
|
|
340
|
-
// Store the cleanup callback in the hook slot (was `true`) so it can be
|
|
341
|
-
// re-pushed after a parent re-render resets `ctx.unmountCallbacks`.
|
|
342
|
-
ctx.hooks[idx] = unmountCb
|
|
343
|
-
ctx.pendingEffects.push({
|
|
344
|
-
fn: () => {
|
|
345
|
-
const c = fn()
|
|
346
|
-
cleanup = typeof c === 'function' ? c : undefined
|
|
347
|
-
return cleanup
|
|
348
|
-
},
|
|
349
|
-
deps: undefined,
|
|
350
|
-
cleanup: undefined,
|
|
351
|
-
})
|
|
352
|
-
ctx.unmountCallbacks.push(unmountCb)
|
|
353
|
-
} else {
|
|
354
|
-
// Re-render of a preserved child: the wrapper reset
|
|
355
|
-
// `ctx.unmountCallbacks = []` (jsx-runtime.ts:172), dropping this hook's
|
|
356
|
-
// cleanup. Re-push it so it still runs on final unmount — the lifecycle
|
|
357
|
-
// sibling of the #739 `writable.subscribe` re-push. `includes()` guards
|
|
358
|
-
// against a double-push within the same render.
|
|
359
|
-
const stored = ctx.hooks[idx]
|
|
360
|
-
if (typeof stored === 'function') {
|
|
361
|
-
const cb = stored as () => void
|
|
362
|
-
if (!ctx.unmountCallbacks.includes(cb)) ctx.unmountCallbacks.push(cb)
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
return
|
|
366
|
-
}
|
|
367
|
-
pyreonOnMount(fn)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Svelte-compatible `onDestroy` — runs when the component unmounts. */
|
|
371
|
-
export function onDestroy(fn: () => void): void {
|
|
372
|
-
const ctx = getCurrentCtx()
|
|
373
|
-
if (ctx) {
|
|
374
|
-
const idx = getHookIndex()
|
|
375
|
-
if (idx >= ctx.hooks.length) {
|
|
376
|
-
// Store the callback in the hook slot (was `true`) so it survives a
|
|
377
|
-
// parent re-render that resets `ctx.unmountCallbacks` (see onMount).
|
|
378
|
-
ctx.hooks[idx] = fn
|
|
379
|
-
ctx.unmountCallbacks.push(fn)
|
|
380
|
-
} else {
|
|
381
|
-
// Re-render: re-push the dropped destroy callback (the #739 lifecycle
|
|
382
|
-
// sibling) so it still fires on final unmount.
|
|
383
|
-
const stored = ctx.hooks[idx]
|
|
384
|
-
if (typeof stored === 'function') {
|
|
385
|
-
const cb = stored as () => void
|
|
386
|
-
if (!ctx.unmountCallbacks.includes(cb)) ctx.unmountCallbacks.push(cb)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
pyreonOnUnmount(fn)
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Svelte-compatible `beforeUpdate` / `afterUpdate`. The compat wrapper
|
|
396
|
-
* re-renders by tearing down + rebuilding (no per-update diff), so these
|
|
397
|
-
* map to a post-first-render hook rather than Svelte's per-tick timing —
|
|
398
|
-
* the documented boundary (most Svelte interop uses onMount/onDestroy).
|
|
399
|
-
*/
|
|
400
|
-
export function beforeUpdate(fn: () => void): void {
|
|
401
|
-
const ctx = getCurrentCtx()
|
|
402
|
-
if (ctx) {
|
|
403
|
-
const idx = getHookIndex()
|
|
404
|
-
if (idx >= ctx.hooks.length) {
|
|
405
|
-
ctx.hooks[idx] = true
|
|
406
|
-
fn() // before the first render commits
|
|
407
|
-
}
|
|
408
|
-
return
|
|
409
|
-
}
|
|
410
|
-
fn()
|
|
411
|
-
}
|
|
412
|
-
export function afterUpdate(fn: () => void): void {
|
|
413
|
-
onMount(() => {
|
|
414
|
-
fn()
|
|
415
|
-
})
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Svelte-compatible `tick` — resolves after the current microtask. */
|
|
419
|
-
export function tick(): Promise<void> {
|
|
420
|
-
return new Promise<void>((resolve) => queueMicrotask(resolve))
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ─── context (svelte) ────────────────────────────────────────────────────────
|
|
424
|
-
|
|
425
|
-
const CTX_REGISTRY = Symbol.for('pyreon:svelte-ctx-registry')
|
|
426
|
-
type CtxMap = Map<unknown, ReturnType<typeof pyreonCreateContext<unknown>>>
|
|
427
|
-
|
|
428
|
-
function ctxFor(key: unknown): ReturnType<typeof pyreonCreateContext<unknown>> {
|
|
429
|
-
const g = globalThis as Record<symbol, unknown>
|
|
430
|
-
let reg = g[CTX_REGISTRY] as CtxMap | undefined
|
|
431
|
-
if (!reg) {
|
|
432
|
-
reg = new Map()
|
|
433
|
-
g[CTX_REGISTRY] = reg
|
|
434
|
-
}
|
|
435
|
-
let c = reg.get(key)
|
|
436
|
-
if (!c) {
|
|
437
|
-
c = pyreonCreateContext<unknown>(undefined)
|
|
438
|
-
reg.set(key, c)
|
|
439
|
-
}
|
|
440
|
-
return c
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/** Svelte-compatible `setContext` — provides a value for descendants. */
|
|
444
|
-
export function setContext<T>(key: unknown, context: T): T {
|
|
445
|
-
pyreonProvide(ctxFor(key), context)
|
|
446
|
-
return context
|
|
447
|
-
}
|
|
448
|
-
/** Svelte-compatible `getContext` — reads the nearest provided value. */
|
|
449
|
-
export function getContext<T>(key: unknown): T {
|
|
450
|
-
return pyreonUseContext(ctxFor(key)) as T
|
|
451
|
-
}
|
|
452
|
-
/** Svelte-compatible `hasContext` — whether a value was provided up-tree. */
|
|
453
|
-
export function hasContext(key: unknown): boolean {
|
|
454
|
-
return pyreonUseContext(ctxFor(key)) !== undefined
|
|
455
|
-
}
|
|
456
|
-
/** Svelte-compatible `getAllContexts` — best-effort (not tracked per-key). */
|
|
457
|
-
export function getAllContexts<T extends Map<unknown, unknown> = Map<unknown, unknown>>(): T {
|
|
458
|
-
return new Map() as T
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// ─── createEventDispatcher (svelte) ──────────────────────────────────────────
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Svelte-compatible `createEventDispatcher`. Svelte's compiler turns
|
|
465
|
-
* `<Child on:foo>` into a prop; here events are forwarded to the
|
|
466
|
-
* current component's `on<Type>` / `on:<type>` prop with a CustomEvent
|
|
467
|
-
* (mirrors how the sibling compat layers map child events to props).
|
|
468
|
-
*/
|
|
469
|
-
export function createEventDispatcher<EventMap extends Record<string, unknown> = Record<string, unknown>>(): <
|
|
470
|
-
Type extends keyof EventMap & string,
|
|
471
|
-
>(
|
|
472
|
-
type: Type,
|
|
473
|
-
detail?: EventMap[Type],
|
|
474
|
-
) => boolean {
|
|
475
|
-
const ctx = getCurrentCtx()
|
|
476
|
-
const props = (ctx?.props ?? {}) as Record<string, unknown>
|
|
477
|
-
return (type, detail) => {
|
|
478
|
-
const evt =
|
|
479
|
-
typeof CustomEvent === 'function'
|
|
480
|
-
? new CustomEvent(type, { detail })
|
|
481
|
-
: ({ type, detail } as unknown as CustomEvent)
|
|
482
|
-
const cap = `on${type.charAt(0).toUpperCase()}${type.slice(1)}`
|
|
483
|
-
const handler = (props[cap] ?? props[`on:${type}`] ?? props[`on${type}`]) as
|
|
484
|
-
| ((e: unknown) => void)
|
|
485
|
-
| undefined
|
|
486
|
-
if (typeof handler === 'function') handler(evt)
|
|
487
|
-
return !(evt as CustomEvent).defaultPrevented
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// ─── mount / unmount / flushSync (Svelte 5 client API) ───────────────────────
|
|
492
|
-
|
|
493
|
-
type MountTargetOptions<P> = { target: Element; props?: P; context?: Map<unknown, unknown> }
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Svelte-5-compatible `mount` — mounts a compat component into a target.
|
|
497
|
-
* Thin wrapper over Pyreon's runtime mount. Returns the props object
|
|
498
|
-
* (Svelte 5 returns the component exports; here props are the surface).
|
|
499
|
-
*/
|
|
500
|
-
export function mount<P extends Record<string, unknown>>(
|
|
501
|
-
Component: (props: P) => VNodeChild,
|
|
502
|
-
options: MountTargetOptions<P>,
|
|
503
|
-
): P {
|
|
504
|
-
const props = (options.props ?? ({} as P)) as P
|
|
505
|
-
// Route through the compat JSX runtime so the component runs inside the
|
|
506
|
-
// shared wrapper (lifecycle + store-driven re-render), exactly as a
|
|
507
|
-
// JSX-rendered child would.
|
|
508
|
-
const vnode = jsx(Component as unknown as ComponentFn, props as unknown as Props)
|
|
509
|
-
const dispose = pyreonMount(vnode, options.target as HTMLElement)
|
|
510
|
-
;(props as Record<symbol, unknown>)[UNMOUNT] = dispose
|
|
511
|
-
return props
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const UNMOUNT = Symbol.for('pyreon:svelte-unmount')
|
|
515
|
-
|
|
516
|
-
/** Svelte-5-compatible `unmount` — disposes a component mounted via `mount`. */
|
|
517
|
-
export function unmount(mounted: Record<symbol, unknown>): void {
|
|
518
|
-
const d = mounted?.[UNMOUNT] as (() => void) | undefined
|
|
519
|
-
if (typeof d === 'function') d()
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Svelte-5-compatible `flushSync` — runs `fn` then flushes. Pyreon
|
|
524
|
-
* batches synchronously, so this just invokes `fn`.
|
|
525
|
-
*/
|
|
526
|
-
export function flushSync<T>(fn?: () => T): T | undefined {
|
|
527
|
-
return fn ? fn() : undefined
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// ─── createEventDispatcher needs props on ctx ────────────────────────────────
|
|
531
|
-
// (jsx-runtime stores `props` on the RenderContext — see jsx-runtime.ts)
|
|
532
|
-
|
|
533
|
-
// ─── Re-exports from @pyreon/core (control-flow parity) ──────────────────────
|
|
534
|
-
|
|
535
|
-
export { ErrorBoundary, For, Match, Show, Suspense, Switch }
|
|
536
|
-
|
|
537
|
-
// Mark the compat surface so framework Providers route natively.
|
|
538
|
-
void nativeCompat
|
package/src/jsx-dev-runtime.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { Fragment, jsx, jsxs } from './jsx-runtime'
|