@pyreon/preact-compat 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -4
- package/src/hooks.ts +176 -58
- package/src/jsx-runtime.ts +162 -0
- package/src/tests/preact-compat.test.ts +549 -141
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/preact-compat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Preact-compatible API shim for Pyreon — write Preact-style code that runs on Pyreon's reactive engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,16 @@
|
|
|
38
38
|
"bun": "./src/signals.ts",
|
|
39
39
|
"import": "./lib/signals.js",
|
|
40
40
|
"types": "./lib/types/signals.d.ts"
|
|
41
|
+
},
|
|
42
|
+
"./jsx-runtime": {
|
|
43
|
+
"bun": "./src/jsx-runtime.ts",
|
|
44
|
+
"import": "./lib/jsx-runtime.js",
|
|
45
|
+
"types": "./lib/types/jsx-runtime.d.ts"
|
|
46
|
+
},
|
|
47
|
+
"./jsx-dev-runtime": {
|
|
48
|
+
"bun": "./src/jsx-runtime.ts",
|
|
49
|
+
"import": "./lib/jsx-runtime.js",
|
|
50
|
+
"types": "./lib/types/jsx-runtime.d.ts"
|
|
41
51
|
}
|
|
42
52
|
},
|
|
43
53
|
"scripts": {
|
|
@@ -49,9 +59,9 @@
|
|
|
49
59
|
"prepublishOnly": "bun run build"
|
|
50
60
|
},
|
|
51
61
|
"dependencies": {
|
|
52
|
-
"@pyreon/core": "^0.
|
|
53
|
-
"@pyreon/reactivity": "^0.
|
|
54
|
-
"@pyreon/runtime-dom": "^0.
|
|
62
|
+
"@pyreon/core": "^0.3.1",
|
|
63
|
+
"@pyreon/reactivity": "^0.3.1",
|
|
64
|
+
"@pyreon/runtime-dom": "^0.3.1"
|
|
55
65
|
},
|
|
56
66
|
"devDependencies": {
|
|
57
67
|
"@happy-dom/global-registrator": "^20.8.3",
|
package/src/hooks.ts
CHANGED
|
@@ -1,129 +1,247 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pyreon/preact-compat/hooks
|
|
3
3
|
*
|
|
4
|
-
* Preact hooks — separate import like `preact/hooks`.
|
|
5
|
-
*
|
|
4
|
+
* Preact-compatible hooks — separate import like `preact/hooks`.
|
|
5
|
+
*
|
|
6
|
+
* Components re-render on state change — just like Preact. Hooks return plain
|
|
7
|
+
* values and use deps arrays for memoization. Existing Preact code works
|
|
8
|
+
* unchanged when paired with `pyreon({ compat: "preact" })` in your vite config.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
import type {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import type { VNodeChild } from "@pyreon/core"
|
|
12
|
+
import { onErrorCaptured, useContext } from "@pyreon/core"
|
|
13
|
+
import type { EffectEntry } from "./jsx-runtime"
|
|
14
|
+
import { getCurrentCtx, getHookIndex } from "./jsx-runtime"
|
|
11
15
|
|
|
12
16
|
export { useContext }
|
|
13
17
|
|
|
18
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function requireCtx() {
|
|
21
|
+
const ctx = getCurrentCtx()
|
|
22
|
+
if (!ctx) throw new Error("Hook called outside of a component render")
|
|
23
|
+
return ctx
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function depsChanged(a: unknown[] | undefined, b: unknown[] | undefined): boolean {
|
|
27
|
+
if (a === undefined || b === undefined) return true
|
|
28
|
+
if (a.length !== b.length) return true
|
|
29
|
+
for (let i = 0; i < a.length; i++) {
|
|
30
|
+
if (!Object.is(a[i], b[i])) return true
|
|
31
|
+
}
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
// ─── useState ────────────────────────────────────────────────────────────────
|
|
15
36
|
|
|
16
37
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
38
|
+
* Preact-compatible `useState` — returns `[value, setter]`.
|
|
39
|
+
* Triggers a component re-render when the setter is called.
|
|
19
40
|
*/
|
|
20
|
-
export function useState<T>(initial: T | (() => T)): [
|
|
21
|
-
const
|
|
41
|
+
export function useState<T>(initial: T | (() => T)): [T, (v: T | ((prev: T) => T)) => void] {
|
|
42
|
+
const ctx = requireCtx()
|
|
43
|
+
const idx = getHookIndex()
|
|
44
|
+
|
|
45
|
+
if (ctx.hooks.length <= idx) {
|
|
46
|
+
ctx.hooks.push(typeof initial === "function" ? (initial as () => T)() : initial)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const value = ctx.hooks[idx] as T
|
|
22
50
|
const setter = (v: T | ((prev: T) => T)) => {
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
const current = ctx.hooks[idx] as T
|
|
52
|
+
const next = typeof v === "function" ? (v as (prev: T) => T)(current) : v
|
|
53
|
+
if (Object.is(current, next)) return
|
|
54
|
+
ctx.hooks[idx] = next
|
|
55
|
+
ctx.scheduleRerender()
|
|
25
56
|
}
|
|
26
|
-
|
|
57
|
+
|
|
58
|
+
return [value, setter]
|
|
27
59
|
}
|
|
28
60
|
|
|
29
61
|
// ─── useEffect ───────────────────────────────────────────────────────────────
|
|
30
62
|
|
|
31
63
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
64
|
+
* Preact-compatible `useEffect` — runs after render when deps change.
|
|
65
|
+
* Returns cleanup on unmount and before re-running.
|
|
34
66
|
*/
|
|
35
|
-
// biome-ignore lint/suspicious/noConfusingVoidType:
|
|
36
|
-
export function useEffect(fn: () =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
67
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: matches Preact's useEffect signature
|
|
68
|
+
export function useEffect(fn: () => (() => void) | void, deps?: unknown[]): void {
|
|
69
|
+
const ctx = requireCtx()
|
|
70
|
+
const idx = getHookIndex()
|
|
71
|
+
|
|
72
|
+
if (ctx.hooks.length <= idx) {
|
|
73
|
+
// First render — always run
|
|
74
|
+
const entry: EffectEntry = { fn, deps, cleanup: undefined }
|
|
75
|
+
ctx.hooks.push(entry)
|
|
76
|
+
ctx.pendingEffects.push(entry)
|
|
42
77
|
} else {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
78
|
+
const entry = ctx.hooks[idx] as EffectEntry
|
|
79
|
+
if (depsChanged(entry.deps, deps)) {
|
|
80
|
+
entry.fn = fn
|
|
81
|
+
entry.deps = deps
|
|
82
|
+
ctx.pendingEffects.push(entry)
|
|
83
|
+
}
|
|
49
84
|
}
|
|
50
85
|
}
|
|
51
86
|
|
|
52
87
|
// ─── useLayoutEffect ─────────────────────────────────────────────────────────
|
|
53
88
|
|
|
54
89
|
/**
|
|
55
|
-
*
|
|
56
|
-
* No distinction from useEffect in Pyreon — same implementation.
|
|
90
|
+
* Preact-compatible `useLayoutEffect` — runs synchronously after DOM mutations.
|
|
57
91
|
*/
|
|
58
|
-
|
|
92
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: matches Preact's useLayoutEffect signature
|
|
93
|
+
export function useLayoutEffect(fn: () => (() => void) | void, deps?: unknown[]): void {
|
|
94
|
+
const ctx = requireCtx()
|
|
95
|
+
const idx = getHookIndex()
|
|
96
|
+
|
|
97
|
+
if (ctx.hooks.length <= idx) {
|
|
98
|
+
const entry: EffectEntry = { fn, deps, cleanup: undefined }
|
|
99
|
+
ctx.hooks.push(entry)
|
|
100
|
+
ctx.pendingLayoutEffects.push(entry)
|
|
101
|
+
} else {
|
|
102
|
+
const entry = ctx.hooks[idx] as EffectEntry
|
|
103
|
+
if (depsChanged(entry.deps, deps)) {
|
|
104
|
+
entry.fn = fn
|
|
105
|
+
entry.deps = deps
|
|
106
|
+
ctx.pendingLayoutEffects.push(entry)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
59
110
|
|
|
60
111
|
// ─── useMemo ─────────────────────────────────────────────────────────────────
|
|
61
112
|
|
|
62
113
|
/**
|
|
63
|
-
*
|
|
64
|
-
* Returns a getter — call `value()` to read.
|
|
114
|
+
* Preact-compatible `useMemo` — returns the cached value, recomputed when deps change.
|
|
65
115
|
*/
|
|
66
|
-
export function useMemo<T>(fn: () => T,
|
|
67
|
-
|
|
116
|
+
export function useMemo<T>(fn: () => T, deps: unknown[]): T {
|
|
117
|
+
const ctx = requireCtx()
|
|
118
|
+
const idx = getHookIndex()
|
|
119
|
+
|
|
120
|
+
if (ctx.hooks.length <= idx) {
|
|
121
|
+
const value = fn()
|
|
122
|
+
ctx.hooks.push({ value, deps })
|
|
123
|
+
return value
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const entry = ctx.hooks[idx] as { value: T; deps: unknown[] }
|
|
127
|
+
if (depsChanged(entry.deps, deps)) {
|
|
128
|
+
entry.value = fn()
|
|
129
|
+
entry.deps = deps
|
|
130
|
+
}
|
|
131
|
+
return entry.value
|
|
68
132
|
}
|
|
69
133
|
|
|
70
134
|
// ─── useCallback ─────────────────────────────────────────────────────────────
|
|
71
135
|
|
|
72
136
|
/**
|
|
73
|
-
*
|
|
74
|
-
* Components run once in Pyreon — returns `fn` as-is.
|
|
137
|
+
* Preact-compatible `useCallback` — returns the cached function when deps haven't changed.
|
|
75
138
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return fn
|
|
139
|
+
export function useCallback<T extends (...args: never[]) => unknown>(fn: T, deps: unknown[]): T {
|
|
140
|
+
return useMemo(() => fn, deps)
|
|
79
141
|
}
|
|
80
142
|
|
|
81
143
|
// ─── useRef ──────────────────────────────────────────────────────────────────
|
|
82
144
|
|
|
83
145
|
/**
|
|
84
|
-
*
|
|
85
|
-
* Returns `{ current: T }`.
|
|
146
|
+
* Preact-compatible `useRef` — returns `{ current }` persisted across re-renders.
|
|
86
147
|
*/
|
|
87
148
|
export function useRef<T>(initial?: T): { current: T | null } {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
149
|
+
const ctx = requireCtx()
|
|
150
|
+
const idx = getHookIndex()
|
|
151
|
+
|
|
152
|
+
if (ctx.hooks.length <= idx) {
|
|
153
|
+
const ref = { current: initial !== undefined ? (initial as T) : null }
|
|
154
|
+
ctx.hooks.push(ref)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return ctx.hooks[idx] as { current: T | null }
|
|
91
158
|
}
|
|
92
159
|
|
|
93
160
|
// ─── useReducer ──────────────────────────────────────────────────────────────
|
|
94
161
|
|
|
95
162
|
/**
|
|
96
|
-
*
|
|
163
|
+
* Preact-compatible `useReducer` — returns `[state, dispatch]`.
|
|
97
164
|
*/
|
|
98
165
|
export function useReducer<S, A>(
|
|
99
166
|
reducer: (state: S, action: A) => S,
|
|
100
167
|
initial: S | (() => S),
|
|
101
|
-
): [
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
168
|
+
): [S, (action: A) => void] {
|
|
169
|
+
const ctx = requireCtx()
|
|
170
|
+
const idx = getHookIndex()
|
|
171
|
+
|
|
172
|
+
if (ctx.hooks.length <= idx) {
|
|
173
|
+
ctx.hooks.push(typeof initial === "function" ? (initial as () => S)() : initial)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const state = ctx.hooks[idx] as S
|
|
177
|
+
const dispatch = (action: A) => {
|
|
178
|
+
const current = ctx.hooks[idx] as S
|
|
179
|
+
const next = reducer(current, action)
|
|
180
|
+
if (Object.is(current, next)) return
|
|
181
|
+
ctx.hooks[idx] = next
|
|
182
|
+
ctx.scheduleRerender()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return [state, dispatch]
|
|
105
186
|
}
|
|
106
187
|
|
|
107
188
|
// ─── useId ───────────────────────────────────────────────────────────────────
|
|
108
189
|
|
|
109
|
-
|
|
190
|
+
let _idCounter = 0
|
|
110
191
|
|
|
111
192
|
/**
|
|
112
|
-
*
|
|
113
|
-
* Returns a stable unique string per component instance.
|
|
193
|
+
* Preact-compatible `useId` — returns a stable unique string per hook call.
|
|
114
194
|
*/
|
|
115
195
|
export function useId(): string {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
196
|
+
const ctx = requireCtx()
|
|
197
|
+
const idx = getHookIndex()
|
|
198
|
+
|
|
199
|
+
if (ctx.hooks.length <= idx) {
|
|
200
|
+
ctx.hooks.push(`:r${(_idCounter++).toString(36)}:`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return ctx.hooks[idx] as string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Optimization ────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Preact-compatible `memo` — wraps a component to skip re-render when props
|
|
210
|
+
* are shallowly equal.
|
|
211
|
+
*/
|
|
212
|
+
export function memo<P extends Record<string, unknown>>(
|
|
213
|
+
component: (props: P) => VNodeChild,
|
|
214
|
+
areEqual?: (prevProps: P, nextProps: P) => boolean,
|
|
215
|
+
): (props: P) => VNodeChild {
|
|
216
|
+
const compare =
|
|
217
|
+
areEqual ??
|
|
218
|
+
((a: P, b: P) => {
|
|
219
|
+
const keysA = Object.keys(a)
|
|
220
|
+
const keysB = Object.keys(b)
|
|
221
|
+
if (keysA.length !== keysB.length) return false
|
|
222
|
+
for (const k of keysA) {
|
|
223
|
+
if (!Object.is(a[k], b[k])) return false
|
|
224
|
+
}
|
|
225
|
+
return true
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
let prevProps: P | null = null
|
|
229
|
+
let prevResult: VNodeChild = null
|
|
230
|
+
|
|
231
|
+
return (props: P) => {
|
|
232
|
+
if (prevProps !== null && compare(prevProps, props)) {
|
|
233
|
+
return prevResult
|
|
234
|
+
}
|
|
235
|
+
prevProps = props
|
|
236
|
+
prevResult = (component as (p: P) => VNodeChild)(props)
|
|
237
|
+
return prevResult
|
|
238
|
+
}
|
|
121
239
|
}
|
|
122
240
|
|
|
123
241
|
// ─── useErrorBoundary ────────────────────────────────────────────────────────
|
|
124
242
|
|
|
125
243
|
/**
|
|
126
|
-
*
|
|
244
|
+
* Preact-compatible `useErrorBoundary`.
|
|
127
245
|
* Wraps Pyreon's `onErrorCaptured`.
|
|
128
246
|
*/
|
|
129
247
|
export { onErrorCaptured as useErrorBoundary }
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compat JSX runtime for Preact compatibility mode.
|
|
3
|
+
*
|
|
4
|
+
* When `jsxImportSource` is redirected to `@pyreon/preact-compat` (via the vite
|
|
5
|
+
* plugin's `compat: "preact"` 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 Preact-style re-renders on state change while Pyreon's
|
|
9
|
+
* existing renderer handles all DOM work.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ComponentFn, Props, VNode, VNodeChild } from "@pyreon/core"
|
|
13
|
+
import { Fragment, h } from "@pyreon/core"
|
|
14
|
+
import { signal } from "@pyreon/reactivity"
|
|
15
|
+
|
|
16
|
+
export { Fragment }
|
|
17
|
+
|
|
18
|
+
// ─── Render context (used by hooks) ──────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface RenderContext {
|
|
21
|
+
hooks: unknown[]
|
|
22
|
+
scheduleRerender: () => void
|
|
23
|
+
/** Effect entries pending execution after render */
|
|
24
|
+
pendingEffects: EffectEntry[]
|
|
25
|
+
/** Layout effect entries pending execution after render */
|
|
26
|
+
pendingLayoutEffects: EffectEntry[]
|
|
27
|
+
/** Set to true when the component is unmounted */
|
|
28
|
+
unmounted: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EffectEntry {
|
|
32
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: matches Preact's effect signature
|
|
33
|
+
fn: () => (() => void) | void
|
|
34
|
+
deps: unknown[] | undefined
|
|
35
|
+
cleanup: (() => void) | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let _currentCtx: RenderContext | null = null
|
|
39
|
+
let _hookIndex = 0
|
|
40
|
+
|
|
41
|
+
export function getCurrentCtx(): RenderContext | null {
|
|
42
|
+
return _currentCtx
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getHookIndex(): number {
|
|
46
|
+
return _hookIndex++
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function beginRender(ctx: RenderContext): void {
|
|
50
|
+
_currentCtx = ctx
|
|
51
|
+
_hookIndex = 0
|
|
52
|
+
ctx.pendingEffects = []
|
|
53
|
+
ctx.pendingLayoutEffects = []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function endRender(): void {
|
|
57
|
+
_currentCtx = null
|
|
58
|
+
_hookIndex = 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Effect runners ──────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function runLayoutEffects(entries: EffectEntry[]): void {
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (entry.cleanup) entry.cleanup()
|
|
66
|
+
const cleanup = entry.fn()
|
|
67
|
+
entry.cleanup = typeof cleanup === "function" ? cleanup : undefined
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scheduleEffects(ctx: RenderContext, entries: EffectEntry[]): void {
|
|
72
|
+
if (entries.length === 0) return
|
|
73
|
+
queueMicrotask(() => {
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (ctx.unmounted) return
|
|
76
|
+
if (entry.cleanup) entry.cleanup()
|
|
77
|
+
const cleanup = entry.fn()
|
|
78
|
+
entry.cleanup = typeof cleanup === "function" ? cleanup : undefined
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Component wrapping ──────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping
|
|
86
|
+
const _wrapperCache = new WeakMap<Function, ComponentFn>()
|
|
87
|
+
|
|
88
|
+
// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping
|
|
89
|
+
function wrapCompatComponent(preactComponent: Function): ComponentFn {
|
|
90
|
+
let wrapped = _wrapperCache.get(preactComponent)
|
|
91
|
+
if (wrapped) return wrapped
|
|
92
|
+
|
|
93
|
+
// The wrapper returns a reactive accessor (() => VNodeChild) which Pyreon's
|
|
94
|
+
// mountChild treats as a reactive expression via mountReactive.
|
|
95
|
+
wrapped = ((props: Props) => {
|
|
96
|
+
const ctx: RenderContext = {
|
|
97
|
+
hooks: [],
|
|
98
|
+
scheduleRerender: () => {
|
|
99
|
+
// Will be replaced below after version signal is created
|
|
100
|
+
},
|
|
101
|
+
pendingEffects: [],
|
|
102
|
+
pendingLayoutEffects: [],
|
|
103
|
+
unmounted: false,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const version = signal(0)
|
|
107
|
+
let updateScheduled = false
|
|
108
|
+
|
|
109
|
+
ctx.scheduleRerender = () => {
|
|
110
|
+
if (ctx.unmounted || updateScheduled) return
|
|
111
|
+
updateScheduled = true
|
|
112
|
+
queueMicrotask(() => {
|
|
113
|
+
updateScheduled = false
|
|
114
|
+
if (!ctx.unmounted) version.set(version.peek() + 1)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Return reactive accessor — Pyreon's mountChild calls mountReactive
|
|
119
|
+
return () => {
|
|
120
|
+
version() // tracked read — triggers re-execution when state changes
|
|
121
|
+
beginRender(ctx)
|
|
122
|
+
const result = (preactComponent as ComponentFn)(props)
|
|
123
|
+
const layoutEffects = ctx.pendingLayoutEffects
|
|
124
|
+
const effects = ctx.pendingEffects
|
|
125
|
+
endRender()
|
|
126
|
+
|
|
127
|
+
runLayoutEffects(layoutEffects)
|
|
128
|
+
scheduleEffects(ctx, effects)
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
}) as unknown as ComponentFn
|
|
133
|
+
|
|
134
|
+
_wrapperCache.set(preactComponent, wrapped)
|
|
135
|
+
return wrapped
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── JSX functions ───────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export function jsx(
|
|
141
|
+
type: string | ComponentFn | symbol,
|
|
142
|
+
props: Props & { children?: VNodeChild | VNodeChild[] },
|
|
143
|
+
key?: string | number | null,
|
|
144
|
+
): VNode {
|
|
145
|
+
const { children, ...rest } = props
|
|
146
|
+
const propsWithKey = (key != null ? { ...rest, key } : rest) as Props
|
|
147
|
+
|
|
148
|
+
if (typeof type === "function") {
|
|
149
|
+
// Wrap Preact-style component for re-render support
|
|
150
|
+
const wrapped = wrapCompatComponent(type)
|
|
151
|
+
const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
|
|
152
|
+
return h(wrapped, componentProps)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// DOM element or symbol (Fragment): children go in vnode.children
|
|
156
|
+
const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]
|
|
157
|
+
|
|
158
|
+
return h(type, propsWithKey, ...(childArray as VNodeChild[]))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const jsxs = jsx
|
|
162
|
+
export const jsxDEV = jsx
|