@pyreon/solid-compat 0.2.0 → 0.3.0
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +132 -12
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +130 -12
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index2.d.ts +32 -4
- package/lib/types/index2.d.ts.map +1 -1
- package/package.json +14 -4
- package/src/index.ts +168 -24
- package/src/jsx-runtime.ts +300 -0
- package/src/tests/repro.test.ts +172 -0
- package/src/tests/solid-compat.test.ts +168 -21
package/src/index.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @pyreon/solid-compat
|
|
3
|
+
*
|
|
4
|
+
* Fully SolidJS-compatible API powered by Pyreon's reactive engine.
|
|
5
|
+
*
|
|
6
|
+
* Components re-render on state change via the compat JSX runtime wrapper.
|
|
7
|
+
* Signals use Pyreon's native signal system internally (enabling auto-tracking
|
|
8
|
+
* for createEffect/createMemo), while the component body runs inside
|
|
9
|
+
* `runUntracked` to prevent signal reads from being tracked by the reactive
|
|
10
|
+
* accessor. Only the version signal triggers re-renders.
|
|
11
|
+
*
|
|
12
|
+
* USAGE:
|
|
13
|
+
* import { createSignal, createEffect } from "solid-js" // aliased by vite plugin
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ComponentFn, LazyComponent, Props, VNodeChild } from "@pyreon/core"
|
|
4
17
|
import {
|
|
5
18
|
ErrorBoundary,
|
|
6
19
|
For,
|
|
@@ -25,6 +38,7 @@ import {
|
|
|
25
38
|
runUntracked,
|
|
26
39
|
setCurrentScope,
|
|
27
40
|
} from "@pyreon/reactivity"
|
|
41
|
+
import { getCurrentCtx, getHookIndex } from "./jsx-runtime"
|
|
28
42
|
|
|
29
43
|
// ─── createSignal ────────────────────────────────────────────────────────────
|
|
30
44
|
|
|
@@ -32,10 +46,30 @@ export type SignalGetter<T> = () => T
|
|
|
32
46
|
export type SignalSetter<T> = (v: T | ((prev: T) => T)) => void
|
|
33
47
|
|
|
34
48
|
export function createSignal<T>(initialValue: T): [SignalGetter<T>, SignalSetter<T>] {
|
|
35
|
-
const
|
|
49
|
+
const ctx = getCurrentCtx()
|
|
50
|
+
if (ctx) {
|
|
51
|
+
const idx = getHookIndex()
|
|
52
|
+
if (idx >= ctx.hooks.length) {
|
|
53
|
+
ctx.hooks[idx] = pyreonSignal<T>(initialValue)
|
|
54
|
+
}
|
|
55
|
+
const s = ctx.hooks[idx] as ReturnType<typeof pyreonSignal<T>>
|
|
56
|
+
const { scheduleRerender } = ctx
|
|
36
57
|
|
|
37
|
-
|
|
58
|
+
const getter: SignalGetter<T> = () => s()
|
|
59
|
+
const setter: SignalSetter<T> = (v) => {
|
|
60
|
+
if (typeof v === "function") {
|
|
61
|
+
s.update(v as (prev: T) => T)
|
|
62
|
+
} else {
|
|
63
|
+
s.set(v)
|
|
64
|
+
}
|
|
65
|
+
scheduleRerender()
|
|
66
|
+
}
|
|
67
|
+
return [getter, setter]
|
|
68
|
+
}
|
|
38
69
|
|
|
70
|
+
// Outside component — plain Pyreon signal
|
|
71
|
+
const s = pyreonSignal<T>(initialValue)
|
|
72
|
+
const getter: SignalGetter<T> = () => s()
|
|
39
73
|
const setter: SignalSetter<T> = (v) => {
|
|
40
74
|
if (typeof v === "function") {
|
|
41
75
|
s.update(v as (prev: T) => T)
|
|
@@ -43,20 +77,53 @@ export function createSignal<T>(initialValue: T): [SignalGetter<T>, SignalSetter
|
|
|
43
77
|
s.set(v)
|
|
44
78
|
}
|
|
45
79
|
}
|
|
46
|
-
|
|
47
80
|
return [getter, setter]
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
// ─── createEffect ────────────────────────────────────────────────────────────
|
|
51
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Solid-compatible `createEffect` — creates a reactive side effect.
|
|
87
|
+
*
|
|
88
|
+
* In component context: hook-indexed, only created on first render. The effect
|
|
89
|
+
* uses Pyreon's native tracking so signal reads are automatically tracked.
|
|
90
|
+
* A re-entrance guard prevents infinite loops from signal writes inside
|
|
91
|
+
* the effect.
|
|
92
|
+
*/
|
|
52
93
|
export function createEffect(fn: () => void): void {
|
|
94
|
+
const ctx = getCurrentCtx()
|
|
95
|
+
if (ctx) {
|
|
96
|
+
const idx = getHookIndex()
|
|
97
|
+
if (idx < ctx.hooks.length) return // Already registered on first render
|
|
98
|
+
|
|
99
|
+
let running = false
|
|
100
|
+
const e = pyreonEffect(() => {
|
|
101
|
+
if (running) return
|
|
102
|
+
running = true
|
|
103
|
+
try {
|
|
104
|
+
fn()
|
|
105
|
+
} finally {
|
|
106
|
+
running = false
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
const stop = () => e.dispose()
|
|
110
|
+
ctx.hooks[idx] = stop
|
|
111
|
+
ctx.unmountCallbacks.push(stop)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Outside component
|
|
53
116
|
pyreonEffect(fn)
|
|
54
117
|
}
|
|
55
118
|
|
|
56
119
|
// ─── createRenderEffect ──────────────────────────────────────────────────────
|
|
57
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Solid-compatible `createRenderEffect` — same as createEffect.
|
|
123
|
+
* In Solid, this runs during the render phase; here it runs as a Pyreon effect.
|
|
124
|
+
*/
|
|
58
125
|
export function createRenderEffect(fn: () => void): void {
|
|
59
|
-
|
|
126
|
+
createEffect(fn)
|
|
60
127
|
}
|
|
61
128
|
|
|
62
129
|
// ─── createComputed (legacy Solid API) ───────────────────────────────────────
|
|
@@ -65,7 +132,24 @@ export { createEffect as createComputed }
|
|
|
65
132
|
|
|
66
133
|
// ─── createMemo ──────────────────────────────────────────────────────────────
|
|
67
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Solid-compatible `createMemo` — derives a value from reactive sources.
|
|
137
|
+
*
|
|
138
|
+
* In component context: hook-indexed, only created on first render.
|
|
139
|
+
* Uses Pyreon's native computed for auto-tracking.
|
|
140
|
+
*/
|
|
68
141
|
export function createMemo<T>(fn: () => T): () => T {
|
|
142
|
+
const ctx = getCurrentCtx()
|
|
143
|
+
if (ctx) {
|
|
144
|
+
const idx = getHookIndex()
|
|
145
|
+
if (idx >= ctx.hooks.length) {
|
|
146
|
+
ctx.hooks[idx] = pyreonComputed(fn)
|
|
147
|
+
}
|
|
148
|
+
const c = ctx.hooks[idx] as ReturnType<typeof pyreonComputed<T>>
|
|
149
|
+
return () => c()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Outside component
|
|
69
153
|
const c = pyreonComputed(fn)
|
|
70
154
|
return () => c()
|
|
71
155
|
}
|
|
@@ -131,11 +215,64 @@ export { runUntracked as untrack }
|
|
|
131
215
|
|
|
132
216
|
// ─── onMount / onCleanup ─────────────────────────────────────────────────────
|
|
133
217
|
|
|
134
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Solid-compatible `onMount` — runs once after the component's first render.
|
|
220
|
+
*/
|
|
221
|
+
type CleanupFn = () => void
|
|
222
|
+
export function onMount(fn: () => CleanupFn | undefined): void {
|
|
223
|
+
const ctx = getCurrentCtx()
|
|
224
|
+
if (ctx) {
|
|
225
|
+
const idx = getHookIndex()
|
|
226
|
+
if (idx >= ctx.hooks.length) {
|
|
227
|
+
ctx.hooks[idx] = true
|
|
228
|
+
ctx.pendingEffects.push({
|
|
229
|
+
fn: () => {
|
|
230
|
+
fn()
|
|
231
|
+
return undefined
|
|
232
|
+
},
|
|
233
|
+
deps: undefined,
|
|
234
|
+
cleanup: undefined,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Outside component
|
|
241
|
+
pyreonOnMount(fn)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Solid-compatible `onCleanup` — registers a callback to run when the component unmounts.
|
|
246
|
+
*/
|
|
247
|
+
export function onCleanup(fn: () => void): void {
|
|
248
|
+
const ctx = getCurrentCtx()
|
|
249
|
+
if (ctx) {
|
|
250
|
+
const idx = getHookIndex()
|
|
251
|
+
if (idx >= ctx.hooks.length) {
|
|
252
|
+
ctx.hooks[idx] = true
|
|
253
|
+
ctx.unmountCallbacks.push(fn)
|
|
254
|
+
}
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Outside component
|
|
259
|
+
pyreonOnUnmount(fn)
|
|
260
|
+
}
|
|
135
261
|
|
|
136
262
|
// ─── createSelector ──────────────────────────────────────────────────────────
|
|
137
263
|
|
|
138
|
-
export
|
|
264
|
+
export function createSelector<T>(source: () => T): (key: T) => boolean {
|
|
265
|
+
const ctx = getCurrentCtx()
|
|
266
|
+
if (ctx) {
|
|
267
|
+
const idx = getHookIndex()
|
|
268
|
+
if (idx >= ctx.hooks.length) {
|
|
269
|
+
ctx.hooks[idx] = pyreonCreateSelector(source)
|
|
270
|
+
}
|
|
271
|
+
return ctx.hooks[idx] as (key: T) => boolean
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return pyreonCreateSelector(source)
|
|
275
|
+
}
|
|
139
276
|
|
|
140
277
|
// ─── mergeProps ──────────────────────────────────────────────────────────────
|
|
141
278
|
|
|
@@ -224,37 +361,44 @@ export function children(fn: () => VNodeChild): () => VNodeChild {
|
|
|
224
361
|
|
|
225
362
|
export function lazy<P extends Props>(
|
|
226
363
|
loader: () => Promise<{ default: ComponentFn<P> }>,
|
|
227
|
-
):
|
|
228
|
-
|
|
229
|
-
|
|
364
|
+
): LazyComponent<P> & { preload: () => Promise<{ default: ComponentFn<P> }> } {
|
|
365
|
+
const loaded = pyreonSignal<ComponentFn<P> | null>(null)
|
|
366
|
+
const error = pyreonSignal<Error | null>(null)
|
|
230
367
|
let promise: Promise<{ default: ComponentFn<P> }> | null = null
|
|
231
368
|
|
|
232
369
|
const load = () => {
|
|
233
370
|
if (!promise) {
|
|
234
371
|
promise = loader()
|
|
235
372
|
.then((mod) => {
|
|
236
|
-
|
|
373
|
+
loaded.set(mod.default)
|
|
237
374
|
return mod
|
|
238
375
|
})
|
|
239
376
|
.catch((err) => {
|
|
240
|
-
|
|
241
|
-
|
|
377
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
378
|
+
error.set(e)
|
|
242
379
|
promise = null
|
|
243
|
-
throw
|
|
380
|
+
throw e
|
|
244
381
|
})
|
|
245
382
|
}
|
|
246
383
|
return promise
|
|
247
384
|
}
|
|
248
385
|
|
|
386
|
+
// Uses Pyreon's __loading protocol — Suspense checks this to show fallback.
|
|
387
|
+
// __loading() triggers load() on first call so loading starts when Suspense
|
|
388
|
+
// first encounters the component (not at module load time, not on first render).
|
|
249
389
|
const LazyComponent = ((props: P) => {
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
390
|
+
const err = error()
|
|
391
|
+
if (err) throw err
|
|
392
|
+
const comp = loaded()
|
|
393
|
+
if (!comp) return null
|
|
394
|
+
return comp(props)
|
|
395
|
+
}) as LazyComponent<P> & { preload: () => Promise<{ default: ComponentFn<P> }> }
|
|
396
|
+
|
|
397
|
+
LazyComponent.__loading = () => {
|
|
398
|
+
const isLoading = loaded() === null && error() === null
|
|
399
|
+
if (isLoading) load()
|
|
400
|
+
return isLoading
|
|
401
|
+
}
|
|
258
402
|
LazyComponent.preload = load
|
|
259
403
|
|
|
260
404
|
return LazyComponent
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compat JSX runtime for SolidJS compatibility mode.
|
|
3
|
+
*
|
|
4
|
+
* When `jsxImportSource` is redirected to `@pyreon/solid-compat` (via the vite
|
|
5
|
+
* plugin's `compat: "solid"` option), OXC rewrites JSX to import from this file.
|
|
6
|
+
*
|
|
7
|
+
* For component VNodes, we wrap the component function so it returns a reactive
|
|
8
|
+
* accessor — enabling Solid-style re-renders on state change while Pyreon's
|
|
9
|
+
* existing renderer handles all DOM work.
|
|
10
|
+
*
|
|
11
|
+
* The component body runs inside `runUntracked` to prevent signal reads (from
|
|
12
|
+
* createSignal getters) from being tracked by the reactive accessor. Only the
|
|
13
|
+
* version signal triggers re-renders.
|
|
14
|
+
*
|
|
15
|
+
* ## Child instance preservation
|
|
16
|
+
*
|
|
17
|
+
* When a parent component re-renders, mountReactive does a full teardown+rebuild
|
|
18
|
+
* of the DOM tree. Without preservation, child components get brand new
|
|
19
|
+
* RenderContexts with empty hooks arrays — causing `onMount` and `onCleanup`
|
|
20
|
+
* to fire again, which can trigger infinite re-render loops.
|
|
21
|
+
*
|
|
22
|
+
* To fix this, we store child RenderContexts in the parent's hooks array (indexed
|
|
23
|
+
* by the parent's hook counter). When the child wrapper is called again after a
|
|
24
|
+
* parent re-render, it reuses the existing ctx (preserving hooks state), so
|
|
25
|
+
* hook-indexed guards like `if (idx >= ctx.hooks.length) return` work correctly
|
|
26
|
+
* and lifecycle hooks don't re-fire.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ComponentFn, Props, VNode, VNodeChild } from "@pyreon/core"
|
|
30
|
+
import {
|
|
31
|
+
ErrorBoundary,
|
|
32
|
+
For,
|
|
33
|
+
Fragment,
|
|
34
|
+
h,
|
|
35
|
+
Match,
|
|
36
|
+
onUnmount,
|
|
37
|
+
Show,
|
|
38
|
+
Suspense,
|
|
39
|
+
Switch,
|
|
40
|
+
} from "@pyreon/core"
|
|
41
|
+
import { runUntracked, signal } from "@pyreon/reactivity"
|
|
42
|
+
|
|
43
|
+
export { Fragment }
|
|
44
|
+
|
|
45
|
+
// ─── Render context (used by hooks) ──────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface RenderContext {
|
|
48
|
+
hooks: unknown[]
|
|
49
|
+
scheduleRerender: () => void
|
|
50
|
+
/** Effect entries pending execution after render */
|
|
51
|
+
pendingEffects: EffectEntry[]
|
|
52
|
+
/** Layout effect entries pending execution after render */
|
|
53
|
+
pendingLayoutEffects: EffectEntry[]
|
|
54
|
+
/** Set to true when the component is unmounted */
|
|
55
|
+
unmounted: boolean
|
|
56
|
+
/** Callbacks to run on unmount (lifecycle + effect cleanups) */
|
|
57
|
+
unmountCallbacks: (() => void)[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface EffectEntry {
|
|
61
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: matches Solid's effect signature
|
|
62
|
+
fn: () => (() => void) | void
|
|
63
|
+
deps: unknown[] | undefined
|
|
64
|
+
cleanup: (() => void) | undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let _currentCtx: RenderContext | null = null
|
|
68
|
+
let _hookIndex = 0
|
|
69
|
+
|
|
70
|
+
export function getCurrentCtx(): RenderContext | null {
|
|
71
|
+
return _currentCtx
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getHookIndex(): number {
|
|
75
|
+
return _hookIndex++
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function beginRender(ctx: RenderContext): void {
|
|
79
|
+
_currentCtx = ctx
|
|
80
|
+
_hookIndex = 0
|
|
81
|
+
ctx.pendingEffects = []
|
|
82
|
+
ctx.pendingLayoutEffects = []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function endRender(): void {
|
|
86
|
+
_currentCtx = null
|
|
87
|
+
_hookIndex = 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Effect runners ──────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function runLayoutEffects(entries: EffectEntry[]): void {
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.cleanup) entry.cleanup()
|
|
95
|
+
const cleanup = entry.fn()
|
|
96
|
+
entry.cleanup = typeof cleanup === "function" ? cleanup : undefined
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function scheduleEffects(ctx: RenderContext, entries: EffectEntry[]): void {
|
|
101
|
+
if (entries.length === 0) return
|
|
102
|
+
queueMicrotask(() => {
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (ctx.unmounted) return
|
|
105
|
+
if (entry.cleanup) entry.cleanup()
|
|
106
|
+
const cleanup = entry.fn()
|
|
107
|
+
entry.cleanup = typeof cleanup === "function" ? cleanup : undefined
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Child instance preservation ─────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/** Stored in the parent's hooks array to preserve child state across re-renders */
|
|
115
|
+
interface ChildInstance {
|
|
116
|
+
ctx: RenderContext
|
|
117
|
+
version: ReturnType<typeof signal<number>>
|
|
118
|
+
updateScheduled: boolean
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Internal prop keys for passing parent context info to child wrappers
|
|
122
|
+
const _CHILD_INSTANCE = Symbol.for("pyreon.childInstance")
|
|
123
|
+
const noop = () => {
|
|
124
|
+
/* noop */
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Component wrapping ──────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping
|
|
130
|
+
const _wrapperCache = new WeakMap<Function, ComponentFn>()
|
|
131
|
+
|
|
132
|
+
// Pyreon core components that must NOT be wrapped — they rely on internal reactivity
|
|
133
|
+
// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component set
|
|
134
|
+
const _nativeComponents: Set<Function> = new Set([
|
|
135
|
+
Show,
|
|
136
|
+
For,
|
|
137
|
+
Switch,
|
|
138
|
+
Match,
|
|
139
|
+
Suspense,
|
|
140
|
+
ErrorBoundary,
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping
|
|
144
|
+
function wrapCompatComponent(solidComponent: Function): ComponentFn {
|
|
145
|
+
if (_nativeComponents.has(solidComponent)) return solidComponent as ComponentFn
|
|
146
|
+
|
|
147
|
+
let wrapped = _wrapperCache.get(solidComponent)
|
|
148
|
+
if (wrapped) return wrapped
|
|
149
|
+
|
|
150
|
+
// The wrapper returns a reactive accessor (() => VNodeChild) which Pyreon's
|
|
151
|
+
// mountChild treats as a reactive expression via mountReactive.
|
|
152
|
+
wrapped = ((props: Props) => {
|
|
153
|
+
// Check for a preserved child instance from the parent's hooks
|
|
154
|
+
const existing = (props as Record<symbol, unknown>)[_CHILD_INSTANCE] as
|
|
155
|
+
| ChildInstance
|
|
156
|
+
| undefined
|
|
157
|
+
|
|
158
|
+
const ctx: RenderContext = existing?.ctx ?? {
|
|
159
|
+
hooks: [],
|
|
160
|
+
scheduleRerender: () => {
|
|
161
|
+
// Will be replaced below after version signal is created
|
|
162
|
+
},
|
|
163
|
+
pendingEffects: [],
|
|
164
|
+
pendingLayoutEffects: [],
|
|
165
|
+
unmounted: false,
|
|
166
|
+
unmountCallbacks: [],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// When reusing an existing ctx after parent re-render, reset unmounted flag
|
|
170
|
+
// and clear stale unmount callbacks (they belong to the previous mount cycle)
|
|
171
|
+
if (existing) {
|
|
172
|
+
ctx.unmounted = false
|
|
173
|
+
ctx.unmountCallbacks = []
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const version = existing?.version ?? signal(0)
|
|
177
|
+
|
|
178
|
+
// Use a shared updateScheduled flag (preserved across parent re-renders)
|
|
179
|
+
let updateScheduled = existing?.updateScheduled ?? false
|
|
180
|
+
|
|
181
|
+
ctx.scheduleRerender = () => {
|
|
182
|
+
if (ctx.unmounted || updateScheduled) return
|
|
183
|
+
updateScheduled = true
|
|
184
|
+
queueMicrotask(() => {
|
|
185
|
+
updateScheduled = false
|
|
186
|
+
if (!ctx.unmounted) version.set(version.peek() + 1)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Register cleanup when component unmounts
|
|
191
|
+
onUnmount(() => {
|
|
192
|
+
ctx.unmounted = true
|
|
193
|
+
for (const cb of ctx.unmountCallbacks) cb()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Strip the internal prop before passing to the component
|
|
197
|
+
const { [_CHILD_INSTANCE]: _stripped, ...cleanProps } = props as Record<
|
|
198
|
+
string | symbol,
|
|
199
|
+
unknown
|
|
200
|
+
>
|
|
201
|
+
|
|
202
|
+
// Return reactive accessor — Pyreon's mountChild calls mountReactive
|
|
203
|
+
return () => {
|
|
204
|
+
version() // tracked read — triggers re-execution when state changes
|
|
205
|
+
beginRender(ctx)
|
|
206
|
+
// runUntracked prevents signal reads (from createSignal getters) from
|
|
207
|
+
// being tracked by this accessor — only the version signal should trigger re-renders
|
|
208
|
+
const result = runUntracked(() => (solidComponent as ComponentFn)(cleanProps as Props))
|
|
209
|
+
const layoutEffects = ctx.pendingLayoutEffects
|
|
210
|
+
const effects = ctx.pendingEffects
|
|
211
|
+
endRender()
|
|
212
|
+
|
|
213
|
+
runLayoutEffects(layoutEffects)
|
|
214
|
+
scheduleEffects(ctx, effects)
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
}
|
|
218
|
+
}) as unknown as ComponentFn
|
|
219
|
+
|
|
220
|
+
// Forward __loading from lazy components so Pyreon's Suspense can detect them
|
|
221
|
+
if ("__loading" in solidComponent) {
|
|
222
|
+
;(wrapped as unknown as Record<string, unknown>).__loading = (
|
|
223
|
+
solidComponent as unknown as Record<string, unknown>
|
|
224
|
+
).__loading
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_wrapperCache.set(solidComponent, wrapped)
|
|
228
|
+
return wrapped
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Child instance lookup ───────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
function createChildInstance(): ChildInstance {
|
|
234
|
+
return {
|
|
235
|
+
ctx: {
|
|
236
|
+
hooks: [],
|
|
237
|
+
scheduleRerender: noop,
|
|
238
|
+
pendingEffects: [],
|
|
239
|
+
pendingLayoutEffects: [],
|
|
240
|
+
unmounted: false,
|
|
241
|
+
unmountCallbacks: [],
|
|
242
|
+
},
|
|
243
|
+
version: signal(0),
|
|
244
|
+
updateScheduled: false,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* During a parent component render, get or create the child instance at the
|
|
250
|
+
* current hook index. Returns undefined when called outside a component render.
|
|
251
|
+
*/
|
|
252
|
+
function resolveChildInstance(): ChildInstance | undefined {
|
|
253
|
+
const parentCtx = _currentCtx
|
|
254
|
+
if (!parentCtx) return undefined
|
|
255
|
+
|
|
256
|
+
const idx = _hookIndex++
|
|
257
|
+
if (idx < parentCtx.hooks.length) {
|
|
258
|
+
return parentCtx.hooks[idx] as ChildInstance
|
|
259
|
+
}
|
|
260
|
+
const instance = createChildInstance()
|
|
261
|
+
parentCtx.hooks[idx] = instance
|
|
262
|
+
return instance
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── JSX functions ───────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
export function jsx(
|
|
268
|
+
type: string | ComponentFn | symbol,
|
|
269
|
+
props: Props & { children?: VNodeChild | VNodeChild[] },
|
|
270
|
+
key?: string | number | null,
|
|
271
|
+
): VNode {
|
|
272
|
+
const { children, ...rest } = props
|
|
273
|
+
const propsWithKey = (key != null ? { ...rest, key } : rest) as Props
|
|
274
|
+
|
|
275
|
+
if (typeof type === "function") {
|
|
276
|
+
if (_nativeComponents.has(type)) {
|
|
277
|
+
const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
|
|
278
|
+
return h(type as ComponentFn, componentProps)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const wrapped = wrapCompatComponent(type)
|
|
282
|
+
const componentProps =
|
|
283
|
+
children !== undefined ? { ...propsWithKey, children } : { ...propsWithKey }
|
|
284
|
+
|
|
285
|
+
const childInstance = resolveChildInstance()
|
|
286
|
+
if (childInstance) {
|
|
287
|
+
;(componentProps as Record<symbol, unknown>)[_CHILD_INSTANCE] = childInstance
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return h(wrapped, componentProps)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// DOM element or symbol (Fragment): children go in vnode.children
|
|
294
|
+
const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]
|
|
295
|
+
|
|
296
|
+
return h(type, propsWithKey, ...(childArray as VNodeChild[]))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export const jsxs = jsx
|
|
300
|
+
export const jsxDEV = jsx
|