@pyreon/react-compat 0.12.8 → 0.12.10

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.
@@ -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","children":[{"uid":"6c313ed2-1","name":"jsx-runtime.ts"},{"uid":"6c313ed2-3","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"6c313ed2-1":{"renderedLength":186,"gzipLength":138,"brotliLength":0,"metaUid":"6c313ed2-0"},"6c313ed2-3":{"renderedLength":4990,"gzipLength":1386,"brotliLength":0,"metaUid":"6c313ed2-2"}},"nodeMetas":{"6c313ed2-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"index.js":"6c313ed2-1"},"imported":[{"uid":"6c313ed2-4"},{"uid":"6c313ed2-5"}],"importedBy":[{"uid":"6c313ed2-2"}]},"6c313ed2-2":{"id":"/src/index.ts","moduleParts":{"index.js":"6c313ed2-3"},"imported":[{"uid":"6c313ed2-4"},{"uid":"6c313ed2-5"},{"uid":"6c313ed2-0"}],"importedBy":[],"isEntry":true},"6c313ed2-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"6c313ed2-2"},{"uid":"6c313ed2-0"}]},"6c313ed2-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"6c313ed2-2"},{"uid":"6c313ed2-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","children":[{"uid":"32dc1380-1","name":"jsx-runtime.ts"},{"uid":"32dc1380-3","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"32dc1380-1":{"renderedLength":186,"gzipLength":138,"brotliLength":0,"metaUid":"32dc1380-0"},"32dc1380-3":{"renderedLength":7034,"gzipLength":1959,"brotliLength":0,"metaUid":"32dc1380-2"}},"nodeMetas":{"32dc1380-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"index.js":"32dc1380-1"},"imported":[{"uid":"32dc1380-4"},{"uid":"32dc1380-5"}],"importedBy":[{"uid":"32dc1380-2"}]},"32dc1380-2":{"id":"/src/index.ts","moduleParts":{"index.js":"32dc1380-3"},"imported":[{"uid":"32dc1380-4"},{"uid":"32dc1380-5"},{"uid":"32dc1380-0"}],"importedBy":[],"isEntry":true},"32dc1380-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"32dc1380-2"},{"uid":"32dc1380-0"}]},"32dc1380-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"32dc1380-2"},{"uid":"32dc1380-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":"d0bc860c-1","name":"jsx-runtime.ts"},{"uid":"d0bc860c-3","name":"jsx-dev-runtime.ts"}]}]}],"isRoot":true},"nodeParts":{"d0bc860c-1":{"renderedLength":2389,"gzipLength":846,"brotliLength":0,"metaUid":"d0bc860c-0"},"d0bc860c-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"d0bc860c-2"}},"nodeMetas":{"d0bc860c-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"jsx-runtime.js":"d0bc860c-1"},"imported":[{"uid":"d0bc860c-4"},{"uid":"d0bc860c-5"}],"importedBy":[{"uid":"d0bc860c-2"}]},"d0bc860c-2":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"d0bc860c-3"},"imported":[{"uid":"d0bc860c-0"}],"importedBy":[],"isEntry":true},"d0bc860c-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"d0bc860c-0"}]},"d0bc860c-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"d0bc860c-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":"243e8c0a-1","name":"jsx-runtime.ts"},{"uid":"243e8c0a-3","name":"jsx-dev-runtime.ts"}]}]}],"isRoot":true},"nodeParts":{"243e8c0a-1":{"renderedLength":2522,"gzipLength":870,"brotliLength":0,"metaUid":"243e8c0a-0"},"243e8c0a-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"243e8c0a-2"}},"nodeMetas":{"243e8c0a-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"jsx-runtime.js":"243e8c0a-1"},"imported":[{"uid":"243e8c0a-4"},{"uid":"243e8c0a-5"}],"importedBy":[{"uid":"243e8c0a-2"}]},"243e8c0a-2":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"243e8c0a-3"},"imported":[{"uid":"243e8c0a-0"}],"importedBy":[],"isEntry":true},"243e8c0a-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"243e8c0a-0"}]},"243e8c0a-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"243e8c0a-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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ErrorBoundary, Fragment, Portal, Suspense, createContext, h, h as createElement, lazy, useContext } from "@pyreon/core";
1
+ import { ErrorBoundary, Fragment, Portal, Suspense, createContext, h, h as createElement, h as h$1, lazy, useContext } from "@pyreon/core";
2
2
  import { batch } from "@pyreon/reactivity";
3
3
 
4
4
  //#region src/jsx-runtime.ts
@@ -208,7 +208,74 @@ function createPortal(children, target) {
208
208
  children
209
209
  });
210
210
  }
211
+ /**
212
+ * React-compatible `forwardRef` — pass-through in Pyreon.
213
+ * Refs are regular props in Pyreon, so no wrapper is needed.
214
+ * The render function receives (props, ref) — we merge ref into props.
215
+ */
216
+ function forwardRef(render) {
217
+ return (props) => {
218
+ const { ref, ...rest } = props;
219
+ return render(rest, ref ?? null);
220
+ };
221
+ }
222
+ /**
223
+ * React-compatible `cloneElement` — creates a new VNode with merged props.
224
+ */
225
+ function cloneElement(element, props, ...children) {
226
+ const mergedProps = {
227
+ ...element.props,
228
+ ...props
229
+ };
230
+ const mergedChildren = children.length > 0 ? children : element.children;
231
+ return h$1(element.type, mergedProps, ...mergedChildren);
232
+ }
233
+ function flattenChildren(children) {
234
+ if (children == null) return [];
235
+ if (!Array.isArray(children)) return [children];
236
+ const result = [];
237
+ for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
238
+ else result.push(child);
239
+ return result;
240
+ }
241
+ /**
242
+ * React-compatible `Children` utilities for working with VNode children.
243
+ */
244
+ const Children = {
245
+ map(children, fn) {
246
+ const flat = flattenChildren(children);
247
+ const result = [];
248
+ for (let i = 0; i < flat.length; i++) {
249
+ const child = flat[i];
250
+ if (child == null || child === true || child === false) continue;
251
+ result.push(fn(child, i));
252
+ }
253
+ return result;
254
+ },
255
+ forEach(children, fn) {
256
+ const flat = flattenChildren(children);
257
+ for (let i = 0; i < flat.length; i++) {
258
+ const child = flat[i];
259
+ if (child == null || child === true || child === false) continue;
260
+ fn(child, i);
261
+ }
262
+ },
263
+ count(children) {
264
+ const flat = flattenChildren(children);
265
+ let count = 0;
266
+ for (const child of flat) if (child != null && child !== true && child !== false) count++;
267
+ return count;
268
+ },
269
+ toArray(children) {
270
+ return flattenChildren(children).filter((child) => child != null && child !== true && child !== false);
271
+ },
272
+ only(children) {
273
+ const arr = Children.toArray(children);
274
+ if (arr.length !== 1) throw new Error("[Pyreon] Children.only expected exactly one child");
275
+ return arr[0];
276
+ }
277
+ };
211
278
 
212
279
  //#endregion
213
- export { ErrorBoundary, Fragment, Suspense, batch, createContext, createElement, createPortal, h, lazy, memo, useCallback, useContext, useDeferredValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useTransition };
280
+ export { Children, ErrorBoundary, Fragment, Suspense, batch, cloneElement, createContext, createElement, createPortal, forwardRef, h, lazy, memo, useCallback, useContext, useDeferredValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useTransition };
214
281
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/jsx-runtime.ts","../src/index.ts"],"sourcesContent":["/**\n * Compat JSX runtime for React compatibility mode.\n *\n * When `jsxImportSource` is set to `@pyreon/react-compat` (via the vite plugin's\n * `compat: \"react\"` option), OXC rewrites JSX to import from this file:\n * <div className=\"x\" /> → jsx(\"div\", { className: \"x\" })\n *\n * For component VNodes, we wrap the component function so it returns a reactive\n * accessor — enabling React-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(reactComponent: Function): ComponentFn {\n let wrapped = _wrapperCache.get(reactComponent)\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 = (reactComponent 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(reactComponent, 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 React-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 // Map className → class for React compat\n if (typeof type === 'string' && propsWithKey.className !== undefined) {\n propsWithKey.class = propsWithKey.className\n delete propsWithKey.className\n }\n\n return h(type, propsWithKey, ...(childArray as VNodeChild[]))\n}\n\nexport const jsxs = jsx\nexport const jsxDEV = jsx\n","/**\n * @pyreon/react-compat\n *\n * Fully React-compatible hook API powered by Pyreon's reactive engine.\n *\n * Components re-render on state change — just like React. Hooks return plain\n * values and use deps arrays for memoization. Existing React code works\n * unchanged when paired with `pyreon({ compat: \"react\" })` in your vite config.\n *\n * USAGE:\n * import { useState, useEffect } from \"react\" // aliased by vite plugin\n * import { createRoot } from \"react-dom/client\" // aliased by vite plugin\n */\n\nexport type { Props, VNode as ReactNode, VNodeChild } from '@pyreon/core'\nexport { Fragment, h as createElement, h } from '@pyreon/core'\n\nimport type { VNodeChild } from '@pyreon/core'\nimport { createContext, ErrorBoundary, Portal, Suspense, useContext } from '@pyreon/core'\nimport { batch } from '@pyreon/reactivity'\nimport type { EffectEntry } from './jsx-runtime'\nimport { getCurrentCtx, getHookIndex } from './jsx-runtime'\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction requireCtx() {\n const ctx = getCurrentCtx()\n if (!ctx) throw new Error('Hook called outside of a component render')\n return ctx\n}\n\nfunction depsChanged(a: unknown[] | undefined, b: unknown[] | undefined): boolean {\n if (a === undefined || b === undefined) return true\n if (a.length !== b.length) return true\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return true\n }\n return false\n}\n\n// ─── State ───────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useState` — returns `[value, setter]`.\n * Triggers a component re-render when the setter is called.\n */\nexport function useState<T>(initial: T | (() => T)): [T, (v: T | ((prev: T) => T)) => void] {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n ctx.hooks.push(typeof initial === 'function' ? (initial as () => T)() : initial)\n }\n\n const value = ctx.hooks[idx] as T\n const setter = (v: T | ((prev: T) => T)) => {\n const current = ctx.hooks[idx] as T\n const next = typeof v === 'function' ? (v as (prev: T) => T)(current) : v\n if (Object.is(current, next)) return\n ctx.hooks[idx] = next\n ctx.scheduleRerender()\n }\n\n return [value, setter]\n}\n\n// ─── Reducer ─────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useReducer` — returns `[state, dispatch]`.\n */\nexport function useReducer<S, A>(\n reducer: (state: S, action: A) => S,\n initial: S | (() => S),\n): [S, (action: A) => void] {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n ctx.hooks.push(typeof initial === 'function' ? (initial as () => S)() : initial)\n }\n\n const state = ctx.hooks[idx] as S\n const dispatch = (action: A) => {\n const current = ctx.hooks[idx] as S\n const next = reducer(current, action)\n if (Object.is(current, next)) return\n ctx.hooks[idx] = next\n ctx.scheduleRerender()\n }\n\n return [state, dispatch]\n}\n\n// ─── Effects ─────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useEffect` — runs after render when deps change.\n * Returns cleanup on unmount and before re-running.\n */\nexport function useEffect(fn: () => (() => void) | void, deps?: unknown[]): void {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n // First render — always run\n const entry: EffectEntry = { fn, deps, cleanup: undefined }\n ctx.hooks.push(entry)\n ctx.pendingEffects.push(entry)\n } else {\n const entry = ctx.hooks[idx] as EffectEntry\n if (depsChanged(entry.deps, deps)) {\n entry.fn = fn\n entry.deps = deps\n ctx.pendingEffects.push(entry)\n }\n }\n}\n\n/**\n * React-compatible `useLayoutEffect` — runs synchronously after DOM mutations.\n */\nexport function useLayoutEffect(fn: () => (() => void) | void, deps?: unknown[]): void {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n const entry: EffectEntry = { fn, deps, cleanup: undefined }\n ctx.hooks.push(entry)\n ctx.pendingLayoutEffects.push(entry)\n } else {\n const entry = ctx.hooks[idx] as EffectEntry\n if (depsChanged(entry.deps, deps)) {\n entry.fn = fn\n entry.deps = deps\n ctx.pendingLayoutEffects.push(entry)\n }\n }\n}\n\n// ─── Memoization ─────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useMemo` — returns the cached value, recomputed when deps change.\n */\nexport function useMemo<T>(fn: () => T, deps: unknown[]): T {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n const value = fn()\n ctx.hooks.push({ value, deps })\n return value\n }\n\n const entry = ctx.hooks[idx] as { value: T; deps: unknown[] }\n if (depsChanged(entry.deps, deps)) {\n entry.value = fn()\n entry.deps = deps\n }\n return entry.value\n}\n\n/**\n * React-compatible `useCallback` — returns the cached function when deps haven't changed.\n */\nexport function useCallback<T extends (...args: never[]) => unknown>(fn: T, deps: unknown[]): T {\n return useMemo(() => fn, deps)\n}\n\n// ─── Refs ────────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useRef` — returns `{ current }` persisted across re-renders.\n */\nexport function useRef<T>(initial?: T): { current: T | null } {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n const ref = { current: initial !== undefined ? (initial as T) : null }\n ctx.hooks.push(ref)\n }\n\n return ctx.hooks[idx] as { current: T | null }\n}\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\nexport { createContext, useContext }\n\n// ─── ID ──────────────────────────────────────────────────────────────────────\n\nlet _idCounter = 0\n\n/**\n * React-compatible `useId` — returns a stable unique string per hook call.\n */\nexport function useId(): string {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n ctx.hooks.push(`:r${(_idCounter++).toString(36)}:`)\n }\n\n return ctx.hooks[idx] as string\n}\n\n// ─── Optimization ────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `memo` — wraps a component to skip re-render when props\n * are shallowly equal.\n */\nexport function memo<P extends Record<string, unknown>>(\n component: (props: P) => VNodeChild,\n areEqual?: (prevProps: P, nextProps: P) => boolean,\n): (props: P) => VNodeChild {\n const compare =\n areEqual ??\n ((a: P, b: P) => {\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n if (keysA.length !== keysB.length) return false\n for (const k of keysA) {\n if (!Object.is(a[k], b[k])) return false\n }\n return true\n })\n\n let prevProps: P | null = null\n let prevResult: VNodeChild = null\n\n return (props: P) => {\n if (prevProps !== null && compare(prevProps, props)) {\n return prevResult\n }\n prevProps = props\n prevResult = (component as (p: P) => VNodeChild)(props)\n return prevResult\n }\n}\n\n/**\n * React-compatible `useTransition` — no concurrent mode in Pyreon.\n */\nexport function useTransition(): [boolean, (fn: () => void) => void] {\n return [false, (fn) => fn()]\n}\n\n/**\n * React-compatible `useDeferredValue` — returns the value as-is.\n */\nexport function useDeferredValue<T>(value: T): T {\n return value\n}\n\n// ─── Imperative handle ───────────────────────────────────────────────────────\n\n/**\n * React-compatible `useImperativeHandle`.\n */\nexport function useImperativeHandle<T>(\n ref: { current: T | null } | null | undefined,\n init: () => T,\n deps?: unknown[],\n): void {\n useLayoutEffect(() => {\n if (ref) ref.current = init()\n return () => {\n if (ref) ref.current = null\n }\n }, deps)\n}\n\n// ─── Batching ────────────────────────────────────────────────────────────────\n\nexport { batch }\n\n// ─── Portals ─────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `createPortal(children, target)`.\n */\nexport function createPortal(children: VNodeChild, target: Element): VNodeChild {\n return Portal({ target, children })\n}\n\n// ─── Suspense / lazy / ErrorBoundary ─────────────────────────────────────────\n\nexport { lazy } from '@pyreon/core'\nexport { ErrorBoundary, Suspense }\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["h"],"sources":["../src/jsx-runtime.ts","../src/index.ts"],"sourcesContent":["/**\n * Compat JSX runtime for React compatibility mode.\n *\n * When `jsxImportSource` is set to `@pyreon/react-compat` (via the vite plugin's\n * `compat: \"react\"` option), OXC rewrites JSX to import from this file:\n * <div className=\"x\" /> → jsx(\"div\", { className: \"x\" })\n *\n * For component VNodes, we wrap the component function so it returns a reactive\n * accessor — enabling React-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(reactComponent: Function): ComponentFn {\n let wrapped = _wrapperCache.get(reactComponent)\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 = (reactComponent 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(reactComponent, 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 React-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 // Map React-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\n return h(type, propsWithKey, ...(childArray as VNodeChild[]))\n}\n\nexport const jsxs = jsx\nexport const jsxDEV = jsx\n","/**\n * @pyreon/react-compat\n *\n * Fully React-compatible hook API powered by Pyreon's reactive engine.\n *\n * Components re-render on state change — just like React. Hooks return plain\n * values and use deps arrays for memoization. Existing React code works\n * unchanged when paired with `pyreon({ compat: \"react\" })` in your vite config.\n *\n * USAGE:\n * import { useState, useEffect } from \"react\" // aliased by vite plugin\n * import { createRoot } from \"react-dom/client\" // aliased by vite plugin\n */\n\nexport type { Props, VNode as ReactNode, VNodeChild } from '@pyreon/core'\nexport { Fragment, h as createElement, h } from '@pyreon/core'\n\nimport type { VNode, VNodeChild } from '@pyreon/core'\nimport { createContext, ErrorBoundary, h, Portal, Suspense, useContext } from '@pyreon/core'\nimport { batch } from '@pyreon/reactivity'\nimport type { EffectEntry } from './jsx-runtime'\nimport { getCurrentCtx, getHookIndex } from './jsx-runtime'\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction requireCtx() {\n const ctx = getCurrentCtx()\n if (!ctx) throw new Error('Hook called outside of a component render')\n return ctx\n}\n\nfunction depsChanged(a: unknown[] | undefined, b: unknown[] | undefined): boolean {\n if (a === undefined || b === undefined) return true\n if (a.length !== b.length) return true\n for (let i = 0; i < a.length; i++) {\n if (!Object.is(a[i], b[i])) return true\n }\n return false\n}\n\n// ─── State ───────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useState` — returns `[value, setter]`.\n * Triggers a component re-render when the setter is called.\n */\nexport function useState<T>(initial: T | (() => T)): [T, (v: T | ((prev: T) => T)) => void] {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n ctx.hooks.push(typeof initial === 'function' ? (initial as () => T)() : initial)\n }\n\n const value = ctx.hooks[idx] as T\n const setter = (v: T | ((prev: T) => T)) => {\n const current = ctx.hooks[idx] as T\n const next = typeof v === 'function' ? (v as (prev: T) => T)(current) : v\n if (Object.is(current, next)) return\n ctx.hooks[idx] = next\n ctx.scheduleRerender()\n }\n\n return [value, setter]\n}\n\n// ─── Reducer ─────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useReducer` — returns `[state, dispatch]`.\n */\nexport function useReducer<S, A>(\n reducer: (state: S, action: A) => S,\n initial: S | (() => S),\n): [S, (action: A) => void] {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n ctx.hooks.push(typeof initial === 'function' ? (initial as () => S)() : initial)\n }\n\n const state = ctx.hooks[idx] as S\n const dispatch = (action: A) => {\n const current = ctx.hooks[idx] as S\n const next = reducer(current, action)\n if (Object.is(current, next)) return\n ctx.hooks[idx] = next\n ctx.scheduleRerender()\n }\n\n return [state, dispatch]\n}\n\n// ─── Effects ─────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useEffect` — runs after render when deps change.\n * Returns cleanup on unmount and before re-running.\n */\nexport function useEffect(fn: () => (() => void) | void, deps?: unknown[]): void {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n // First render — always run\n const entry: EffectEntry = { fn, deps, cleanup: undefined }\n ctx.hooks.push(entry)\n ctx.pendingEffects.push(entry)\n } else {\n const entry = ctx.hooks[idx] as EffectEntry\n if (depsChanged(entry.deps, deps)) {\n entry.fn = fn\n entry.deps = deps\n ctx.pendingEffects.push(entry)\n }\n }\n}\n\n/**\n * React-compatible `useLayoutEffect` — runs synchronously after DOM mutations.\n */\nexport function useLayoutEffect(fn: () => (() => void) | void, deps?: unknown[]): void {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n const entry: EffectEntry = { fn, deps, cleanup: undefined }\n ctx.hooks.push(entry)\n ctx.pendingLayoutEffects.push(entry)\n } else {\n const entry = ctx.hooks[idx] as EffectEntry\n if (depsChanged(entry.deps, deps)) {\n entry.fn = fn\n entry.deps = deps\n ctx.pendingLayoutEffects.push(entry)\n }\n }\n}\n\n// ─── Memoization ─────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useMemo` — returns the cached value, recomputed when deps change.\n */\nexport function useMemo<T>(fn: () => T, deps: unknown[]): T {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n const value = fn()\n ctx.hooks.push({ value, deps })\n return value\n }\n\n const entry = ctx.hooks[idx] as { value: T; deps: unknown[] }\n if (depsChanged(entry.deps, deps)) {\n entry.value = fn()\n entry.deps = deps\n }\n return entry.value\n}\n\n/**\n * React-compatible `useCallback` — returns the cached function when deps haven't changed.\n */\nexport function useCallback<T extends (...args: never[]) => unknown>(fn: T, deps: unknown[]): T {\n return useMemo(() => fn, deps)\n}\n\n// ─── Refs ────────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `useRef` — returns `{ current }` persisted across re-renders.\n */\nexport function useRef<T>(initial?: T): { current: T | null } {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n const ref = { current: initial !== undefined ? (initial as T) : null }\n ctx.hooks.push(ref)\n }\n\n return ctx.hooks[idx] as { current: T | null }\n}\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\nexport { createContext, useContext }\n\n// ─── ID ──────────────────────────────────────────────────────────────────────\n\nlet _idCounter = 0\n\n/**\n * React-compatible `useId` — returns a stable unique string per hook call.\n */\nexport function useId(): string {\n const ctx = requireCtx()\n const idx = getHookIndex()\n\n if (ctx.hooks.length <= idx) {\n ctx.hooks.push(`:r${(_idCounter++).toString(36)}:`)\n }\n\n return ctx.hooks[idx] as string\n}\n\n// ─── Optimization ────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `memo` — wraps a component to skip re-render when props\n * are shallowly equal.\n */\nexport function memo<P extends Record<string, unknown>>(\n component: (props: P) => VNodeChild,\n areEqual?: (prevProps: P, nextProps: P) => boolean,\n): (props: P) => VNodeChild {\n const compare =\n areEqual ??\n ((a: P, b: P) => {\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n if (keysA.length !== keysB.length) return false\n for (const k of keysA) {\n if (!Object.is(a[k], b[k])) return false\n }\n return true\n })\n\n let prevProps: P | null = null\n let prevResult: VNodeChild = null\n\n return (props: P) => {\n if (prevProps !== null && compare(prevProps, props)) {\n return prevResult\n }\n prevProps = props\n prevResult = (component as (p: P) => VNodeChild)(props)\n return prevResult\n }\n}\n\n/**\n * React-compatible `useTransition` — no concurrent mode in Pyreon.\n */\nexport function useTransition(): [boolean, (fn: () => void) => void] {\n return [false, (fn) => fn()]\n}\n\n/**\n * React-compatible `useDeferredValue` — returns the value as-is.\n */\nexport function useDeferredValue<T>(value: T): T {\n return value\n}\n\n// ─── Imperative handle ───────────────────────────────────────────────────────\n\n/**\n * React-compatible `useImperativeHandle`.\n */\nexport function useImperativeHandle<T>(\n ref: { current: T | null } | null | undefined,\n init: () => T,\n deps?: unknown[],\n): void {\n useLayoutEffect(() => {\n if (ref) ref.current = init()\n return () => {\n if (ref) ref.current = null\n }\n }, deps)\n}\n\n// ─── Batching ────────────────────────────────────────────────────────────────\n\nexport { batch }\n\n// ─── Portals ─────────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `createPortal(children, target)`.\n */\nexport function createPortal(children: VNodeChild, target: Element): VNodeChild {\n return Portal({ target, children })\n}\n\n// ─── Suspense / lazy / ErrorBoundary ─────────────────────────────────────────\n\nexport { lazy } from '@pyreon/core'\nexport { ErrorBoundary, Suspense }\n\n// ─── forwardRef ─────────────────────────────────────────────────────────────\n\n/**\n * React-compatible `forwardRef` — pass-through in Pyreon.\n * Refs are regular props in Pyreon, so no wrapper is needed.\n * The render function receives (props, ref) — we merge ref into props.\n */\nexport function forwardRef<P extends Record<string, unknown>>(\n render: (props: P, ref: { current: unknown } | null) => VNodeChild,\n): (props: P & { ref?: { current: unknown } | null }) => VNodeChild {\n return (props: P & { ref?: { current: unknown } | null }) => {\n const { ref, ...rest } = props\n return render(rest as P, ref ?? null)\n }\n}\n\n// ─── cloneElement ───────────────────────────────────────────────────────────\n\n/**\n * React-compatible `cloneElement` — creates a new VNode with merged props.\n */\nexport function cloneElement(\n element: VNode,\n props?: Record<string, unknown>,\n ...children: VNodeChild[]\n): VNode {\n const mergedProps = { ...element.props, ...props }\n const mergedChildren = children.length > 0 ? children : element.children\n return h(element.type, mergedProps, ...mergedChildren)\n}\n\n// ─── Children utilities ─────────────────────────────────────────────────────\n\nfunction flattenChildren(children: VNodeChild | VNodeChild[]): VNodeChild[] {\n if (children == null) return []\n if (!Array.isArray(children)) return [children]\n const result: VNodeChild[] = []\n for (const child of children) {\n if (Array.isArray(child)) {\n result.push(...flattenChildren(child))\n } else {\n result.push(child)\n }\n }\n return result\n}\n\n/**\n * React-compatible `Children` utilities for working with VNode children.\n */\nexport const Children = {\n /**\n * Iterate over children, calling `fn` for each non-null child.\n */\n map<T>(children: VNodeChild | VNodeChild[], fn: (child: VNodeChild, index: number) => T): T[] {\n const flat = flattenChildren(children)\n const result: T[] = []\n for (let i = 0; i < flat.length; i++) {\n const child = flat[i]\n if (child == null || child === true || child === false) continue\n result.push(fn(child, i))\n }\n return result\n },\n\n /**\n * Call `fn` for each non-null child (no return value).\n */\n forEach(children: VNodeChild | VNodeChild[], fn: (child: VNodeChild, index: number) => void): void {\n const flat = flattenChildren(children)\n for (let i = 0; i < flat.length; i++) {\n const child = flat[i]\n if (child == null || child === true || child === false) continue\n fn(child, i)\n }\n },\n\n /**\n * Count non-null children.\n */\n count(children: VNodeChild | VNodeChild[]): number {\n const flat = flattenChildren(children)\n let count = 0\n for (const child of flat) {\n if (child != null && child !== true && child !== false) count++\n }\n return count\n },\n\n /**\n * Convert children to a flat array.\n */\n toArray(children: VNodeChild | VNodeChild[]): VNodeChild[] {\n const flat = flattenChildren(children)\n return flat.filter((child) => child != null && child !== true && child !== false)\n },\n\n /**\n * Assert and return the only child. Throws if not exactly one child.\n */\n only(children: VNodeChild | VNodeChild[]): VNodeChild {\n const arr = Children.toArray(children)\n if (arr.length !== 1) {\n throw new Error('[Pyreon] Children.only expected exactly one child')\n }\n return arr[0] as VNodeChild\n },\n}\n"],"mappings":"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"}
@@ -80,9 +80,15 @@ function jsx(type, props, key) {
80
80
  children
81
81
  } : propsWithKey);
82
82
  const childArray = children === void 0 ? [] : Array.isArray(children) ? children : [children];
83
- if (typeof type === "string" && propsWithKey.className !== void 0) {
84
- propsWithKey.class = propsWithKey.className;
85
- delete propsWithKey.className;
83
+ if (typeof type === "string") {
84
+ if (propsWithKey.className !== void 0) {
85
+ propsWithKey.class = propsWithKey.className;
86
+ delete propsWithKey.className;
87
+ }
88
+ if (propsWithKey.htmlFor !== void 0) {
89
+ propsWithKey.for = propsWithKey.htmlFor;
90
+ delete propsWithKey.htmlFor;
91
+ }
86
92
  }
87
93
  return h(type, propsWithKey, ...childArray);
88
94
  }
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-runtime.js","names":[],"sources":["../src/jsx-runtime.ts"],"sourcesContent":["/**\n * Compat JSX runtime for React compatibility mode.\n *\n * When `jsxImportSource` is set to `@pyreon/react-compat` (via the vite plugin's\n * `compat: \"react\"` option), OXC rewrites JSX to import from this file:\n * <div className=\"x\" /> → jsx(\"div\", { className: \"x\" })\n *\n * For component VNodes, we wrap the component function so it returns a reactive\n * accessor — enabling React-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(reactComponent: Function): ComponentFn {\n let wrapped = _wrapperCache.get(reactComponent)\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 = (reactComponent 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(reactComponent, 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 React-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 // Map className class for React compat\n if (typeof type === 'string' && propsWithKey.className !== undefined) {\n propsWithKey.class = propsWithKey.className\n delete propsWithKey.className\n }\n\n return h(type, propsWithKey, ...(childArray as VNodeChild[]))\n}\n\nexport const jsxs = jsx\nexport const jsxDEV = jsx\n"],"mappings":";;;;AAqCA,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,gBAAuC;CAClE,IAAI,UAAU,cAAc,IAAI,eAAe;AAC/C,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,eAA+B,MAAM;GACrD,MAAM,gBAAgB,IAAI;GAC1B,MAAM,UAAU,IAAI;AACpB,cAAW;AAEX,oBAAiB,cAAc;AAC/B,mBAAgB,KAAK,QAAQ;AAE7B,UAAO;;;AAIX,eAAc,IAAI,gBAAgB,QAAQ;AAC1C,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;CAInC,MAAM,aAAa,aAAa,SAAY,EAAE,GAAG,MAAM,QAAQ,SAAS,GAAG,WAAW,CAAC,SAAS;AAGhG,KAAI,OAAO,SAAS,YAAY,aAAa,cAAc,QAAW;AACpE,eAAa,QAAQ,aAAa;AAClC,SAAO,aAAa;;AAGtB,QAAO,EAAE,MAAM,cAAc,GAAI,WAA4B;;AAG/D,MAAa,OAAO"}
1
+ {"version":3,"file":"jsx-runtime.js","names":[],"sources":["../src/jsx-runtime.ts"],"sourcesContent":["/**\n * Compat JSX runtime for React compatibility mode.\n *\n * When `jsxImportSource` is set to `@pyreon/react-compat` (via the vite plugin's\n * `compat: \"react\"` option), OXC rewrites JSX to import from this file:\n * <div className=\"x\" /> → jsx(\"div\", { className: \"x\" })\n *\n * For component VNodes, we wrap the component function so it returns a reactive\n * accessor — enabling React-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(reactComponent: Function): ComponentFn {\n let wrapped = _wrapperCache.get(reactComponent)\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 = (reactComponent 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(reactComponent, 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 React-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 // Map React-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\n return h(type, propsWithKey, ...(childArray as VNodeChild[]))\n}\n\nexport const jsxs = jsx\nexport const jsxDEV = jsx\n"],"mappings":";;;;AAqCA,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,gBAAuC;CAClE,IAAI,UAAU,cAAc,IAAI,eAAe;AAC/C,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,eAA+B,MAAM;GACrD,MAAM,gBAAgB,IAAI;GAC1B,MAAM,UAAU,IAAI;AACpB,cAAW;AAEX,oBAAiB,cAAc;AAC/B,mBAAgB,KAAK,QAAQ;AAE7B,UAAO;;;AAIX,eAAc,IAAI,gBAAgB,QAAQ;AAC1C,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;CAInC,MAAM,aAAa,aAAa,SAAY,EAAE,GAAG,MAAM,QAAQ,SAAS,GAAG,WAAW,CAAC,SAAS;AAGhG,KAAI,OAAO,SAAS,UAAU;AAC5B,MAAI,aAAa,cAAc,QAAW;AACxC,gBAAa,QAAQ,aAAa;AAClC,UAAO,aAAa;;AAEtB,MAAI,aAAa,YAAY,QAAW;AACtC,gBAAa,MAAM,aAAa;AAChC,UAAO,aAAa;;;AAIxB,QAAO,EAAE,MAAM,cAAc,GAAI,WAA4B;;AAG/D,MAAa,OAAO"}
@@ -1,4 +1,4 @@
1
- import { ErrorBoundary, Fragment, Props, Suspense, VNode as ReactNode, VNodeChild, VNodeChild as VNodeChild$1, createContext, h, h as createElement, lazy, useContext } from "@pyreon/core";
1
+ import { ErrorBoundary, Fragment, Props, Suspense, VNode, VNode as ReactNode, VNodeChild, VNodeChild as VNodeChild$1, createContext, h, h as createElement, lazy, useContext } from "@pyreon/core";
2
2
  import { batch } from "@pyreon/reactivity";
3
3
 
4
4
  //#region src/index.d.ts
@@ -61,6 +61,47 @@ declare function useImperativeHandle<T>(ref: {
61
61
  * React-compatible `createPortal(children, target)`.
62
62
  */
63
63
  declare function createPortal(children: VNodeChild$1, target: Element): VNodeChild$1;
64
+ /**
65
+ * React-compatible `forwardRef` — pass-through in Pyreon.
66
+ * Refs are regular props in Pyreon, so no wrapper is needed.
67
+ * The render function receives (props, ref) — we merge ref into props.
68
+ */
69
+ declare function forwardRef<P extends Record<string, unknown>>(render: (props: P, ref: {
70
+ current: unknown;
71
+ } | null) => VNodeChild$1): (props: P & {
72
+ ref?: {
73
+ current: unknown;
74
+ } | null;
75
+ }) => VNodeChild$1;
76
+ /**
77
+ * React-compatible `cloneElement` — creates a new VNode with merged props.
78
+ */
79
+ declare function cloneElement(element: VNode, props?: Record<string, unknown>, ...children: VNodeChild$1[]): VNode;
80
+ /**
81
+ * React-compatible `Children` utilities for working with VNode children.
82
+ */
83
+ declare const Children: {
84
+ /**
85
+ * Iterate over children, calling `fn` for each non-null child.
86
+ */
87
+ map<T>(children: VNodeChild$1 | VNodeChild$1[], fn: (child: VNodeChild$1, index: number) => T): T[];
88
+ /**
89
+ * Call `fn` for each non-null child (no return value).
90
+ */
91
+ forEach(children: VNodeChild$1 | VNodeChild$1[], fn: (child: VNodeChild$1, index: number) => void): void;
92
+ /**
93
+ * Count non-null children.
94
+ */
95
+ count(children: VNodeChild$1 | VNodeChild$1[]): number;
96
+ /**
97
+ * Convert children to a flat array.
98
+ */
99
+ toArray(children: VNodeChild$1 | VNodeChild$1[]): VNodeChild$1[];
100
+ /**
101
+ * Assert and return the only child. Throws if not exactly one child.
102
+ */
103
+ only(children: VNodeChild$1 | VNodeChild$1[]): VNodeChild$1;
104
+ };
64
105
  //#endregion
65
- export { ErrorBoundary, Fragment, type Props, type ReactNode, Suspense, type VNodeChild, batch, createContext, createElement, createPortal, h, lazy, memo, useCallback, useContext, useDeferredValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useTransition };
106
+ export { Children, ErrorBoundary, Fragment, type Props, type ReactNode, Suspense, type VNodeChild, batch, cloneElement, createContext, createElement, createPortal, forwardRef, h, lazy, memo, useCallback, useContext, useDeferredValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useTransition };
66
107
  //# sourceMappingURL=index2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;;;;;;iBA8CgB,QAAA,GAAA,CAAY,OAAA,EAAS,CAAA,UAAW,CAAA,KAAM,CAAA,GAAI,CAAA,EAAG,CAAA,KAAM,IAAA,EAAM,CAAA,KAAM,CAAA;;;;iBAyB/D,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;AAHhB;;;;AAAA,iBA6BgB,SAAA,CAAU,EAAA,6BAA+B,IAAA;;;;iBAsBzC,eAAA,CAAgB,EAAA,6BAA+B,IAAA;;;;iBAuB/C,OAAA,GAAA,CAAW,EAAA,QAAU,CAAA,EAAG,IAAA,cAAkB,CAAA;;;;iBAqB1C,WAAA,eAA0B,IAAA,sBAAA,CAA2B,EAAA,EAAI,CAAA,EAAG,IAAA,cAAkB,CAAA;;;;iBAS9E,MAAA,GAAA,CAAU,OAAA,GAAU,CAAA;EAAM,OAAA,EAAS,CAAA;AAAA;;;;iBAuBnC,KAAA,CAAA;AAlGhB;;;;AAAA,iBAmHgB,IAAA,WAAe,MAAA,kBAAA,CAC7B,SAAA,GAAY,KAAA,EAAO,CAAA,KAAM,YAAA,EACzB,QAAA,IAAY,SAAA,EAAW,CAAA,EAAG,SAAA,EAAW,CAAA,gBACnC,KAAA,EAAO,CAAA,KAAM,YAAA;AAhGjB;;;AAAA,iBA6HgB,aAAA,CAAA,cAA4B,EAAA;;AAtG5C;;iBA6GgB,gBAAA,GAAA,CAAoB,KAAA,EAAO,CAAA,GAAI,CAAA;;;;iBAS/B,mBAAA,GAAA,CACd,GAAA;EAAO,OAAA,EAAS,CAAA;AAAA,sBAChB,IAAA,QAAY,CAAA,EACZ,IAAA;;AApGF;;iBAuHgB,YAAA,CAAa,QAAA,EAAU,YAAA,EAAY,MAAA,EAAQ,OAAA,GAAU,YAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/index.ts"],"mappings":";;;;;;;;iBA8CgB,QAAA,GAAA,CAAY,OAAA,EAAS,CAAA,UAAW,CAAA,KAAM,CAAA,GAAI,CAAA,EAAG,CAAA,KAAM,IAAA,EAAM,CAAA,KAAM,CAAA;;;;iBAyB/D,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;AAHhB;;;;AAAA,iBA6BgB,SAAA,CAAU,EAAA,6BAA+B,IAAA;;;;iBAsBzC,eAAA,CAAgB,EAAA,6BAA+B,IAAA;;;;iBAuB/C,OAAA,GAAA,CAAW,EAAA,QAAU,CAAA,EAAG,IAAA,cAAkB,CAAA;;;;iBAqB1C,WAAA,eAA0B,IAAA,sBAAA,CAA2B,EAAA,EAAI,CAAA,EAAG,IAAA,cAAkB,CAAA;;;;iBAS9E,MAAA,GAAA,CAAU,OAAA,GAAU,CAAA;EAAM,OAAA,EAAS,CAAA;AAAA;;;;iBAuBnC,KAAA,CAAA;AAlGhB;;;;AAAA,iBAmHgB,IAAA,WAAe,MAAA,kBAAA,CAC7B,SAAA,GAAY,KAAA,EAAO,CAAA,KAAM,YAAA,EACzB,QAAA,IAAY,SAAA,EAAW,CAAA,EAAG,SAAA,EAAW,CAAA,gBACnC,KAAA,EAAO,CAAA,KAAM,YAAA;AAhGjB;;;AAAA,iBA6HgB,aAAA,CAAA,cAA4B,EAAA;;AAtG5C;;iBA6GgB,gBAAA,GAAA,CAAoB,KAAA,EAAO,CAAA,GAAI,CAAA;;;;iBAS/B,mBAAA,GAAA,CACd,GAAA;EAAO,OAAA,EAAS,CAAA;AAAA,sBAChB,IAAA,QAAY,CAAA,EACZ,IAAA;;AApGF;;iBAuHgB,YAAA,CAAa,QAAA,EAAU,YAAA,EAAY,MAAA,EAAQ,OAAA,GAAU,YAAA;;;;;;iBAgBrD,UAAA,WAAqB,MAAA,kBAAA,CACnC,MAAA,GAAS,KAAA,EAAO,CAAA,EAAG,GAAA;EAAO,OAAA;AAAA,aAA8B,YAAA,IACtD,KAAA,EAAO,CAAA;EAAM,GAAA;IAAQ,OAAA;EAAA;AAAA,MAAgC,YAAA;;;;iBAYzC,YAAA,CACd,OAAA,EAAS,KAAA,EACT,KAAA,GAAQ,MAAA,sBACL,QAAA,EAAU,YAAA,KACZ,KAAA;;;AAzHH;cAkJa,QAAA;;;;SAIN,QAAA,EAAY,YAAA,GAAa,YAAA,IAAY,EAAA,GAAO,KAAA,EAAO,YAAA,EAAY,KAAA,aAAkB,CAAA,GAAI,CAAA;EArIxE;;;oBAmJA,YAAA,GAAa,YAAA,IAAY,EAAA,GAAO,KAAA,EAAO,YAAA,EAAY,KAAA;EAlJ5C;;;kBA8JT,YAAA,GAAa,YAAA;EA5Jd;;;oBAwKG,YAAA,GAAa,YAAA,KAAe,YAAA;EA3KjB;;;iBAmLd,YAAA,GAAa,YAAA,KAAe,YAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-runtime2.d.ts","names":[],"sources":["../../../src/jsx-runtime.ts"],"mappings":";;;iBAyIgB,GAAA,CACd,IAAA,WAAe,WAAA,WACf,KAAA,EAAO,KAAA;EAAU,QAAA,GAAW,UAAA,GAAa,UAAA;AAAA,GACzC,GAAA,4BACC,KAAA;AAAA,cAuBU,IAAA,SAAI,GAAA"}
1
+ {"version":3,"file":"jsx-runtime2.d.ts","names":[],"sources":["../../../src/jsx-runtime.ts"],"mappings":";;;iBAyIgB,GAAA,CACd,IAAA,WAAe,WAAA,WACf,KAAA,EAAO,KAAA;EAAU,QAAA,GAAW,UAAA,GAAa,UAAA;AAAA,GACzC,GAAA,4BACC,KAAA;AAAA,cA6BU,IAAA,SAAI,GAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/react-compat",
3
- "version": "0.12.8",
3
+ "version": "0.12.10",
4
4
  "description": "React-compatible API shim for Pyreon — write React-style hooks that run on Pyreon's reactive engine",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/react-compat#readme",
6
6
  "bugs": {
@@ -57,9 +57,9 @@
57
57
  "prepublishOnly": "bun run build"
58
58
  },
59
59
  "dependencies": {
60
- "@pyreon/core": "^0.12.8",
61
- "@pyreon/reactivity": "^0.12.8",
62
- "@pyreon/runtime-dom": "^0.12.8"
60
+ "@pyreon/core": "^0.12.10",
61
+ "@pyreon/reactivity": "^0.12.10",
62
+ "@pyreon/runtime-dom": "^0.12.10"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@happy-dom/global-registrator": "^20.8.9",
package/src/index.ts CHANGED
@@ -15,8 +15,8 @@
15
15
  export type { Props, VNode as ReactNode, VNodeChild } from '@pyreon/core'
16
16
  export { Fragment, h as createElement, h } from '@pyreon/core'
17
17
 
18
- import type { VNodeChild } from '@pyreon/core'
19
- import { createContext, ErrorBoundary, Portal, Suspense, useContext } from '@pyreon/core'
18
+ import type { VNode, VNodeChild } from '@pyreon/core'
19
+ import { createContext, ErrorBoundary, h, Portal, Suspense, useContext } from '@pyreon/core'
20
20
  import { batch } from '@pyreon/reactivity'
21
21
  import type { EffectEntry } from './jsx-runtime'
22
22
  import { getCurrentCtx, getHookIndex } from './jsx-runtime'
@@ -291,3 +291,112 @@ export function createPortal(children: VNodeChild, target: Element): VNodeChild
291
291
 
292
292
  export { lazy } from '@pyreon/core'
293
293
  export { ErrorBoundary, Suspense }
294
+
295
+ // ─── forwardRef ─────────────────────────────────────────────────────────────
296
+
297
+ /**
298
+ * React-compatible `forwardRef` — pass-through in Pyreon.
299
+ * Refs are regular props in Pyreon, so no wrapper is needed.
300
+ * The render function receives (props, ref) — we merge ref into props.
301
+ */
302
+ export function forwardRef<P extends Record<string, unknown>>(
303
+ render: (props: P, ref: { current: unknown } | null) => VNodeChild,
304
+ ): (props: P & { ref?: { current: unknown } | null }) => VNodeChild {
305
+ return (props: P & { ref?: { current: unknown } | null }) => {
306
+ const { ref, ...rest } = props
307
+ return render(rest as P, ref ?? null)
308
+ }
309
+ }
310
+
311
+ // ─── cloneElement ───────────────────────────────────────────────────────────
312
+
313
+ /**
314
+ * React-compatible `cloneElement` — creates a new VNode with merged props.
315
+ */
316
+ export function cloneElement(
317
+ element: VNode,
318
+ props?: Record<string, unknown>,
319
+ ...children: VNodeChild[]
320
+ ): VNode {
321
+ const mergedProps = { ...element.props, ...props }
322
+ const mergedChildren = children.length > 0 ? children : element.children
323
+ return h(element.type, mergedProps, ...mergedChildren)
324
+ }
325
+
326
+ // ─── Children utilities ─────────────────────────────────────────────────────
327
+
328
+ function flattenChildren(children: VNodeChild | VNodeChild[]): VNodeChild[] {
329
+ if (children == null) return []
330
+ if (!Array.isArray(children)) return [children]
331
+ const result: VNodeChild[] = []
332
+ for (const child of children) {
333
+ if (Array.isArray(child)) {
334
+ result.push(...flattenChildren(child))
335
+ } else {
336
+ result.push(child)
337
+ }
338
+ }
339
+ return result
340
+ }
341
+
342
+ /**
343
+ * React-compatible `Children` utilities for working with VNode children.
344
+ */
345
+ export const Children = {
346
+ /**
347
+ * Iterate over children, calling `fn` for each non-null child.
348
+ */
349
+ map<T>(children: VNodeChild | VNodeChild[], fn: (child: VNodeChild, index: number) => T): T[] {
350
+ const flat = flattenChildren(children)
351
+ const result: T[] = []
352
+ for (let i = 0; i < flat.length; i++) {
353
+ const child = flat[i]
354
+ if (child == null || child === true || child === false) continue
355
+ result.push(fn(child, i))
356
+ }
357
+ return result
358
+ },
359
+
360
+ /**
361
+ * Call `fn` for each non-null child (no return value).
362
+ */
363
+ forEach(children: VNodeChild | VNodeChild[], fn: (child: VNodeChild, index: number) => void): void {
364
+ const flat = flattenChildren(children)
365
+ for (let i = 0; i < flat.length; i++) {
366
+ const child = flat[i]
367
+ if (child == null || child === true || child === false) continue
368
+ fn(child, i)
369
+ }
370
+ },
371
+
372
+ /**
373
+ * Count non-null children.
374
+ */
375
+ count(children: VNodeChild | VNodeChild[]): number {
376
+ const flat = flattenChildren(children)
377
+ let count = 0
378
+ for (const child of flat) {
379
+ if (child != null && child !== true && child !== false) count++
380
+ }
381
+ return count
382
+ },
383
+
384
+ /**
385
+ * Convert children to a flat array.
386
+ */
387
+ toArray(children: VNodeChild | VNodeChild[]): VNodeChild[] {
388
+ const flat = flattenChildren(children)
389
+ return flat.filter((child) => child != null && child !== true && child !== false)
390
+ },
391
+
392
+ /**
393
+ * Assert and return the only child. Throws if not exactly one child.
394
+ */
395
+ only(children: VNodeChild | VNodeChild[]): VNodeChild {
396
+ const arr = Children.toArray(children)
397
+ if (arr.length !== 1) {
398
+ throw new Error('[Pyreon] Children.only expected exactly one child')
399
+ }
400
+ return arr[0] as VNodeChild
401
+ },
402
+ }
@@ -153,10 +153,16 @@ export function jsx(
153
153
  // DOM element or symbol (Fragment): children go in vnode.children
154
154
  const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]
155
155
 
156
- // Map className class for React compat
157
- if (typeof type === 'string' && propsWithKey.className !== undefined) {
158
- propsWithKey.class = propsWithKey.className
159
- delete propsWithKey.className
156
+ // Map React-style attributes to standard HTML attributes
157
+ if (typeof type === 'string') {
158
+ if (propsWithKey.className !== undefined) {
159
+ propsWithKey.class = propsWithKey.className
160
+ delete propsWithKey.className
161
+ }
162
+ if (propsWithKey.htmlFor !== undefined) {
163
+ propsWithKey.for = propsWithKey.htmlFor
164
+ delete propsWithKey.htmlFor
165
+ }
160
166
  }
161
167
 
162
168
  return h(type, propsWithKey, ...(childArray as VNodeChild[]))
@@ -0,0 +1,397 @@
1
+ import { h } from '@pyreon/core'
2
+ import { mount } from '@pyreon/runtime-dom'
3
+ import {
4
+ Children,
5
+ cloneElement,
6
+ forwardRef,
7
+ memo,
8
+ useEffect,
9
+ useMemo,
10
+ useReducer,
11
+ useRef,
12
+ useState,
13
+ } from '../index'
14
+ import type { RenderContext } from '../jsx-runtime'
15
+ import { beginRender, endRender, jsx } from '../jsx-runtime'
16
+
17
+ function container(): HTMLElement {
18
+ const el = document.createElement('div')
19
+ document.body.appendChild(el)
20
+ return el
21
+ }
22
+
23
+ /** Creates a RenderContext for testing hooks outside of full render cycle */
24
+ function createHookRunner() {
25
+ const ctx: RenderContext = {
26
+ hooks: [],
27
+ scheduleRerender: () => {},
28
+ pendingEffects: [],
29
+ pendingLayoutEffects: [],
30
+ unmounted: false,
31
+ }
32
+ return {
33
+ ctx,
34
+ run<T>(fn: () => T): T {
35
+ beginRender(ctx)
36
+ const result = fn()
37
+ endRender()
38
+ return result
39
+ },
40
+ }
41
+ }
42
+
43
+ function withHookCtx<T>(fn: () => T): T {
44
+ const runner = createHookRunner()
45
+ return runner.run(fn)
46
+ }
47
+
48
+ // ─── useState ────────────────────────────────────────────────────────────────
49
+
50
+ describe('useState', () => {
51
+ test('returns [value, setter]', () => {
52
+ const [count, setCount] = withHookCtx(() => useState(0))
53
+ expect(count).toBe(0)
54
+ expect(typeof setCount).toBe('function')
55
+ })
56
+
57
+ test('setter with value updates signal', () => {
58
+ const runner = createHookRunner()
59
+ const [, setCount] = runner.run(() => useState(0))
60
+ setCount(42)
61
+ const [count2] = runner.run(() => useState(0))
62
+ expect(count2).toBe(42)
63
+ })
64
+
65
+ test('setter with function updates based on current', () => {
66
+ const runner = createHookRunner()
67
+ const [, setCount] = runner.run(() => useState(10))
68
+ setCount((prev) => prev * 2)
69
+ const [count2] = runner.run(() => useState(10))
70
+ expect(count2).toBe(20)
71
+ })
72
+ })
73
+
74
+ // ─── useEffect ───────────────────────────────────────────────────────────────
75
+
76
+ describe('useEffect', () => {
77
+ test('runs immediately without deps', async () => {
78
+ const el = container()
79
+ let effectRuns = 0
80
+
81
+ const Comp = () => {
82
+ useEffect(() => {
83
+ effectRuns++
84
+ })
85
+ return h('div', null, 'test')
86
+ }
87
+
88
+ mount(jsx(Comp, {}), el)
89
+ await new Promise<void>((r) => queueMicrotask(r))
90
+ expect(effectRuns).toBeGreaterThanOrEqual(1)
91
+ })
92
+
93
+ test('runs cleanup on re-run', async () => {
94
+ const el = container()
95
+ let cleanups = 0
96
+ let triggerSet: (v: number | ((p: number) => number)) => void = () => {}
97
+
98
+ const Comp = () => {
99
+ const [count, setCount] = useState(0)
100
+ triggerSet = setCount
101
+ useEffect(() => {
102
+ return () => {
103
+ cleanups++
104
+ }
105
+ }, [count])
106
+ return h('div', null, String(count))
107
+ }
108
+
109
+ mount(jsx(Comp, {}), el)
110
+ await new Promise<void>((r) => queueMicrotask(r))
111
+ expect(cleanups).toBe(0)
112
+
113
+ triggerSet((p) => p + 1)
114
+ await new Promise<void>((r) => queueMicrotask(r))
115
+ await new Promise<void>((r) => queueMicrotask(r))
116
+ await new Promise<void>((r) => queueMicrotask(r))
117
+ expect(cleanups).toBe(1)
118
+ })
119
+
120
+ test('empty deps [] = runs once', async () => {
121
+ const el = container()
122
+ let effectRuns = 0
123
+ let triggerSet: (v: number) => void = () => {}
124
+
125
+ const Comp = () => {
126
+ const [count, setCount] = useState(0)
127
+ triggerSet = setCount
128
+ useEffect(() => {
129
+ effectRuns++
130
+ }, [])
131
+ return h('div', null, String(count))
132
+ }
133
+
134
+ mount(jsx(Comp, {}), el)
135
+ await new Promise<void>((r) => queueMicrotask(r))
136
+ expect(effectRuns).toBe(1)
137
+
138
+ // Re-render should NOT re-run the effect
139
+ triggerSet(1)
140
+ await new Promise<void>((r) => queueMicrotask(r))
141
+ await new Promise<void>((r) => queueMicrotask(r))
142
+ expect(effectRuns).toBe(1)
143
+ })
144
+ })
145
+
146
+ // ─── useMemo ─────────────────────────────────────────────────────────────────
147
+
148
+ describe('useMemo', () => {
149
+ test('returns computed value', () => {
150
+ const value = withHookCtx(() => useMemo(() => 6 * 7, []))
151
+ expect(value).toBe(42)
152
+ })
153
+
154
+ test('recalculates when dep changes', () => {
155
+ const runner = createHookRunner()
156
+ const v1 = runner.run(() => useMemo(() => 'a', ['x']))
157
+ expect(v1).toBe('a')
158
+
159
+ // Same deps — cached
160
+ const v2 = runner.run(() => useMemo(() => 'b', ['x']))
161
+ expect(v2).toBe('a')
162
+
163
+ // Different deps — recompute
164
+ const v3 = runner.run(() => useMemo(() => 'c', ['y']))
165
+ expect(v3).toBe('c')
166
+ })
167
+ })
168
+
169
+ // ─── useRef ──────────────────────────────────────────────────────────────────
170
+
171
+ describe('useRef', () => {
172
+ test('initial current value', () => {
173
+ const ref = withHookCtx(() => useRef(99))
174
+ expect(ref.current).toBe(99)
175
+ })
176
+
177
+ test('mutable current', () => {
178
+ const ref = withHookCtx(() => useRef(0))
179
+ ref.current = 123
180
+ expect(ref.current).toBe(123)
181
+ })
182
+ })
183
+
184
+ // ─── useReducer ──────────────────────────────────────────────────────────────
185
+
186
+ describe('useReducer', () => {
187
+ test('dispatch actions update state', () => {
188
+ const runner = createHookRunner()
189
+ type Action = { type: 'add'; payload: number } | { type: 'reset' }
190
+ const reducer = (state: number, action: Action): number => {
191
+ switch (action.type) {
192
+ case 'add':
193
+ return state + action.payload
194
+ case 'reset':
195
+ return 0
196
+ default:
197
+ return state
198
+ }
199
+ }
200
+
201
+ const [s0, dispatch] = runner.run(() => useReducer(reducer, 10))
202
+ expect(s0).toBe(10)
203
+
204
+ dispatch({ type: 'add', payload: 5 })
205
+ const [s1] = runner.run(() => useReducer(reducer, 10))
206
+ expect(s1).toBe(15)
207
+
208
+ dispatch({ type: 'reset' })
209
+ const [s2] = runner.run(() => useReducer(reducer, 10))
210
+ expect(s2).toBe(0)
211
+ })
212
+ })
213
+
214
+ // ─── memo ────────────────────────────────────────────────────────────────────
215
+
216
+ describe('memo', () => {
217
+ test('returns same component (memoized)', () => {
218
+ let renders = 0
219
+ const Inner = (props: { x: number }) => {
220
+ renders++
221
+ return h('span', null, String(props.x))
222
+ }
223
+ const Memoized = memo(Inner)
224
+
225
+ Memoized({ x: 1 })
226
+ expect(renders).toBe(1)
227
+
228
+ Memoized({ x: 1 })
229
+ expect(renders).toBe(1) // same props — skipped
230
+
231
+ Memoized({ x: 2 })
232
+ expect(renders).toBe(2) // different props — re-rendered
233
+ })
234
+ })
235
+
236
+ // ─── forwardRef ──────────────────────────────────────────────────────────────
237
+
238
+ describe('forwardRef', () => {
239
+ test('passes ref through to render function', () => {
240
+ const ref = { current: null as HTMLDivElement | null }
241
+ let receivedRef: { current: unknown } | null = null
242
+
243
+ const FancyInput = forwardRef<{ label: string }>((props, fwdRef) => {
244
+ receivedRef = fwdRef
245
+ return h('div', null, props.label)
246
+ })
247
+
248
+ FancyInput({ label: 'test', ref })
249
+ expect(receivedRef).toBe(ref)
250
+ })
251
+
252
+ test('ref defaults to null when not provided', () => {
253
+ let receivedRef: { current: unknown } | null = 'not-called' as unknown as null
254
+
255
+ const Comp = forwardRef<Record<string, unknown>>((_props, fwdRef) => {
256
+ receivedRef = fwdRef
257
+ return h('div', null, 'no-ref')
258
+ })
259
+
260
+ Comp({})
261
+ expect(receivedRef).toBeNull()
262
+ })
263
+ })
264
+
265
+ // ─── Children utilities ──────────────────────────────────────────────────────
266
+
267
+ describe('Children utilities', () => {
268
+ test('Children.map iterates VNode children', () => {
269
+ const children = [h('span', null, 'a'), h('span', null, 'b'), h('span', null, 'c')]
270
+ const mapped = Children.map(children, (child, index) => ({ child, index }))
271
+ expect(mapped).toHaveLength(3)
272
+ expect(mapped[0]?.index).toBe(0)
273
+ expect(mapped[1]?.index).toBe(1)
274
+ expect(mapped[2]?.index).toBe(2)
275
+ })
276
+
277
+ test('Children.count returns count', () => {
278
+ const children = [h('span', null, 'a'), h('span', null, 'b')]
279
+ expect(Children.count(children)).toBe(2)
280
+ })
281
+
282
+ test('Children.count skips null/undefined/boolean', () => {
283
+ const children = [h('span', null, 'a'), null, undefined, true, false, h('span', null, 'b')]
284
+ expect(Children.count(children)).toBe(2)
285
+ })
286
+
287
+ test('Children.toArray converts to flat array', () => {
288
+ const children = [h('span', null, 'a'), [h('span', null, 'b'), h('span', null, 'c')]]
289
+ const arr = Children.toArray(children)
290
+ expect(arr).toHaveLength(3)
291
+ })
292
+
293
+ test('Children.toArray filters out null/undefined/boolean', () => {
294
+ const children = [null, h('span', null, 'a'), undefined, false, true]
295
+ const arr = Children.toArray(children)
296
+ expect(arr).toHaveLength(1)
297
+ })
298
+
299
+ test('Children.only returns single child', () => {
300
+ const child = h('span', null, 'only')
301
+ const result = Children.only([child])
302
+ expect(result).toBe(child)
303
+ })
304
+
305
+ test('Children.only throws with multiple children', () => {
306
+ const children = [h('span', null, 'a'), h('span', null, 'b')]
307
+ expect(() => Children.only(children)).toThrow('exactly one child')
308
+ })
309
+
310
+ test('Children.only throws with no children', () => {
311
+ expect(() => Children.only([])).toThrow('exactly one child')
312
+ })
313
+
314
+ test('Children.forEach iterates without return', () => {
315
+ const children = [h('span', null, 'a'), h('span', null, 'b')]
316
+ const indices: number[] = []
317
+ Children.forEach(children, (_child, index) => {
318
+ indices.push(index)
319
+ })
320
+ expect(indices).toEqual([0, 1])
321
+ })
322
+
323
+ test('Children.map with single child (not array)', () => {
324
+ const child = h('span', null, 'solo')
325
+ const mapped = Children.map(child, (_c, i) => i)
326
+ expect(mapped).toEqual([0])
327
+ })
328
+
329
+ test('Children.count with single child', () => {
330
+ expect(Children.count(h('span', null, 'x'))).toBe(1)
331
+ })
332
+
333
+ test('Children.count with null', () => {
334
+ expect(Children.count(null)).toBe(0)
335
+ })
336
+ })
337
+
338
+ // ─── cloneElement ────────────────────────────────────────────────────────────
339
+
340
+ describe('cloneElement', () => {
341
+ test('clones element with merged props', () => {
342
+ const original = h('div', { id: 'original', class: 'a' }, 'hello')
343
+ const cloned = cloneElement(original, { class: 'b', 'data-new': true })
344
+ expect(cloned.type).toBe('div')
345
+ expect(cloned.props.id).toBe('original')
346
+ expect(cloned.props.class).toBe('b')
347
+ expect(cloned.props['data-new']).toBe(true)
348
+ })
349
+
350
+ test('clones element preserving children when none provided', () => {
351
+ const original = h('div', null, 'child')
352
+ const cloned = cloneElement(original)
353
+ expect(cloned.children).toHaveLength(1)
354
+ expect(cloned.children[0]).toBe('child')
355
+ })
356
+
357
+ test('clones element with new children', () => {
358
+ const original = h('div', null, 'old')
359
+ const cloned = cloneElement(original, {}, 'new1', 'new2')
360
+ expect(cloned.children).toHaveLength(2)
361
+ expect(cloned.children[0]).toBe('new1')
362
+ expect(cloned.children[1]).toBe('new2')
363
+ })
364
+ })
365
+
366
+ // ─── JSX runtime attribute mapping ──────────────────────────────────────────
367
+
368
+ describe('jsx-runtime attribute mapping', () => {
369
+ test('className is mapped to class', () => {
370
+ const vnode = jsx('div', { className: 'my-class', children: 'text' })
371
+ expect(vnode.props.class).toBe('my-class')
372
+ expect(vnode.props.className).toBeUndefined()
373
+ })
374
+
375
+ test('htmlFor is mapped to for', () => {
376
+ const vnode = jsx('label', { htmlFor: 'input-id', children: 'Label' })
377
+ expect(vnode.props.for).toBe('input-id')
378
+ expect(vnode.props.htmlFor).toBeUndefined()
379
+ })
380
+
381
+ test('className and htmlFor on same element', () => {
382
+ const vnode = jsx('label', { className: 'label-class', htmlFor: 'name', children: 'Name' })
383
+ expect(vnode.props.class).toBe('label-class')
384
+ expect(vnode.props.for).toBe('name')
385
+ expect(vnode.props.className).toBeUndefined()
386
+ expect(vnode.props.htmlFor).toBeUndefined()
387
+ })
388
+
389
+ test('className/htmlFor not mapped on component types', () => {
390
+ const MyComp = (props: { className?: string; htmlFor?: string }) =>
391
+ h('div', null, props.className ?? '')
392
+ const vnode = jsx(MyComp, { className: 'keep', htmlFor: 'keep' })
393
+ // Component props should pass through to the wrapper — no rename
394
+ expect(vnode.type).not.toBe(MyComp) // wrapped by compat runtime
395
+ expect(typeof vnode.type).toBe('function')
396
+ })
397
+ })