@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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/analysis/jsx-runtime.js.html +1 -1
- package/lib/index.js +69 -2
- package/lib/index.js.map +1 -1
- package/lib/jsx-runtime.js +9 -3
- package/lib/jsx-runtime.js.map +1 -1
- package/lib/types/index.d.ts +43 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/jsx-runtime.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +111 -2
- package/src/jsx-runtime.ts +10 -4
- package/src/tests/compat-integration.test.tsx +397 -0
|
@@ -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":"
|
|
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":"
|
|
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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"}
|
package/lib/jsx-runtime.js
CHANGED
|
@@ -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"
|
|
84
|
-
propsWithKey.
|
|
85
|
-
|
|
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
|
}
|
package/lib/jsx-runtime.js.map
CHANGED
|
@@ -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
|
|
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"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
61
|
-
"@pyreon/reactivity": "^0.12.
|
|
62
|
-
"@pyreon/runtime-dom": "^0.12.
|
|
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
|
+
}
|
package/src/jsx-runtime.ts
CHANGED
|
@@ -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
|
|
157
|
-
if (typeof type === 'string'
|
|
158
|
-
propsWithKey.
|
|
159
|
-
|
|
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
|
+
})
|