@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.
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-runtime.js","names":[],"sources":["../src/jsx-runtime.ts"],"sourcesContent":["/**\n * Compat JSX runtime for Preact compatibility mode.\n *\n * When `jsxImportSource` is redirected to `@pyreon/preact-compat` (via the vite\n * plugin's `compat: \"preact\"` option), OXC rewrites JSX to import from this file.\n *\n * For component VNodes, we wrap the component function so it returns a reactive\n * accessor — enabling Preact-style re-renders on state change while Pyreon's\n * existing renderer handles all DOM work.\n */\n\nimport type { ComponentFn, Props, VNode, VNodeChild } from '@pyreon/core'\nimport { Fragment, h } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\n\nexport { Fragment }\n\n// ─── Render context (used by hooks) ──────────────────────────────────────────\n\nexport interface RenderContext {\n hooks: unknown[]\n scheduleRerender: () => void\n /** Effect entries pending execution after render */\n pendingEffects: EffectEntry[]\n /** Layout effect entries pending execution after render */\n pendingLayoutEffects: EffectEntry[]\n /** Set to true when the component is unmounted */\n unmounted: boolean\n}\n\nexport interface EffectEntry {\n fn: () => (() => void) | void\n deps: unknown[] | undefined\n cleanup: (() => void) | undefined\n}\n\nlet _currentCtx: RenderContext | null = null\nlet _hookIndex = 0\n\nexport function getCurrentCtx(): RenderContext | null {\n return _currentCtx\n}\n\nexport function getHookIndex(): number {\n return _hookIndex++\n}\n\nexport function beginRender(ctx: RenderContext): void {\n _currentCtx = ctx\n _hookIndex = 0\n ctx.pendingEffects = []\n ctx.pendingLayoutEffects = []\n}\n\nexport function endRender(): void {\n _currentCtx = null\n _hookIndex = 0\n}\n\n// ─── Effect runners ──────────────────────────────────────────────────────────\n\nfunction runLayoutEffects(entries: EffectEntry[]): void {\n for (const entry of entries) {\n if (entry.cleanup) entry.cleanup()\n const cleanup = entry.fn()\n entry.cleanup = typeof cleanup === 'function' ? cleanup : undefined\n }\n}\n\nfunction scheduleEffects(ctx: RenderContext, entries: EffectEntry[]): void {\n if (entries.length === 0) return\n queueMicrotask(() => {\n for (const entry of entries) {\n if (ctx.unmounted) return\n if (entry.cleanup) entry.cleanup()\n const cleanup = entry.fn()\n entry.cleanup = typeof cleanup === 'function' ? cleanup : undefined\n }\n })\n}\n\n// ─── Component wrapping ──────────────────────────────────────────────────────\n\nconst _wrapperCache = new WeakMap<Function, ComponentFn>()\n\nfunction wrapCompatComponent(preactComponent: Function): ComponentFn {\n let wrapped = _wrapperCache.get(preactComponent)\n if (wrapped) return wrapped\n\n // The wrapper returns a reactive accessor (() => VNodeChild) which Pyreon's\n // mountChild treats as a reactive expression via mountReactive.\n wrapped = ((props: Props) => {\n const ctx: RenderContext = {\n hooks: [],\n scheduleRerender: () => {\n // Will be replaced below after version signal is created\n },\n pendingEffects: [],\n pendingLayoutEffects: [],\n unmounted: false,\n }\n\n const version = signal(0)\n let updateScheduled = false\n\n ctx.scheduleRerender = () => {\n if (ctx.unmounted || updateScheduled) return\n updateScheduled = true\n queueMicrotask(() => {\n updateScheduled = false\n if (!ctx.unmounted) version.set(version.peek() + 1)\n })\n }\n\n // Return reactive accessor — Pyreon's mountChild calls mountReactive\n return () => {\n version() // tracked read — triggers re-execution when state changes\n beginRender(ctx)\n const result = (preactComponent as ComponentFn)(props)\n const layoutEffects = ctx.pendingLayoutEffects\n const effects = ctx.pendingEffects\n endRender()\n\n runLayoutEffects(layoutEffects)\n scheduleEffects(ctx, effects)\n\n return result\n }\n }) as unknown as ComponentFn\n\n _wrapperCache.set(preactComponent, wrapped)\n return wrapped\n}\n\n// ─── JSX functions ───────────────────────────────────────────────────────────\n\nexport function jsx(\n type: string | ComponentFn | symbol,\n props: Props & { children?: VNodeChild | VNodeChild[] },\n key?: string | number | null,\n): VNode {\n const { children, ...rest } = props\n const propsWithKey = (key != null ? { ...rest, key } : rest) as Props\n\n if (typeof type === 'function') {\n // Wrap Preact-style component for re-render support\n const wrapped = wrapCompatComponent(type)\n const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey\n return h(wrapped, componentProps)\n }\n\n // DOM element or symbol (Fragment): children go in vnode.children\n const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]\n\n return h(type, propsWithKey, ...(childArray as VNodeChild[]))\n}\n\nexport const jsxs = jsx\nexport const jsxDEV = jsx\n"],"mappings":";;;;AAoCA,IAAI,cAAoC;AACxC,IAAI,aAAa;AAUjB,SAAgB,YAAY,KAA0B;AACpD,eAAc;AACd,cAAa;AACb,KAAI,iBAAiB,EAAE;AACvB,KAAI,uBAAuB,EAAE;;AAG/B,SAAgB,YAAkB;AAChC,eAAc;AACd,cAAa;;AAKf,SAAS,iBAAiB,SAA8B;AACtD,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,MAAM,QAAS,OAAM,SAAS;EAClC,MAAM,UAAU,MAAM,IAAI;AAC1B,QAAM,UAAU,OAAO,YAAY,aAAa,UAAU;;;AAI9D,SAAS,gBAAgB,KAAoB,SAA8B;AACzE,KAAI,QAAQ,WAAW,EAAG;AAC1B,sBAAqB;AACnB,OAAK,MAAM,SAAS,SAAS;AAC3B,OAAI,IAAI,UAAW;AACnB,OAAI,MAAM,QAAS,OAAM,SAAS;GAClC,MAAM,UAAU,MAAM,IAAI;AAC1B,SAAM,UAAU,OAAO,YAAY,aAAa,UAAU;;GAE5D;;AAKJ,MAAM,gCAAgB,IAAI,SAAgC;AAE1D,SAAS,oBAAoB,iBAAwC;CACnE,IAAI,UAAU,cAAc,IAAI,gBAAgB;AAChD,KAAI,QAAS,QAAO;AAIpB,aAAY,UAAiB;EAC3B,MAAM,MAAqB;GACzB,OAAO,EAAE;GACT,wBAAwB;GAGxB,gBAAgB,EAAE;GAClB,sBAAsB,EAAE;GACxB,WAAW;GACZ;EAED,MAAM,UAAU,OAAO,EAAE;EACzB,IAAI,kBAAkB;AAEtB,MAAI,yBAAyB;AAC3B,OAAI,IAAI,aAAa,gBAAiB;AACtC,qBAAkB;AAClB,wBAAqB;AACnB,sBAAkB;AAClB,QAAI,CAAC,IAAI,UAAW,SAAQ,IAAI,QAAQ,MAAM,GAAG,EAAE;KACnD;;AAIJ,eAAa;AACX,YAAS;AACT,eAAY,IAAI;GAChB,MAAM,SAAU,gBAAgC,MAAM;GACtD,MAAM,gBAAgB,IAAI;GAC1B,MAAM,UAAU,IAAI;AACpB,cAAW;AAEX,oBAAiB,cAAc;AAC/B,mBAAgB,KAAK,QAAQ;AAE7B,UAAO;;;AAIX,eAAc,IAAI,iBAAiB,QAAQ;AAC3C,QAAO;;AAKT,SAAgB,IACd,MACA,OACA,KACO;CACP,MAAM,EAAE,UAAU,GAAG,SAAS;CAC9B,MAAM,eAAgB,OAAO,OAAO;EAAE,GAAG;EAAM;EAAK,GAAG;AAEvD,KAAI,OAAO,SAAS,WAIlB,QAAO,EAFS,oBAAoB,KAAK,EAClB,aAAa,SAAY;EAAE,GAAG;EAAc;EAAU,GAAG,aAC/C;AAMnC,QAAO,EAAE,MAAM,cAAc,GAFV,aAAa,SAAY,EAAE,GAAG,MAAM,QAAQ,SAAS,GAAG,WAAW,CAAC,SAAS,CAEnC;;AAG/D,MAAa,OAAO"}
1
+ {"version":3,"file":"jsx-runtime.js","names":[],"sources":["../src/jsx-runtime.ts"],"sourcesContent":["/**\n * Compat JSX runtime for Preact compatibility mode.\n *\n * When `jsxImportSource` is redirected to `@pyreon/preact-compat` (via the vite\n * plugin's `compat: \"preact\"` option), OXC rewrites JSX to import from this file.\n *\n * For component VNodes, we wrap the component function so it returns a reactive\n * accessor — enabling Preact-style re-renders on state change while Pyreon's\n * existing renderer handles all DOM work.\n */\n\nimport type { ComponentFn, Props, VNode, VNodeChild } from '@pyreon/core'\nimport { Fragment, h, onUnmount } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Component } from './index'\n\nexport { Fragment }\n\n// ─── Render context (used by hooks) ──────────────────────────────────────────\n\nexport interface RenderContext {\n hooks: unknown[]\n scheduleRerender: () => void\n /** Effect entries pending execution after render */\n pendingEffects: EffectEntry[]\n /** Layout effect entries pending execution after render */\n pendingLayoutEffects: EffectEntry[]\n /** Set to true when the component is unmounted */\n unmounted: boolean\n}\n\nexport interface EffectEntry {\n fn: () => (() => void) | void\n deps: unknown[] | undefined\n cleanup: (() => void) | undefined\n}\n\nlet _currentCtx: RenderContext | null = null\nlet _hookIndex = 0\n\nexport function getCurrentCtx(): RenderContext | null {\n return _currentCtx\n}\n\nexport function getHookIndex(): number {\n return _hookIndex++\n}\n\nexport function beginRender(ctx: RenderContext): void {\n _currentCtx = ctx\n _hookIndex = 0\n ctx.pendingEffects = []\n ctx.pendingLayoutEffects = []\n}\n\nexport function endRender(): void {\n _currentCtx = null\n _hookIndex = 0\n}\n\n// ─── Effect runners ──────────────────────────────────────────────────────────\n\nfunction runLayoutEffects(entries: EffectEntry[]): void {\n for (const entry of entries) {\n if (entry.cleanup) entry.cleanup()\n const cleanup = entry.fn()\n entry.cleanup = typeof cleanup === 'function' ? cleanup : undefined\n }\n}\n\nfunction scheduleEffects(ctx: RenderContext, entries: EffectEntry[]): void {\n if (entries.length === 0) return\n queueMicrotask(() => {\n for (const entry of entries) {\n if (ctx.unmounted) return\n if (entry.cleanup) entry.cleanup()\n const cleanup = entry.fn()\n entry.cleanup = typeof cleanup === 'function' ? cleanup : undefined\n }\n })\n}\n\n// ─── Native component marker ────────────────────────────────────────────────\n\nconst NATIVE_COMPONENT = Symbol.for('pyreon:native-compat')\n\n// ─── Class component detection ──────────────────────────────────────────────\n\nfunction isClassComponent(type: Function): boolean {\n return type.prototype != null && typeof type.prototype.render === 'function'\n}\n\n// ─── Class component wrapping ───────────────────────────────────────────────\n\nfunction wrapClassComponent(ClassComp: Function): ComponentFn {\n const wrapped = ((props: Props) => {\n const instance = new (ClassComp as new (props: Props) => Component)(props)\n const version = signal(0)\n let updateScheduled = false\n\n // Override setState to trigger re-render via version signal\n const origSetState = instance.setState.bind(instance)\n instance.setState = (partial: Partial<Record<string, unknown>>) => {\n origSetState(partial)\n if (!updateScheduled) {\n updateScheduled = true\n queueMicrotask(() => {\n updateScheduled = false\n version.set(version.peek() + 1)\n })\n }\n }\n\n // Override forceUpdate\n instance.forceUpdate = () => {\n version.set(version.peek() + 1)\n }\n\n // Lifecycle: componentWillUnmount\n let didMountFired = false\n onUnmount(() => {\n if (typeof instance.componentWillUnmount === 'function') {\n instance.componentWillUnmount()\n }\n })\n\n // Return reactive accessor for re-renders\n return () => {\n const ver = version() // track for re-renders\n instance.props = props // update props on re-render\n\n // shouldComponentUpdate only applies after mount (ver > 0 means setState/forceUpdate)\n if (didMountFired && ver > 0 && typeof instance.shouldComponentUpdate === 'function') {\n if (!instance.shouldComponentUpdate(props, instance.state)) {\n return instance._lastResult // skip render\n }\n }\n\n const result = instance.render()\n instance._lastResult = result\n\n // componentDidMount fires once after the initial render settles\n if (!didMountFired) {\n didMountFired = true\n if (typeof instance.componentDidMount === 'function') {\n queueMicrotask(() => instance.componentDidMount!())\n }\n } else if (ver > 0) {\n // componentDidUpdate only fires on explicit re-renders (setState/forceUpdate)\n if (typeof instance.componentDidUpdate === 'function') {\n queueMicrotask(() => instance.componentDidUpdate!())\n }\n }\n\n return result\n }\n }) as unknown as ComponentFn\n return wrapped\n}\n\n// ─── Component wrapping ──────────────────────────────────────────────────────\n\nconst _wrapperCache = new WeakMap<Function, ComponentFn>()\n\nfunction wrapCompatComponent(preactComponent: Function): ComponentFn {\n let wrapped = _wrapperCache.get(preactComponent)\n if (wrapped) return wrapped\n\n // Handle class components (those with prototype.render)\n if (isClassComponent(preactComponent)) {\n wrapped = wrapClassComponent(preactComponent)\n _wrapperCache.set(preactComponent, wrapped)\n return wrapped\n }\n\n // The wrapper returns a reactive accessor (() => VNodeChild) which Pyreon's\n // mountChild treats as a reactive expression via mountReactive.\n wrapped = ((props: Props) => {\n const ctx: RenderContext = {\n hooks: [],\n scheduleRerender: () => {\n // Will be replaced below after version signal is created\n },\n pendingEffects: [],\n pendingLayoutEffects: [],\n unmounted: false,\n }\n\n const version = signal(0)\n let updateScheduled = false\n\n ctx.scheduleRerender = () => {\n if (ctx.unmounted || updateScheduled) return\n updateScheduled = true\n queueMicrotask(() => {\n updateScheduled = false\n if (!ctx.unmounted) version.set(version.peek() + 1)\n })\n }\n\n // Register cleanup for all hooks on unmount\n onUnmount(() => {\n ctx.unmounted = true\n for (const hook of ctx.hooks) {\n if (hook && typeof hook === 'object' && 'cleanup' in hook) {\n const entry = hook as EffectEntry\n if (typeof entry.cleanup === 'function') entry.cleanup()\n }\n }\n })\n\n // Return reactive accessor — Pyreon's mountChild calls mountReactive\n return () => {\n version() // tracked read — triggers re-execution when state changes\n beginRender(ctx)\n const result = (preactComponent as ComponentFn)(props)\n const layoutEffects = ctx.pendingLayoutEffects\n const effects = ctx.pendingEffects\n endRender()\n\n runLayoutEffects(layoutEffects)\n scheduleEffects(ctx, effects)\n\n return result\n }\n }) as unknown as ComponentFn\n\n _wrapperCache.set(preactComponent, wrapped)\n return wrapped\n}\n\n// ─── JSX functions ───────────────────────────────────────────────────────────\n\nexport function jsx(\n type: string | ComponentFn | symbol,\n props: Props & { children?: VNodeChild | VNodeChild[] },\n key?: string | number | null,\n): VNode {\n const { children, ...rest } = props\n const propsWithKey = (key != null ? { ...rest, key } : rest) as Props\n\n if (typeof type === 'function') {\n const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey\n // Native Pyreon components (e.g. context Provider) skip compat wrapping\n if ((type as unknown as Record<symbol, boolean>)[NATIVE_COMPONENT]) {\n return h(type as ComponentFn, componentProps)\n }\n // Wrap Preact-style component for re-render support\n const wrapped = wrapCompatComponent(type)\n return h(wrapped, componentProps)\n }\n\n // DOM element or symbol (Fragment): children go in vnode.children\n const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]\n\n // Map Preact-style attributes to standard HTML attributes\n if (typeof type === 'string') {\n if (propsWithKey.className !== undefined) {\n propsWithKey.class = propsWithKey.className\n delete propsWithKey.className\n }\n if (propsWithKey.htmlFor !== undefined) {\n propsWithKey.for = propsWithKey.htmlFor\n delete propsWithKey.htmlFor\n }\n\n // Preact's onChange fires on every keystroke for form elements (like onInput)\n if (\n (type === 'input' || type === 'textarea' || type === 'select') &&\n propsWithKey.onChange !== undefined\n ) {\n if (propsWithKey.onInput === undefined) {\n propsWithKey.onInput = propsWithKey.onChange\n }\n delete propsWithKey.onChange\n }\n\n // autoFocus → autofocus\n if (propsWithKey.autoFocus !== undefined) {\n propsWithKey.autofocus = propsWithKey.autoFocus\n delete propsWithKey.autoFocus\n }\n\n // defaultValue / defaultChecked → value / checked when no controlled value\n if (type === 'input' || type === 'textarea') {\n if (propsWithKey.defaultValue !== undefined && propsWithKey.value === undefined) {\n propsWithKey.value = propsWithKey.defaultValue\n delete propsWithKey.defaultValue\n }\n if (propsWithKey.defaultChecked !== undefined && propsWithKey.checked === undefined) {\n propsWithKey.checked = propsWithKey.defaultChecked\n delete propsWithKey.defaultChecked\n }\n }\n\n // Strip Preact-only props that have no DOM equivalent\n delete propsWithKey.suppressHydrationWarning\n }\n\n return h(type, propsWithKey, ...(childArray as VNodeChild[]))\n}\n\nexport const jsxs = jsx\nexport const jsxDEV = jsx\n"],"mappings":"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"}
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"signals.js","names":["pyreonSignal","pyreonComputed","pyreonEffect"],"sources":["../src/signals.ts"],"sourcesContent":["/**\n * @pyreon/preact-compat/signals\n *\n * Preact Signals compatibility layer (`@preact/signals` style).\n * Wraps Pyreon's signal/computed in `{ value }` accessor objects.\n */\n\nimport type { Effect } from '@pyreon/reactivity'\nimport {\n batch as pyreonBatch,\n computed as pyreonComputed,\n effect as pyreonEffect,\n signal as pyreonSignal,\n} from '@pyreon/reactivity'\n\n// ─── Signal ──────────────────────────────────────────────────────────────────\n\nexport interface ReadonlySignal<T> {\n readonly value: T\n peek(): T\n}\n\nexport interface WritableSignal<T> extends ReadonlySignal<T> {\n value: T\n}\n\n/**\n * Create a Preact-style signal with `.value` accessor.\n *\n * @example\n * const count = signal(0)\n * count.value++ // write\n * console.log(count.value) // read (tracked)\n */\nexport function signal<T>(initial: T): WritableSignal<T> {\n const s = pyreonSignal<T>(initial)\n return {\n get value(): T {\n return s()\n },\n set value(v: T) {\n s.set(v)\n },\n peek(): T {\n return s.peek()\n },\n }\n}\n\n// ─── Computed ────────────────────────────────────────────────────────────────\n\n/**\n * Create a Preact-style computed with `.value` accessor.\n *\n * @example\n * const doubled = computed(() => count.value * 2)\n * console.log(doubled.value)\n */\nexport function computed<T>(fn: () => T): ReadonlySignal<T> {\n const c = pyreonComputed(fn)\n return {\n get value(): T {\n return c()\n },\n peek(): T {\n // computed doesn't have peek — just read the value untracked\n return c()\n },\n }\n}\n\n// ─── Effect ──────────────────────────────────────────────────────────────────\n\n/**\n * Run a side-effect that auto-tracks signal reads.\n * Returns a dispose function.\n */\nexport function effect(fn: () => void | (() => void)): () => void {\n // Pyreon's effect() natively supports cleanup return values\n const e: Effect = pyreonEffect(fn)\n return () => {\n e.dispose()\n }\n}\n\n// ─── Batch ───────────────────────────────────────────────────────────────────\n\n/**\n * Batch multiple signal writes into a single update.\n */\nexport { pyreonBatch as batch }\n"],"mappings":";;;;;;;;;;;AAkCA,SAAgB,OAAU,SAA+B;CACvD,MAAM,IAAIA,SAAgB,QAAQ;AAClC,QAAO;EACL,IAAI,QAAW;AACb,UAAO,GAAG;;EAEZ,IAAI,MAAM,GAAM;AACd,KAAE,IAAI,EAAE;;EAEV,OAAU;AACR,UAAO,EAAE,MAAM;;EAElB;;;;;;;;;AAYH,SAAgB,SAAY,IAAgC;CAC1D,MAAM,IAAIC,WAAe,GAAG;AAC5B,QAAO;EACL,IAAI,QAAW;AACb,UAAO,GAAG;;EAEZ,OAAU;AAER,UAAO,GAAG;;EAEb;;;;;;AASH,SAAgB,OAAO,IAA2C;CAEhE,MAAM,IAAYC,SAAa,GAAG;AAClC,cAAa;AACX,IAAE,SAAS"}
1
+ {"version":3,"file":"signals.js","names":["pyreonSignal","pyreonComputed","pyreonRunUntracked","pyreonEffect"],"sources":["../src/signals.ts"],"sourcesContent":["/**\n * @pyreon/preact-compat/signals\n *\n * Preact Signals compatibility layer (`@preact/signals` style).\n * Wraps Pyreon's signal/computed in `{ value }` accessor objects.\n */\n\nimport type { Effect } from '@pyreon/reactivity'\nimport {\n batch as pyreonBatch,\n computed as pyreonComputed,\n effect as pyreonEffect,\n runUntracked as pyreonRunUntracked,\n signal as pyreonSignal,\n} from '@pyreon/reactivity'\n\n// ─── Signal ──────────────────────────────────────────────────────────────────\n\nexport interface ReadonlySignal<T> {\n readonly value: T\n peek(): T\n}\n\nexport interface WritableSignal<T> extends ReadonlySignal<T> {\n value: T\n}\n\n/**\n * Create a Preact-style signal with `.value` accessor.\n *\n * @example\n * const count = signal(0)\n * count.value++ // write\n * console.log(count.value) // read (tracked)\n */\nexport function signal<T>(initial: T): WritableSignal<T> {\n const s = pyreonSignal<T>(initial)\n return {\n get value(): T {\n return s()\n },\n set value(v: T) {\n s.set(v)\n },\n peek(): T {\n return s.peek()\n },\n }\n}\n\n// ─── Computed ────────────────────────────────────────────────────────────────\n\n/**\n * Create a Preact-style computed with `.value` accessor.\n *\n * @example\n * const doubled = computed(() => count.value * 2)\n * console.log(doubled.value)\n */\nexport function computed<T>(fn: () => T): ReadonlySignal<T> {\n const c = pyreonComputed(fn)\n return {\n get value(): T {\n return c()\n },\n peek(): T {\n return pyreonRunUntracked(() => c())\n },\n }\n}\n\n// ─── Effect ──────────────────────────────────────────────────────────────────\n\n/**\n * Run a side-effect that auto-tracks signal reads.\n * Returns a dispose function.\n */\nexport function effect(fn: () => void | (() => void)): () => void {\n // Pyreon's effect() natively supports cleanup return values\n const e: Effect = pyreonEffect(fn)\n return () => {\n e.dispose()\n }\n}\n\n// ─── Batch ───────────────────────────────────────────────────────────────────\n\n/**\n * Batch multiple signal writes into a single update.\n */\nexport { pyreonBatch as batch }\n"],"mappings":";;;;;;;;;;;AAmCA,SAAgB,OAAU,SAA+B;CACvD,MAAM,IAAIA,SAAgB,QAAQ;AAClC,QAAO;EACL,IAAI,QAAW;AACb,UAAO,GAAG;;EAEZ,IAAI,MAAM,GAAM;AACd,KAAE,IAAI,EAAE;;EAEV,OAAU;AACR,UAAO,EAAE,MAAM;;EAElB;;;;;;;;;AAYH,SAAgB,SAAY,IAAgC;CAC1D,MAAM,IAAIC,WAAe,GAAG;AAC5B,QAAO;EACL,IAAI,QAAW;AACb,UAAO,GAAG;;EAEZ,OAAU;AACR,UAAOC,mBAAyB,GAAG,CAAC;;EAEvC;;;;;;AASH,SAAgB,OAAO,IAA2C;CAEhE,MAAM,IAAYC,SAAa,GAAG;AAClC,cAAa;AACX,IAAE,SAAS"}
@@ -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 +1 @@
1
- {"version":3,"file":"hooks2.d.ts","names":[],"sources":["../../../src/hooks.ts"],"mappings":";;;;;;;iBAwCgB,QAAA,GAAA,CAAY,OAAA,EAAS,CAAA,UAAW,CAAA,KAAM,CAAA,GAAI,CAAA,EAAG,CAAA,KAAM,IAAA,EAAM,CAAA,KAAM,CAAA;;;;;iBA0B/D,SAAA,CAAU,EAAA,6BAA+B,IAAA;;;;iBAwBzC,eAAA,CAAgB,EAAA,6BAA+B,IAAA;AAxB/D;;;AAAA,iBA+CgB,OAAA,GAAA,CAAW,EAAA,QAAU,CAAA,EAAG,IAAA,cAAkB,CAAA;;AAvB1D;;iBA8CgB,WAAA,eAA0B,IAAA,sBAAA,CAA2B,EAAA,EAAI,CAAA,EAAG,IAAA,cAAkB,CAAA;;;AAvB9F;iBAgCgB,MAAA,GAAA,CAAU,OAAA,GAAU,CAAA;EAAM,OAAA,EAAS,CAAA;AAAA;;;;iBAiBnC,UAAA,MAAA,CACd,OAAA,GAAU,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,CAAA,KAAM,CAAA,EAClC,OAAA,EAAS,CAAA,UAAW,CAAA,KAClB,CAAA,GAAI,MAAA,EAAQ,CAAA;;;;iBA2BA,KAAA,CAAA;;;;;iBAiBA,IAAA,WAAe,MAAA,kBAAA,CAC7B,SAAA,GAAY,KAAA,EAAO,CAAA,KAAM,UAAA,EACzB,QAAA,IAAY,SAAA,EAAW,CAAA,EAAG,SAAA,EAAW,CAAA,gBACnC,KAAA,EAAO,CAAA,KAAM,UAAA"}
1
+ {"version":3,"file":"hooks2.d.ts","names":[],"sources":["../../../src/hooks.ts"],"mappings":";;;;;;;;;iBAoDgB,QAAA,GAAA,CAAY,OAAA,EAAS,CAAA,UAAW,CAAA,KAAM,CAAA,GAAI,CAAA,EAAG,CAAA,KAAM,IAAA,EAAM,CAAA,KAAM,CAAA;;;;;iBA6B/D,SAAA,CAAU,EAAA,6BAA+B,IAAA;;;AAAzD;iBAwBgB,eAAA,CAAgB,EAAA,6BAA+B,IAAA;;;;iBAuB/C,OAAA,GAAA,CAAW,EAAA,QAAU,CAAA,EAAG,IAAA,cAAkB,CAAA;;;;iBAuB1C,WAAA,eAA0B,IAAA,sBAAA,CAA2B,EAAA,EAAI,CAAA,EAAG,IAAA,cAAkB,CAAA;AAvB9F;;;AAAA,iBAgCgB,MAAA,GAAA,CAAU,OAAA,GAAU,CAAA;EAAM,OAAA,EAAS,CAAA;AAAA;;;;;;AATnD;iBA6BgB,UAAA,MAAA,CACd,OAAA,GAAU,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,CAAA,KAAM,CAAA,EAClC,UAAA,EAAY,CAAA,UAAW,CAAA,GACvB,IAAA,IAAQ,GAAA,EAAK,CAAA,KAAM,CAAA,IACjB,CAAA,GAAI,MAAA,EAAQ,CAAA;;;;iBAqCA,KAAA,CAAA;;;;;;;AA7DhB;iBAiFgB,IAAA,WAAe,MAAA,kBAAA,CAC7B,SAAA,GAAY,KAAA,EAAO,CAAA,KAAM,UAAA,EACzB,QAAA,IAAY,SAAA,EAAW,CAAA,EAAG,SAAA,EAAW,CAAA,gBACnC,KAAA,EAAO,CAAA,KAAM,UAAA;;;;;;iBA6CD,UAAA,WAAqB,MAAA,kBAAA,CACnC,MAAA,GAAS,KAAA,EAAO,CAAA,EAAG,GAAA;EAAO,OAAA;AAAA,aAA8B,UAAA,IACtD,KAAA,EAAO,CAAA;EAAM,GAAA;IAAQ,OAAA;EAAA;AAAA,MAAgC,UAAA;;;;iBAezC,mBAAA,GAAA,CACd,GAAA;EAAO,OAAA,EAAS,CAAA;AAAA,sBAChB,IAAA,QAAY,CAAA,EACZ,IAAA;;;;iBAec,aAAA,GAAA,CAAiB,MAAA,EAAQ,CAAA,EAAG,OAAA,IAAW,CAAA,EAAG,CAAA;;;;;;;;;iBAY1C,aAAA,CAAA,cAA4B,EAAA;;;;;;;;iBAa5B,gBAAA,GAAA,CAAoB,KAAA,EAAO,CAAA,GAAI,CAAA"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;;cA+Ba,aAAA,SAAa,OAAA;;;;;iBAUV,MAAA,CAAO,KAAA,EAAO,UAAA,EAAY,SAAA,EAAW,OAAA;;;;;iBAQrC,OAAA,CAAQ,KAAA,EAAO,UAAA,EAAY,SAAA,EAAW,OAAA;AAAA,UAMrC,aAAA;EAAA,SACN,EAAA;EAAA,SACA,YAAA,EAAc,CAAA;EACvB,QAAA,EAAU,WAAA;IAAc,KAAA,EAAO,CAAA;IAAG,QAAA,GAAW,UAAA;EAAA;AAAA;;;;iBAM/B,aAAA,GAAA,CAAiB,YAAA,EAAc,CAAA,GAAI,aAAA,CAAc,CAAA;;;;;;;cAuBpD,SAAA,WACD,KAAA,GAAQ,KAAA,YACR,MAAA,oBAA0B,MAAA;EAEpC,KAAA,EAAO,CAAA;EACP,KAAA,EAAO,CAAA;EAAA,QACC,YAAA;cAEI,KAAA,EAAO,CAAA;EA/B8B;;;;EAyCjD,QAAA,CAAS,OAAA,EAAS,OAAA,CAAQ,CAAA,MAAO,IAAA,EAAM,CAAA,KAAM,OAAA,CAAQ,CAAA;EAzCtB;;;EAuD/B,WAAA,CAAA;EAvDgE;AAuBlE;;EAuCE,MAAA,CAAA,GAAU,UAAA;AAAA;;;;iBAUI,YAAA,CAAa,KAAA,EAAO,KAAA,EAAO,KAAA,GAAQ,KAAA,KAAU,QAAA,EAAU,UAAA,KAAe,KAAA;;;;;KAiBjF,cAAA,GAAiB,UAAA,GAAa,cAAA;AAAA,iBAEnB,YAAA,CAAa,QAAA,EAAU,cAAA,GAAiB,UAAA;;;;iBAsBxC,cAAA,CAAe,CAAA,YAAa,CAAA,IAAK,KAAA;;;;;cAgBpC,OAAA,EAAS,MAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;;;cAoCa,aAAA,SAAa,OAAA;;;;;iBAUV,MAAA,CAAO,KAAA,EAAO,UAAA,EAAY,SAAA,EAAW,OAAA;;;;AAcrD;iBANgB,OAAA,CAAQ,KAAA,EAAO,UAAA,EAAY,SAAA,EAAW,OAAA;AAAA,UAMrC,aAAA;EAAA,SACN,EAAA;EAAA,SACA,YAAA,EAAc,CAAA;EACvB,QAAA,EAAU,WAAA;IAAc,KAAA,EAAO,CAAA;IAAG,QAAA,GAAW,UAAA;EAAA;AAAA;;;;iBAS/B,aAAA,GAAA,CAAiB,YAAA,EAAc,CAAA,GAAI,aAAA,CAAc,CAAA;;;;;;AAAjE;cAyBa,SAAA,WACD,KAAA,GAAQ,KAAA,YACR,MAAA,oBAA0B,MAAA;EAEpC,KAAA,EAAO,CAAA;EACP,KAAA,EAAO,CAAA;EACP,YAAA,EAAc,UAAA,QAAkB,MAAA,CAAO,CAAA;EACvC,WAAA,GAAc,UAAA;EAGd,iBAAA,CAAA,CAAA;EACA,kBAAA,CAAA,CAAA;EACA,oBAAA,CAAA,CAAA;EACA,qBAAA,CAAA,CAAuB,SAAA,EAAW,CAAA,EAAG,SAAA,EAAW,CAAA;cAEpC,KAAA,EAAO,CAAA;EAxC8B;;;;EAkDjD,QAAA,CAAS,OAAA,EAAS,OAAA,CAAQ,CAAA,MAAO,IAAA,EAAM,CAAA,KAAM,OAAA,CAAQ,CAAA;EAzBjC;;;EAuCpB,WAAA,CAAA;EArCU;;;EA4CV,MAAA,CAAA,GAAU,UAAA;AAAA;;;;;;cAYC,aAAA,WACD,KAAA,GAAQ,KAAA,YACR,MAAA,oBAA0B,MAAA,2BAC5B,SAAA,CAAU,CAAA,EAAG,CAAA;;;;iBAOP,YAAA,CAAa,KAAA,EAAO,KAAA,EAAO,KAAA,GAAQ,KAAA,KAAU,QAAA,EAAU,UAAA,KAAe,KAAA;;;;;KAiBjF,cAAA,GAAiB,UAAA,GAAa,cAAA;AAAA,iBAEnB,YAAA,CAAa,QAAA,EAAU,cAAA,GAAiB,UAAA;;;;iBAsBxC,cAAA,CAAe,CAAA,YAAa,CAAA,IAAK,KAAA;;;;iBAejC,YAAA,CAAa,QAAA,EAAU,UAAA,EAAY,MAAA,EAAQ,OAAA,GAAU,UAAA;;;;;cAcxD,OAAA,EAAS,MAAA;AAAA,cAIT,OAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-runtime2.d.ts","names":[],"sources":["../../../src/jsx-runtime.ts"],"mappings":";;;iBAwIgB,GAAA,CACd,IAAA,WAAe,WAAA,WACf,KAAA,EAAO,KAAA;EAAU,QAAA,GAAW,UAAA,GAAa,UAAA;AAAA,GACzC,GAAA,4BACC,KAAA;AAAA,cAiBU,IAAA,SAAI,GAAA"}
1
+ {"version":3,"file":"jsx-runtime2.d.ts","names":[],"sources":["../../../src/jsx-runtime.ts"],"mappings":";;;iBAyOgB,GAAA,CACd,IAAA,WAAe,WAAA,WACf,KAAA,EAAO,KAAA;EAAU,QAAA,GAAW,UAAA,GAAa,UAAA;AAAA,GACzC,GAAA,4BACC,KAAA;AAAA,cAiEU,IAAA,SAAI,GAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"signals2.d.ts","names":[],"sources":["../../../src/signals.ts"],"mappings":";;;UAiBiB,cAAA;EAAA,SACN,KAAA,EAAO,CAAA;EAChB,IAAA,IAAQ,CAAA;AAAA;AAAA,UAGO,cAAA,YAA0B,cAAA,CAAe,CAAA;EACxD,KAAA,EAAO,CAAA;AAAA;AADT;;;;;;;;AAAA,iBAYgB,MAAA,GAAA,CAAU,OAAA,EAAS,CAAA,GAAI,cAAA,CAAe,CAAA;;;;;;;AAAtD;iBAwBgB,QAAA,GAAA,CAAY,EAAA,QAAU,CAAA,GAAI,cAAA,CAAe,CAAA;;;;;iBAmBzC,MAAA,CAAO,EAAA"}
1
+ {"version":3,"file":"signals2.d.ts","names":[],"sources":["../../../src/signals.ts"],"mappings":";;;UAkBiB,cAAA;EAAA,SACN,KAAA,EAAO,CAAA;EAChB,IAAA,IAAQ,CAAA;AAAA;AAAA,UAGO,cAAA,YAA0B,cAAA,CAAe,CAAA;EACxD,KAAA,EAAO,CAAA;AAAA;AADT;;;;;;;;AAAA,iBAYgB,MAAA,GAAA,CAAU,OAAA,EAAS,CAAA,GAAI,cAAA,CAAe,CAAA;;;;;;;AAAtD;iBAwBgB,QAAA,GAAA,CAAY,EAAA,QAAU,CAAA,GAAI,cAAA,CAAe,CAAA;;;;;iBAkBzC,MAAA,CAAO,EAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/preact-compat",
3
- "version": "0.13.1",
3
+ "version": "0.14.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": {
@@ -62,9 +62,9 @@
62
62
  "prepublishOnly": "bun run build"
63
63
  },
64
64
  "dependencies": {
65
- "@pyreon/core": "^0.13.1",
66
- "@pyreon/reactivity": "^0.13.1",
67
- "@pyreon/runtime-dom": "^0.13.1"
65
+ "@pyreon/core": "^0.14.0",
66
+ "@pyreon/reactivity": "^0.14.0",
67
+ "@pyreon/runtime-dom": "^0.14.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@happy-dom/global-registrator": "^20.8.9",
package/src/hooks.ts CHANGED
@@ -32,30 +32,45 @@ function depsChanged(a: unknown[] | undefined, b: unknown[] | undefined): boolea
32
32
  return false
33
33
  }
34
34
 
35
+ function shallowEqual<P extends Record<string, unknown>>(a: P, b: P): boolean {
36
+ const keysA = Object.keys(a)
37
+ const keysB = Object.keys(b)
38
+ if (keysA.length !== keysB.length) return false
39
+ for (const k of keysA) {
40
+ if (!Object.is(a[k], b[k])) return false
41
+ }
42
+ return true
43
+ }
44
+
35
45
  // ─── useState ────────────────────────────────────────────────────────────────
36
46
 
37
47
  /**
38
48
  * Preact-compatible `useState` — returns `[value, setter]`.
39
49
  * Triggers a component re-render when the setter is called.
50
+ *
51
+ * The setter has stable identity across renders (same reference every time).
40
52
  */
41
53
  export function useState<T>(initial: T | (() => T)): [T, (v: T | ((prev: T) => T)) => void] {
42
54
  const ctx = requireCtx()
43
55
  const idx = getHookIndex()
44
56
 
45
57
  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
50
- const setter = (v: T | ((prev: T) => T)) => {
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()
58
+ const val = typeof initial === 'function' ? (initial as () => T)() : initial
59
+ // Store both value and a STABLE setter in one hook slot so setter identity
60
+ // never changes across renders (Preact/React guarantee).
61
+ const entry = { value: val, setter: null as unknown as (v: T | ((prev: T) => T)) => void }
62
+ entry.setter = (v: T | ((prev: T) => T)) => {
63
+ const current = entry.value
64
+ const next = typeof v === 'function' ? (v as (prev: T) => T)(current) : v
65
+ if (Object.is(current, next)) return
66
+ entry.value = next
67
+ ctx.scheduleRerender()
68
+ }
69
+ ctx.hooks.push(entry)
56
70
  }
57
71
 
58
- return [value, setter]
72
+ const entry = ctx.hooks[idx] as { value: T; setter: (v: T | ((prev: T) => T)) => void }
73
+ return [entry.value, entry.setter]
59
74
  }
60
75
 
61
76
  // ─── useEffect ───────────────────────────────────────────────────────────────
@@ -159,28 +174,42 @@ export function useRef<T>(initial?: T): { current: T | null } {
159
174
 
160
175
  /**
161
176
  * Preact-compatible `useReducer` — returns `[state, dispatch]`.
177
+ * Supports the 3-argument form: `useReducer(reducer, initialArg, init)`.
178
+ *
179
+ * Dispatch has stable identity across renders (same reference every time).
162
180
  */
163
181
  export function useReducer<S, A>(
164
182
  reducer: (state: S, action: A) => S,
165
- initial: S | (() => S),
183
+ initialArg: S | (() => S),
184
+ init?: (arg: S) => S,
166
185
  ): [S, (action: A) => void] {
167
186
  const ctx = requireCtx()
168
187
  const idx = getHookIndex()
169
188
 
170
189
  if (ctx.hooks.length <= idx) {
171
- ctx.hooks.push(typeof initial === 'function' ? (initial as () => S)() : initial)
172
- }
173
-
174
- const state = ctx.hooks[idx] as S
175
- const dispatch = (action: A) => {
176
- const current = ctx.hooks[idx] as S
177
- const next = reducer(current, action)
178
- if (Object.is(current, next)) return
179
- ctx.hooks[idx] = next
180
- ctx.scheduleRerender()
190
+ let initial: S
191
+ if (init) {
192
+ initial = init(initialArg as S)
193
+ } else if (typeof initialArg === 'function') {
194
+ initial = (initialArg as () => S)()
195
+ } else {
196
+ initial = initialArg
197
+ }
198
+ // Store both value and a STABLE dispatch in one hook slot so dispatch identity
199
+ // never changes across renders (Preact/React guarantee).
200
+ const entry = { value: initial, dispatch: null as unknown as (action: A) => void }
201
+ entry.dispatch = (action: A) => {
202
+ const current = entry.value
203
+ const next = reducer(current, action)
204
+ if (Object.is(current, next)) return
205
+ entry.value = next
206
+ ctx.scheduleRerender()
207
+ }
208
+ ctx.hooks.push(entry)
181
209
  }
182
210
 
183
- return [state, dispatch]
211
+ const entry = ctx.hooks[idx] as { value: S; dispatch: (action: A) => void }
212
+ return [entry.value, entry.dispatch]
184
213
  }
185
214
 
186
215
  // ─── useId ───────────────────────────────────────────────────────────────────
@@ -206,34 +235,120 @@ export function useId(): string {
206
235
  /**
207
236
  * Preact-compatible `memo` — wraps a component to skip re-render when props
208
237
  * are shallowly equal.
238
+ *
239
+ * Each component INSTANCE gets its own props/result cache via a hook slot,
240
+ * so two `<MemoComp />` usages don't share memoization state.
209
241
  */
210
242
  export function memo<P extends Record<string, unknown>>(
211
243
  component: (props: P) => VNodeChild,
212
244
  areEqual?: (prevProps: P, nextProps: P) => boolean,
213
245
  ): (props: P) => VNodeChild {
214
- const compare =
215
- areEqual ??
216
- ((a: P, b: P) => {
217
- const keysA = Object.keys(a)
218
- const keysB = Object.keys(b)
219
- if (keysA.length !== keysB.length) return false
220
- for (const k of keysA) {
221
- if (!Object.is(a[k], b[k])) return false
246
+ const compare = areEqual ?? shallowEqual
247
+
248
+ // Fallback closure-level cache for calls outside a compat render context
249
+ // (e.g. direct function calls in tests). Inside a render context, each
250
+ // component instance gets its own cache via a hook slot.
251
+ let _fallbackPrevProps: P | null = null
252
+ let _fallbackPrevResult: VNodeChild = null
253
+
254
+ const memoized = (props: P) => {
255
+ const ctx = getCurrentCtx()
256
+ if (ctx) {
257
+ // Per-instance cache via hook slot
258
+ const idx = getHookIndex()
259
+ if (ctx.hooks.length <= idx) {
260
+ ctx.hooks.push({ prevProps: null as P | null, prevResult: null as VNodeChild })
222
261
  }
223
- return true
224
- })
262
+ const cache = ctx.hooks[idx] as { prevProps: P | null; prevResult: VNodeChild }
263
+ if (cache.prevProps !== null && compare(cache.prevProps, props)) {
264
+ return cache.prevResult
265
+ }
266
+ cache.prevProps = props
267
+ cache.prevResult = component(props)
268
+ return cache.prevResult
269
+ }
270
+ // No compat context — use closure-level fallback cache
271
+ if (_fallbackPrevProps !== null && compare(_fallbackPrevProps, props)) {
272
+ return _fallbackPrevResult
273
+ }
274
+ _fallbackPrevProps = props
275
+ _fallbackPrevResult = component(props)
276
+ return _fallbackPrevResult
277
+ }
278
+ memoized.displayName =
279
+ (component as unknown as { displayName?: string }).displayName || component.name || 'Memo'
280
+ return memoized
281
+ }
225
282
 
226
- let prevProps: P | null = null
227
- let prevResult: VNodeChild = null
283
+ // ─── forwardRef ─────────────────────────────────────────────────────────────
228
284
 
229
- return (props: P) => {
230
- if (prevProps !== null && compare(prevProps, props)) {
231
- return prevResult
232
- }
233
- prevProps = props
234
- prevResult = (component as (p: P) => VNodeChild)(props)
235
- return prevResult
285
+ /**
286
+ * Preact-compatible `forwardRef` pass-through in Pyreon.
287
+ * Refs are regular props in Pyreon, so no wrapper is needed.
288
+ * The render function receives (props, ref) — we merge ref into props.
289
+ */
290
+ export function forwardRef<P extends Record<string, unknown>>(
291
+ render: (props: P, ref: { current: unknown } | null) => VNodeChild,
292
+ ): (props: P & { ref?: { current: unknown } | null }) => VNodeChild {
293
+ const forwarded = (props: P & { ref?: { current: unknown } | null }) => {
294
+ const { ref, ...rest } = props
295
+ return render(rest as P, ref ?? null)
236
296
  }
297
+ forwarded.displayName =
298
+ (render as unknown as { displayName?: string }).displayName || render.name || 'ForwardRef'
299
+ return forwarded
300
+ }
301
+
302
+ // ─── useImperativeHandle ────────────────────────────────────────────────────
303
+
304
+ /**
305
+ * Preact-compatible `useImperativeHandle`.
306
+ */
307
+ export function useImperativeHandle<T>(
308
+ ref: { current: T | null } | null | undefined,
309
+ init: () => T,
310
+ deps?: unknown[],
311
+ ): void {
312
+ useLayoutEffect(() => {
313
+ if (ref) ref.current = init()
314
+ return () => {
315
+ if (ref) ref.current = null
316
+ }
317
+ }, deps)
318
+ }
319
+
320
+ // ─── useDebugValue ──────────────────────────────────────────────────────────
321
+
322
+ /**
323
+ * Preact-compatible `useDebugValue` — no-op in Pyreon (no Preact DevTools integration).
324
+ */
325
+ export function useDebugValue<T>(_value: T, _format?: (v: T) => unknown): void {}
326
+
327
+ // ─── useTransition ──────────────────────────────────────────────────────
328
+
329
+ /**
330
+ * Preact-compatible `useTransition` — returns `[isPending, startTransition]`.
331
+ *
332
+ * In Pyreon's signal-based reactivity there is no concept of concurrent
333
+ * rendering lanes. The callback is executed synchronously and `isPending`
334
+ * is always `false`. This shim exists so Preact/React code that uses
335
+ * `useTransition` compiles and runs without changes.
336
+ */
337
+ export function useTransition(): [boolean, (fn: () => void) => void] {
338
+ return [false, (fn) => fn()]
339
+ }
340
+
341
+ // ─── useDeferredValue ───────────────────────────────────────────────────
342
+
343
+ /**
344
+ * Preact-compatible `useDeferredValue` — returns the value as-is.
345
+ *
346
+ * In Pyreon's signal-based reactivity there are no concurrent rendering lanes,
347
+ * so the value is never "deferred". This shim exists so Preact/React code that
348
+ * uses `useDeferredValue` compiles and runs without changes.
349
+ */
350
+ export function useDeferredValue<T>(value: T): T {
351
+ return value
237
352
  }
238
353
 
239
354
  // ─── useErrorBoundary ────────────────────────────────────────────────────────
package/src/index.ts CHANGED
@@ -4,8 +4,9 @@
4
4
  * Preact-compatible API shim that runs on Pyreon's reactive engine.
5
5
  *
6
6
  * Provides the core Preact API surface: h, Fragment, render, hydrate,
7
- * Component class, createContext, createRef, cloneElement, toChildArray,
8
- * isValidElement, and the options hook object.
7
+ * Component class, PureComponent, createContext, createRef, cloneElement,
8
+ * toChildArray, isValidElement, createPortal, lazy, Suspense, ErrorBoundary,
9
+ * and the options hook object.
9
10
  *
10
11
  * For hooks, import from "@pyreon/preact-compat/hooks".
11
12
  * For signals, import from "@pyreon/preact-compat/signals".
@@ -14,8 +15,12 @@
14
15
  import type { ComponentFn, Props, VNode, VNodeChild } from '@pyreon/core'
15
16
  import {
16
17
  createRef,
18
+ ErrorBoundary,
17
19
  Fragment,
20
+ lazy,
21
+ Portal,
18
22
  provide,
23
+ Suspense,
19
24
  createContext as pyreonCreateContext,
20
25
  h as pyreonH,
21
26
  useContext,
@@ -59,6 +64,9 @@ export interface PreactContext<T> {
59
64
  Provider: ComponentFn<{ value: T; children?: VNodeChild }>
60
65
  }
61
66
 
67
+ // Tag the Provider so wrapCompatComponent skips it (it's already a native component)
68
+ const NATIVE_COMPONENT = Symbol.for('pyreon:native-compat')
69
+
62
70
  /**
63
71
  * Preact-compatible createContext — returns a context with a `.Provider` component.
64
72
  */
@@ -68,6 +76,8 @@ export function createContext<T>(defaultValue: T): PreactContext<T> {
68
76
  provide(ctx, props.value)
69
77
  return props.children
70
78
  }) as ComponentFn<{ value: T; children?: VNodeChild }>
79
+ // Mark as native so jsx() doesn't wrap it with wrapCompatComponent
80
+ ;(Provider as unknown as Record<symbol, boolean>)[NATIVE_COMPONENT] = true
71
81
  return { ...ctx, Provider }
72
82
  }
73
83
 
@@ -91,7 +101,14 @@ export class Component<
91
101
  > {
92
102
  props: P
93
103
  state: S
94
- private _stateSignal: ReturnType<typeof signal<S>>
104
+ _stateSignal: ReturnType<typeof signal<S>>
105
+ _lastResult?: VNodeChild
106
+
107
+ // Lifecycle methods (overridden by subclasses)
108
+ componentDidMount?(): void
109
+ componentDidUpdate?(): void
110
+ componentWillUnmount?(): void
111
+ shouldComponentUpdate?(nextProps: P, nextState: S): boolean
95
112
 
96
113
  constructor(props: P) {
97
114
  this.props = props
@@ -129,13 +146,25 @@ export class Component<
129
146
  }
130
147
  }
131
148
 
149
+ // ─── PureComponent ──────────────────────────────────────────────────────────
150
+
151
+ /**
152
+ * Preact-compatible PureComponent — extends Component.
153
+ * In Pyreon's compat layer this behaves identically to Component
154
+ * (signal-based reactivity already avoids unnecessary re-renders).
155
+ */
156
+ export class PureComponent<
157
+ P extends Props = Props,
158
+ S extends Record<string, unknown> = Record<string, unknown>,
159
+ > extends Component<P, S> {}
160
+
132
161
  // ─── cloneElement ────────────────────────────────────────────────────────────
133
162
 
134
163
  /**
135
164
  * Clone a VNode with merged props (like Preact's cloneElement).
136
165
  */
137
166
  export function cloneElement(vnode: VNode, props?: Props, ...children: VNodeChild[]): VNode {
138
- const mergedProps = { ...vnode.props, ...(props ?? {}) }
167
+ const mergedProps = props ? { ...vnode.props, ...props } : { ...vnode.props }
139
168
  const mergedChildren = children.length > 0 ? children : vnode.children
140
169
  return {
141
170
  type: vnode.type,
@@ -185,6 +214,19 @@ export function isValidElement(x: unknown): x is VNode {
185
214
  )
186
215
  }
187
216
 
217
+ // ─── createPortal ───────────────────────────────────────────────────────────
218
+
219
+ /**
220
+ * Preact-compatible `createPortal(children, target)`.
221
+ */
222
+ export function createPortal(children: VNodeChild, target: Element): VNodeChild {
223
+ return Portal({ target, children })
224
+ }
225
+
226
+ // ─── Suspense / lazy / ErrorBoundary ─────────────────────────────────────────
227
+
228
+ export { ErrorBoundary, lazy, Suspense }
229
+
188
230
  // ─── options ─────────────────────────────────────────────────────────────────
189
231
 
190
232
  /**
@@ -192,3 +234,7 @@ export function isValidElement(x: unknown): x is VNode {
192
234
  * with libraries that check for `options._hook`, `options.vnode`, etc.
193
235
  */
194
236
  export const options: Record<string, unknown> = {}
237
+
238
+ // ─── version ────────────────────────────────────────────────────────────────
239
+
240
+ export const version = '10.0.0-pyreon'