@pyreon/react-compat 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/analysis/jsx-runtime.js.html +1 -1
- package/lib/index.js +370 -40
- package/lib/jsx-runtime.js +57 -5
- package/lib/types/index.d.ts +205 -4
- package/package.json +8 -4
- package/src/env.d.ts +6 -0
- package/src/index.ts +549 -52
- package/src/jsx-runtime.ts +93 -2
- package/src/react-compat-rerender.browser.test.tsx +59 -0
- package/src/react-compat.browser.test.tsx +34 -0
- package/src/tests/compat-integration.test.tsx +1 -0
- package/src/tests/native-marker-bypass.test.tsx +88 -0
- package/src/tests/new-apis.test.ts +1519 -0
- package/src/tests/react-compat.test.ts +2 -0
- package/lib/dom.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/jsx-runtime.js.map +0 -1
- package/lib/types/dom.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/jsx-runtime.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -147,3 +147,23 @@ function Dashboard() {
|
|
|
147
147
|
- **`useErrorBoundary`** -- alias for `onErrorCaptured`.
|
|
148
148
|
- **`createSelector`** -- O(1) equality selector from `@pyreon/reactivity`.
|
|
149
149
|
- **`createElement` / `h`**, **`Fragment`** -- JSX runtime.
|
|
150
|
+
|
|
151
|
+
## Composing Pyreon framework components inside react-compat
|
|
152
|
+
|
|
153
|
+
Pyreon's framework components (`RouterView`, `PyreonUI`, `FormProvider`, `QueryClientProvider`, …) ship marked with `nativeCompat()` from `@pyreon/core` — react-compat's JSX runtime detects the marker and routes them through Pyreon's setup frame instead of the compat wrapper. **You don't need to do anything** for the 24 components shipped marked.
|
|
154
|
+
|
|
155
|
+
If you write your **own** Pyreon-flavored helper that uses `provide()` / `onMount()` / `onUnmount()` / `effect()` at component-body scope and use it in a react-compat app, mark it explicitly:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { nativeCompat, provide, createContext } from '@pyreon/core'
|
|
159
|
+
|
|
160
|
+
const MyCtx = createContext<string>('default')
|
|
161
|
+
|
|
162
|
+
function MyProvider(props: { value: string; children?: unknown }) {
|
|
163
|
+
provide(MyCtx, props.value)
|
|
164
|
+
return props.children as never
|
|
165
|
+
}
|
|
166
|
+
nativeCompat(MyProvider) // ← required for compat-mode apps
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Without the marker, the wrapper relocates the body's render context and `provide()` lands in a torn-down context stack — descendants read the default. See [`packages/core/core/src/compat-marker.ts`](../../core/core/src/compat-marker.ts) for details.
|
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"4d7452bf-1","name":"jsx-runtime.ts"},{"uid":"4d7452bf-3","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"4d7452bf-1":{"renderedLength":186,"gzipLength":138,"brotliLength":0,"metaUid":"4d7452bf-0"},"4d7452bf-3":{"renderedLength":16657,"gzipLength":4670,"brotliLength":0,"metaUid":"4d7452bf-2"}},"nodeMetas":{"4d7452bf-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"index.js":"4d7452bf-1"},"imported":[{"uid":"4d7452bf-4"},{"uid":"4d7452bf-5"}],"importedBy":[{"uid":"4d7452bf-2"}]},"4d7452bf-2":{"id":"/src/index.ts","moduleParts":{"index.js":"4d7452bf-3"},"imported":[{"uid":"4d7452bf-4"},{"uid":"4d7452bf-5"},{"uid":"4d7452bf-0"}],"importedBy":[],"isEntry":true},"4d7452bf-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"4d7452bf-2"},{"uid":"4d7452bf-0"}]},"4d7452bf-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"4d7452bf-2"},{"uid":"4d7452bf-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"jsx-runtime.js","children":[{"name":"src","children":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"jsx-runtime.js","children":[{"name":"src","children":[{"uid":"8146f520-1","name":"jsx-runtime.ts"},{"uid":"8146f520-3","name":"jsx-dev-runtime.ts"}]}]}],"isRoot":true},"nodeParts":{"8146f520-1":{"renderedLength":4794,"gzipLength":1456,"brotliLength":0,"metaUid":"8146f520-0"},"8146f520-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"8146f520-2"}},"nodeMetas":{"8146f520-0":{"id":"/src/jsx-runtime.ts","moduleParts":{"jsx-runtime.js":"8146f520-1"},"imported":[{"uid":"8146f520-4"},{"uid":"8146f520-5"}],"importedBy":[{"uid":"8146f520-2"}]},"8146f520-2":{"id":"/src/jsx-dev-runtime.ts","moduleParts":{"jsx-runtime.js":"8146f520-3"},"imported":[{"uid":"8146f520-0"}],"importedBy":[],"isEntry":true},"8146f520-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"8146f520-0"}]},"8146f520-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"8146f520-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ErrorBoundary, Fragment, Portal, Suspense, createContext, h, h as createElement, h as h$1, lazy, useContext } from "@pyreon/core";
|
|
1
|
+
import { ErrorBoundary, Fragment, Portal, Suspense, createContext as createContext$1, createRef, h, h as createElement, h as h$1, lazy, nativeCompat, provide, useContext as useContext$1 } from "@pyreon/core";
|
|
2
2
|
import { batch } from "@pyreon/reactivity";
|
|
3
3
|
|
|
4
4
|
//#region src/jsx-runtime.ts
|
|
@@ -31,33 +31,50 @@ function depsChanged(a, b) {
|
|
|
31
31
|
function useState(initial) {
|
|
32
32
|
const ctx = requireCtx();
|
|
33
33
|
const idx = getHookIndex();
|
|
34
|
-
if (ctx.hooks.length <= idx)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
if (ctx.hooks.length <= idx) {
|
|
35
|
+
const entry = {
|
|
36
|
+
value: typeof initial === "function" ? initial() : initial,
|
|
37
|
+
setter: null
|
|
38
|
+
};
|
|
39
|
+
entry.setter = (v) => {
|
|
40
|
+
const current = entry.value;
|
|
41
|
+
const next = typeof v === "function" ? v(current) : v;
|
|
42
|
+
if (Object.is(current, next)) return;
|
|
43
|
+
entry.value = next;
|
|
44
|
+
ctx.scheduleRerender();
|
|
45
|
+
};
|
|
46
|
+
ctx.hooks.push(entry);
|
|
47
|
+
}
|
|
48
|
+
const entry = ctx.hooks[idx];
|
|
49
|
+
return [entry.value, entry.setter];
|
|
44
50
|
}
|
|
45
51
|
/**
|
|
46
52
|
* React-compatible `useReducer` — returns `[state, dispatch]`.
|
|
53
|
+
* Supports the 3-argument form: `useReducer(reducer, initialArg, init)`.
|
|
47
54
|
*/
|
|
48
|
-
function useReducer(reducer,
|
|
55
|
+
function useReducer(reducer, initialArg, init) {
|
|
49
56
|
const ctx = requireCtx();
|
|
50
57
|
const idx = getHookIndex();
|
|
51
|
-
if (ctx.hooks.length <= idx)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if (ctx.hooks.length <= idx) {
|
|
59
|
+
let initial;
|
|
60
|
+
if (init) initial = init(initialArg);
|
|
61
|
+
else if (typeof initialArg === "function") initial = initialArg();
|
|
62
|
+
else initial = initialArg;
|
|
63
|
+
const entry = {
|
|
64
|
+
value: initial,
|
|
65
|
+
dispatch: null
|
|
66
|
+
};
|
|
67
|
+
entry.dispatch = (action) => {
|
|
68
|
+
const current = entry.value;
|
|
69
|
+
const next = reducer(current, action);
|
|
70
|
+
if (Object.is(current, next)) return;
|
|
71
|
+
entry.value = next;
|
|
72
|
+
ctx.scheduleRerender();
|
|
73
|
+
};
|
|
74
|
+
ctx.hooks.push(entry);
|
|
75
|
+
}
|
|
76
|
+
const entry = ctx.hooks[idx];
|
|
77
|
+
return [entry.value, entry.dispatch];
|
|
61
78
|
}
|
|
62
79
|
/**
|
|
63
80
|
* React-compatible `useEffect` — runs after render when deps change.
|
|
@@ -107,6 +124,30 @@ function useLayoutEffect(fn, deps) {
|
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
126
|
/**
|
|
127
|
+
* React-compatible `useInsertionEffect` — runs synchronously before layout effects.
|
|
128
|
+
* Intended for CSS-in-JS libraries to inject styles before DOM reads.
|
|
129
|
+
*/
|
|
130
|
+
function useInsertionEffect(fn, deps) {
|
|
131
|
+
const ctx = requireCtx();
|
|
132
|
+
const idx = getHookIndex();
|
|
133
|
+
if (ctx.hooks.length <= idx) {
|
|
134
|
+
const entry = {
|
|
135
|
+
fn,
|
|
136
|
+
deps,
|
|
137
|
+
cleanup: void 0
|
|
138
|
+
};
|
|
139
|
+
ctx.hooks.push(entry);
|
|
140
|
+
ctx.pendingInsertionEffects.push(entry);
|
|
141
|
+
} else {
|
|
142
|
+
const entry = ctx.hooks[idx];
|
|
143
|
+
if (depsChanged(entry.deps, deps)) {
|
|
144
|
+
entry.fn = fn;
|
|
145
|
+
entry.deps = deps;
|
|
146
|
+
ctx.pendingInsertionEffects.push(entry);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
110
151
|
* React-compatible `useMemo` — returns the cached value, recomputed when deps change.
|
|
111
152
|
*/
|
|
112
153
|
function useMemo(fn, deps) {
|
|
@@ -145,6 +186,69 @@ function useRef(initial) {
|
|
|
145
186
|
}
|
|
146
187
|
return ctx.hooks[idx];
|
|
147
188
|
}
|
|
189
|
+
const COMPAT_CTX = Symbol.for("pyreon:compat-ctx");
|
|
190
|
+
const COMPAT_CTX_BRAND = COMPAT_CTX;
|
|
191
|
+
/**
|
|
192
|
+
* React-compatible `createContext` — creates a context with a Provider that
|
|
193
|
+
* supports nested Providers (inner overrides outer for its subtree) and
|
|
194
|
+
* notifies all `useContext` consumers when its value changes.
|
|
195
|
+
*/
|
|
196
|
+
function createContext(defaultValue) {
|
|
197
|
+
const pyreonCtx = createContext$1({
|
|
198
|
+
value: defaultValue,
|
|
199
|
+
subscribers: /* @__PURE__ */ new Set()
|
|
200
|
+
});
|
|
201
|
+
const defaultSubscribers = /* @__PURE__ */ new Set();
|
|
202
|
+
const Provider = (props) => {
|
|
203
|
+
const frame = {
|
|
204
|
+
value: props.value,
|
|
205
|
+
subscribers: /* @__PURE__ */ new Set()
|
|
206
|
+
};
|
|
207
|
+
provide(pyreonCtx, frame);
|
|
208
|
+
return () => {
|
|
209
|
+
const { value, children } = props;
|
|
210
|
+
if (!Object.is(frame.value, value)) {
|
|
211
|
+
frame.value = value;
|
|
212
|
+
for (const sub of frame.subscribers) sub();
|
|
213
|
+
}
|
|
214
|
+
return children ?? null;
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
nativeCompat(Provider);
|
|
218
|
+
return {
|
|
219
|
+
[COMPAT_CTX_BRAND]: true,
|
|
220
|
+
_defaultValue: defaultValue,
|
|
221
|
+
_pyreonCtx: pyreonCtx,
|
|
222
|
+
_subscribers: defaultSubscribers,
|
|
223
|
+
Provider
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* React-compatible `useContext` — reads the current context value and
|
|
228
|
+
* subscribes the calling component to future value changes.
|
|
229
|
+
*
|
|
230
|
+
* Reads from Pyreon's tree-scoped context stack (correct nesting) and
|
|
231
|
+
* subscribes to the nearest Provider's subscriber set for re-rendering.
|
|
232
|
+
*
|
|
233
|
+
* Works with both compat contexts (from this module's `createContext`) and
|
|
234
|
+
* Pyreon native contexts (from `@pyreon/core`).
|
|
235
|
+
*/
|
|
236
|
+
function useContext(context) {
|
|
237
|
+
if (COMPAT_CTX in context) {
|
|
238
|
+
const frame = useContext$1(context._pyreonCtx);
|
|
239
|
+
const renderCtx = getCurrentCtx();
|
|
240
|
+
if (renderCtx) {
|
|
241
|
+
const idx = getHookIndex();
|
|
242
|
+
if (renderCtx.hooks.length <= idx) {
|
|
243
|
+
const sub = () => renderCtx.scheduleRerender();
|
|
244
|
+
frame.subscribers.add(sub);
|
|
245
|
+
renderCtx.hooks.push({ _contextUnsub: () => frame.subscribers.delete(sub) });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return frame.value;
|
|
249
|
+
}
|
|
250
|
+
return useContext$1(context);
|
|
251
|
+
}
|
|
148
252
|
let _idCounter = 0;
|
|
149
253
|
/**
|
|
150
254
|
* React-compatible `useId` — returns a stable unique string per hook call.
|
|
@@ -155,26 +259,47 @@ function useId() {
|
|
|
155
259
|
if (ctx.hooks.length <= idx) ctx.hooks.push(`:r${(_idCounter++).toString(36)}:`);
|
|
156
260
|
return ctx.hooks[idx];
|
|
157
261
|
}
|
|
262
|
+
function shallowEqual(a, b) {
|
|
263
|
+
const keysA = Object.keys(a);
|
|
264
|
+
const keysB = Object.keys(b);
|
|
265
|
+
if (keysA.length !== keysB.length) return false;
|
|
266
|
+
for (const k of keysA) if (!Object.is(a[k], b[k])) return false;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
158
269
|
/**
|
|
159
270
|
* React-compatible `memo` — wraps a component to skip re-render when props
|
|
160
271
|
* are shallowly equal.
|
|
272
|
+
*
|
|
273
|
+
* Each component INSTANCE gets its own props/result cache via a hook slot,
|
|
274
|
+
* so two `<MemoComp />` usages don't share memoization state.
|
|
161
275
|
*/
|
|
162
276
|
function memo(component, areEqual) {
|
|
163
|
-
const compare = areEqual ??
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
277
|
+
const compare = areEqual ?? shallowEqual;
|
|
278
|
+
const MEMO_MARKER = Symbol.for("pyreon:memo");
|
|
279
|
+
let _fallbackPrevProps = null;
|
|
280
|
+
let _fallbackPrevResult = null;
|
|
281
|
+
const memoized = (props) => {
|
|
282
|
+
const ctx = getCurrentCtx();
|
|
283
|
+
if (ctx) {
|
|
284
|
+
const idx = getHookIndex();
|
|
285
|
+
if (ctx.hooks.length <= idx) ctx.hooks.push({
|
|
286
|
+
prevProps: null,
|
|
287
|
+
prevResult: null
|
|
288
|
+
});
|
|
289
|
+
const cache = ctx.hooks[idx];
|
|
290
|
+
if (cache.prevProps !== null && compare(cache.prevProps, props)) return cache.prevResult;
|
|
291
|
+
cache.prevProps = props;
|
|
292
|
+
cache.prevResult = component(props);
|
|
293
|
+
return cache.prevResult;
|
|
294
|
+
}
|
|
295
|
+
if (_fallbackPrevProps !== null && compare(_fallbackPrevProps, props)) return _fallbackPrevResult;
|
|
296
|
+
_fallbackPrevProps = props;
|
|
297
|
+
_fallbackPrevResult = component(props);
|
|
298
|
+
return _fallbackPrevResult;
|
|
177
299
|
};
|
|
300
|
+
memoized[MEMO_MARKER] = true;
|
|
301
|
+
memoized.displayName = component.displayName || component.name || "Memo";
|
|
302
|
+
return memoized;
|
|
178
303
|
}
|
|
179
304
|
/**
|
|
180
305
|
* React-compatible `useTransition` — no concurrent mode in Pyreon.
|
|
@@ -214,10 +339,12 @@ function createPortal(children, target) {
|
|
|
214
339
|
* The render function receives (props, ref) — we merge ref into props.
|
|
215
340
|
*/
|
|
216
341
|
function forwardRef(render) {
|
|
217
|
-
|
|
342
|
+
const forwarded = (props) => {
|
|
218
343
|
const { ref, ...rest } = props;
|
|
219
344
|
return render(rest, ref ?? null);
|
|
220
345
|
};
|
|
346
|
+
forwarded.displayName = render.displayName || render.name || "ForwardRef";
|
|
347
|
+
return forwarded;
|
|
221
348
|
}
|
|
222
349
|
/**
|
|
223
350
|
* React-compatible `cloneElement` — creates a new VNode with merged props.
|
|
@@ -242,40 +369,243 @@ function flattenChildren(children) {
|
|
|
242
369
|
* React-compatible `Children` utilities for working with VNode children.
|
|
243
370
|
*/
|
|
244
371
|
const Children = {
|
|
372
|
+
/**
|
|
373
|
+
* Iterate over children, calling `fn` for each non-null child.
|
|
374
|
+
*/
|
|
245
375
|
map(children, fn) {
|
|
246
376
|
const flat = flattenChildren(children);
|
|
247
377
|
const result = [];
|
|
378
|
+
let validIndex = 0;
|
|
248
379
|
for (let i = 0; i < flat.length; i++) {
|
|
249
380
|
const child = flat[i];
|
|
250
381
|
if (child == null || child === true || child === false) continue;
|
|
251
|
-
|
|
382
|
+
const mapped = fn(child, validIndex);
|
|
383
|
+
if (mapped && typeof mapped === "object" && "type" in mapped && "props" in mapped) {
|
|
384
|
+
const vnode = mapped;
|
|
385
|
+
if (vnode.key == null) vnode.key = `.${validIndex}`;
|
|
386
|
+
}
|
|
387
|
+
result.push(mapped);
|
|
388
|
+
validIndex++;
|
|
252
389
|
}
|
|
253
390
|
return result;
|
|
254
391
|
},
|
|
392
|
+
/**
|
|
393
|
+
* Call `fn` for each non-null child (no return value).
|
|
394
|
+
*/
|
|
255
395
|
forEach(children, fn) {
|
|
256
396
|
const flat = flattenChildren(children);
|
|
397
|
+
let validIndex = 0;
|
|
257
398
|
for (let i = 0; i < flat.length; i++) {
|
|
258
399
|
const child = flat[i];
|
|
259
400
|
if (child == null || child === true || child === false) continue;
|
|
260
|
-
fn(child,
|
|
401
|
+
fn(child, validIndex++);
|
|
261
402
|
}
|
|
262
403
|
},
|
|
404
|
+
/**
|
|
405
|
+
* Count non-null children.
|
|
406
|
+
*/
|
|
263
407
|
count(children) {
|
|
264
408
|
const flat = flattenChildren(children);
|
|
265
409
|
let count = 0;
|
|
266
410
|
for (const child of flat) if (child != null && child !== true && child !== false) count++;
|
|
267
411
|
return count;
|
|
268
412
|
},
|
|
413
|
+
/**
|
|
414
|
+
* Convert children to a flat array.
|
|
415
|
+
*/
|
|
269
416
|
toArray(children) {
|
|
270
417
|
return flattenChildren(children).filter((child) => child != null && child !== true && child !== false);
|
|
271
418
|
},
|
|
419
|
+
/**
|
|
420
|
+
* Assert and return the only child. Throws if not exactly one child.
|
|
421
|
+
*/
|
|
272
422
|
only(children) {
|
|
273
423
|
const arr = Children.toArray(children);
|
|
274
424
|
if (arr.length !== 1) throw new Error("[Pyreon] Children.only expected exactly one child");
|
|
275
425
|
return arr[0];
|
|
276
426
|
}
|
|
277
427
|
};
|
|
428
|
+
/**
|
|
429
|
+
* React-compatible `useSyncExternalStore` — subscribes to an external store.
|
|
430
|
+
* Re-subscribes automatically when the `subscribe` function identity changes.
|
|
431
|
+
*/
|
|
432
|
+
function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
|
|
433
|
+
const ctx = requireCtx();
|
|
434
|
+
const idx = getHookIndex();
|
|
435
|
+
if (typeof window === "undefined" && getServerSnapshot) {
|
|
436
|
+
if (ctx.hooks.length <= idx) ctx.hooks.push({
|
|
437
|
+
subscribe,
|
|
438
|
+
unsubscribe: void 0,
|
|
439
|
+
snapshot: getServerSnapshot()
|
|
440
|
+
});
|
|
441
|
+
return ctx.hooks[idx].snapshot;
|
|
442
|
+
}
|
|
443
|
+
if (ctx.hooks.length <= idx) {
|
|
444
|
+
const snapshot = getSnapshot();
|
|
445
|
+
const entry = {
|
|
446
|
+
subscribe,
|
|
447
|
+
unsubscribe: void 0,
|
|
448
|
+
snapshot
|
|
449
|
+
};
|
|
450
|
+
const onChange = () => {
|
|
451
|
+
const next = getSnapshot();
|
|
452
|
+
if (!Object.is(entry.snapshot, next)) {
|
|
453
|
+
entry.snapshot = next;
|
|
454
|
+
ctx.scheduleRerender();
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
entry.unsubscribe = subscribe(onChange);
|
|
458
|
+
ctx.hooks.push(entry);
|
|
459
|
+
return snapshot;
|
|
460
|
+
}
|
|
461
|
+
const entry = ctx.hooks[idx];
|
|
462
|
+
if (entry.subscribe !== subscribe) {
|
|
463
|
+
if (entry.unsubscribe) entry.unsubscribe();
|
|
464
|
+
const onChange = () => {
|
|
465
|
+
const next = getSnapshot();
|
|
466
|
+
if (!Object.is(entry.snapshot, next)) {
|
|
467
|
+
entry.snapshot = next;
|
|
468
|
+
ctx.scheduleRerender();
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
entry.unsubscribe = subscribe(onChange);
|
|
472
|
+
entry.subscribe = subscribe;
|
|
473
|
+
}
|
|
474
|
+
entry.snapshot = getSnapshot();
|
|
475
|
+
return entry.snapshot;
|
|
476
|
+
}
|
|
477
|
+
const _promiseCache = /* @__PURE__ */ new WeakMap();
|
|
478
|
+
/**
|
|
479
|
+
* React-compatible `use` — reads a Context or suspends on a Promise.
|
|
480
|
+
* Can be called conditionally (unlike other hooks).
|
|
481
|
+
*
|
|
482
|
+
* IMPORTANT: Promises must have a stable identity across renders.
|
|
483
|
+
* Create promises outside the component or memoize them. Calling
|
|
484
|
+
* `use(fetch('/api'))` creates a new promise each render and will
|
|
485
|
+
* cause infinite suspension.
|
|
486
|
+
*/
|
|
487
|
+
function use(resource) {
|
|
488
|
+
if (resource && typeof resource === "object" && COMPAT_CTX in resource) return useContext(resource);
|
|
489
|
+
if (resource && typeof resource === "object" && "id" in resource && "defaultValue" in resource) return useContext$1(resource);
|
|
490
|
+
const promise = resource;
|
|
491
|
+
let entry = _promiseCache.get(promise);
|
|
492
|
+
if (!entry) {
|
|
493
|
+
entry = { status: "pending" };
|
|
494
|
+
_promiseCache.set(promise, entry);
|
|
495
|
+
promise.then((value) => {
|
|
496
|
+
entry.status = "resolved";
|
|
497
|
+
entry.value = value;
|
|
498
|
+
}, (error) => {
|
|
499
|
+
entry.status = "rejected";
|
|
500
|
+
entry.error = error;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (entry.status === "resolved") return entry.value;
|
|
504
|
+
if (entry.status === "rejected") throw entry.error;
|
|
505
|
+
throw promise;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* React-compatible `useActionState` — manages async action state with pending indicator.
|
|
509
|
+
*/
|
|
510
|
+
function useActionState(action, initialState) {
|
|
511
|
+
const [state, setState] = useState(initialState);
|
|
512
|
+
const [isPending, setIsPending] = useState(false);
|
|
513
|
+
const dispatch = (payload) => {
|
|
514
|
+
setIsPending(true);
|
|
515
|
+
const result = action(state, payload);
|
|
516
|
+
if (result instanceof Promise) result.then((next) => {
|
|
517
|
+
setState(next);
|
|
518
|
+
setIsPending(false);
|
|
519
|
+
});
|
|
520
|
+
else {
|
|
521
|
+
setState(result);
|
|
522
|
+
setIsPending(false);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
return [
|
|
526
|
+
state,
|
|
527
|
+
dispatch,
|
|
528
|
+
isPending
|
|
529
|
+
];
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* React-compatible `startTransition` — runs the callback synchronously.
|
|
533
|
+
* No concurrent mode in Pyreon, so transitions are immediate.
|
|
534
|
+
*/
|
|
535
|
+
function startTransition(fn) {
|
|
536
|
+
fn();
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* React-compatible `isValidElement` — checks if a value is a VNode.
|
|
540
|
+
*/
|
|
541
|
+
function isValidElement(value) {
|
|
542
|
+
return value != null && typeof value === "object" && "type" in value && "props" in value;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* React-compatible `useDebugValue` — no-op in Pyreon (no React DevTools integration).
|
|
546
|
+
*/
|
|
547
|
+
function useDebugValue(_value, _format) {}
|
|
548
|
+
/**
|
|
549
|
+
* React-compatible `flushSync` — runs the callback synchronously.
|
|
550
|
+
*
|
|
551
|
+
* BEHAVIORAL DIFFERENCE: In Pyreon's compat model, state updates are
|
|
552
|
+
* batched via microtask. flushSync runs the callback and returns its
|
|
553
|
+
* result, but the DOM updates triggered by state changes inside the
|
|
554
|
+
* callback still fire asynchronously. For DOM measurement after state
|
|
555
|
+
* updates, use `await act(() => setState(...))` in tests, or
|
|
556
|
+
* `requestAnimationFrame` in production code.
|
|
557
|
+
*/
|
|
558
|
+
function flushSync(fn) {
|
|
559
|
+
return fn();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* React-compatible `act` — flushes pending microtasks for testing.
|
|
563
|
+
*/
|
|
564
|
+
async function act(fn) {
|
|
565
|
+
const result = fn();
|
|
566
|
+
if (result instanceof Promise) await result;
|
|
567
|
+
await new Promise((r) => queueMicrotask(r));
|
|
568
|
+
await new Promise((r) => queueMicrotask(r));
|
|
569
|
+
}
|
|
570
|
+
const version = "19.0.0-pyreon";
|
|
571
|
+
/**
|
|
572
|
+
* React-compatible `StrictMode` — pass-through in Pyreon (no double-invoke behavior).
|
|
573
|
+
*/
|
|
574
|
+
function StrictMode(props) {
|
|
575
|
+
return props.children ?? null;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* React-compatible `Profiler` — pass-through in Pyreon (no profiling integration).
|
|
579
|
+
*/
|
|
580
|
+
function Profiler(props) {
|
|
581
|
+
return props.children ?? null;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* React-compatible `Component` class stub.
|
|
585
|
+
* Class components are not fully supported — use function components with hooks.
|
|
586
|
+
*/
|
|
587
|
+
var Component = class {
|
|
588
|
+
props;
|
|
589
|
+
state;
|
|
590
|
+
constructor(props) {
|
|
591
|
+
this.props = props;
|
|
592
|
+
this.state = {};
|
|
593
|
+
}
|
|
594
|
+
setState(_partial) {
|
|
595
|
+
console.warn("[Pyreon] Class component setState is not supported. Use function components with hooks.");
|
|
596
|
+
}
|
|
597
|
+
forceUpdate() {
|
|
598
|
+
console.warn("[Pyreon] Class component forceUpdate is not supported. Use function components with hooks.");
|
|
599
|
+
}
|
|
600
|
+
render() {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
/**
|
|
605
|
+
* React-compatible `PureComponent` class stub.
|
|
606
|
+
*/
|
|
607
|
+
var PureComponent = class extends Component {};
|
|
278
608
|
|
|
279
609
|
//#endregion
|
|
280
|
-
export { Children, ErrorBoundary, Fragment, Suspense, batch, cloneElement, createContext, createElement, createPortal, forwardRef, h, lazy, memo, useCallback, useContext, useDeferredValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useTransition };
|
|
610
|
+
export { Children, Component, ErrorBoundary, Fragment, Profiler, PureComponent, StrictMode, Suspense, act, batch, cloneElement, createContext, createElement, createPortal, createRef, flushSync, forwardRef, h, isValidElement, lazy, memo, startTransition, use, useActionState, useCallback, useContext, useDebugValue, useDeferredValue, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore, useTransition, version };
|
|
281
611
|
//# sourceMappingURL=index.js.map
|
package/lib/jsx-runtime.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
import { Fragment, h } from "@pyreon/core";
|
|
1
|
+
import { Fragment, h, isNativeCompat, onUnmount } from "@pyreon/core";
|
|
2
2
|
import { signal } from "@pyreon/reactivity";
|
|
3
3
|
|
|
4
4
|
//#region src/jsx-runtime.ts
|
|
5
5
|
let _currentCtx = null;
|
|
6
6
|
let _hookIndex = 0;
|
|
7
|
+
let _expectedHookCount = -1;
|
|
7
8
|
function beginRender(ctx) {
|
|
8
9
|
_currentCtx = ctx;
|
|
9
10
|
_hookIndex = 0;
|
|
11
|
+
ctx.pendingInsertionEffects = [];
|
|
10
12
|
ctx.pendingEffects = [];
|
|
11
13
|
ctx.pendingLayoutEffects = [];
|
|
14
|
+
if (ctx._hookCount !== void 0) _expectedHookCount = ctx._hookCount;
|
|
15
|
+
else _expectedHookCount = -1;
|
|
12
16
|
}
|
|
13
17
|
function endRender() {
|
|
18
|
+
if (_currentCtx) {
|
|
19
|
+
if (process.env.NODE_ENV !== "production" && _expectedHookCount !== -1 && _hookIndex !== _expectedHookCount) console.error(`[Pyreon] Hook count changed between renders (expected ${_expectedHookCount}, got ${_hookIndex}). This usually means a hook is called conditionally. Hooks must be called in the same order every render.`);
|
|
20
|
+
_currentCtx._hookCount = _hookIndex;
|
|
21
|
+
}
|
|
14
22
|
_currentCtx = null;
|
|
15
23
|
_hookIndex = 0;
|
|
16
24
|
}
|
|
@@ -40,6 +48,7 @@ function wrapCompatComponent(reactComponent) {
|
|
|
40
48
|
const ctx = {
|
|
41
49
|
hooks: [],
|
|
42
50
|
scheduleRerender: () => {},
|
|
51
|
+
pendingInsertionEffects: [],
|
|
43
52
|
pendingEffects: [],
|
|
44
53
|
pendingLayoutEffects: [],
|
|
45
54
|
unmounted: false
|
|
@@ -54,13 +63,32 @@ function wrapCompatComponent(reactComponent) {
|
|
|
54
63
|
if (!ctx.unmounted) version.set(version.peek() + 1);
|
|
55
64
|
});
|
|
56
65
|
};
|
|
66
|
+
onUnmount(() => {
|
|
67
|
+
ctx.unmounted = true;
|
|
68
|
+
for (const hook of ctx.hooks) {
|
|
69
|
+
if (hook && typeof hook === "object" && "cleanup" in hook) {
|
|
70
|
+
const entry = hook;
|
|
71
|
+
if (typeof entry.cleanup === "function") entry.cleanup();
|
|
72
|
+
}
|
|
73
|
+
if (hook && typeof hook === "object" && "unsubscribe" in hook) {
|
|
74
|
+
const sub = hook;
|
|
75
|
+
if (typeof sub.unsubscribe === "function") sub.unsubscribe();
|
|
76
|
+
}
|
|
77
|
+
if (hook && typeof hook === "object" && "_contextUnsub" in hook) {
|
|
78
|
+
const ctxHook = hook;
|
|
79
|
+
if (typeof ctxHook._contextUnsub === "function") ctxHook._contextUnsub();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
57
83
|
return () => {
|
|
58
84
|
version();
|
|
59
85
|
beginRender(ctx);
|
|
60
86
|
const result = reactComponent(props);
|
|
87
|
+
const insertionEffects = ctx.pendingInsertionEffects;
|
|
61
88
|
const layoutEffects = ctx.pendingLayoutEffects;
|
|
62
89
|
const effects = ctx.pendingEffects;
|
|
63
90
|
endRender();
|
|
91
|
+
runLayoutEffects(insertionEffects);
|
|
64
92
|
runLayoutEffects(layoutEffects);
|
|
65
93
|
scheduleEffects(ctx, effects);
|
|
66
94
|
return result;
|
|
@@ -75,10 +103,14 @@ function jsx(type, props, key) {
|
|
|
75
103
|
...rest,
|
|
76
104
|
key
|
|
77
105
|
} : rest;
|
|
78
|
-
if (typeof type === "function")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
if (typeof type === "function") {
|
|
107
|
+
const componentProps = children !== void 0 ? {
|
|
108
|
+
...propsWithKey,
|
|
109
|
+
children
|
|
110
|
+
} : propsWithKey;
|
|
111
|
+
if (isNativeCompat(type)) return h(type, componentProps);
|
|
112
|
+
return h(wrapCompatComponent(type), componentProps);
|
|
113
|
+
}
|
|
82
114
|
const childArray = children === void 0 ? [] : Array.isArray(children) ? children : [children];
|
|
83
115
|
if (typeof type === "string") {
|
|
84
116
|
if (propsWithKey.className !== void 0) {
|
|
@@ -89,6 +121,26 @@ function jsx(type, props, key) {
|
|
|
89
121
|
propsWithKey.for = propsWithKey.htmlFor;
|
|
90
122
|
delete propsWithKey.htmlFor;
|
|
91
123
|
}
|
|
124
|
+
if ((type === "input" || type === "textarea" || type === "select") && propsWithKey.onChange !== void 0) {
|
|
125
|
+
if (propsWithKey.onInput === void 0) propsWithKey.onInput = propsWithKey.onChange;
|
|
126
|
+
delete propsWithKey.onChange;
|
|
127
|
+
}
|
|
128
|
+
if (propsWithKey.autoFocus !== void 0) {
|
|
129
|
+
propsWithKey.autofocus = propsWithKey.autoFocus;
|
|
130
|
+
delete propsWithKey.autoFocus;
|
|
131
|
+
}
|
|
132
|
+
if (type === "input" || type === "textarea") {
|
|
133
|
+
if (propsWithKey.defaultValue !== void 0 && propsWithKey.value === void 0) {
|
|
134
|
+
propsWithKey.value = propsWithKey.defaultValue;
|
|
135
|
+
delete propsWithKey.defaultValue;
|
|
136
|
+
}
|
|
137
|
+
if (propsWithKey.defaultChecked !== void 0 && propsWithKey.checked === void 0) {
|
|
138
|
+
propsWithKey.checked = propsWithKey.defaultChecked;
|
|
139
|
+
delete propsWithKey.defaultChecked;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
delete propsWithKey.suppressHydrationWarning;
|
|
143
|
+
delete propsWithKey.suppressContentEditableWarning;
|
|
92
144
|
}
|
|
93
145
|
return h(type, propsWithKey, ...childArray);
|
|
94
146
|
}
|