@pyreon/preact-compat 0.13.1 → 0.14.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/hooks.js.html +1 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/analysis/jsx-runtime.js.html +1 -1
- package/lib/analysis/signals.js.html +1 -1
- package/lib/hooks.js +125 -36
- package/lib/hooks.js.map +1 -1
- package/lib/index.js +24 -5
- package/lib/index.js.map +1 -1
- package/lib/jsx-runtime.js +98 -6
- package/lib/jsx-runtime.js.map +1 -1
- package/lib/signals.js +2 -2
- package/lib/signals.js.map +1 -1
- package/lib/types/hooks.d.ts +49 -2
- package/lib/types/hooks.d.ts.map +1 -1
- package/lib/types/index.d.ts +20 -3
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/jsx-runtime.d.ts.map +1 -1
- package/lib/types/signals.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/hooks.ts +157 -42
- package/src/index.ts +50 -4
- package/src/jsx-runtime.ts +147 -2
- package/src/signals.ts +2 -2
- package/src/tests/new-apis.test.ts +1084 -0
package/src/jsx-runtime.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ComponentFn, Props, VNode, VNodeChild } from '@pyreon/core'
|
|
13
|
-
import { Fragment, h } from '@pyreon/core'
|
|
13
|
+
import { Fragment, h, onUnmount } from '@pyreon/core'
|
|
14
14
|
import { signal } from '@pyreon/reactivity'
|
|
15
|
+
import type { Component } from './index'
|
|
15
16
|
|
|
16
17
|
export { Fragment }
|
|
17
18
|
|
|
@@ -79,6 +80,84 @@ function scheduleEffects(ctx: RenderContext, entries: EffectEntry[]): void {
|
|
|
79
80
|
})
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// ─── Native component marker ────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
const NATIVE_COMPONENT = Symbol.for('pyreon:native-compat')
|
|
86
|
+
|
|
87
|
+
// ─── Class component detection ──────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function isClassComponent(type: Function): boolean {
|
|
90
|
+
return type.prototype != null && typeof type.prototype.render === 'function'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Class component wrapping ───────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function wrapClassComponent(ClassComp: Function): ComponentFn {
|
|
96
|
+
const wrapped = ((props: Props) => {
|
|
97
|
+
const instance = new (ClassComp as new (props: Props) => Component)(props)
|
|
98
|
+
const version = signal(0)
|
|
99
|
+
let updateScheduled = false
|
|
100
|
+
|
|
101
|
+
// Override setState to trigger re-render via version signal
|
|
102
|
+
const origSetState = instance.setState.bind(instance)
|
|
103
|
+
instance.setState = (partial: Partial<Record<string, unknown>>) => {
|
|
104
|
+
origSetState(partial)
|
|
105
|
+
if (!updateScheduled) {
|
|
106
|
+
updateScheduled = true
|
|
107
|
+
queueMicrotask(() => {
|
|
108
|
+
updateScheduled = false
|
|
109
|
+
version.set(version.peek() + 1)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Override forceUpdate
|
|
115
|
+
instance.forceUpdate = () => {
|
|
116
|
+
version.set(version.peek() + 1)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Lifecycle: componentWillUnmount
|
|
120
|
+
let didMountFired = false
|
|
121
|
+
onUnmount(() => {
|
|
122
|
+
if (typeof instance.componentWillUnmount === 'function') {
|
|
123
|
+
instance.componentWillUnmount()
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Return reactive accessor for re-renders
|
|
128
|
+
return () => {
|
|
129
|
+
const ver = version() // track for re-renders
|
|
130
|
+
instance.props = props // update props on re-render
|
|
131
|
+
|
|
132
|
+
// shouldComponentUpdate only applies after mount (ver > 0 means setState/forceUpdate)
|
|
133
|
+
if (didMountFired && ver > 0 && typeof instance.shouldComponentUpdate === 'function') {
|
|
134
|
+
if (!instance.shouldComponentUpdate(props, instance.state)) {
|
|
135
|
+
return instance._lastResult // skip render
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = instance.render()
|
|
140
|
+
instance._lastResult = result
|
|
141
|
+
|
|
142
|
+
// componentDidMount fires once after the initial render settles
|
|
143
|
+
if (!didMountFired) {
|
|
144
|
+
didMountFired = true
|
|
145
|
+
if (typeof instance.componentDidMount === 'function') {
|
|
146
|
+
queueMicrotask(() => instance.componentDidMount!())
|
|
147
|
+
}
|
|
148
|
+
} else if (ver > 0) {
|
|
149
|
+
// componentDidUpdate only fires on explicit re-renders (setState/forceUpdate)
|
|
150
|
+
if (typeof instance.componentDidUpdate === 'function') {
|
|
151
|
+
queueMicrotask(() => instance.componentDidUpdate!())
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result
|
|
156
|
+
}
|
|
157
|
+
}) as unknown as ComponentFn
|
|
158
|
+
return wrapped
|
|
159
|
+
}
|
|
160
|
+
|
|
82
161
|
// ─── Component wrapping ──────────────────────────────────────────────────────
|
|
83
162
|
|
|
84
163
|
const _wrapperCache = new WeakMap<Function, ComponentFn>()
|
|
@@ -87,6 +166,13 @@ function wrapCompatComponent(preactComponent: Function): ComponentFn {
|
|
|
87
166
|
let wrapped = _wrapperCache.get(preactComponent)
|
|
88
167
|
if (wrapped) return wrapped
|
|
89
168
|
|
|
169
|
+
// Handle class components (those with prototype.render)
|
|
170
|
+
if (isClassComponent(preactComponent)) {
|
|
171
|
+
wrapped = wrapClassComponent(preactComponent)
|
|
172
|
+
_wrapperCache.set(preactComponent, wrapped)
|
|
173
|
+
return wrapped
|
|
174
|
+
}
|
|
175
|
+
|
|
90
176
|
// The wrapper returns a reactive accessor (() => VNodeChild) which Pyreon's
|
|
91
177
|
// mountChild treats as a reactive expression via mountReactive.
|
|
92
178
|
wrapped = ((props: Props) => {
|
|
@@ -112,6 +198,17 @@ function wrapCompatComponent(preactComponent: Function): ComponentFn {
|
|
|
112
198
|
})
|
|
113
199
|
}
|
|
114
200
|
|
|
201
|
+
// Register cleanup for all hooks on unmount
|
|
202
|
+
onUnmount(() => {
|
|
203
|
+
ctx.unmounted = true
|
|
204
|
+
for (const hook of ctx.hooks) {
|
|
205
|
+
if (hook && typeof hook === 'object' && 'cleanup' in hook) {
|
|
206
|
+
const entry = hook as EffectEntry
|
|
207
|
+
if (typeof entry.cleanup === 'function') entry.cleanup()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
115
212
|
// Return reactive accessor — Pyreon's mountChild calls mountReactive
|
|
116
213
|
return () => {
|
|
117
214
|
version() // tracked read — triggers re-execution when state changes
|
|
@@ -143,15 +240,63 @@ export function jsx(
|
|
|
143
240
|
const propsWithKey = (key != null ? { ...rest, key } : rest) as Props
|
|
144
241
|
|
|
145
242
|
if (typeof type === 'function') {
|
|
243
|
+
const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
|
|
244
|
+
// Native Pyreon components (e.g. context Provider) skip compat wrapping
|
|
245
|
+
if ((type as unknown as Record<symbol, boolean>)[NATIVE_COMPONENT]) {
|
|
246
|
+
return h(type as ComponentFn, componentProps)
|
|
247
|
+
}
|
|
146
248
|
// Wrap Preact-style component for re-render support
|
|
147
249
|
const wrapped = wrapCompatComponent(type)
|
|
148
|
-
const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
|
|
149
250
|
return h(wrapped, componentProps)
|
|
150
251
|
}
|
|
151
252
|
|
|
152
253
|
// DOM element or symbol (Fragment): children go in vnode.children
|
|
153
254
|
const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]
|
|
154
255
|
|
|
256
|
+
// Map Preact-style attributes to standard HTML attributes
|
|
257
|
+
if (typeof type === 'string') {
|
|
258
|
+
if (propsWithKey.className !== undefined) {
|
|
259
|
+
propsWithKey.class = propsWithKey.className
|
|
260
|
+
delete propsWithKey.className
|
|
261
|
+
}
|
|
262
|
+
if (propsWithKey.htmlFor !== undefined) {
|
|
263
|
+
propsWithKey.for = propsWithKey.htmlFor
|
|
264
|
+
delete propsWithKey.htmlFor
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Preact's onChange fires on every keystroke for form elements (like onInput)
|
|
268
|
+
if (
|
|
269
|
+
(type === 'input' || type === 'textarea' || type === 'select') &&
|
|
270
|
+
propsWithKey.onChange !== undefined
|
|
271
|
+
) {
|
|
272
|
+
if (propsWithKey.onInput === undefined) {
|
|
273
|
+
propsWithKey.onInput = propsWithKey.onChange
|
|
274
|
+
}
|
|
275
|
+
delete propsWithKey.onChange
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// autoFocus → autofocus
|
|
279
|
+
if (propsWithKey.autoFocus !== undefined) {
|
|
280
|
+
propsWithKey.autofocus = propsWithKey.autoFocus
|
|
281
|
+
delete propsWithKey.autoFocus
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// defaultValue / defaultChecked → value / checked when no controlled value
|
|
285
|
+
if (type === 'input' || type === 'textarea') {
|
|
286
|
+
if (propsWithKey.defaultValue !== undefined && propsWithKey.value === undefined) {
|
|
287
|
+
propsWithKey.value = propsWithKey.defaultValue
|
|
288
|
+
delete propsWithKey.defaultValue
|
|
289
|
+
}
|
|
290
|
+
if (propsWithKey.defaultChecked !== undefined && propsWithKey.checked === undefined) {
|
|
291
|
+
propsWithKey.checked = propsWithKey.defaultChecked
|
|
292
|
+
delete propsWithKey.defaultChecked
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Strip Preact-only props that have no DOM equivalent
|
|
297
|
+
delete propsWithKey.suppressHydrationWarning
|
|
298
|
+
}
|
|
299
|
+
|
|
155
300
|
return h(type, propsWithKey, ...(childArray as VNodeChild[]))
|
|
156
301
|
}
|
|
157
302
|
|
package/src/signals.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
batch as pyreonBatch,
|
|
11
11
|
computed as pyreonComputed,
|
|
12
12
|
effect as pyreonEffect,
|
|
13
|
+
runUntracked as pyreonRunUntracked,
|
|
13
14
|
signal as pyreonSignal,
|
|
14
15
|
} from '@pyreon/reactivity'
|
|
15
16
|
|
|
@@ -63,8 +64,7 @@ export function computed<T>(fn: () => T): ReadonlySignal<T> {
|
|
|
63
64
|
return c()
|
|
64
65
|
},
|
|
65
66
|
peek(): T {
|
|
66
|
-
|
|
67
|
-
return c()
|
|
67
|
+
return pyreonRunUntracked(() => c())
|
|
68
68
|
},
|
|
69
69
|
}
|
|
70
70
|
}
|