@pyreon/preact-compat 0.21.0 → 0.23.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
@@ -1,6 +1,8 @@
1
1
  # @pyreon/preact-compat
2
2
 
3
- Preact-compatible API shim that runs on Pyreon's signal-based reactive engine. Migrate Preact code by swapping the import path.
3
+ Preact-compatible API shim — write Preact-style code that runs on Pyreon's reactive engine.
4
+
5
+ `@pyreon/preact-compat` mirrors Preact's module structure (`@pyreon/preact-compat`, `@pyreon/preact-compat/hooks`, `@pyreon/preact-compat/signals`) and provides `h` / `Fragment` / `render` / `hydrate` / `Component` / `PureComponent` / `createContext` / `createRef` / `cloneElement` / `createPortal` / `lazy` / `Suspense` / `ErrorBoundary` plus the standard hooks set, all backed by Pyreon's signal-based reactivity. **This is a compat shim, not Preact** — it intentionally diverges in places where Preact's render-on-state-change model conflicts with Pyreon's run-once + fine-grained-reactivity model. The escape hatch is to drop the compat layer and use Pyreon's native API directly.
4
6
 
5
7
  ## Install
6
8
 
@@ -8,139 +10,124 @@ Preact-compatible API shim that runs on Pyreon's signal-based reactive engine. M
8
10
  bun add @pyreon/preact-compat
9
11
  ```
10
12
 
11
- ## Quick Start
13
+ Then alias your Preact imports (or use `pyreon({ compat: 'preact' })` from `@pyreon/vite-plugin` for zero code changes):
14
+
15
+ ```ts
16
+ import { h, render, Fragment } from '@pyreon/preact-compat'
17
+ import { useState, useEffect } from '@pyreon/preact-compat/hooks'
18
+ import { signal, computed } from '@pyreon/preact-compat/signals'
19
+ ```
20
+
21
+ ## Quick start
12
22
 
13
23
  ```tsx
14
- // Replace:
15
- // import { render } from "preact"
16
- // import { useState, useEffect } from "preact/hooks"
17
- // With:
18
- import { render } from '@pyreon/preact-compat'
24
+ import { h, render } from '@pyreon/preact-compat'
19
25
  import { useState, useEffect } from '@pyreon/preact-compat/hooks'
20
26
 
21
27
  function Counter() {
22
28
  const [count, setCount] = useState(0)
29
+
23
30
  useEffect(() => {
24
- console.log('count changed:', count())
31
+ document.title = `Count: ${count()}`
25
32
  })
26
- return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
27
- }
28
-
29
- render(<Counter />, document.getElementById('app')!)
30
- ```
31
-
32
- ### Signals
33
-
34
- ```tsx
35
- import { signal, computed, effect } from '@pyreon/preact-compat/signals'
36
-
37
- const count = signal(0)
38
- const doubled = computed(() => count.value * 2)
39
33
 
40
- function Display() {
41
- effect(() => console.log('doubled:', doubled.value))
42
34
  return (
43
35
  <div>
44
- <span>{doubled.value}</span>
45
- <button onClick={() => count.value++}>+</button>
36
+ <p>Count: {count()}</p>
37
+ <button onClick={() => setCount((prev) => prev + 1)}>+1</button>
46
38
  </div>
47
39
  )
48
40
  }
49
- ```
50
-
51
- ### Class Components
52
41
 
53
- ```tsx
54
- import { Component } from '@pyreon/preact-compat'
42
+ render(<Counter />, document.getElementById('app')!)
43
+ ```
55
44
 
56
- interface Props {
57
- name: string
58
- }
59
- interface State {
60
- clicked: boolean
61
- }
45
+ ## Subpath exports
62
46
 
63
- class Greeting extends Component<Props, State> {
64
- state = { clicked: false }
47
+ | Subpath | Surface |
48
+ | --------------------------------------- | --------------------------------------------------------------------------------------------- |
49
+ | `@pyreon/preact-compat` | Core: `h` / `createElement`, `Fragment`, `render`, `hydrate`, `Component`, `PureComponent`, `createContext` / `useContext`, `createRef`, `cloneElement`, `toChildArray`, `isValidElement`, `createPortal`, `lazy`, `Suspense`, `ErrorBoundary`, `options`, `version` |
50
+ | `@pyreon/preact-compat/hooks` | `useState`, `useReducer`, `useEffect`, `useLayoutEffect`, `useMemo`, `useCallback`, `useRef`, `useId`, `memo`, `forwardRef`, `useImperativeHandle`, `useDebugValue`, `useTransition`, `useDeferredValue`, `useErrorBoundary` |
51
+ | `@pyreon/preact-compat/signals` | `signal`, `computed`, `effect`, `batch`, `ReadonlySignal`, `WritableSignal` |
52
+ | `@pyreon/preact-compat/jsx-runtime` | JSX automatic runtime (`jsx`, `jsxs`, `Fragment`) |
53
+ | `@pyreon/preact-compat/jsx-dev-runtime` | Dev variant — same runtime, with source location info |
65
54
 
66
- handleClick = () => {
67
- this.setState({ clicked: true })
68
- }
55
+ ## Key differences from Preact
69
56
 
70
- render() {
71
- return (
72
- <div>
73
- <p>Hello, {this.props.name}!</p>
74
- <button onClick={this.handleClick}>{this.state.clicked ? 'Clicked' : 'Click me'}</button>
75
- </div>
76
- )
77
- }
78
- }
79
- ```
57
+ | Behavior | Preact | `@pyreon/preact-compat` |
58
+ | ------------------- | ------------------------------------- | ---------------------------------------------------------------------- |
59
+ | Component execution | Re-runs render on every state change | Runs **once** (setup phase) |
60
+ | `useState` getter | Returns the value directly | Returns a **getter function** — call `count()` to read |
61
+ | `useEffect` deps | Controls when the effect re-runs | Deps array is **ignored** — Pyreon tracks dependencies automatically |
62
+ | `useCallback` | Memoizes across renders | **No-op** — returns `fn` as-is |
63
+ | `useMemo` | Returns the memoized value | Returns a **getter function** — call `value()` to read |
64
+ | `useLayoutEffect` | Fires synchronously before paint | Same as `useEffect` |
65
+ | Signals `.value` | Native Preact Signals API | Wrapped Pyreon signals with the same `.value` interface |
66
+ | Class components | Full lifecycle support | `setState` and `forceUpdate` work; lifecycle methods are not called |
67
+ | Hooks rules | Must be called at top level | **No restrictions** — call anywhere in component setup |
80
68
 
81
- ## Key Differences from Preact
69
+ ### Read state via a getter
82
70
 
83
- - **No hooks rules.** Call hooks anywhere -- in loops, conditions, nested functions.
84
- - **Components run once** (setup phase only), not on every render.
85
- - **`useEffect` deps are ignored.** Pyreon tracks reactive dependencies automatically. Pass `[]` to run once on mount.
86
- - **`useCallback` and `memo` are no-ops.** No re-renders means no stale closures.
71
+ ```tsx
72
+ // Preact
73
+ const [count, setCount] = useState(0)
74
+ console.log(count) // 0
87
75
 
88
- ## Entry Points
76
+ // @pyreon/preact-compat
77
+ const [count, setCount] = useState(0)
78
+ console.log(count()) // 0 — call the function
79
+ ```
89
80
 
90
- ### `@pyreon/preact-compat`
81
+ ### No stale closures
91
82
 
92
- Core Preact API.
83
+ Signal reads always return the current value. Preact-style `setInterval` callbacks that needed `[count]` deps to avoid stale closures Just Work without them:
93
84
 
94
- - **`h` / `createElement`** -- JSX factory.
95
- - **`Fragment`** -- fragment component.
96
- - **`render(vnode, container)`** -- mount a tree into a DOM element.
97
- - **`hydrate(vnode, container)`** -- hydrate server-rendered HTML.
98
- - **`createContext` / `useContext`** -- context API.
99
- - **`createRef`** -- mutable ref container.
100
- - **`Component`** -- class component base (lifecycle methods supported).
101
- - **`cloneElement(vnode, props, ...children)`** -- clone with overrides.
102
- - **`toChildArray(children)`** -- normalize children to a flat array.
103
- - **`isValidElement(x)`** -- type guard for VNodes.
85
+ ```tsx
86
+ useEffect(() => {
87
+ const id = setInterval(() => {
88
+ setCount((prev) => prev + 1) // always reads the latest
89
+ }, 1000)
90
+ return () => clearInterval(id)
91
+ })
92
+ ```
104
93
 
105
- ### `@pyreon/preact-compat/hooks`
94
+ ### Signals subpath
106
95
 
107
- Hooks API (mirrors `preact/hooks`).
96
+ `@pyreon/preact-compat/signals` mirrors `@preact/signals` — `signal(initial)` / `computed(fn)` / `effect(fn)` / `batch(fn)` — and the returned objects expose a `.value` getter/setter so existing `@preact/signals` consumer code keeps working.
108
97
 
109
- - **`useState(initial)`** -- returns `[getter, setter]`. Call `getter()` to read.
110
- - **`useReducer(reducer, initial)`** -- returns `[getter, dispatch]`.
111
- - **`useEffect(fn, deps?)`** -- reactive effect. `[]` deps means mount-only.
112
- - **`useLayoutEffect`** -- alias for `useEffect`.
113
- - **`useMemo(fn, deps?)`** -- returns a computed getter. Deps are ignored.
114
- - **`useCallback(fn, deps?)`** -- returns `fn` as-is (no-op).
115
- - **`useRef(initial?)`** -- returns `{ current }`.
116
- - **`useId()`** -- stable unique string per component instance.
117
- - **`useErrorBoundary`** -- alias for `onErrorCaptured`.
98
+ ## Drop-in compat mode
118
99
 
119
- ### `@pyreon/preact-compat/signals`
100
+ `@pyreon/vite-plugin` can alias every `preact` / `preact/hooks` / `@preact/signals` import to this package — no code changes:
120
101
 
121
- Preact Signals API (mirrors `@preact/signals`).
102
+ ```ts
103
+ // vite.config.ts
104
+ import pyreon from '@pyreon/vite-plugin'
105
+ export default { plugins: [pyreon({ compat: 'preact' })] }
106
+ ```
122
107
 
123
- - **`signal(initial)`** -- returns `{ value }` read/write accessor.
124
- - **`computed(fn)`** -- returns `{ value }` read-only accessor.
125
- - **`effect(fn)`** -- reactive side effect, returns dispose function.
126
- - **`batch(fn)`** -- coalesce multiple signal writes.
108
+ `tsconfig.json`:
127
109
 
128
- ## Composing Pyreon framework components inside preact-compat
110
+ ```jsonc
111
+ {
112
+ "compilerOptions": {
113
+ "jsx": "react-jsx",
114
+ "jsxImportSource": "@pyreon/preact-compat"
115
+ }
116
+ }
117
+ ```
129
118
 
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.
119
+ ## Gotchas
131
120
 
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:
121
+ - **Run-once mental model.** Components don't re-run on state change read signals/getters where they're used, not destructured into locals at the top of the function.
122
+ - **`useEffect` deps are ignored.** Dependency tracking is automatic. Effects re-run when any signal they read changes.
123
+ - **`useCallback` is a no-op.** Pyreon doesn't need referential stability across renders because there are no renders.
124
+ - **Class-component lifecycle methods don't fire.** `setState` + `forceUpdate` work, but `componentDidMount` / `componentDidUpdate` / `componentWillUnmount` are not invoked. Use `onMount` / `onUnmount` from `@pyreon/core` for lifecycle.
125
+ - **`version`** reports `10.0.0-pyreon` — code that gates on Preact 10 keeps working; code that asserts equality to a specific Preact version won't match.
133
126
 
134
- ```tsx
135
- import { nativeCompat, provide, createContext } from '@pyreon/core'
127
+ ## Documentation
136
128
 
137
- const MyCtx = createContext<string>('default')
129
+ Full docs: [docs.pyreon.dev/docs/preact-compat](https://docs.pyreon.dev/docs/preact-compat) (or `docs/docs/preact-compat.md` in this repo).
138
130
 
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
- ```
131
+ ## License
145
132
 
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.
133
+ MIT
@@ -0,0 +1,157 @@
1
+ import { Fragment, h, isNativeCompat, mapCompatDomProps, onUnmount } from "@pyreon/core";
2
+ import { signal } from "@pyreon/reactivity";
3
+
4
+ //#region src/jsx-runtime.ts
5
+ let _currentCtx = null;
6
+ let _hookIndex = 0;
7
+ function getCurrentCtx() {
8
+ return _currentCtx;
9
+ }
10
+ function getHookIndex() {
11
+ return _hookIndex++;
12
+ }
13
+ function beginRender(ctx) {
14
+ _currentCtx = ctx;
15
+ _hookIndex = 0;
16
+ ctx.pendingEffects = [];
17
+ ctx.pendingLayoutEffects = [];
18
+ }
19
+ function endRender() {
20
+ _currentCtx = null;
21
+ _hookIndex = 0;
22
+ }
23
+ function runLayoutEffects(entries) {
24
+ for (const entry of entries) {
25
+ if (entry.cleanup) entry.cleanup();
26
+ const cleanup = entry.fn();
27
+ entry.cleanup = typeof cleanup === "function" ? cleanup : void 0;
28
+ }
29
+ }
30
+ function scheduleEffects(ctx, entries) {
31
+ if (entries.length === 0) return;
32
+ queueMicrotask(() => {
33
+ for (const entry of entries) {
34
+ if (ctx.unmounted) return;
35
+ if (entry.cleanup) entry.cleanup();
36
+ const cleanup = entry.fn();
37
+ entry.cleanup = typeof cleanup === "function" ? cleanup : void 0;
38
+ }
39
+ });
40
+ }
41
+ function isClassComponent(type) {
42
+ return type.prototype != null && typeof type.prototype.render === "function";
43
+ }
44
+ function wrapClassComponent(ClassComp) {
45
+ const wrapped = ((props) => {
46
+ const instance = new ClassComp(props);
47
+ const version = signal(0);
48
+ let updateScheduled = false;
49
+ const origSetState = instance.setState.bind(instance);
50
+ instance.setState = (partial) => {
51
+ origSetState(partial);
52
+ if (!updateScheduled) {
53
+ updateScheduled = true;
54
+ queueMicrotask(() => {
55
+ updateScheduled = false;
56
+ version.set(version.peek() + 1);
57
+ });
58
+ }
59
+ };
60
+ instance.forceUpdate = () => {
61
+ version.set(version.peek() + 1);
62
+ };
63
+ let didMountFired = false;
64
+ onUnmount(() => {
65
+ if (typeof instance.componentWillUnmount === "function") instance.componentWillUnmount();
66
+ });
67
+ return () => {
68
+ const ver = version();
69
+ instance.props = props;
70
+ if (didMountFired && ver > 0 && typeof instance.shouldComponentUpdate === "function") {
71
+ if (!instance.shouldComponentUpdate(props, instance.state)) return instance._lastResult;
72
+ }
73
+ const result = instance.render();
74
+ instance._lastResult = result;
75
+ if (!didMountFired) {
76
+ didMountFired = true;
77
+ if (typeof instance.componentDidMount === "function") queueMicrotask(() => instance.componentDidMount());
78
+ } else if (ver > 0) {
79
+ if (typeof instance.componentDidUpdate === "function") queueMicrotask(() => instance.componentDidUpdate());
80
+ }
81
+ return result;
82
+ };
83
+ });
84
+ return wrapped;
85
+ }
86
+ const _wrapperCache = /* @__PURE__ */ new WeakMap();
87
+ function wrapCompatComponent(preactComponent) {
88
+ let wrapped = _wrapperCache.get(preactComponent);
89
+ if (wrapped) return wrapped;
90
+ if (isClassComponent(preactComponent)) {
91
+ wrapped = wrapClassComponent(preactComponent);
92
+ _wrapperCache.set(preactComponent, wrapped);
93
+ return wrapped;
94
+ }
95
+ wrapped = ((props) => {
96
+ const ctx = {
97
+ hooks: [],
98
+ scheduleRerender: () => {},
99
+ pendingEffects: [],
100
+ pendingLayoutEffects: [],
101
+ unmounted: false
102
+ };
103
+ const version = signal(0);
104
+ let updateScheduled = false;
105
+ ctx.scheduleRerender = () => {
106
+ if (ctx.unmounted || updateScheduled) return;
107
+ updateScheduled = true;
108
+ queueMicrotask(() => {
109
+ updateScheduled = false;
110
+ if (!ctx.unmounted) version.set(version.peek() + 1);
111
+ });
112
+ };
113
+ onUnmount(() => {
114
+ ctx.unmounted = true;
115
+ for (const hook of ctx.hooks) if (hook && typeof hook === "object" && "cleanup" in hook) {
116
+ const entry = hook;
117
+ if (typeof entry.cleanup === "function") entry.cleanup();
118
+ }
119
+ });
120
+ return () => {
121
+ version();
122
+ beginRender(ctx);
123
+ const result = preactComponent(props);
124
+ const layoutEffects = ctx.pendingLayoutEffects;
125
+ const effects = ctx.pendingEffects;
126
+ endRender();
127
+ runLayoutEffects(layoutEffects);
128
+ scheduleEffects(ctx, effects);
129
+ return result;
130
+ };
131
+ });
132
+ _wrapperCache.set(preactComponent, wrapped);
133
+ return wrapped;
134
+ }
135
+ function jsx(type, props, key) {
136
+ const { children, ...rest } = props;
137
+ const propsWithKey = key != null ? {
138
+ ...rest,
139
+ key
140
+ } : rest;
141
+ if (typeof type === "function") {
142
+ const componentProps = children !== void 0 ? {
143
+ ...propsWithKey,
144
+ children
145
+ } : propsWithKey;
146
+ if (isNativeCompat(type)) return h(type, componentProps);
147
+ return h(wrapCompatComponent(type), componentProps);
148
+ }
149
+ const childArray = children === void 0 ? [] : Array.isArray(children) ? children : [children];
150
+ mapCompatDomProps(propsWithKey, type);
151
+ return h(type, propsWithKey, ...childArray);
152
+ }
153
+ const jsxs = jsx;
154
+
155
+ //#endregion
156
+ export { jsxs as a, jsx as i, getCurrentCtx as n, getHookIndex as r, Fragment as t };
157
+ //# sourceMappingURL=jsx-runtime-D77N0lwB.js.map
@@ -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":"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}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"hooks.js","children":[{"name":"src/hooks.ts","uid":"cdd5db5a-1"}]},{"name":"index.js","children":[{"name":"src/index.ts","uid":"cdd5db5a-3"}]},{"name":"jsx-runtime.js","children":[{"name":"src/jsx-dev-runtime.ts","uid":"cdd5db5a-5"}]},{"name":"signals.js","children":[{"name":"src/signals.ts","uid":"cdd5db5a-7"}]},{"name":"_chunks/jsx-runtime-D77N0lwB.js","children":[{"name":"src/jsx-runtime.ts","uid":"cdd5db5a-9"}]}],"isRoot":true},"nodeParts":{"cdd5db5a-1":{"renderedLength":6949,"gzipLength":1987,"brotliLength":0,"metaUid":"cdd5db5a-0"},"cdd5db5a-3":{"renderedLength":3350,"gzipLength":1329,"brotliLength":0,"metaUid":"cdd5db5a-2"},"cdd5db5a-5":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"cdd5db5a-4"},"cdd5db5a-7":{"renderedLength":876,"gzipLength":383,"brotliLength":0,"metaUid":"cdd5db5a-6"},"cdd5db5a-9":{"renderedLength":4364,"gzipLength":1292,"brotliLength":0,"metaUid":"cdd5db5a-8"}},"nodeMetas":{"cdd5db5a-0":{"id":"/src/hooks.ts","moduleParts":{"hooks.js":"cdd5db5a-1"},"imported":[{"uid":"cdd5db5a-10"},{"uid":"cdd5db5a-8"}],"importedBy":[],"isEntry":true},"cdd5db5a-2":{"id":"/src/index.ts","moduleParts":{"index.js":"cdd5db5a-3"},"imported":[{"uid":"cdd5db5a-10"},{"uid":"cdd5db5a-11"},{"uid":"cdd5db5a-12"}],"importedBy":[],"isEntry":true},"cdd5db5a-4":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"cdd5db5a-5"},"imported":[{"uid":"cdd5db5a-8"}],"importedBy":[],"isEntry":true},"cdd5db5a-6":{"id":"/src/signals.ts","moduleParts":{"signals.js":"cdd5db5a-7"},"imported":[{"uid":"cdd5db5a-11"}],"importedBy":[],"isEntry":true},"cdd5db5a-8":{"id":"/src/jsx-runtime.ts","moduleParts":{"_chunks/jsx-runtime-D77N0lwB.js":"cdd5db5a-9"},"imported":[{"uid":"cdd5db5a-10"},{"uid":"cdd5db5a-11"}],"importedBy":[{"uid":"cdd5db5a-0"},{"uid":"cdd5db5a-4"}]},"cdd5db5a-10":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"cdd5db5a-0"},{"uid":"cdd5db5a-8"},{"uid":"cdd5db5a-2"}]},"cdd5db5a-11":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"cdd5db5a-8"},{"uid":"cdd5db5a-2"},{"uid":"cdd5db5a-6"}]},"cdd5db5a-12":{"id":"@pyreon/runtime-dom","moduleParts":{},"imported":[],"importedBy":[{"uid":"cdd5db5a-2"}]}},"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
@@ -1,16 +1,6 @@
1
+ import { n as getCurrentCtx, r as getHookIndex } from "./_chunks/jsx-runtime-D77N0lwB.js";
1
2
  import { onErrorCaptured, shallowEqualProps, useContext } from "@pyreon/core";
2
3
 
3
- //#region src/jsx-runtime.ts
4
- let _currentCtx = null;
5
- let _hookIndex = 0;
6
- function getCurrentCtx() {
7
- return _currentCtx;
8
- }
9
- function getHookIndex() {
10
- return _hookIndex++;
11
- }
12
-
13
- //#endregion
14
4
  //#region src/hooks.ts
15
5
  function requireCtx() {
16
6
  const ctx = getCurrentCtx();
package/lib/index.js CHANGED
@@ -108,6 +108,10 @@ function toChildArray(children) {
108
108
  }
109
109
  function flatten(value, out) {
110
110
  if (value == null || typeof value === "boolean") return;
111
+ if (typeof value === "function") {
112
+ flatten(value(), out);
113
+ return;
114
+ }
111
115
  if (Array.isArray(value)) for (const child of value) flatten(child, out);
112
116
  else out.push(value);
113
117
  }
@@ -1,151 +1,3 @@
1
- import { Fragment, h, isNativeCompat, mapCompatDomProps, onUnmount } from "@pyreon/core";
2
- import { signal } from "@pyreon/reactivity";
1
+ import { a as jsxs, i as jsx, t as Fragment } from "./_chunks/jsx-runtime-D77N0lwB.js";
3
2
 
4
- //#region src/jsx-runtime.ts
5
- let _currentCtx = null;
6
- let _hookIndex = 0;
7
- function beginRender(ctx) {
8
- _currentCtx = ctx;
9
- _hookIndex = 0;
10
- ctx.pendingEffects = [];
11
- ctx.pendingLayoutEffects = [];
12
- }
13
- function endRender() {
14
- _currentCtx = null;
15
- _hookIndex = 0;
16
- }
17
- function runLayoutEffects(entries) {
18
- for (const entry of entries) {
19
- if (entry.cleanup) entry.cleanup();
20
- const cleanup = entry.fn();
21
- entry.cleanup = typeof cleanup === "function" ? cleanup : void 0;
22
- }
23
- }
24
- function scheduleEffects(ctx, entries) {
25
- if (entries.length === 0) return;
26
- queueMicrotask(() => {
27
- for (const entry of entries) {
28
- if (ctx.unmounted) return;
29
- if (entry.cleanup) entry.cleanup();
30
- const cleanup = entry.fn();
31
- entry.cleanup = typeof cleanup === "function" ? cleanup : void 0;
32
- }
33
- });
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
- }
80
- const _wrapperCache = /* @__PURE__ */ new WeakMap();
81
- function wrapCompatComponent(preactComponent) {
82
- let wrapped = _wrapperCache.get(preactComponent);
83
- if (wrapped) return wrapped;
84
- if (isClassComponent(preactComponent)) {
85
- wrapped = wrapClassComponent(preactComponent);
86
- _wrapperCache.set(preactComponent, wrapped);
87
- return wrapped;
88
- }
89
- wrapped = ((props) => {
90
- const ctx = {
91
- hooks: [],
92
- scheduleRerender: () => {},
93
- pendingEffects: [],
94
- pendingLayoutEffects: [],
95
- unmounted: false
96
- };
97
- const version = signal(0);
98
- let updateScheduled = false;
99
- ctx.scheduleRerender = () => {
100
- if (ctx.unmounted || updateScheduled) return;
101
- updateScheduled = true;
102
- queueMicrotask(() => {
103
- updateScheduled = false;
104
- if (!ctx.unmounted) version.set(version.peek() + 1);
105
- });
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
- });
114
- return () => {
115
- version();
116
- beginRender(ctx);
117
- const result = preactComponent(props);
118
- const layoutEffects = ctx.pendingLayoutEffects;
119
- const effects = ctx.pendingEffects;
120
- endRender();
121
- runLayoutEffects(layoutEffects);
122
- scheduleEffects(ctx, effects);
123
- return result;
124
- };
125
- });
126
- _wrapperCache.set(preactComponent, wrapped);
127
- return wrapped;
128
- }
129
- function jsx(type, props, key) {
130
- const { children, ...rest } = props;
131
- const propsWithKey = key != null ? {
132
- ...rest,
133
- key
134
- } : rest;
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
- mapCompatDomProps(propsWithKey, type);
145
- return h(type, propsWithKey, ...childArray);
146
- }
147
- const jsxs = jsx;
148
-
149
- //#endregion
150
- export { Fragment, jsx, jsxs };
151
- //# sourceMappingURL=jsx-runtime.js.map
3
+ export { Fragment, jsx, jsxs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/preact-compat",
3
- "version": "0.21.0",
3
+ "version": "0.23.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": {
@@ -64,13 +64,13 @@
64
64
  "prepublishOnly": "bun run build"
65
65
  },
66
66
  "dependencies": {
67
- "@pyreon/core": "^0.21.0",
68
- "@pyreon/reactivity": "^0.21.0",
69
- "@pyreon/runtime-dom": "^0.21.0"
67
+ "@pyreon/core": "^0.23.0",
68
+ "@pyreon/reactivity": "^0.23.0",
69
+ "@pyreon/runtime-dom": "^0.23.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@happy-dom/global-registrator": "^20.8.9",
73
- "@pyreon/test-utils": "^0.13.8",
73
+ "@pyreon/test-utils": "^0.13.10",
74
74
  "@vitest/browser-playwright": "^4.1.4",
75
75
  "happy-dom": "^20.8.3"
76
76
  }
package/src/index.ts CHANGED
@@ -188,6 +188,19 @@ export function toChildArray(children: NestedChildren): VNodeChild[] {
188
188
 
189
189
  function flatten(value: NestedChildren, out: VNodeChild[]): void {
190
190
  if (value == null || typeof value === 'boolean') return
191
+ // Unwrap the Pyreon compiler's `() => x` accessor wrap. When a parent
192
+ // emits `<MyComp>{data.map(fn)}</MyComp>` (any non-stable expression
193
+ // as children — CallExpression, ConditionalExpression, etc.), the
194
+ // compiler's prop-inlining pass rewrites it as
195
+ // `MyComp({ children: () => data.map(fn) })`. Iterating the function
196
+ // as a child would silently treat the function as ONE child and the
197
+ // downstream `toChildArray` call would lose every real child after
198
+ // position 0. Mirrors the kinetic Iterator + elements Iterator fix
199
+ // from PRs #731 / #736.
200
+ if (typeof value === 'function') {
201
+ flatten((value as () => NestedChildren)(), out)
202
+ return
203
+ }
191
204
  if (Array.isArray(value)) {
192
205
  for (const child of value) {
193
206
  flatten(child, out)