@pyreon/react-compat 0.11.4 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,12 +14,12 @@ bun add @pyreon/react-compat
14
14
  // Replace:
15
15
  // import { useState, useEffect } from "react"
16
16
  // With:
17
- import { useState, useEffect } from "@pyreon/react-compat"
17
+ import { useState, useEffect } from '@pyreon/react-compat'
18
18
 
19
19
  function Counter() {
20
20
  const [count, setCount] = useState(0)
21
21
  useEffect(() => {
22
- console.log("count changed:", count())
22
+ console.log('count changed:', count())
23
23
  })
24
24
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
25
25
  }
@@ -28,17 +28,17 @@ function Counter() {
28
28
  ### Using Refs and Context
29
29
 
30
30
  ```tsx
31
- import { useRef, useEffect, createContext, useContext } from "@pyreon/react-compat"
31
+ import { useRef, useEffect, createContext, useContext } from '@pyreon/react-compat'
32
32
 
33
- const ThemeContext = createContext("light")
33
+ const ThemeContext = createContext('light')
34
34
 
35
35
  function ThemeDisplay() {
36
36
  const theme = useContext(ThemeContext)
37
37
  const divRef = useRef<HTMLDivElement>(null)
38
38
 
39
39
  useEffect(() => {
40
- console.log("mounted, div is:", divRef.current)
41
- return () => console.log("unmounted")
40
+ console.log('mounted, div is:', divRef.current)
41
+ return () => console.log('unmounted')
42
42
  }, [])
43
43
 
44
44
  return <div ref={divRef}>Current theme: {theme}</div>
@@ -56,14 +56,16 @@ function App() {
56
56
  ### Reducer Pattern
57
57
 
58
58
  ```tsx
59
- import { useReducer } from "@pyreon/react-compat"
59
+ import { useReducer } from '@pyreon/react-compat'
60
60
 
61
- type Action = { type: "increment" } | { type: "decrement" }
61
+ type Action = { type: 'increment' } | { type: 'decrement' }
62
62
 
63
63
  function reducer(state: number, action: Action) {
64
64
  switch (action.type) {
65
- case "increment": return state + 1
66
- case "decrement": return state - 1
65
+ case 'increment':
66
+ return state + 1
67
+ case 'decrement':
68
+ return state - 1
67
69
  }
68
70
  }
69
71
 
@@ -72,8 +74,8 @@ function Counter() {
72
74
  return (
73
75
  <div>
74
76
  <span>{count}</span>
75
- <button onClick={() => dispatch({ type: "increment" })}>+</button>
76
- <button onClick={() => dispatch({ type: "decrement" })}>-</button>
77
+ <button onClick={() => dispatch({ type: 'increment' })}>+</button>
78
+ <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
77
79
  </div>
78
80
  )
79
81
  }
@@ -82,9 +84,9 @@ function Counter() {
82
84
  ### Lazy Loading
83
85
 
84
86
  ```tsx
85
- import { lazy, Suspense } from "@pyreon/react-compat"
87
+ import { lazy, Suspense } from '@pyreon/react-compat'
86
88
 
87
- const HeavyChart = lazy(() => import("./HeavyChart"))
89
+ const HeavyChart = lazy(() => import('./HeavyChart'))
88
90
 
89
91
  function Dashboard() {
90
92
  return (
package/lib/dom.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"dom.js","names":[],"sources":["../src/dom.ts"],"sourcesContent":["import type { VNodeChild } from \"@pyreon/core\"\n/**\n * @pyreon/react-compat/dom\n *\n * Drop-in for `react-dom/client` — provides `createRoot` so you can keep\n * the same entry-point pattern as a React app.\n */\nimport { mount } from \"@pyreon/runtime-dom\"\n\n/**\n * Drop-in for React 18's `createRoot(container).render(element)`.\n *\n * @example\n * import { createRoot } from \"@pyreon/react-compat/dom\"\n * createRoot(document.getElementById(\"app\")!).render(<App />)\n */\nexport function createRoot(container: Element): {\n render: (element: VNodeChild) => void\n unmount: () => void\n} {\n let cleanup: (() => void) | null = null\n return {\n render(element: VNodeChild) {\n if (cleanup) cleanup()\n cleanup = mount(element, container as HTMLElement)\n },\n unmount() {\n if (cleanup) {\n cleanup()\n cleanup = null\n }\n },\n }\n}\n\n/** Alias — matches React 17's `render(element, container)` signature. */\nexport function render(element: VNodeChild, container: Element): void {\n mount(element, container as HTMLElement)\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,WAAW,WAGzB;CACA,IAAI,UAA+B;AACnC,QAAO;EACL,OAAO,SAAqB;AAC1B,OAAI,QAAS,UAAS;AACtB,aAAU,MAAM,SAAS,UAAyB;;EAEpD,UAAU;AACR,OAAI,SAAS;AACX,aAAS;AACT,cAAU;;;EAGf;;;AAIH,SAAgB,OAAO,SAAqB,WAA0B;AACpE,OAAM,SAAS,UAAyB"}
1
+ {"version":3,"file":"dom.js","names":[],"sources":["../src/dom.ts"],"sourcesContent":["import type { VNodeChild } from '@pyreon/core'\n/**\n * @pyreon/react-compat/dom\n *\n * Drop-in for `react-dom/client` — provides `createRoot` so you can keep\n * the same entry-point pattern as a React app.\n */\nimport { mount } from '@pyreon/runtime-dom'\n\n/**\n * Drop-in for React 18's `createRoot(container).render(element)`.\n *\n * @example\n * import { createRoot } from \"@pyreon/react-compat/dom\"\n * createRoot(document.getElementById(\"app\")!).render(<App />)\n */\nexport function createRoot(container: Element): {\n render: (element: VNodeChild) => void\n unmount: () => void\n} {\n let cleanup: (() => void) | null = null\n return {\n render(element: VNodeChild) {\n if (cleanup) cleanup()\n cleanup = mount(element, container as HTMLElement)\n },\n unmount() {\n if (cleanup) {\n cleanup()\n cleanup = null\n }\n },\n }\n}\n\n/** Alias — matches React 17's `render(element, container)` signature. */\nexport function render(element: VNodeChild, container: Element): void {\n mount(element, container as HTMLElement)\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,WAAW,WAGzB;CACA,IAAI,UAA+B;AACnC,QAAO;EACL,OAAO,SAAqB;AAC1B,OAAI,QAAS,UAAS;AACtB,aAAU,MAAM,SAAS,UAAyB;;EAEpD,UAAU;AACR,OAAI,SAAS;AACX,aAAS;AACT,cAAU;;;EAGf;;;AAIH,SAAgB,OAAO,SAAqB,WAA0B;AACpE,OAAM,SAAS,UAAyB"}
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 // biome-ignore lint/suspicious/noConfusingVoidType: matches React's effect signature\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\n// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping\nconst _wrapperCache = new WeakMap<Function, ComponentFn>()\n\n// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping\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 */\n// biome-ignore lint/suspicious/noConfusingVoidType: matches React's useEffect signature\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 */\n// biome-ignore lint/suspicious/noConfusingVoidType: matches React's useLayoutEffect signature\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":[],"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 // biome-ignore lint/suspicious/noConfusingVoidType: matches React's effect signature\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\n// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping\nconst _wrapperCache = new WeakMap<Function, ComponentFn>()\n\n// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping\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 */\n// biome-ignore lint/suspicious/noConfusingVoidType: matches React's useEffect signature\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 */\n// biome-ignore lint/suspicious/noConfusingVoidType: matches React's useLayoutEffect signature\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 +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 // biome-ignore lint/suspicious/noConfusingVoidType: matches React's effect signature\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\n// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping\nconst _wrapperCache = new WeakMap<Function, ComponentFn>()\n\n// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping\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":";;;;AAsCA,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;;AAMJ,MAAM,gCAAgB,IAAI,SAAgC;AAG1D,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 // biome-ignore lint/suspicious/noConfusingVoidType: matches React's effect signature\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\n// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping\nconst _wrapperCache = new WeakMap<Function, ComponentFn>()\n\n// biome-ignore lint/complexity/noBannedTypes: Function is needed for generic component wrapping\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":";;;;AAsCA,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;;AAMJ,MAAM,gCAAgB,IAAI,SAAgC;AAG1D,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"}
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@pyreon/react-compat",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "React-compatible API shim for Pyreon — write React-style hooks that run on Pyreon's reactive engine",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/react-compat#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/tools/react-compat"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/react-compat#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
15
  "files": [
16
16
  "lib",
17
17
  "src",
18
18
  "README.md",
19
19
  "LICENSE"
20
20
  ],
21
- "sideEffects": false,
22
21
  "type": "module",
22
+ "sideEffects": false,
23
23
  "main": "./lib/index.js",
24
24
  "module": "./lib/index.js",
25
25
  "types": "./lib/types/index.d.ts",
@@ -45,24 +45,24 @@
45
45
  "types": "./lib/types/jsx-runtime.d.ts"
46
46
  }
47
47
  },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
48
51
  "scripts": {
49
52
  "build": "vl_rolldown_build",
50
53
  "dev": "vl_rolldown_build-watch",
51
54
  "test": "vitest run",
52
55
  "typecheck": "tsc --noEmit",
53
- "lint": "biome check .",
56
+ "lint": "oxlint .",
54
57
  "prepublishOnly": "bun run build"
55
58
  },
56
59
  "dependencies": {
57
- "@pyreon/core": "^0.11.4",
58
- "@pyreon/reactivity": "^0.11.4",
59
- "@pyreon/runtime-dom": "^0.11.4"
60
+ "@pyreon/core": "^0.11.6",
61
+ "@pyreon/reactivity": "^0.11.6",
62
+ "@pyreon/runtime-dom": "^0.11.6"
60
63
  },
61
64
  "devDependencies": {
62
65
  "@happy-dom/global-registrator": "^20.8.3",
63
66
  "happy-dom": "^20.8.3"
64
- },
65
- "publishConfig": {
66
- "access": "public"
67
67
  }
68
68
  }
package/src/dom.ts CHANGED
@@ -1,11 +1,11 @@
1
- import type { VNodeChild } from "@pyreon/core"
1
+ import type { VNodeChild } from '@pyreon/core'
2
2
  /**
3
3
  * @pyreon/react-compat/dom
4
4
  *
5
5
  * Drop-in for `react-dom/client` — provides `createRoot` so you can keep
6
6
  * the same entry-point pattern as a React app.
7
7
  */
8
- import { mount } from "@pyreon/runtime-dom"
8
+ import { mount } from '@pyreon/runtime-dom'
9
9
 
10
10
  /**
11
11
  * Drop-in for React 18's `createRoot(container).render(element)`.
package/src/index.ts CHANGED
@@ -12,20 +12,20 @@
12
12
  * import { createRoot } from "react-dom/client" // aliased by vite plugin
13
13
  */
14
14
 
15
- export type { Props, VNode as ReactNode, VNodeChild } from "@pyreon/core"
16
- export { Fragment, h as createElement, h } from "@pyreon/core"
15
+ export type { Props, VNode as ReactNode, VNodeChild } from '@pyreon/core'
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"
20
- import { batch } from "@pyreon/reactivity"
21
- import type { EffectEntry } from "./jsx-runtime"
22
- import { getCurrentCtx, getHookIndex } from "./jsx-runtime"
18
+ import type { VNodeChild } from '@pyreon/core'
19
+ import { createContext, ErrorBoundary, Portal, Suspense, useContext } from '@pyreon/core'
20
+ import { batch } from '@pyreon/reactivity'
21
+ import type { EffectEntry } from './jsx-runtime'
22
+ import { getCurrentCtx, getHookIndex } from './jsx-runtime'
23
23
 
24
24
  // ─── Helpers ─────────────────────────────────────────────────────────────────
25
25
 
26
26
  function requireCtx() {
27
27
  const ctx = getCurrentCtx()
28
- if (!ctx) throw new Error("Hook called outside of a component render")
28
+ if (!ctx) throw new Error('Hook called outside of a component render')
29
29
  return ctx
30
30
  }
31
31
 
@@ -49,13 +49,13 @@ export function useState<T>(initial: T | (() => T)): [T, (v: T | ((prev: T) => T
49
49
  const idx = getHookIndex()
50
50
 
51
51
  if (ctx.hooks.length <= idx) {
52
- ctx.hooks.push(typeof initial === "function" ? (initial as () => T)() : initial)
52
+ ctx.hooks.push(typeof initial === 'function' ? (initial as () => T)() : initial)
53
53
  }
54
54
 
55
55
  const value = ctx.hooks[idx] as T
56
56
  const setter = (v: T | ((prev: T) => T)) => {
57
57
  const current = ctx.hooks[idx] as T
58
- const next = typeof v === "function" ? (v as (prev: T) => T)(current) : v
58
+ const next = typeof v === 'function' ? (v as (prev: T) => T)(current) : v
59
59
  if (Object.is(current, next)) return
60
60
  ctx.hooks[idx] = next
61
61
  ctx.scheduleRerender()
@@ -77,7 +77,7 @@ export function useReducer<S, A>(
77
77
  const idx = getHookIndex()
78
78
 
79
79
  if (ctx.hooks.length <= idx) {
80
- ctx.hooks.push(typeof initial === "function" ? (initial as () => S)() : initial)
80
+ ctx.hooks.push(typeof initial === 'function' ? (initial as () => S)() : initial)
81
81
  }
82
82
 
83
83
  const state = ctx.hooks[idx] as S
@@ -291,5 +291,5 @@ export function createPortal(children: VNodeChild, target: Element): VNodeChild
291
291
 
292
292
  // ─── Suspense / lazy / ErrorBoundary ─────────────────────────────────────────
293
293
 
294
- export { lazy } from "@pyreon/core"
294
+ export { lazy } from '@pyreon/core'
295
295
  export { ErrorBoundary, Suspense }
@@ -1 +1 @@
1
- export { Fragment, jsx, jsxs } from "./jsx-runtime"
1
+ export { Fragment, jsx, jsxs } from './jsx-runtime'
@@ -10,9 +10,9 @@
10
10
  * existing renderer handles all DOM work.
11
11
  */
12
12
 
13
- import type { ComponentFn, Props, VNode, VNodeChild } from "@pyreon/core"
14
- import { Fragment, h } from "@pyreon/core"
15
- import { signal } from "@pyreon/reactivity"
13
+ import type { ComponentFn, Props, VNode, VNodeChild } from '@pyreon/core'
14
+ import { Fragment, h } from '@pyreon/core'
15
+ import { signal } from '@pyreon/reactivity'
16
16
 
17
17
  export { Fragment }
18
18
 
@@ -65,7 +65,7 @@ function runLayoutEffects(entries: EffectEntry[]): void {
65
65
  for (const entry of entries) {
66
66
  if (entry.cleanup) entry.cleanup()
67
67
  const cleanup = entry.fn()
68
- entry.cleanup = typeof cleanup === "function" ? cleanup : undefined
68
+ entry.cleanup = typeof cleanup === 'function' ? cleanup : undefined
69
69
  }
70
70
  }
71
71
 
@@ -76,7 +76,7 @@ function scheduleEffects(ctx: RenderContext, entries: EffectEntry[]): void {
76
76
  if (ctx.unmounted) return
77
77
  if (entry.cleanup) entry.cleanup()
78
78
  const cleanup = entry.fn()
79
- entry.cleanup = typeof cleanup === "function" ? cleanup : undefined
79
+ entry.cleanup = typeof cleanup === 'function' ? cleanup : undefined
80
80
  }
81
81
  })
82
82
  }
@@ -146,7 +146,7 @@ export function jsx(
146
146
  const { children, ...rest } = props
147
147
  const propsWithKey = (key != null ? { ...rest, key } : rest) as Props
148
148
 
149
- if (typeof type === "function") {
149
+ if (typeof type === 'function') {
150
150
  // Wrap React-style component for re-render support
151
151
  const wrapped = wrapCompatComponent(type)
152
152
  const componentProps = children !== undefined ? { ...propsWithKey, children } : propsWithKey
@@ -157,7 +157,7 @@ export function jsx(
157
157
  const childArray = children === undefined ? [] : Array.isArray(children) ? children : [children]
158
158
 
159
159
  // Map className → class for React compat
160
- if (typeof type === "string" && propsWithKey.className !== undefined) {
160
+ if (typeof type === 'string' && propsWithKey.className !== undefined) {
161
161
  propsWithKey.class = propsWithKey.className
162
162
  delete propsWithKey.className
163
163
  }
@@ -1,7 +1,7 @@
1
- import { h } from "@pyreon/core"
2
- import { effect, signal } from "@pyreon/reactivity"
3
- import { mount } from "@pyreon/runtime-dom"
4
- import { createRoot, render } from "../dom"
1
+ import { h } from '@pyreon/core'
2
+ import { effect, signal } from '@pyreon/reactivity'
3
+ import { mount } from '@pyreon/runtime-dom'
4
+ import { createRoot, render } from '../dom'
5
5
  import {
6
6
  batch,
7
7
  createContext,
@@ -24,12 +24,12 @@ import {
24
24
  useRef,
25
25
  useState,
26
26
  useTransition,
27
- } from "../index"
28
- import type { RenderContext } from "../jsx-runtime"
29
- import { beginRender, endRender, jsx } from "../jsx-runtime"
27
+ } from '../index'
28
+ import type { RenderContext } from '../jsx-runtime'
29
+ import { beginRender, endRender, jsx } from '../jsx-runtime'
30
30
 
31
31
  function container(): HTMLElement {
32
- const el = document.createElement("div")
32
+ const el = document.createElement('div')
33
33
  document.body.appendChild(el)
34
34
  return el
35
35
  }
@@ -71,13 +71,13 @@ function createHookRunner() {
71
71
 
72
72
  // ─── useState ─────────────────────────────────────────────────────────────────
73
73
 
74
- describe("useState", () => {
75
- test("returns [value, setter] — value is the initial value", () => {
74
+ describe('useState', () => {
75
+ test('returns [value, setter] — value is the initial value', () => {
76
76
  const [count] = withHookCtx(() => useState(0))
77
77
  expect(count).toBe(0)
78
78
  })
79
79
 
80
- test("setter updates value on re-render", () => {
80
+ test('setter updates value on re-render', () => {
81
81
  const runner = createHookRunner()
82
82
  const [, setCount] = runner.run(() => useState(0))
83
83
  setCount(5)
@@ -85,7 +85,7 @@ describe("useState", () => {
85
85
  expect(count2).toBe(5)
86
86
  })
87
87
 
88
- test("setter with function updater", () => {
88
+ test('setter with function updater', () => {
89
89
  const runner = createHookRunner()
90
90
  const [, setCount] = runner.run(() => useState(10))
91
91
  setCount((prev) => prev + 1)
@@ -93,7 +93,7 @@ describe("useState", () => {
93
93
  expect(count2).toBe(11)
94
94
  })
95
95
 
96
- test("initializer function is called once", () => {
96
+ test('initializer function is called once', () => {
97
97
  let calls = 0
98
98
  const runner = createHookRunner()
99
99
  runner.run(() =>
@@ -113,7 +113,7 @@ describe("useState", () => {
113
113
  expect(calls).toBe(1)
114
114
  })
115
115
 
116
- test("setter does nothing when value is the same (Object.is)", () => {
116
+ test('setter does nothing when value is the same (Object.is)', () => {
117
117
  const runner = createHookRunner()
118
118
  let rerenders = 0
119
119
  runner.ctx.scheduleRerender = () => {
@@ -126,7 +126,7 @@ describe("useState", () => {
126
126
  expect(rerenders).toBe(1)
127
127
  })
128
128
 
129
- test("re-render in a component via compat JSX runtime", async () => {
129
+ test('re-render in a component via compat JSX runtime', async () => {
130
130
  const el = container()
131
131
  let renderCount = 0
132
132
  let triggerSet: (v: number | ((p: number) => number)) => void = () => {}
@@ -135,13 +135,13 @@ describe("useState", () => {
135
135
  const [count, setCount] = useState(0)
136
136
  renderCount++
137
137
  triggerSet = setCount
138
- return h("span", null, String(count))
138
+ return h('span', null, String(count))
139
139
  }
140
140
 
141
141
  // Use compat jsx() to wrap the component
142
142
  const vnode = jsx(Counter, {})
143
143
  mount(vnode, el)
144
- expect(el.textContent).toBe("0")
144
+ expect(el.textContent).toBe('0')
145
145
  // mountChild samples the accessor once (untracked) + effect runs it once = 2 renders
146
146
  const initialRenders = renderCount
147
147
 
@@ -150,33 +150,33 @@ describe("useState", () => {
150
150
  await new Promise<void>((r) => queueMicrotask(r))
151
151
  // Need another microtask for the effect to propagate
152
152
  await new Promise<void>((r) => queueMicrotask(r))
153
- expect(el.textContent).toBe("1")
153
+ expect(el.textContent).toBe('1')
154
154
  expect(renderCount).toBe(initialRenders + 1)
155
155
  })
156
156
  })
157
157
 
158
158
  // ─── useReducer ───────────────────────────────────────────────────────────────
159
159
 
160
- describe("useReducer", () => {
161
- test("dispatch applies reducer", () => {
160
+ describe('useReducer', () => {
161
+ test('dispatch applies reducer', () => {
162
162
  const runner = createHookRunner()
163
- type Action = { type: "inc" } | { type: "dec" }
163
+ type Action = { type: 'inc' } | { type: 'dec' }
164
164
  const reducer = (state: number, action: Action) =>
165
- action.type === "inc" ? state + 1 : state - 1
165
+ action.type === 'inc' ? state + 1 : state - 1
166
166
 
167
167
  const [state0, dispatch] = runner.run(() => useReducer(reducer, 0))
168
168
  expect(state0).toBe(0)
169
169
 
170
- dispatch({ type: "inc" })
170
+ dispatch({ type: 'inc' })
171
171
  const [state1] = runner.run(() => useReducer(reducer, 0))
172
172
  expect(state1).toBe(1)
173
173
 
174
- dispatch({ type: "dec" })
174
+ dispatch({ type: 'dec' })
175
175
  const [state2] = runner.run(() => useReducer(reducer, 0))
176
176
  expect(state2).toBe(0)
177
177
  })
178
178
 
179
- test("initializer function is called once", () => {
179
+ test('initializer function is called once', () => {
180
180
  let calls = 0
181
181
  const runner = createHookRunner()
182
182
  const [state] = runner.run(() =>
@@ -203,22 +203,22 @@ describe("useReducer", () => {
203
203
  expect(calls).toBe(1)
204
204
  })
205
205
 
206
- test("dispatch does nothing when reducer returns same state", () => {
206
+ test('dispatch does nothing when reducer returns same state', () => {
207
207
  const runner = createHookRunner()
208
208
  let rerenders = 0
209
209
  runner.ctx.scheduleRerender = () => {
210
210
  rerenders++
211
211
  }
212
212
  const [, dispatch] = runner.run(() => useReducer((_s: number, _a: string) => 5, 5))
213
- dispatch("anything") // reducer returns 5, same as current
213
+ dispatch('anything') // reducer returns 5, same as current
214
214
  expect(rerenders).toBe(0)
215
215
  })
216
216
  })
217
217
 
218
218
  // ─── useEffect ────────────────────────────────────────────────────────────────
219
219
 
220
- describe("useEffect", () => {
221
- test("effect runs after render via compat JSX runtime", async () => {
220
+ describe('useEffect', () => {
221
+ test('effect runs after render via compat JSX runtime', async () => {
222
222
  const el = container()
223
223
  let effectRuns = 0
224
224
 
@@ -226,7 +226,7 @@ describe("useEffect", () => {
226
226
  useEffect(() => {
227
227
  effectRuns++
228
228
  })
229
- return h("div", null, "test")
229
+ return h('div', null, 'test')
230
230
  }
231
231
 
232
232
  mount(jsx(Comp, {}), el)
@@ -235,7 +235,7 @@ describe("useEffect", () => {
235
235
  expect(effectRuns).toBeGreaterThanOrEqual(1)
236
236
  })
237
237
 
238
- test("effect with empty deps runs once", async () => {
238
+ test('effect with empty deps runs once', async () => {
239
239
  const el = container()
240
240
  let effectRuns = 0
241
241
  let triggerSet: (v: number) => void = () => {}
@@ -246,7 +246,7 @@ describe("useEffect", () => {
246
246
  useEffect(() => {
247
247
  effectRuns++
248
248
  }, [])
249
- return h("div", null, String(count))
249
+ return h('div', null, String(count))
250
250
  }
251
251
 
252
252
  mount(jsx(Comp, {}), el)
@@ -260,7 +260,7 @@ describe("useEffect", () => {
260
260
  expect(effectRuns).toBe(1)
261
261
  })
262
262
 
263
- test("effect with deps re-runs when deps change", async () => {
263
+ test('effect with deps re-runs when deps change', async () => {
264
264
  const el = container()
265
265
  let effectRuns = 0
266
266
  let triggerSet: (v: number | ((p: number) => number)) => void = () => {}
@@ -271,7 +271,7 @@ describe("useEffect", () => {
271
271
  useEffect(() => {
272
272
  effectRuns++
273
273
  }, [count])
274
- return h("div", null, String(count))
274
+ return h('div', null, String(count))
275
275
  }
276
276
 
277
277
  mount(jsx(Comp, {}), el)
@@ -286,7 +286,7 @@ describe("useEffect", () => {
286
286
  expect(effectRuns).toBe(2)
287
287
  })
288
288
 
289
- test("effect cleanup runs before re-execution", async () => {
289
+ test('effect cleanup runs before re-execution', async () => {
290
290
  const el = container()
291
291
  let cleanups = 0
292
292
  let triggerSet: (v: number | ((p: number) => number)) => void = () => {}
@@ -299,7 +299,7 @@ describe("useEffect", () => {
299
299
  cleanups++
300
300
  }
301
301
  }, [count])
302
- return h("div", null, String(count))
302
+ return h('div', null, String(count))
303
303
  }
304
304
 
305
305
  mount(jsx(Comp, {}), el)
@@ -313,7 +313,7 @@ describe("useEffect", () => {
313
313
  expect(cleanups).toBe(1)
314
314
  })
315
315
 
316
- test("pendingEffects populated during render", () => {
316
+ test('pendingEffects populated during render', () => {
317
317
  const runner = createHookRunner()
318
318
  runner.run(() => {
319
319
  useEffect(() => {
@@ -323,7 +323,7 @@ describe("useEffect", () => {
323
323
  expect(runner.ctx.pendingEffects).toHaveLength(1)
324
324
  })
325
325
 
326
- test("effect with same deps does not re-queue", () => {
326
+ test('effect with same deps does not re-queue', () => {
327
327
  const runner = createHookRunner()
328
328
  runner.run(() => {
329
329
  useEffect(() => {}, [1, 2])
@@ -340,8 +340,8 @@ describe("useEffect", () => {
340
340
 
341
341
  // ─── useLayoutEffect ─────────────────────────────────────────────────────────
342
342
 
343
- describe("useLayoutEffect", () => {
344
- test("layout effect runs synchronously during render in compat runtime", () => {
343
+ describe('useLayoutEffect', () => {
344
+ test('layout effect runs synchronously during render in compat runtime', () => {
345
345
  const el = container()
346
346
  let effectRuns = 0
347
347
 
@@ -349,7 +349,7 @@ describe("useLayoutEffect", () => {
349
349
  useLayoutEffect(() => {
350
350
  effectRuns++
351
351
  })
352
- return h("div", null, "layout")
352
+ return h('div', null, 'layout')
353
353
  }
354
354
 
355
355
  mount(jsx(Comp, {}), el)
@@ -357,7 +357,7 @@ describe("useLayoutEffect", () => {
357
357
  expect(effectRuns).toBeGreaterThanOrEqual(1)
358
358
  })
359
359
 
360
- test("pendingLayoutEffects populated during render", () => {
360
+ test('pendingLayoutEffects populated during render', () => {
361
361
  const runner = createHookRunner()
362
362
  runner.run(() => {
363
363
  useLayoutEffect(() => {})
@@ -365,7 +365,7 @@ describe("useLayoutEffect", () => {
365
365
  expect(runner.ctx.pendingLayoutEffects).toHaveLength(1)
366
366
  })
367
367
 
368
- test("layout effect with same deps does not re-queue", () => {
368
+ test('layout effect with same deps does not re-queue', () => {
369
369
  const runner = createHookRunner()
370
370
  runner.run(() => {
371
371
  useLayoutEffect(() => {}, [1])
@@ -381,13 +381,13 @@ describe("useLayoutEffect", () => {
381
381
 
382
382
  // ─── useMemo ──────────────────────────────────────────────────────────────────
383
383
 
384
- describe("useMemo", () => {
385
- test("returns computed value", () => {
384
+ describe('useMemo', () => {
385
+ test('returns computed value', () => {
386
386
  const value = withHookCtx(() => useMemo(() => 3 * 2, []))
387
387
  expect(value).toBe(6)
388
388
  })
389
389
 
390
- test("recomputes when deps change", () => {
390
+ test('recomputes when deps change', () => {
391
391
  const runner = createHookRunner()
392
392
  const v1 = runner.run(() => useMemo(() => 10, [1]))
393
393
  expect(v1).toBe(10)
@@ -404,8 +404,8 @@ describe("useMemo", () => {
404
404
 
405
405
  // ─── useCallback ──────────────────────────────────────────────────────────────
406
406
 
407
- describe("useCallback", () => {
408
- test("returns the same function when deps unchanged", () => {
407
+ describe('useCallback', () => {
408
+ test('returns the same function when deps unchanged', () => {
409
409
  const runner = createHookRunner()
410
410
  const fn1 = () => 42
411
411
  const fn2 = () => 99
@@ -415,7 +415,7 @@ describe("useCallback", () => {
415
415
  expect(result1()).toBe(42)
416
416
  })
417
417
 
418
- test("returns new function when deps change", () => {
418
+ test('returns new function when deps change', () => {
419
419
  const runner = createHookRunner()
420
420
  const fn1 = () => 42
421
421
  const fn2 = () => 99
@@ -429,24 +429,24 @@ describe("useCallback", () => {
429
429
 
430
430
  // ─── useRef ───────────────────────────────────────────────────────────────────
431
431
 
432
- describe("useRef", () => {
433
- test("returns { current } with null default", () => {
432
+ describe('useRef', () => {
433
+ test('returns { current } with null default', () => {
434
434
  const ref = withHookCtx(() => useRef<HTMLDivElement>())
435
435
  expect(ref.current).toBeNull()
436
436
  })
437
437
 
438
- test("returns { current } with initial value", () => {
438
+ test('returns { current } with initial value', () => {
439
439
  const ref = withHookCtx(() => useRef(42))
440
440
  expect(ref.current).toBe(42)
441
441
  })
442
442
 
443
- test("current is mutable", () => {
443
+ test('current is mutable', () => {
444
444
  const ref = withHookCtx(() => useRef(0))
445
445
  ref.current = 10
446
446
  expect(ref.current).toBe(10)
447
447
  })
448
448
 
449
- test("same ref object persists across re-renders", () => {
449
+ test('same ref object persists across re-renders', () => {
450
450
  const runner = createHookRunner()
451
451
  const ref1 = runner.run(() => useRef(0))
452
452
  ref1.current = 99
@@ -458,27 +458,27 @@ describe("useRef", () => {
458
458
 
459
459
  // ─── memo ─────────────────────────────────────────────────────────────────────
460
460
 
461
- describe("memo", () => {
462
- test("skips re-render when props are shallowly equal", () => {
461
+ describe('memo', () => {
462
+ test('skips re-render when props are shallowly equal', () => {
463
463
  let renderCount = 0
464
464
  const MyComp = (props: { name: string }) => {
465
465
  renderCount++
466
- return h("span", null, props.name)
466
+ return h('span', null, props.name)
467
467
  }
468
468
  const Memoized = memo(MyComp)
469
- Memoized({ name: "a" })
469
+ Memoized({ name: 'a' })
470
470
  expect(renderCount).toBe(1)
471
- Memoized({ name: "a" })
471
+ Memoized({ name: 'a' })
472
472
  expect(renderCount).toBe(1) // same props — skipped
473
- Memoized({ name: "b" })
473
+ Memoized({ name: 'b' })
474
474
  expect(renderCount).toBe(2) // different props — re-rendered
475
475
  })
476
476
 
477
- test("custom areEqual function", () => {
477
+ test('custom areEqual function', () => {
478
478
  let renderCount = 0
479
479
  const MyComp = (props: { x: number; y: number }) => {
480
480
  renderCount++
481
- return h("span", null, String(props.x))
481
+ return h('span', null, String(props.x))
482
482
  }
483
483
  // Only compare x, ignore y
484
484
  const Memoized = memo(MyComp, (prev, next) => prev.x === next.x)
@@ -490,11 +490,11 @@ describe("memo", () => {
490
490
  expect(renderCount).toBe(2) // x changed → re-rendered
491
491
  })
492
492
 
493
- test("different number of keys triggers re-render", () => {
493
+ test('different number of keys triggers re-render', () => {
494
494
  let renderCount = 0
495
495
  const MyComp = (_props: Record<string, unknown>) => {
496
496
  renderCount++
497
- return h("span", null, "x")
497
+ return h('span', null, 'x')
498
498
  }
499
499
  const Memoized = memo(MyComp)
500
500
  Memoized({ a: 1 })
@@ -506,8 +506,8 @@ describe("memo", () => {
506
506
 
507
507
  // ─── useTransition ────────────────────────────────────────────────────────────
508
508
 
509
- describe("useTransition", () => {
510
- test("returns [false, fn => fn()]", () => {
509
+ describe('useTransition', () => {
510
+ test('returns [false, fn => fn()]', () => {
511
511
  const [isPending, startTransition] = useTransition()
512
512
  expect(isPending).toBe(false)
513
513
  let ran = false
@@ -520,10 +520,10 @@ describe("useTransition", () => {
520
520
 
521
521
  // ─── useDeferredValue ─────────────────────────────────────────────────────────
522
522
 
523
- describe("useDeferredValue", () => {
524
- test("returns value as-is", () => {
523
+ describe('useDeferredValue', () => {
524
+ test('returns value as-is', () => {
525
525
  expect(useDeferredValue(42)).toBe(42)
526
- expect(useDeferredValue("hello")).toBe("hello")
526
+ expect(useDeferredValue('hello')).toBe('hello')
527
527
  const obj = { a: 1 }
528
528
  expect(useDeferredValue(obj)).toBe(obj)
529
529
  })
@@ -531,15 +531,15 @@ describe("useDeferredValue", () => {
531
531
 
532
532
  // ─── useId ────────────────────────────────────────────────────────────────────
533
533
 
534
- describe("useId", () => {
535
- test("returns a unique string within a component", () => {
534
+ describe('useId', () => {
535
+ test('returns a unique string within a component', () => {
536
536
  const el = container()
537
537
  const ids: string[] = []
538
538
 
539
539
  const Comp = () => {
540
540
  ids.push(useId())
541
541
  ids.push(useId())
542
- return h("div", null, "id-test")
542
+ return h('div', null, 'id-test')
543
543
  }
544
544
 
545
545
  mount(jsx(Comp, {}), el)
@@ -547,11 +547,11 @@ describe("useId", () => {
547
547
  expect(ids.length).toBeGreaterThanOrEqual(2)
548
548
  // Within a single render, two useId calls produce different IDs
549
549
  expect(ids[0]).not.toBe(ids[1])
550
- expect(typeof ids[0]).toBe("string")
551
- expect(ids[0]?.startsWith(":r")).toBe(true)
550
+ expect(typeof ids[0]).toBe('string')
551
+ expect(ids[0]?.startsWith(':r')).toBe(true)
552
552
  })
553
553
 
554
- test("IDs are stable across re-renders", async () => {
554
+ test('IDs are stable across re-renders', async () => {
555
555
  const el = container()
556
556
  const idHistory: string[] = []
557
557
  let triggerSet: (v: number) => void = () => {}
@@ -561,7 +561,7 @@ describe("useId", () => {
561
561
  triggerSet = setCount
562
562
  const id = useId()
563
563
  idHistory.push(id)
564
- return h("div", null, `${id}-${count}`)
564
+ return h('div', null, `${id}-${count}`)
565
565
  }
566
566
 
567
567
  mount(jsx(Comp, {}), el)
@@ -581,32 +581,32 @@ describe("useId", () => {
581
581
 
582
582
  // ─── createPortal ─────────────────────────────────────────────────────────────
583
583
 
584
- describe("createPortal", () => {
585
- test("creates a portal VNode that renders into target", () => {
584
+ describe('createPortal', () => {
585
+ test('creates a portal VNode that renders into target', () => {
586
586
  const src = container()
587
587
  const target = container()
588
- mount(createPortal(h("span", null, "portaled"), target), src)
589
- expect(target.querySelector("span")?.textContent).toBe("portaled")
590
- expect(src.querySelector("span")).toBeNull()
588
+ mount(createPortal(h('span', null, 'portaled'), target), src)
589
+ expect(target.querySelector('span')?.textContent).toBe('portaled')
590
+ expect(src.querySelector('span')).toBeNull()
591
591
  })
592
592
  })
593
593
 
594
594
  // ─── lazy ─────────────────────────────────────────────────────────────────────
595
595
 
596
- describe("lazy", () => {
597
- test("returns a component that loads async", async () => {
598
- const MyComp = (props: { text: string }) => h("p", null, props.text)
596
+ describe('lazy', () => {
597
+ test('returns a component that loads async', async () => {
598
+ const MyComp = (props: { text: string }) => h('p', null, props.text)
599
599
  const Lazy = lazy(() => Promise.resolve({ default: MyComp }))
600
600
 
601
- expect(Lazy({ text: "hello" })).toBeNull()
601
+ expect(Lazy({ text: 'hello' })).toBeNull()
602
602
 
603
603
  await new Promise<void>((r) => setTimeout(r, 10))
604
- const result = Lazy({ text: "hello" })
604
+ const result = Lazy({ text: 'hello' })
605
605
  expect(result).not.toBeNull()
606
606
  })
607
607
 
608
- test("__loading reports loading state", async () => {
609
- const MyComp = () => h("div", null, "loaded")
608
+ test('__loading reports loading state', async () => {
609
+ const MyComp = () => h('div', null, 'loaded')
610
610
  const Lazy = lazy(() => Promise.resolve({ default: MyComp }))
611
611
  expect(Lazy.__loading()).toBe(true)
612
612
  await new Promise<void>((r) => setTimeout(r, 10))
@@ -616,8 +616,8 @@ describe("lazy", () => {
616
616
 
617
617
  // ─── batch ────────────────────────────────────────────────────────────────────
618
618
 
619
- describe("batch", () => {
620
- test("groups multiple signal updates into one flush", () => {
619
+ describe('batch', () => {
620
+ test('groups multiple signal updates into one flush', () => {
621
621
  const a = signal(0)
622
622
  const b = signal(0)
623
623
  let runs = 0
@@ -637,88 +637,88 @@ describe("batch", () => {
637
637
 
638
638
  // ─── createRoot (dom.ts) ──────────────────────────────────────────────────────
639
639
 
640
- describe("createRoot", () => {
641
- test("render mounts element into container", () => {
640
+ describe('createRoot', () => {
641
+ test('render mounts element into container', () => {
642
642
  const el = container()
643
643
  const root = createRoot(el)
644
- root.render(h("div", { id: "root-test" }, "hello"))
645
- expect(el.querySelector("#root-test")?.textContent).toBe("hello")
644
+ root.render(h('div', { id: 'root-test' }, 'hello'))
645
+ expect(el.querySelector('#root-test')?.textContent).toBe('hello')
646
646
  })
647
647
 
648
- test("unmount removes content", () => {
648
+ test('unmount removes content', () => {
649
649
  const el = container()
650
650
  const root = createRoot(el)
651
- root.render(h("p", null, "mounted"))
652
- expect(el.querySelector("p")).not.toBeNull()
651
+ root.render(h('p', null, 'mounted'))
652
+ expect(el.querySelector('p')).not.toBeNull()
653
653
  root.unmount()
654
- expect(el.innerHTML).toBe("")
654
+ expect(el.innerHTML).toBe('')
655
655
  })
656
656
 
657
- test("re-render replaces previous content", () => {
657
+ test('re-render replaces previous content', () => {
658
658
  const el = container()
659
659
  const root = createRoot(el)
660
- root.render(h("span", null, "first"))
661
- expect(el.textContent).toBe("first")
662
- root.render(h("span", null, "second"))
663
- expect(el.textContent).toBe("second")
660
+ root.render(h('span', null, 'first'))
661
+ expect(el.textContent).toBe('first')
662
+ root.render(h('span', null, 'second'))
663
+ expect(el.textContent).toBe('second')
664
664
  })
665
665
 
666
- test("unmount after unmount is safe (no-op)", () => {
666
+ test('unmount after unmount is safe (no-op)', () => {
667
667
  const el = container()
668
668
  const root = createRoot(el)
669
- root.render(h("div", null, "x"))
669
+ root.render(h('div', null, 'x'))
670
670
  root.unmount()
671
671
  root.unmount()
672
- expect(el.innerHTML).toBe("")
672
+ expect(el.innerHTML).toBe('')
673
673
  })
674
674
  })
675
675
 
676
676
  // ─── render (dom.ts) ──────────────────────────────────────────────────────────
677
677
 
678
- describe("render", () => {
679
- test("mounts element into container", () => {
678
+ describe('render', () => {
679
+ test('mounts element into container', () => {
680
680
  const el = container()
681
- render(h("div", { id: "render-test" }, "world"), el)
682
- expect(el.querySelector("#render-test")?.textContent).toBe("world")
681
+ render(h('div', { id: 'render-test' }, 'world'), el)
682
+ expect(el.querySelector('#render-test')?.textContent).toBe('world')
683
683
  })
684
684
  })
685
685
 
686
686
  // ─── useImperativeHandle ─────────────────────────────────────────────────────
687
687
 
688
- describe("useImperativeHandle", () => {
689
- test("sets ref.current via layout effect", () => {
688
+ describe('useImperativeHandle', () => {
689
+ test('sets ref.current via layout effect', () => {
690
690
  const el = container()
691
691
  const ref = { current: null as { greet: () => string } | null }
692
692
 
693
693
  const Comp = () => {
694
694
  useImperativeHandle(ref, () => ({
695
- greet: () => "hello",
695
+ greet: () => 'hello',
696
696
  }))
697
- return h("div", null, "imp")
697
+ return h('div', null, 'imp')
698
698
  }
699
699
 
700
700
  mount(jsx(Comp, {}), el)
701
701
  expect(ref.current).not.toBeNull()
702
- expect(ref.current?.greet()).toBe("hello")
702
+ expect(ref.current?.greet()).toBe('hello')
703
703
  })
704
704
 
705
- test("no-op when ref is null", () => {
705
+ test('no-op when ref is null', () => {
706
706
  const el = container()
707
707
 
708
708
  const Comp = () => {
709
709
  useImperativeHandle(null, () => ({ value: 42 }))
710
- return h("div", null, "no-ref")
710
+ return h('div', null, 'no-ref')
711
711
  }
712
712
 
713
713
  mount(jsx(Comp, {}), el)
714
714
  })
715
715
 
716
- test("no-op when ref is undefined", () => {
716
+ test('no-op when ref is undefined', () => {
717
717
  const el = container()
718
718
 
719
719
  const Comp = () => {
720
720
  useImperativeHandle(undefined, () => ({ value: 42 }))
721
- return h("div", null, "undef-ref")
721
+ return h('div', null, 'undef-ref')
722
722
  }
723
723
 
724
724
  mount(jsx(Comp, {}), el)
@@ -727,125 +727,125 @@ describe("useImperativeHandle", () => {
727
727
 
728
728
  // ─── Re-exports ───────────────────────────────────────────────────────────────
729
729
 
730
- describe("re-exports", () => {
731
- test("createElement is h", () => {
730
+ describe('re-exports', () => {
731
+ test('createElement is h', () => {
732
732
  expect(createElement).toBe(h)
733
733
  })
734
734
 
735
- test("Fragment is exported", () => {
736
- expect(typeof Fragment).toBe("symbol")
735
+ test('Fragment is exported', () => {
736
+ expect(typeof Fragment).toBe('symbol')
737
737
  })
738
738
 
739
- test("createContext creates context with default", () => {
740
- const Ctx = createContext("default")
741
- expect(useContext(Ctx)).toBe("default")
739
+ test('createContext creates context with default', () => {
740
+ const Ctx = createContext('default')
741
+ expect(useContext(Ctx)).toBe('default')
742
742
  })
743
743
 
744
- test("useContext reads from context", () => {
744
+ test('useContext reads from context', () => {
745
745
  const Ctx = createContext(42)
746
746
  expect(useContext(Ctx)).toBe(42)
747
747
  })
748
748
 
749
- test("Suspense is exported", () => {
750
- expect(typeof Suspense).toBe("function")
749
+ test('Suspense is exported', () => {
750
+ expect(typeof Suspense).toBe('function')
751
751
  })
752
752
 
753
- test("ErrorBoundary is exported", () => {
754
- expect(typeof ErrorBoundary).toBe("function")
753
+ test('ErrorBoundary is exported', () => {
754
+ expect(typeof ErrorBoundary).toBe('function')
755
755
  })
756
756
 
757
- test("useLayoutEffect is a function", () => {
758
- expect(typeof useLayoutEffect).toBe("function")
757
+ test('useLayoutEffect is a function', () => {
758
+ expect(typeof useLayoutEffect).toBe('function')
759
759
  })
760
760
  })
761
761
 
762
762
  // ─── jsx-runtime ──────────────────────────────────────────────────────────────
763
763
 
764
- describe("jsx-runtime", () => {
765
- test("jsx with string type creates element VNode", () => {
766
- const vnode = jsx("div", { className: "test", children: "hello" })
764
+ describe('jsx-runtime', () => {
765
+ test('jsx with string type creates element VNode', () => {
766
+ const vnode = jsx('div', { className: 'test', children: 'hello' })
767
767
  // className should be mapped to class
768
- expect(vnode.props.class).toBe("test")
768
+ expect(vnode.props.class).toBe('test')
769
769
  expect(vnode.props.className).toBeUndefined()
770
770
  })
771
771
 
772
- test("jsx with key prop", () => {
773
- const vnode = jsx("div", { children: "x" }, "my-key")
774
- expect(vnode.props.key).toBe("my-key")
772
+ test('jsx with key prop', () => {
773
+ const vnode = jsx('div', { children: 'x' }, 'my-key')
774
+ expect(vnode.props.key).toBe('my-key')
775
775
  })
776
776
 
777
- test("jsx with component wraps for re-render", () => {
778
- const MyComp = () => h("span", null, "hi")
777
+ test('jsx with component wraps for re-render', () => {
778
+ const MyComp = () => h('span', null, 'hi')
779
779
  const vnode = jsx(MyComp, {})
780
780
  // The component should be wrapped (different from original)
781
781
  expect(vnode.type).not.toBe(MyComp)
782
- expect(typeof vnode.type).toBe("function")
782
+ expect(typeof vnode.type).toBe('function')
783
783
  })
784
784
 
785
- test("jsx with Fragment", () => {
786
- const vnode = jsx(Fragment, { children: [h("span", null, "a"), h("span", null, "b")] })
785
+ test('jsx with Fragment', () => {
786
+ const vnode = jsx(Fragment, { children: [h('span', null, 'a'), h('span', null, 'b')] })
787
787
  expect(vnode.type).toBe(Fragment)
788
788
  })
789
789
 
790
- test("jsx with single child (not array)", () => {
791
- const vnode = jsx("div", { children: "text" })
790
+ test('jsx with single child (not array)', () => {
791
+ const vnode = jsx('div', { children: 'text' })
792
792
  expect(vnode.children).toHaveLength(1)
793
793
  })
794
794
 
795
- test("jsx with no children", () => {
796
- const vnode = jsx("div", {})
795
+ test('jsx with no children', () => {
796
+ const vnode = jsx('div', {})
797
797
  expect(vnode.children).toHaveLength(0)
798
798
  })
799
799
 
800
- test("jsx component with children in props", () => {
801
- const MyComp = (props: { children?: string }) => h("div", null, props.children ?? "")
802
- const vnode = jsx(MyComp, { children: "child-text" })
803
- expect(typeof vnode.type).toBe("function")
800
+ test('jsx component with children in props', () => {
801
+ const MyComp = (props: { children?: string }) => h('div', null, props.children ?? '')
802
+ const vnode = jsx(MyComp, { children: 'child-text' })
803
+ expect(typeof vnode.type).toBe('function')
804
804
  })
805
805
  })
806
806
 
807
807
  // ─── Hook error when called outside component ────────────────────────────────
808
808
 
809
- describe("hooks outside component", () => {
810
- test("useState throws when called outside render", () => {
811
- expect(() => useState(0)).toThrow("Hook called outside")
809
+ describe('hooks outside component', () => {
810
+ test('useState throws when called outside render', () => {
811
+ expect(() => useState(0)).toThrow('Hook called outside')
812
812
  })
813
813
 
814
- test("useEffect throws when called outside render", () => {
815
- expect(() => useEffect(() => {})).toThrow("Hook called outside")
814
+ test('useEffect throws when called outside render', () => {
815
+ expect(() => useEffect(() => {})).toThrow('Hook called outside')
816
816
  })
817
817
 
818
- test("useRef throws when called outside render", () => {
819
- expect(() => useRef(0)).toThrow("Hook called outside")
818
+ test('useRef throws when called outside render', () => {
819
+ expect(() => useRef(0)).toThrow('Hook called outside')
820
820
  })
821
821
 
822
- test("useMemo throws when called outside render", () => {
823
- expect(() => useMemo(() => 0, [])).toThrow("Hook called outside")
822
+ test('useMemo throws when called outside render', () => {
823
+ expect(() => useMemo(() => 0, [])).toThrow('Hook called outside')
824
824
  })
825
825
 
826
- test("useId throws when called outside render", () => {
827
- expect(() => useId()).toThrow("Hook called outside")
826
+ test('useId throws when called outside render', () => {
827
+ expect(() => useId()).toThrow('Hook called outside')
828
828
  })
829
829
 
830
- test("useReducer throws when called outside render", () => {
831
- expect(() => useReducer((s: number) => s, 0)).toThrow("Hook called outside")
830
+ test('useReducer throws when called outside render', () => {
831
+ expect(() => useReducer((s: number) => s, 0)).toThrow('Hook called outside')
832
832
  })
833
833
  })
834
834
 
835
835
  // ─── Edge cases ──────────────────────────────────────────────────────────────
836
836
 
837
- describe("edge cases", () => {
838
- test("useState with string initial", () => {
839
- const [val] = withHookCtx(() => useState("hello"))
840
- expect(val).toBe("hello")
837
+ describe('edge cases', () => {
838
+ test('useState with string initial', () => {
839
+ const [val] = withHookCtx(() => useState('hello'))
840
+ expect(val).toBe('hello')
841
841
  })
842
842
 
843
- test("useReducer with non-function initial", () => {
844
- const [state] = withHookCtx(() => useReducer((s: string, a: string) => s + a, "start"))
845
- expect(state).toBe("start")
843
+ test('useReducer with non-function initial', () => {
844
+ const [state] = withHookCtx(() => useReducer((s: string, a: string) => s + a, 'start'))
845
+ expect(state).toBe('start')
846
846
  })
847
847
 
848
- test("depsChanged handles different length arrays", () => {
848
+ test('depsChanged handles different length arrays', () => {
849
849
  const runner = createHookRunner()
850
850
  runner.run(() => {
851
851
  useEffect(() => {}, [1, 2])
@@ -859,7 +859,7 @@ describe("edge cases", () => {
859
859
  expect(runner.ctx.pendingEffects).toHaveLength(1)
860
860
  })
861
861
 
862
- test("depsChanged with undefined deps always re-runs", () => {
862
+ test('depsChanged with undefined deps always re-runs', () => {
863
863
  const runner = createHookRunner()
864
864
  runner.run(() => {
865
865
  useEffect(() => {})
@@ -1,3 +1,3 @@
1
- import { GlobalRegistrator } from "@happy-dom/global-registrator"
1
+ import { GlobalRegistrator } from '@happy-dom/global-registrator'
2
2
 
3
3
  GlobalRegistrator.register()