@ipxjs/refract 0.3.1 → 0.4.1

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
@@ -3,7 +3,7 @@
3
3
  # Refract
4
4
 
5
5
  A minimal React-like virtual DOM library, written in TypeScript with split entrypoints
6
- so you can keep bundles small and targetetd.
6
+ so you can keep bundles small and targeted.
7
7
 
8
8
  Refract implements the core ideas behind React in TypeScript
9
9
  - a virtual DOM
@@ -82,6 +82,44 @@ yarn test
82
82
  - `refract/full` -- complete API including hooks, context, memo, sanitizer defaults, and devtools integration
83
83
  - `refract` -- alias of `refract/full` for backward compatibility
84
84
  - Feature entrypoints for custom bundles: `refract/hooks`, `refract/context`, `refract/memo`, `refract/security`, `refract/devtools`
85
+ - Optional React-compat entrypoints (opt-in): `refract/compat/react`, `refract/compat/react-dom`, `refract/compat/react-dom/client`, `refract/compat/react/jsx-runtime`, `refract/compat/react/jsx-dev-runtime`
86
+
87
+ ### React Ecosystem Compatibility (Opt-in)
88
+
89
+ Refract now includes an opt-in compatibility layer so you can alias React imports
90
+ for selected ecosystem libraries (for example MUI, `react-router-dom`, and
91
+ `@dnd-kit`) without inflating the default `refract/core` path.
92
+
93
+ Supported compat APIs include:
94
+ - `forwardRef`, `cloneElement`, `Children`, `isValidElement`
95
+ - `useLayoutEffect`, `useInsertionEffect`, `useId`
96
+ - `useSyncExternalStore`, `useImperativeHandle`
97
+ - `createPortal`
98
+ - `createRoot`, `flushSync`, `unstable_batchedUpdates`
99
+ - `jsx/jsxs/jsxDEV` runtime entrypoints
100
+ - React hook dispatcher bridge internals (`__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE`) with optional `registerExternalReactModule(...)` for mixed-runtime environments (tests/Node)
101
+
102
+ Example Vite aliases:
103
+
104
+ ```ts
105
+ // vite.config.ts
106
+ import { defineConfig } from "vite";
107
+
108
+ export default defineConfig({
109
+ resolve: {
110
+ alias: {
111
+ react: "refract/compat/react",
112
+ "react-dom": "refract/compat/react-dom",
113
+ "react-dom/client": "refract/compat/react-dom/client",
114
+ "react/jsx-runtime": "refract/compat/react/jsx-runtime",
115
+ "react/jsx-dev-runtime": "refract/compat/react/jsx-dev-runtime",
116
+ },
117
+ },
118
+ });
119
+ ```
120
+
121
+ The compat layer is intentionally separate from core so users who do not need
122
+ React ecosystem compatibility keep the smallest and fastest Refract bundles.
85
123
 
86
124
  ## API
87
125
 
@@ -181,20 +219,20 @@ The values below are from a local run on February 15, 2026.
181
219
 
182
220
  | Framework | JS bundle (raw) | JS bundle (gzip) |
183
221
  |---------------------------|----------------:|-----------------:|
184
- | Refract (`core`) | 7.46 kB | 2.93 kB |
185
- | Refract (`core+hooks`) | 8.75 kB | 3.38 kB |
186
- | Refract (`core+context`) | 7.94 kB | 3.15 kB |
187
- | Refract (`core+memo`) | 8.09 kB | 3.15 kB |
188
- | Refract (`core+security`) | 8.51 kB | 3.29 kB |
189
- | Refract (`refract`) | 13.55 kB | 5.04 kB |
222
+ | Refract (`core`) | 8.36 kB | 3.16 kB |
223
+ | Refract (`core+hooks`) | 9.76 kB | 3.65 kB |
224
+ | Refract (`core+context`) | 8.85 kB | 3.38 kB |
225
+ | Refract (`core+memo`) | 9.03 kB | 3.39 kB |
226
+ | Refract (`core+security`) | 9.27 kB | 3.46 kB |
227
+ | Refract (`refract`) | 14.64 kB | 5.34 kB |
190
228
  | React | 189.74 kB | 59.52 kB |
191
229
  | Preact | 14.46 kB | 5.95 kB |
192
230
 
193
231
  Load-time metrics are machine-dependent, so the benchmark script prints a fresh
194
232
  per-run timing table (median, p95, min/max, sd) for every framework.
195
233
 
196
- From this snapshot, Refract `core` gzip JS is about 20.3x smaller than React,
197
- and the full `refract` entrypoint is about 11.8x smaller.
234
+ From this snapshot, Refract `core` gzip JS is about 18.8x smaller than React,
235
+ and the full `refract` entrypoint is about 11.1x smaller.
198
236
 
199
237
  ### Component Combination Benchmarks (Vitest)
200
238
 
@@ -204,13 +242,13 @@ Higher `hz` is better.
204
242
 
205
243
  | Component usage profile | Mount (hz) | Mount vs base | Reconcile (hz) | Reconcile vs base |
206
244
  |-------------------------|------------|---------------|----------------|-------------------|
207
- | `base` | 5209.15 | baseline | 4432.98 | baseline |
208
- | `memo` | 5924.46 | +13.7% | 5367.20 | +21.1% |
209
- | `context` | 3457.71 | -33.6% | 5243.29 | +18.3% |
210
- | `fragment` | 5189.17 | -0.4% | 3964.90 | -10.6% |
211
- | `keyed` | 6084.45 | +16.8% | 5037.30 | +13.6% |
212
- | `memo+context` | 6113.94 | +17.4% | 5347.56 | +20.6% |
213
- | `memo+context+keyed` | 6040.74 | +16.0% | 5088.81 | +14.8% |
245
+ | `base` | 5068.40 | baseline | 4144.37 | baseline |
246
+ | `memo` | 5883.23 | +16.1% | 5154.56 | +24.4% |
247
+ | `context` | 3521.54 | -30.5% | 5063.92 | +22.2% |
248
+ | `fragment` | 4880.23 | -3.7% | 4079.08 | -1.6% |
249
+ | `keyed` | 5763.70 | +13.7% | 4844.23 | +16.9% |
250
+ | `memo+context` | 6173.01 | +21.8% | 5144.98 | +24.1% |
251
+ | `memo+context+keyed` | 5606.73 | +10.6% | 4732.23 | +14.2% |
214
252
 
215
253
  In this run, `memo+context` was the fastest mount profile, while
216
254
  `memo` was the fastest reconcile profile.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ipxjs/refract",
3
- "version": "0.3.1",
4
- "description": "A minimal React-like virtual DOM library focused on image rendering",
3
+ "version": "0.4.1",
4
+ "description": "A minimal React-like virtual DOM library with an optional React compat layer",
5
5
  "type": "module",
6
6
  "main": "src/refract/index.ts",
7
7
  "exports": {
@@ -12,7 +12,12 @@
12
12
  "./context": "./src/refract/features/context.ts",
13
13
  "./memo": "./src/refract/memo.ts",
14
14
  "./security": "./src/refract/features/security.ts",
15
- "./devtools": "./src/refract/devtools.ts"
15
+ "./devtools": "./src/refract/devtools.ts",
16
+ "./compat/react": "./src/refract/compat/react.ts",
17
+ "./compat/react-dom": "./src/refract/compat/react-dom.ts",
18
+ "./compat/react-dom/client": "./src/refract/compat/react-dom-client.ts",
19
+ "./compat/react/jsx-runtime": "./src/refract/compat/react-jsx-runtime.ts",
20
+ "./compat/react/jsx-dev-runtime": "./src/refract/compat/react-jsx-dev-runtime.ts"
16
21
  },
17
22
  "scripts": {
18
23
  "dev": "vite demo",
@@ -25,6 +30,9 @@
25
30
  "devDependencies": {
26
31
  "@types/jsdom": "^27.0.0",
27
32
  "jsdom": "^28.0.0",
33
+ "react": "^19.2.4",
34
+ "react-dom": "^19.2.4",
35
+ "react-router-dom": "^7.13.0",
28
36
  "typescript": "^5.9.3",
29
37
  "vite": "^7.3.1",
30
38
  "vitest": "^4.0.18"
@@ -0,0 +1,27 @@
1
+ import { createElement, Fragment } from "../createElement.js";
2
+ import { render } from "../render.js";
3
+ import { setReactCompatEventMode } from "../dom.js";
4
+
5
+ setReactCompatEventMode(true);
6
+
7
+ export interface RefractCompatRoot {
8
+ render(children: unknown): void;
9
+ unmount(): void;
10
+ }
11
+
12
+ export function createRoot(container: HTMLElement): RefractCompatRoot {
13
+ return {
14
+ render(children: unknown): void {
15
+ render(children as Parameters<typeof render>[0], container);
16
+ },
17
+ unmount(): void {
18
+ render(createElement(Fragment, null), container);
19
+ },
20
+ };
21
+ }
22
+
23
+ export function hydrateRoot(container: HTMLElement, children: unknown): RefractCompatRoot {
24
+ const root = createRoot(container);
25
+ root.render(children);
26
+ return root;
27
+ }
@@ -0,0 +1,42 @@
1
+ import { createElement, Fragment } from "../createElement.js";
2
+ import { flushPendingRenders } from "../coreRenderer.js";
3
+ import { setReactCompatEventMode } from "../dom.js";
4
+ import { createPortal as createPortalImpl } from "../portal.js";
5
+ import type { PortalChild } from "../portal.js";
6
+ import { render } from "../render.js";
7
+
8
+ setReactCompatEventMode(true);
9
+
10
+ export function createPortal(children: PortalChild, container: Node, key?: string | number | null): ReturnType<typeof createPortalImpl> {
11
+ return createPortalImpl(children, container, key);
12
+ }
13
+
14
+ export function unstable_batchedUpdates<T>(callback: () => T): T {
15
+ return callback();
16
+ }
17
+
18
+ export function flushSync<T>(callback: () => T): T {
19
+ const result = callback();
20
+ flushPendingRenders();
21
+ return result;
22
+ }
23
+
24
+ export function renderCompat(vnode: unknown, container: HTMLElement): void {
25
+ render(vnode as Parameters<typeof render>[0], container);
26
+ }
27
+
28
+ export function unmountComponentAtNode(container: HTMLElement): boolean {
29
+ render(createElement(Fragment, null), container);
30
+ return true;
31
+ }
32
+
33
+ const ReactDomCompat = {
34
+ createPortal,
35
+ flushSync,
36
+ render: renderCompat,
37
+ unstable_batchedUpdates,
38
+ unmountComponentAtNode,
39
+ };
40
+
41
+ export { renderCompat as render };
42
+ export default ReactDomCompat;
@@ -0,0 +1,15 @@
1
+ import { Fragment, jsx } from "./react-jsx-runtime.js";
2
+ import type { VNodeType } from "../types.js";
3
+
4
+ type JsxProps = Record<string, unknown> | null | undefined;
5
+
6
+ export function jsxDEV(
7
+ type: VNodeType,
8
+ props: JsxProps,
9
+ key?: string,
10
+ ): ReturnType<typeof jsx> {
11
+ return jsx(type, props, key);
12
+ }
13
+
14
+ export { Fragment };
15
+
@@ -0,0 +1,32 @@
1
+ import { createElement, Fragment } from "../createElement.js";
2
+ import type { VNode, VNodeType } from "../types.js";
3
+
4
+ type JsxProps = Record<string, unknown> | null | undefined;
5
+ type JsxChild = VNode | string | number | boolean | null | undefined | JsxChild[];
6
+
7
+ function createJsxElement(type: VNodeType, rawProps: JsxProps, key?: string): ReturnType<typeof createElement> {
8
+ const props = { ...(rawProps ?? {}) };
9
+ if (key !== undefined) {
10
+ props.key = key;
11
+ }
12
+ const children = props.children as JsxChild | JsxChild[] | undefined;
13
+ delete props.children;
14
+
15
+ if (children === undefined) {
16
+ return createElement(type, props);
17
+ }
18
+ if (Array.isArray(children)) {
19
+ return createElement(type, props, ...(children as JsxChild[]));
20
+ }
21
+ return createElement(type, props, children as JsxChild);
22
+ }
23
+
24
+ export function jsx(type: VNodeType, props: JsxProps, key?: string): ReturnType<typeof createElement> {
25
+ return createJsxElement(type, props, key);
26
+ }
27
+
28
+ export function jsxs(type: VNodeType, props: JsxProps, key?: string): ReturnType<typeof createElement> {
29
+ return createJsxElement(type, props, key);
30
+ }
31
+
32
+ export { Fragment };
@@ -0,0 +1,238 @@
1
+ import { createElement, Fragment } from "../createElement.js";
2
+ import { memo } from "../memo.js";
3
+ import { createContext } from "../features/context.js";
4
+ import {
5
+ createRef,
6
+ } from "../features/hooks.js";
7
+ import type { Component, Props, VNode } from "../types.js";
8
+ import {
9
+ __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
10
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
11
+ ensureHookDispatcherRuntime,
12
+ registerExternalReactModule,
13
+ resolveDispatcher,
14
+ } from "./sharedInternals.js";
15
+
16
+ const REACT_ELEMENT_TYPE = Symbol.for("react.element");
17
+ type ElementChild = VNode | string | number | boolean | null | undefined | ElementChild[];
18
+
19
+ ensureHookDispatcherRuntime();
20
+
21
+ function normalizeChildren(children: unknown): unknown[] {
22
+ if (children === undefined) return [];
23
+ return Array.isArray(children) ? children : [children];
24
+ }
25
+
26
+ function parseChildrenArgs(children: unknown[]): unknown {
27
+ if (children.length === 0) return undefined;
28
+ if (children.length === 1) return children[0];
29
+ return children;
30
+ }
31
+
32
+ export function forwardRef<T, P extends Record<string, unknown> = Record<string, unknown>>(
33
+ render: (props: P, ref: { current: T | null } | ((value: T | null) => void) | null) => VNode,
34
+ ): Component {
35
+ const ForwardRefComponent: Component = (props: Props) => {
36
+ const { ref, ...rest } = props as Props & { ref?: { current: T | null } | ((value: T | null) => void) | null };
37
+ return render(rest as unknown as P, ref ?? null);
38
+ };
39
+ return ForwardRefComponent;
40
+ }
41
+
42
+ export function isValidElement(value: unknown): value is VNode {
43
+ return !!value
44
+ && typeof value === "object"
45
+ && "type" in (value as Record<string, unknown>)
46
+ && "props" in (value as Record<string, unknown>);
47
+ }
48
+
49
+ export function cloneElement(
50
+ element: VNode,
51
+ props?: Record<string, unknown> | null,
52
+ ...children: unknown[]
53
+ ): VNode {
54
+ const mergedProps: Record<string, unknown> = {
55
+ ...element.props,
56
+ ...(props ?? {}),
57
+ };
58
+
59
+ if (children.length > 0) {
60
+ mergedProps.children = normalizeChildren(parseChildrenArgs(children)) as VNode[];
61
+ }
62
+
63
+ if (props?.key !== undefined) {
64
+ mergedProps.key = props.key;
65
+ } else if (element.key != null) {
66
+ mergedProps.key = element.key;
67
+ }
68
+
69
+ const nextChildren = normalizeChildren(mergedProps.children);
70
+ delete mergedProps.children;
71
+
72
+ return createElement(element.type, mergedProps, ...(nextChildren as ElementChild[]));
73
+ }
74
+
75
+ function childrenToArray(children: unknown): unknown[] {
76
+ if (children === undefined || children === null) return [];
77
+ if (!Array.isArray(children)) return [children];
78
+ const out: unknown[] = [];
79
+ const stack = [...children];
80
+ while (stack.length > 0) {
81
+ const child = stack.shift();
82
+ if (Array.isArray(child)) {
83
+ stack.unshift(...child);
84
+ continue;
85
+ }
86
+ if (child === undefined || child === null || typeof child === "boolean") {
87
+ continue;
88
+ }
89
+ out.push(child);
90
+ }
91
+ return out;
92
+ }
93
+
94
+ export const Children = {
95
+ map<T>(children: unknown, fn: (child: unknown, index: number) => T): T[] {
96
+ return childrenToArray(children).map(fn);
97
+ },
98
+ forEach(children: unknown, fn: (child: unknown, index: number) => void): void {
99
+ childrenToArray(children).forEach(fn);
100
+ },
101
+ count(children: unknown): number {
102
+ return childrenToArray(children).length;
103
+ },
104
+ toArray(children: unknown): unknown[] {
105
+ return childrenToArray(children);
106
+ },
107
+ only(children: unknown): unknown {
108
+ const array = childrenToArray(children);
109
+ if (array.length !== 1) {
110
+ throw new Error("React.Children.only expected to receive a single React element child.");
111
+ }
112
+ return array[0];
113
+ },
114
+ };
115
+
116
+ export function useState<T>(initial: T | (() => T)): [T, (value: T | ((prev: T) => T)) => void] {
117
+ return resolveDispatcher().useState(initial);
118
+ }
119
+
120
+ export function useEffect(effect: () => void | (() => void), deps?: unknown[]): void {
121
+ return resolveDispatcher().useEffect(effect, deps);
122
+ }
123
+
124
+ export function useLayoutEffect(effect: () => void | (() => void), deps?: unknown[]): void {
125
+ return resolveDispatcher().useLayoutEffect(effect, deps);
126
+ }
127
+
128
+ export function useInsertionEffect(effect: () => void | (() => void), deps?: unknown[]): void {
129
+ return resolveDispatcher().useInsertionEffect(effect, deps);
130
+ }
131
+
132
+ export function useRef<T>(initial: T): { current: T } {
133
+ return resolveDispatcher().useRef(initial);
134
+ }
135
+
136
+ export function useMemo<T>(factory: () => T, deps: unknown[]): T {
137
+ return resolveDispatcher().useMemo(factory, deps);
138
+ }
139
+
140
+ export function useCallback<T extends Function>(cb: T, deps: unknown[]): T {
141
+ return resolveDispatcher().useCallback(cb, deps) as T;
142
+ }
143
+
144
+ export function useReducer<S, A, I = S>(
145
+ reducer: (state: S, action: A) => S,
146
+ initialArg: I,
147
+ init?: (arg: I) => S,
148
+ ): [S, (action: A) => void] {
149
+ if (init) {
150
+ return resolveDispatcher().useReducer(reducer, initialArg, init) as [S, (action: A) => void];
151
+ }
152
+ return resolveDispatcher().useReducer(reducer, initialArg as unknown as S) as [S, (action: A) => void];
153
+ }
154
+
155
+ export function useSyncExternalStore<T>(
156
+ subscribe: (onStoreChange: () => void) => () => void,
157
+ getSnapshot: () => T,
158
+ getServerSnapshot?: () => T,
159
+ ): T {
160
+ return resolveDispatcher().useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
161
+ }
162
+
163
+ export function useImperativeHandle<T>(
164
+ ref: { current: T | null } | ((value: T | null) => void) | null | undefined,
165
+ create: () => T,
166
+ deps?: unknown[],
167
+ ): void {
168
+ return resolveDispatcher().useImperativeHandle(ref, create, deps);
169
+ }
170
+
171
+ export function useDebugValue(value: unknown): void {
172
+ return resolveDispatcher().useDebugValue(value);
173
+ }
174
+
175
+ export function useContext<T>(context: unknown): T {
176
+ return resolveDispatcher().useContext(context as never) as T;
177
+ }
178
+
179
+ export function useId(): string {
180
+ return resolveDispatcher().useId();
181
+ }
182
+
183
+ export function startTransition(callback: () => void): void {
184
+ callback();
185
+ }
186
+
187
+ export function useTransition(): [boolean, (callback: () => void) => void] {
188
+ return resolveDispatcher().useTransition();
189
+ }
190
+
191
+ export function useDeferredValue<T>(value: T, initialValue?: T): T {
192
+ return resolveDispatcher().useDeferredValue(value, initialValue);
193
+ }
194
+
195
+ export { createElement, Fragment, createContext, memo };
196
+ export {
197
+ createRef,
198
+ registerExternalReactModule,
199
+ __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
200
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
201
+ };
202
+
203
+ export const version = "19.0.0-refract-compat";
204
+
205
+ const ReactCompat = {
206
+ Children,
207
+ Fragment,
208
+ createContext,
209
+ createElement,
210
+ cloneElement,
211
+ createRef,
212
+ forwardRef,
213
+ isValidElement,
214
+ memo,
215
+ registerExternalReactModule,
216
+ startTransition,
217
+ useCallback,
218
+ useContext,
219
+ useDebugValue,
220
+ useDeferredValue,
221
+ useEffect,
222
+ useId,
223
+ useImperativeHandle,
224
+ useInsertionEffect,
225
+ useLayoutEffect,
226
+ useMemo,
227
+ useReducer,
228
+ useRef,
229
+ useState,
230
+ useSyncExternalStore,
231
+ useTransition,
232
+ version,
233
+ $$typeof: REACT_ELEMENT_TYPE,
234
+ __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
235
+ __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
236
+ };
237
+
238
+ export default ReactCompat;