@pyreon/preact-compat 0.13.1 → 0.15.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/README.md CHANGED
@@ -124,3 +124,23 @@ Preact Signals API (mirrors `@preact/signals`).
124
124
  - **`computed(fn)`** -- returns `{ value }` read-only accessor.
125
125
  - **`effect(fn)`** -- reactive side effect, returns dispose function.
126
126
  - **`batch(fn)`** -- coalesce multiple signal writes.
127
+
128
+ ## Composing Pyreon framework components inside preact-compat
129
+
130
+ Pyreon's framework components (`RouterView`, `PyreonUI`, `FormProvider`, `QueryClientProvider`, …) ship marked with `nativeCompat()` from `@pyreon/core` — preact-compat's JSX runtime detects the marker and routes them through Pyreon's setup frame instead of the compat wrapper. **You don't need to do anything** for the 24 components shipped marked.
131
+
132
+ If you write your **own** Pyreon-flavored helper that uses `provide()` / `onMount()` / `onUnmount()` / `effect()` at component-body scope and use it in a preact-compat app, mark it explicitly:
133
+
134
+ ```tsx
135
+ import { nativeCompat, provide, createContext } from '@pyreon/core'
136
+
137
+ const MyCtx = createContext<string>('default')
138
+
139
+ function MyProvider(props: { value: string; children?: unknown }) {
140
+ provide(MyCtx, props.value)
141
+ return props.children as never
142
+ }
143
+ nativeCompat(MyProvider) // ← required for compat-mode apps
144
+ ```
145
+
146
+ Without the marker, the wrapper relocates the body's render context and `provide()` lands in a torn-down context stack — descendants read the default. See [`packages/core/core/src/compat-marker.ts`](../../core/core/src/compat-marker.ts) for details.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"hooks.js","children":[{"name":"src","children":[{"uid":"48c2c8ab-1","name":"jsx-runtime.ts"},{"uid":"48c2c8ab-3","name":"hooks.ts"}]}]}],"isRoot":true},"nodeParts":{"48c2c8ab-1":{"renderedLength":186,"gzipLength":138,"brotliLength":0,"metaUid":"48c2c8ab-0"},"48c2c8ab-3":{"renderedLength":4368,"gzipLength":1216,"brotliLength":0,"metaUid":"48c2c8ab-2"}},"nodeMetas":{"48c2c8ab-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"hooks.js":"48c2c8ab-1"},"imported":[{"uid":"48c2c8ab-4"},{"uid":"48c2c8ab-5"}],"importedBy":[{"uid":"48c2c8ab-2"}]},"48c2c8ab-2":{"id":"/src/hooks.ts","moduleParts":{"hooks.js":"48c2c8ab-3"},"imported":[{"uid":"48c2c8ab-4"},{"uid":"48c2c8ab-0"}],"importedBy":[],"isEntry":true},"48c2c8ab-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"48c2c8ab-2"},{"uid":"48c2c8ab-0"}]},"48c2c8ab-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"48c2c8ab-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"hooks.js","children":[{"name":"src","children":[{"uid":"3bf7c0f6-1","name":"jsx-runtime.ts"},{"uid":"3bf7c0f6-3","name":"hooks.ts"}]}]}],"isRoot":true},"nodeParts":{"3bf7c0f6-1":{"renderedLength":186,"gzipLength":138,"brotliLength":0,"metaUid":"3bf7c0f6-0"},"3bf7c0f6-3":{"renderedLength":7168,"gzipLength":2046,"brotliLength":0,"metaUid":"3bf7c0f6-2"}},"nodeMetas":{"3bf7c0f6-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"hooks.js":"3bf7c0f6-1"},"imported":[{"uid":"3bf7c0f6-4"},{"uid":"3bf7c0f6-5"}],"importedBy":[{"uid":"3bf7c0f6-2"}]},"3bf7c0f6-2":{"id":"/src/hooks.ts","moduleParts":{"hooks.js":"3bf7c0f6-3"},"imported":[{"uid":"3bf7c0f6-4"},{"uid":"3bf7c0f6-0"}],"importedBy":[],"isEntry":true},"3bf7c0f6-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bf7c0f6-2"},{"uid":"3bf7c0f6-0"}]},"3bf7c0f6-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bf7c0f6-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"e7c3b550-1"}]}],"isRoot":true},"nodeParts":{"e7c3b550-1":{"renderedLength":2789,"gzipLength":1150,"brotliLength":0,"metaUid":"e7c3b550-0"}},"nodeMetas":{"e7c3b550-0":{"id":"/src/index.ts","moduleParts":{"index.js":"e7c3b550-1"},"imported":[{"uid":"e7c3b550-2"},{"uid":"e7c3b550-3"},{"uid":"e7c3b550-4"}],"importedBy":[],"isEntry":true},"e7c3b550-2":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"e7c3b550-0"}]},"e7c3b550-3":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"e7c3b550-0"}]},"e7c3b550-4":{"id":"@pyreon/runtime-dom","moduleParts":{},"imported":[],"importedBy":[{"uid":"e7c3b550-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"2f430366-1"}]}],"isRoot":true},"nodeParts":{"2f430366-1":{"renderedLength":3276,"gzipLength":1311,"brotliLength":0,"metaUid":"2f430366-0"}},"nodeMetas":{"2f430366-0":{"id":"/src/index.ts","moduleParts":{"index.js":"2f430366-1"},"imported":[{"uid":"2f430366-2"},{"uid":"2f430366-3"},{"uid":"2f430366-4"}],"importedBy":[],"isEntry":true},"2f430366-2":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"2f430366-0"}]},"2f430366-3":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"2f430366-0"}]},"2f430366-4":{"id":"@pyreon/runtime-dom","moduleParts":{},"imported":[],"importedBy":[{"uid":"2f430366-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"jsx-runtime.js","children":[{"name":"src","children":[{"uid":"0c0e5fed-1","name":"jsx-runtime.ts"},{"uid":"0c0e5fed-3","name":"jsx-dev-runtime.ts"}]}]}],"isRoot":true},"nodeParts":{"0c0e5fed-1":{"renderedLength":2208,"gzipLength":798,"brotliLength":0,"metaUid":"0c0e5fed-0"},"0c0e5fed-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"0c0e5fed-2"}},"nodeMetas":{"0c0e5fed-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"jsx-runtime.js":"0c0e5fed-1"},"imported":[{"uid":"0c0e5fed-4"},{"uid":"0c0e5fed-5"}],"importedBy":[{"uid":"0c0e5fed-2"}]},"0c0e5fed-2":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"0c0e5fed-3"},"imported":[{"uid":"0c0e5fed-0"}],"importedBy":[],"isEntry":true},"0c0e5fed-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"0c0e5fed-0"}]},"0c0e5fed-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"0c0e5fed-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"jsx-runtime.js","children":[{"name":"src","children":[{"uid":"86b5580e-1","name":"jsx-runtime.ts"},{"uid":"86b5580e-3","name":"jsx-dev-runtime.ts"}]}]}],"isRoot":true},"nodeParts":{"86b5580e-1":{"renderedLength":5335,"gzipLength":1477,"brotliLength":0,"metaUid":"86b5580e-0"},"86b5580e-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"86b5580e-2"}},"nodeMetas":{"86b5580e-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"jsx-runtime.js":"86b5580e-1"},"imported":[{"uid":"86b5580e-4"},{"uid":"86b5580e-5"}],"importedBy":[{"uid":"86b5580e-2"}]},"86b5580e-2":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"86b5580e-3"},"imported":[{"uid":"86b5580e-0"}],"importedBy":[],"isEntry":true},"86b5580e-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"86b5580e-0"}]},"86b5580e-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"86b5580e-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"signals.js","children":[{"name":"src/signals.ts","uid":"ef3b7036-1"}]}],"isRoot":true},"nodeParts":{"ef3b7036-1":{"renderedLength":856,"gzipLength":375,"brotliLength":0,"metaUid":"ef3b7036-0"}},"nodeMetas":{"ef3b7036-0":{"id":"/src/signals.ts","moduleParts":{"signals.js":"ef3b7036-1"},"imported":[{"uid":"ef3b7036-2"}],"importedBy":[],"isEntry":true},"ef3b7036-2":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"ef3b7036-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"signals.js","children":[{"name":"src/signals.ts","uid":"660af5f2-1"}]}],"isRoot":true},"nodeParts":{"660af5f2-1":{"renderedLength":876,"gzipLength":383,"brotliLength":0,"metaUid":"660af5f2-0"}},"nodeMetas":{"660af5f2-0":{"id":"/src/signals.ts","moduleParts":{"signals.js":"660af5f2-1"},"imported":[{"uid":"660af5f2-2"}],"importedBy":[],"isEntry":true},"660af5f2-2":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"660af5f2-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/hooks.js CHANGED
@@ -23,23 +23,38 @@ function depsChanged(a, b) {
23
23
  for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return true;
24
24
  return false;
25
25
  }
26
+ function shallowEqual(a, b) {
27
+ const keysA = Object.keys(a);
28
+ const keysB = Object.keys(b);
29
+ if (keysA.length !== keysB.length) return false;
30
+ for (const k of keysA) if (!Object.is(a[k], b[k])) return false;
31
+ return true;
32
+ }
26
33
  /**
27
34
  * Preact-compatible `useState` — returns `[value, setter]`.
28
35
  * Triggers a component re-render when the setter is called.
36
+ *
37
+ * The setter has stable identity across renders (same reference every time).
29
38
  */
30
39
  function useState(initial) {
31
40
  const ctx = requireCtx();
32
41
  const idx = getHookIndex();
33
- if (ctx.hooks.length <= idx) ctx.hooks.push(typeof initial === "function" ? initial() : initial);
34
- const value = ctx.hooks[idx];
35
- const setter = (v) => {
36
- const current = ctx.hooks[idx];
37
- const next = typeof v === "function" ? v(current) : v;
38
- if (Object.is(current, next)) return;
39
- ctx.hooks[idx] = next;
40
- ctx.scheduleRerender();
41
- };
42
- return [value, setter];
42
+ if (ctx.hooks.length <= idx) {
43
+ const entry = {
44
+ value: typeof initial === "function" ? initial() : initial,
45
+ setter: null
46
+ };
47
+ entry.setter = (v) => {
48
+ const current = entry.value;
49
+ const next = typeof v === "function" ? v(current) : v;
50
+ if (Object.is(current, next)) return;
51
+ entry.value = next;
52
+ ctx.scheduleRerender();
53
+ };
54
+ ctx.hooks.push(entry);
55
+ }
56
+ const entry = ctx.hooks[idx];
57
+ return [entry.value, entry.setter];
43
58
  }
44
59
  /**
45
60
  * Preact-compatible `useEffect` — runs after render when deps change.
@@ -129,20 +144,33 @@ function useRef(initial) {
129
144
  }
130
145
  /**
131
146
  * Preact-compatible `useReducer` — returns `[state, dispatch]`.
147
+ * Supports the 3-argument form: `useReducer(reducer, initialArg, init)`.
148
+ *
149
+ * Dispatch has stable identity across renders (same reference every time).
132
150
  */
133
- function useReducer(reducer, initial) {
151
+ function useReducer(reducer, initialArg, init) {
134
152
  const ctx = requireCtx();
135
153
  const idx = getHookIndex();
136
- if (ctx.hooks.length <= idx) ctx.hooks.push(typeof initial === "function" ? initial() : initial);
137
- const state = ctx.hooks[idx];
138
- const dispatch = (action) => {
139
- const current = ctx.hooks[idx];
140
- const next = reducer(current, action);
141
- if (Object.is(current, next)) return;
142
- ctx.hooks[idx] = next;
143
- ctx.scheduleRerender();
144
- };
145
- return [state, dispatch];
154
+ if (ctx.hooks.length <= idx) {
155
+ let initial;
156
+ if (init) initial = init(initialArg);
157
+ else if (typeof initialArg === "function") initial = initialArg();
158
+ else initial = initialArg;
159
+ const entry = {
160
+ value: initial,
161
+ dispatch: null
162
+ };
163
+ entry.dispatch = (action) => {
164
+ const current = entry.value;
165
+ const next = reducer(current, action);
166
+ if (Object.is(current, next)) return;
167
+ entry.value = next;
168
+ ctx.scheduleRerender();
169
+ };
170
+ ctx.hooks.push(entry);
171
+ }
172
+ const entry = ctx.hooks[idx];
173
+ return [entry.value, entry.dispatch];
146
174
  }
147
175
  let _idCounter = 0;
148
176
  /**
@@ -157,25 +185,86 @@ function useId() {
157
185
  /**
158
186
  * Preact-compatible `memo` — wraps a component to skip re-render when props
159
187
  * are shallowly equal.
188
+ *
189
+ * Each component INSTANCE gets its own props/result cache via a hook slot,
190
+ * so two `<MemoComp />` usages don't share memoization state.
160
191
  */
161
192
  function memo(component, areEqual) {
162
- const compare = areEqual ?? ((a, b) => {
163
- const keysA = Object.keys(a);
164
- const keysB = Object.keys(b);
165
- if (keysA.length !== keysB.length) return false;
166
- for (const k of keysA) if (!Object.is(a[k], b[k])) return false;
167
- return true;
168
- });
169
- let prevProps = null;
170
- let prevResult = null;
171
- return (props) => {
172
- if (prevProps !== null && compare(prevProps, props)) return prevResult;
173
- prevProps = props;
174
- prevResult = component(props);
175
- return prevResult;
193
+ const compare = areEqual ?? shallowEqual;
194
+ let _fallbackPrevProps = null;
195
+ let _fallbackPrevResult = null;
196
+ const memoized = (props) => {
197
+ const ctx = getCurrentCtx();
198
+ if (ctx) {
199
+ const idx = getHookIndex();
200
+ if (ctx.hooks.length <= idx) ctx.hooks.push({
201
+ prevProps: null,
202
+ prevResult: null
203
+ });
204
+ const cache = ctx.hooks[idx];
205
+ if (cache.prevProps !== null && compare(cache.prevProps, props)) return cache.prevResult;
206
+ cache.prevProps = props;
207
+ cache.prevResult = component(props);
208
+ return cache.prevResult;
209
+ }
210
+ if (_fallbackPrevProps !== null && compare(_fallbackPrevProps, props)) return _fallbackPrevResult;
211
+ _fallbackPrevProps = props;
212
+ _fallbackPrevResult = component(props);
213
+ return _fallbackPrevResult;
214
+ };
215
+ memoized.displayName = component.displayName || component.name || "Memo";
216
+ return memoized;
217
+ }
218
+ /**
219
+ * Preact-compatible `forwardRef` — pass-through in Pyreon.
220
+ * Refs are regular props in Pyreon, so no wrapper is needed.
221
+ * The render function receives (props, ref) — we merge ref into props.
222
+ */
223
+ function forwardRef(render) {
224
+ const forwarded = (props) => {
225
+ const { ref, ...rest } = props;
226
+ return render(rest, ref ?? null);
176
227
  };
228
+ forwarded.displayName = render.displayName || render.name || "ForwardRef";
229
+ return forwarded;
230
+ }
231
+ /**
232
+ * Preact-compatible `useImperativeHandle`.
233
+ */
234
+ function useImperativeHandle(ref, init, deps) {
235
+ useLayoutEffect(() => {
236
+ if (ref) ref.current = init();
237
+ return () => {
238
+ if (ref) ref.current = null;
239
+ };
240
+ }, deps);
241
+ }
242
+ /**
243
+ * Preact-compatible `useDebugValue` — no-op in Pyreon (no Preact DevTools integration).
244
+ */
245
+ function useDebugValue(_value, _format) {}
246
+ /**
247
+ * Preact-compatible `useTransition` — returns `[isPending, startTransition]`.
248
+ *
249
+ * In Pyreon's signal-based reactivity there is no concept of concurrent
250
+ * rendering lanes. The callback is executed synchronously and `isPending`
251
+ * is always `false`. This shim exists so Preact/React code that uses
252
+ * `useTransition` compiles and runs without changes.
253
+ */
254
+ function useTransition() {
255
+ return [false, (fn) => fn()];
256
+ }
257
+ /**
258
+ * Preact-compatible `useDeferredValue` — returns the value as-is.
259
+ *
260
+ * In Pyreon's signal-based reactivity there are no concurrent rendering lanes,
261
+ * so the value is never "deferred". This shim exists so Preact/React code that
262
+ * uses `useDeferredValue` compiles and runs without changes.
263
+ */
264
+ function useDeferredValue(value) {
265
+ return value;
177
266
  }
178
267
 
179
268
  //#endregion
180
- export { memo, useCallback, useContext, useEffect, onErrorCaptured as useErrorBoundary, useId, useLayoutEffect, useMemo, useReducer, useRef, useState };
269
+ export { forwardRef, memo, useCallback, useContext, useDebugValue, useDeferredValue, useEffect, onErrorCaptured as useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useTransition };
181
270
  //# sourceMappingURL=hooks.js.map
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Fragment, createContext as createContext$1, createRef, h as pyreonH, provide, useContext } from "@pyreon/core";
1
+ import { ErrorBoundary, Fragment, Portal, Suspense, createContext as createContext$1, createRef, h as pyreonH, lazy, nativeCompat, provide, useContext } from "@pyreon/core";
2
2
  import { batch, signal } from "@pyreon/reactivity";
3
3
  import { hydrateRoot, mount } from "@pyreon/runtime-dom";
4
4
 
@@ -28,6 +28,7 @@ function createContext(defaultValue) {
28
28
  provide(ctx, props.value);
29
29
  return props.children;
30
30
  });
31
+ nativeCompat(Provider);
31
32
  return {
32
33
  ...ctx,
33
34
  Provider
@@ -43,6 +44,7 @@ var Component = class {
43
44
  props;
44
45
  state;
45
46
  _stateSignal;
47
+ _lastResult;
46
48
  constructor(props) {
47
49
  this.props = props;
48
50
  this.state = {};
@@ -78,13 +80,19 @@ var Component = class {
78
80
  }
79
81
  };
80
82
  /**
83
+ * Preact-compatible PureComponent — extends Component.
84
+ * In Pyreon's compat layer this behaves identically to Component
85
+ * (signal-based reactivity already avoids unnecessary re-renders).
86
+ */
87
+ var PureComponent = class extends Component {};
88
+ /**
81
89
  * Clone a VNode with merged props (like Preact's cloneElement).
82
90
  */
83
91
  function cloneElement(vnode, props, ...children) {
84
- const mergedProps = {
92
+ const mergedProps = props ? {
85
93
  ...vnode.props,
86
- ...props ?? {}
87
- };
94
+ ...props
95
+ } : { ...vnode.props };
88
96
  const mergedChildren = children.length > 0 ? children : vnode.children;
89
97
  return {
90
98
  type: vnode.type,
@@ -110,11 +118,21 @@ function isValidElement(x) {
110
118
  return x !== null && typeof x === "object" && "type" in x && "props" in x && "children" in x;
111
119
  }
112
120
  /**
121
+ * Preact-compatible `createPortal(children, target)`.
122
+ */
123
+ function createPortal(children, target) {
124
+ return Portal({
125
+ target,
126
+ children
127
+ });
128
+ }
129
+ /**
113
130
  * Preact's plugin/hook system. Exposed as an empty object for compatibility
114
131
  * with libraries that check for `options._hook`, `options.vnode`, etc.
115
132
  */
116
133
  const options = {};
134
+ const version = "10.0.0-pyreon";
117
135
 
118
136
  //#endregion
119
- export { Component, Fragment, cloneElement, createContext, createElement, createRef, pyreonH as h, hydrate, isValidElement, options, render, toChildArray, useContext };
137
+ export { Component, ErrorBoundary, Fragment, PureComponent, Suspense, cloneElement, createContext, createElement, createPortal, createRef, pyreonH as h, hydrate, isValidElement, lazy, options, render, toChildArray, useContext, version };
120
138
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import { Fragment, h } from "@pyreon/core";
1
+ import { Fragment, h, isNativeCompat, onUnmount } from "@pyreon/core";
2
2
  import { signal } from "@pyreon/reactivity";
3
3
 
4
4
  //#region src/jsx-runtime.ts
@@ -32,10 +32,60 @@ function scheduleEffects(ctx, entries) {
32
32
  }
33
33
  });
34
34
  }
35
+ function isClassComponent(type) {
36
+ return type.prototype != null && typeof type.prototype.render === "function";
37
+ }
38
+ function wrapClassComponent(ClassComp) {
39
+ const wrapped = ((props) => {
40
+ const instance = new ClassComp(props);
41
+ const version = signal(0);
42
+ let updateScheduled = false;
43
+ const origSetState = instance.setState.bind(instance);
44
+ instance.setState = (partial) => {
45
+ origSetState(partial);
46
+ if (!updateScheduled) {
47
+ updateScheduled = true;
48
+ queueMicrotask(() => {
49
+ updateScheduled = false;
50
+ version.set(version.peek() + 1);
51
+ });
52
+ }
53
+ };
54
+ instance.forceUpdate = () => {
55
+ version.set(version.peek() + 1);
56
+ };
57
+ let didMountFired = false;
58
+ onUnmount(() => {
59
+ if (typeof instance.componentWillUnmount === "function") instance.componentWillUnmount();
60
+ });
61
+ return () => {
62
+ const ver = version();
63
+ instance.props = props;
64
+ if (didMountFired && ver > 0 && typeof instance.shouldComponentUpdate === "function") {
65
+ if (!instance.shouldComponentUpdate(props, instance.state)) return instance._lastResult;
66
+ }
67
+ const result = instance.render();
68
+ instance._lastResult = result;
69
+ if (!didMountFired) {
70
+ didMountFired = true;
71
+ if (typeof instance.componentDidMount === "function") queueMicrotask(() => instance.componentDidMount());
72
+ } else if (ver > 0) {
73
+ if (typeof instance.componentDidUpdate === "function") queueMicrotask(() => instance.componentDidUpdate());
74
+ }
75
+ return result;
76
+ };
77
+ });
78
+ return wrapped;
79
+ }
35
80
  const _wrapperCache = /* @__PURE__ */ new WeakMap();
36
81
  function wrapCompatComponent(preactComponent) {
37
82
  let wrapped = _wrapperCache.get(preactComponent);
38
83
  if (wrapped) return wrapped;
84
+ if (isClassComponent(preactComponent)) {
85
+ wrapped = wrapClassComponent(preactComponent);
86
+ _wrapperCache.set(preactComponent, wrapped);
87
+ return wrapped;
88
+ }
39
89
  wrapped = ((props) => {
40
90
  const ctx = {
41
91
  hooks: [],
@@ -54,6 +104,13 @@ function wrapCompatComponent(preactComponent) {
54
104
  if (!ctx.unmounted) version.set(version.peek() + 1);
55
105
  });
56
106
  };
107
+ onUnmount(() => {
108
+ ctx.unmounted = true;
109
+ for (const hook of ctx.hooks) if (hook && typeof hook === "object" && "cleanup" in hook) {
110
+ const entry = hook;
111
+ if (typeof entry.cleanup === "function") entry.cleanup();
112
+ }
113
+ });
57
114
  return () => {
58
115
  version();
59
116
  beginRender(ctx);
@@ -75,11 +132,45 @@ function jsx(type, props, key) {
75
132
  ...rest,
76
133
  key
77
134
  } : rest;
78
- if (typeof type === "function") return h(wrapCompatComponent(type), children !== void 0 ? {
79
- ...propsWithKey,
80
- children
81
- } : propsWithKey);
82
- return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
135
+ if (typeof type === "function") {
136
+ const componentProps = children !== void 0 ? {
137
+ ...propsWithKey,
138
+ children
139
+ } : propsWithKey;
140
+ if (isNativeCompat(type)) return h(type, componentProps);
141
+ return h(wrapCompatComponent(type), componentProps);
142
+ }
143
+ const childArray = children === void 0 ? [] : Array.isArray(children) ? children : [children];
144
+ if (typeof type === "string") {
145
+ if (propsWithKey.className !== void 0) {
146
+ propsWithKey.class = propsWithKey.className;
147
+ delete propsWithKey.className;
148
+ }
149
+ if (propsWithKey.htmlFor !== void 0) {
150
+ propsWithKey.for = propsWithKey.htmlFor;
151
+ delete propsWithKey.htmlFor;
152
+ }
153
+ if ((type === "input" || type === "textarea" || type === "select") && propsWithKey.onChange !== void 0) {
154
+ if (propsWithKey.onInput === void 0) propsWithKey.onInput = propsWithKey.onChange;
155
+ delete propsWithKey.onChange;
156
+ }
157
+ if (propsWithKey.autoFocus !== void 0) {
158
+ propsWithKey.autofocus = propsWithKey.autoFocus;
159
+ delete propsWithKey.autoFocus;
160
+ }
161
+ if (type === "input" || type === "textarea") {
162
+ if (propsWithKey.defaultValue !== void 0 && propsWithKey.value === void 0) {
163
+ propsWithKey.value = propsWithKey.defaultValue;
164
+ delete propsWithKey.defaultValue;
165
+ }
166
+ if (propsWithKey.defaultChecked !== void 0 && propsWithKey.checked === void 0) {
167
+ propsWithKey.checked = propsWithKey.defaultChecked;
168
+ delete propsWithKey.defaultChecked;
169
+ }
170
+ }
171
+ delete propsWithKey.suppressHydrationWarning;
172
+ }
173
+ return h(type, propsWithKey, ...childArray);
83
174
  }
84
175
  const jsxs = jsx;
85
176
 
package/lib/signals.js CHANGED
@@ -1,4 +1,4 @@
1
- import { batch as pyreonBatch, computed as computed$1, effect as effect$1, signal as signal$1 } from "@pyreon/reactivity";
1
+ import { batch as pyreonBatch, computed as computed$1, effect as effect$1, runUntracked, signal as signal$1 } from "@pyreon/reactivity";
2
2
 
3
3
  //#region src/signals.ts
4
4
  /**
@@ -37,7 +37,7 @@ function computed(fn) {
37
37
  return c();
38
38
  },
39
39
  peek() {
40
- return c();
40
+ return runUntracked(() => c());
41
41
  }
42
42
  };
43
43
  }
@@ -4,6 +4,8 @@ import { VNodeChild, onErrorCaptured, useContext } from "@pyreon/core";
4
4
  /**
5
5
  * Preact-compatible `useState` — returns `[value, setter]`.
6
6
  * Triggers a component re-render when the setter is called.
7
+ *
8
+ * The setter has stable identity across renders (same reference every time).
7
9
  */
8
10
  declare function useState<T>(initial: T | (() => T)): [T, (v: T | ((prev: T) => T)) => void];
9
11
  /**
@@ -31,8 +33,11 @@ declare function useRef<T>(initial?: T): {
31
33
  };
32
34
  /**
33
35
  * Preact-compatible `useReducer` — returns `[state, dispatch]`.
36
+ * Supports the 3-argument form: `useReducer(reducer, initialArg, init)`.
37
+ *
38
+ * Dispatch has stable identity across renders (same reference every time).
34
39
  */
35
- declare function useReducer<S, A>(reducer: (state: S, action: A) => S, initial: S | (() => S)): [S, (action: A) => void];
40
+ declare function useReducer<S, A>(reducer: (state: S, action: A) => S, initialArg: S | (() => S), init?: (arg: S) => S): [S, (action: A) => void];
36
41
  /**
37
42
  * Preact-compatible `useId` — returns a stable unique string per hook call.
38
43
  */
@@ -40,8 +45,50 @@ declare function useId(): string;
40
45
  /**
41
46
  * Preact-compatible `memo` — wraps a component to skip re-render when props
42
47
  * are shallowly equal.
48
+ *
49
+ * Each component INSTANCE gets its own props/result cache via a hook slot,
50
+ * so two `<MemoComp />` usages don't share memoization state.
43
51
  */
44
52
  declare function memo<P extends Record<string, unknown>>(component: (props: P) => VNodeChild, areEqual?: (prevProps: P, nextProps: P) => boolean): (props: P) => VNodeChild;
53
+ /**
54
+ * Preact-compatible `forwardRef` — pass-through in Pyreon.
55
+ * Refs are regular props in Pyreon, so no wrapper is needed.
56
+ * The render function receives (props, ref) — we merge ref into props.
57
+ */
58
+ declare function forwardRef<P extends Record<string, unknown>>(render: (props: P, ref: {
59
+ current: unknown;
60
+ } | null) => VNodeChild): (props: P & {
61
+ ref?: {
62
+ current: unknown;
63
+ } | null;
64
+ }) => VNodeChild;
65
+ /**
66
+ * Preact-compatible `useImperativeHandle`.
67
+ */
68
+ declare function useImperativeHandle<T>(ref: {
69
+ current: T | null;
70
+ } | null | undefined, init: () => T, deps?: unknown[]): void;
71
+ /**
72
+ * Preact-compatible `useDebugValue` — no-op in Pyreon (no Preact DevTools integration).
73
+ */
74
+ declare function useDebugValue<T>(_value: T, _format?: (v: T) => unknown): void;
75
+ /**
76
+ * Preact-compatible `useTransition` — returns `[isPending, startTransition]`.
77
+ *
78
+ * In Pyreon's signal-based reactivity there is no concept of concurrent
79
+ * rendering lanes. The callback is executed synchronously and `isPending`
80
+ * is always `false`. This shim exists so Preact/React code that uses
81
+ * `useTransition` compiles and runs without changes.
82
+ */
83
+ declare function useTransition(): [boolean, (fn: () => void) => void];
84
+ /**
85
+ * Preact-compatible `useDeferredValue` — returns the value as-is.
86
+ *
87
+ * In Pyreon's signal-based reactivity there are no concurrent rendering lanes,
88
+ * so the value is never "deferred". This shim exists so Preact/React code that
89
+ * uses `useDeferredValue` compiles and runs without changes.
90
+ */
91
+ declare function useDeferredValue<T>(value: T): T;
45
92
  //#endregion
46
- export { memo, useCallback, useContext, useEffect, onErrorCaptured as useErrorBoundary, useId, useLayoutEffect, useMemo, useReducer, useRef, useState };
93
+ export { forwardRef, memo, useCallback, useContext, useDebugValue, useDeferredValue, useEffect, onErrorCaptured as useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useTransition };
47
94
  //# sourceMappingURL=hooks2.d.ts.map
@@ -1,4 +1,5 @@
1
- import { ComponentFn, Fragment, Props, VNode, VNodeChild, createRef, h as pyreonH, useContext } from "@pyreon/core";
1
+ import { ComponentFn, ErrorBoundary, Fragment, Props, Suspense, VNode, VNodeChild, createRef, h as pyreonH, lazy, useContext } from "@pyreon/core";
2
+ import { signal } from "@pyreon/reactivity";
2
3
 
3
4
  //#region src/index.d.ts
4
5
  /** Alias: Preact also exports createElement */
@@ -34,7 +35,12 @@ declare function createContext<T>(defaultValue: T): PreactContext<T>;
34
35
  declare class Component<P extends Props = Props, S extends Record<string, unknown> = Record<string, unknown>> {
35
36
  props: P;
36
37
  state: S;
37
- private _stateSignal;
38
+ _stateSignal: ReturnType<typeof signal<S>>;
39
+ _lastResult?: VNodeChild;
40
+ componentDidMount?(): void;
41
+ componentDidUpdate?(): void;
42
+ componentWillUnmount?(): void;
43
+ shouldComponentUpdate?(nextProps: P, nextState: S): boolean;
38
44
  constructor(props: P);
39
45
  /**
40
46
  * Update state — accepts a partial state object or an updater function.
@@ -50,6 +56,12 @@ declare class Component<P extends Props = Props, S extends Record<string, unknow
50
56
  */
51
57
  render(): VNodeChild;
52
58
  }
59
+ /**
60
+ * Preact-compatible PureComponent — extends Component.
61
+ * In Pyreon's compat layer this behaves identically to Component
62
+ * (signal-based reactivity already avoids unnecessary re-renders).
63
+ */
64
+ declare class PureComponent<P extends Props = Props, S extends Record<string, unknown> = Record<string, unknown>> extends Component<P, S> {}
53
65
  /**
54
66
  * Clone a VNode with merged props (like Preact's cloneElement).
55
67
  */
@@ -64,11 +76,16 @@ declare function toChildArray(children: NestedChildren): VNodeChild[];
64
76
  * Check if a value is a VNode (like Preact's isValidElement).
65
77
  */
66
78
  declare function isValidElement(x: unknown): x is VNode;
79
+ /**
80
+ * Preact-compatible `createPortal(children, target)`.
81
+ */
82
+ declare function createPortal(children: VNodeChild, target: Element): VNodeChild;
67
83
  /**
68
84
  * Preact's plugin/hook system. Exposed as an empty object for compatibility
69
85
  * with libraries that check for `options._hook`, `options.vnode`, etc.
70
86
  */
71
87
  declare const options: Record<string, unknown>;
88
+ declare const version = "10.0.0-pyreon";
72
89
  //#endregion
73
- export { Component, Fragment, PreactContext, cloneElement, createContext, createElement, createRef, pyreonH as h, hydrate, isValidElement, options, render, toChildArray, useContext };
90
+ export { Component, ErrorBoundary, Fragment, PreactContext, PureComponent, Suspense, cloneElement, createContext, createElement, createPortal, createRef, pyreonH as h, hydrate, isValidElement, lazy, options, render, toChildArray, useContext, version };
74
91
  //# sourceMappingURL=index2.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/preact-compat",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Preact-compatible API shim for Pyreon — write Preact-style code that runs on Pyreon's reactive engine",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/preact-compat#readme",
6
6
  "bugs": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "lib",
17
+ "!lib/**/*.map",
17
18
  "src",
18
19
  "README.md",
19
20
  "LICENSE"
@@ -57,17 +58,20 @@
57
58
  "build": "vl_rolldown_build",
58
59
  "dev": "vl_rolldown_build-watch",
59
60
  "test": "vitest run",
61
+ "test:browser": "vitest run --config ./vitest.browser.config.ts",
60
62
  "typecheck": "tsc --noEmit",
61
63
  "lint": "oxlint .",
62
64
  "prepublishOnly": "bun run build"
63
65
  },
64
66
  "dependencies": {
65
- "@pyreon/core": "^0.13.1",
66
- "@pyreon/reactivity": "^0.13.1",
67
- "@pyreon/runtime-dom": "^0.13.1"
67
+ "@pyreon/core": "^0.15.0",
68
+ "@pyreon/reactivity": "^0.15.0",
69
+ "@pyreon/runtime-dom": "^0.15.0"
68
70
  },
69
71
  "devDependencies": {
70
72
  "@happy-dom/global-registrator": "^20.8.9",
73
+ "@pyreon/test-utils": "^0.13.2",
74
+ "@vitest/browser-playwright": "^4.1.4",
71
75
  "happy-dom": "^20.8.3"
72
76
  }
73
77
  }