@pyreon/core 0.22.0 → 0.24.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 +172 -54
- package/lib/_chunks/h-CYSD6aBx.js +48 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +140 -58
- package/lib/jsx-dev-runtime.js +3 -97
- package/lib/jsx-runtime.js +1 -45
- package/lib/types/index.d.ts +91 -2
- package/package.json +2 -2
- package/src/component.ts +33 -2
- package/src/context.ts +166 -17
- package/src/error-boundary.ts +7 -1
- package/src/index.ts +2 -0
- package/src/tests/context.test.ts +368 -0
- package/lib/analysis/jsx-dev-runtime.js.html +0 -5406
- package/lib/analysis/jsx-runtime.js.html +0 -5406
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as Fragment, r as h, t as EMPTY_PROPS } from "./_chunks/h-CYSD6aBx.js";
|
|
1
2
|
import { effect, getReactiveTrace, setSnapshotCapture, signal } from "@pyreon/reactivity";
|
|
2
3
|
|
|
3
4
|
//#region src/lifecycle.ts
|
|
@@ -145,8 +146,19 @@ const _errorBoundaryStack = [];
|
|
|
145
146
|
function pushErrorBoundary(handler) {
|
|
146
147
|
_errorBoundaryStack.push(handler);
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Remove a SPECIFIC handler from the error-boundary stack by reference
|
|
151
|
+
* identity. Each `ErrorBoundary` registers `onUnmount(() => popErrorBoundary(handler))`
|
|
152
|
+
* with its OWN handler — so unmount in any order (LIFO, FIFO, middle-out)
|
|
153
|
+
* correctly removes the right handler.
|
|
154
|
+
*/
|
|
155
|
+
function popErrorBoundary(handler) {
|
|
156
|
+
if (handler === void 0) {
|
|
157
|
+
_errorBoundaryStack.pop();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const idx = _errorBoundaryStack.lastIndexOf(handler);
|
|
161
|
+
if (idx !== -1) _errorBoundaryStack.splice(idx, 1);
|
|
150
162
|
}
|
|
151
163
|
/**
|
|
152
164
|
* Dispatch an error to the nearest active ErrorBoundary.
|
|
@@ -334,11 +346,59 @@ process.env.NODE_ENV;
|
|
|
334
346
|
function pushContext(values) {
|
|
335
347
|
getStack().push(values);
|
|
336
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Pop the LAST frame from the context stack.
|
|
351
|
+
*
|
|
352
|
+
* NOTE: position-based pop. Safe ONLY when the caller can guarantee that the
|
|
353
|
+
* top of the stack is the frame they want to remove (the strict LIFO contract).
|
|
354
|
+
* The `provide()` helper does NOT use this — it uses identity-based removal
|
|
355
|
+
* via `removeContextFrame` because reactive boundaries can push snapshot
|
|
356
|
+
* frames between a component's `provide(ctx, value)` and its eventual
|
|
357
|
+
* unmount, making the top-of-stack unsafe to assume.
|
|
358
|
+
*/
|
|
337
359
|
function popContext() {
|
|
338
360
|
const stack = getStack();
|
|
339
361
|
if (stack.length === 0) return;
|
|
340
362
|
stack.pop();
|
|
341
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Read the current live stack length WITHOUT allocating a snapshot.
|
|
366
|
+
*
|
|
367
|
+
* SSR cleanup uses this as a position marker: capture the live length
|
|
368
|
+
* before a component renders, pop the live stack back to that length
|
|
369
|
+
* after. Previously these sites called `captureContextStack().length`,
|
|
370
|
+
* which allocated a full snapshot array (potentially 40k+ entries
|
|
371
|
+
* under deeply-nested reactive boundaries — the same allocation the
|
|
372
|
+
* `captureContextStack` dedup work is designed to shrink) just to
|
|
373
|
+
* read its length. This helper avoids the allocation entirely AND
|
|
374
|
+
* decouples SSR cleanup from `captureContextStack`'s snapshot shape,
|
|
375
|
+
* so dedup at capture time can never silently break SSR length
|
|
376
|
+
* bookkeeping.
|
|
377
|
+
*/
|
|
378
|
+
function getContextStackLength() {
|
|
379
|
+
return getStack().length;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Remove a SPECIFIC frame from the context stack by reference identity.
|
|
383
|
+
*
|
|
384
|
+
* Internal — used by `provide()` and `withContext()` to safely clean up
|
|
385
|
+
* their pushed frame on unmount even when other frames have been pushed
|
|
386
|
+
* between push and pop (e.g. a reactive boundary's `restoreContextStack`
|
|
387
|
+
* pushing snapshot frames during the descendant's lifecycle). The
|
|
388
|
+
* symmetric position-based `popContext()` would pop the wrong frame in
|
|
389
|
+
* that case and orphan the descendant's provider frame on the live stack
|
|
390
|
+
* — the root cause of the 321k-entry context-stack leak under repeated
|
|
391
|
+
* reactive remounts.
|
|
392
|
+
*
|
|
393
|
+
* Uses `lastIndexOf` (LIFO match) — picks the most-recently-pushed frame
|
|
394
|
+
* with that exact reference, so `provide(ctx, a); provide(ctx, b)` followed
|
|
395
|
+
* by two unmounts removes them in reverse order.
|
|
396
|
+
*/
|
|
397
|
+
function removeContextFrame(frame) {
|
|
398
|
+
const stack = getStack();
|
|
399
|
+
const idx = stack.lastIndexOf(frame);
|
|
400
|
+
if (idx !== -1) stack.splice(idx, 1);
|
|
401
|
+
}
|
|
342
402
|
function useContext(context) {
|
|
343
403
|
const stack = getStack();
|
|
344
404
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
@@ -359,31 +419,95 @@ function useContext(context) {
|
|
|
359
419
|
* }
|
|
360
420
|
*/
|
|
361
421
|
function provide(context, value) {
|
|
362
|
-
|
|
363
|
-
|
|
422
|
+
const frame = new Map([[context.id, value]]);
|
|
423
|
+
pushContext(frame);
|
|
424
|
+
onUnmount(() => removeContextFrame(frame));
|
|
364
425
|
}
|
|
365
426
|
/**
|
|
366
427
|
* Provide a value for `context` during `fn()`.
|
|
367
428
|
* Used by the renderer when it encounters a `<Provider>` component.
|
|
368
429
|
*/
|
|
369
430
|
function withContext(context, value, fn) {
|
|
370
|
-
|
|
431
|
+
const frame = new Map([[context.id, value]]);
|
|
432
|
+
pushContext(frame);
|
|
371
433
|
try {
|
|
372
434
|
fn();
|
|
373
435
|
} finally {
|
|
374
|
-
|
|
436
|
+
removeContextFrame(frame);
|
|
375
437
|
}
|
|
376
438
|
}
|
|
377
439
|
/**
|
|
378
|
-
* Capture a snapshot of the current context stack
|
|
440
|
+
* Capture a snapshot of the current context stack, **deduplicated** so
|
|
441
|
+
* only the topmost frame for each context-id is retained.
|
|
379
442
|
*
|
|
380
443
|
* Used by `mountReactive` to preserve the context that was active when a
|
|
381
444
|
* reactive boundary (e.g. `<Show>`, `<For>`) was set up. When the boundary
|
|
382
445
|
* later mounts new children inside an effect, the snapshot is restored so
|
|
383
446
|
* those children can see ancestor providers via `useContext()`.
|
|
447
|
+
*
|
|
448
|
+
* **Why dedup is semantically equivalent to a full snapshot:**
|
|
449
|
+
* `useContext()` walks the stack in reverse and returns the first frame
|
|
450
|
+
* matching the requested context-id (`for (let i = stack.length - 1; i >= 0; i--)`
|
|
451
|
+
* — see implementation below in this file). Any frame deeper in the
|
|
452
|
+
* stack that ALSO provides the same id is unreachable by definition —
|
|
453
|
+
* the reverse walk stops at the first match. Those shadowed frames are
|
|
454
|
+
* dead weight in the snapshot: they carry no observable value, they
|
|
455
|
+
* cost memory, and they can NEVER affect program behavior.
|
|
456
|
+
*
|
|
457
|
+
* The dedup walks frames from top to bottom keeping a `seen` set of
|
|
458
|
+
* already-resolved context ids. A frame is kept iff at least one of
|
|
459
|
+
* its keys is NOT in `seen` (i.e. it's the topmost provider for at
|
|
460
|
+
* least one id). All of a frame's keys are added to `seen` regardless
|
|
461
|
+
* of whether the frame is kept — `seen` represents "ids that are
|
|
462
|
+
* already provided by a more-recent frame".
|
|
463
|
+
*
|
|
464
|
+
* **Why this is safe for `restoreContextStack`:**
|
|
465
|
+
* `restoreContextStack` pushes the snapshot's frames onto the live
|
|
466
|
+
* stack, runs `fn()`, then removes those frames by **reference
|
|
467
|
+
* identity** (`stack.lastIndexOf(frame)`) — NOT by position or count
|
|
468
|
+
* of the snapshot. A deduped snapshot pushes fewer frames; the same
|
|
469
|
+
* reference-identity cleanup removes exactly those frames. No
|
|
470
|
+
* bookkeeping invariant breaks.
|
|
471
|
+
*
|
|
472
|
+
* **Why this is safe for the live stack length invariant:**
|
|
473
|
+
* SSR cleanup uses `getContextStackLength()` (a sibling helper) for
|
|
474
|
+
* position-marker bookkeeping. That helper reads the LIVE stack
|
|
475
|
+
* length, NOT the snapshot length, so dedup at capture time has zero
|
|
476
|
+
* effect on SSR cleanup behavior.
|
|
477
|
+
*
|
|
478
|
+
* **Why this is needed:**
|
|
479
|
+
* Under deeply-nested reactive boundaries (a `<Show>` inside a `<For>`
|
|
480
|
+
* inside a `<Suspense>`, each effect capturing its own snapshot at
|
|
481
|
+
* setup time), the live stack temporarily holds the same context-id
|
|
482
|
+
* pushed multiple times during nested `restoreContextStack` windows.
|
|
483
|
+
* The pre-dedup `[...getStack()]` snapshot baked those duplicates in
|
|
484
|
+
* permanently — each effect's closure retained an O(stack-depth)
|
|
485
|
+
* array for its lifetime. Reported heap snapshots from 0.21.x showed
|
|
486
|
+
* 1.22 MB / 321k-entry arrays from this pattern. The 0.23.0
|
|
487
|
+
* restoreContextStack reference-identity fix cleaned the LIVE stack
|
|
488
|
+
* but left the residual snapshot-amplification — observable as 20
|
|
489
|
+
* arrays at 157 KB each (40k entries) retained by effect closures.
|
|
490
|
+
* This dedup collapses each captured snapshot to ~N entries, where
|
|
491
|
+
* N is the number of DISTINCT context ids in scope (typically 2-10
|
|
492
|
+
* in real apps).
|
|
384
493
|
*/
|
|
385
494
|
function captureContextStack() {
|
|
386
|
-
|
|
495
|
+
const stack = getStack();
|
|
496
|
+
if (stack.length <= 1) return stack.slice();
|
|
497
|
+
const seen = /* @__PURE__ */ new Set();
|
|
498
|
+
const reversed = [];
|
|
499
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
500
|
+
const frame = stack[i];
|
|
501
|
+
if (!frame) continue;
|
|
502
|
+
let unique = false;
|
|
503
|
+
for (const id of frame.keys()) if (!seen.has(id)) {
|
|
504
|
+
seen.add(id);
|
|
505
|
+
unique = true;
|
|
506
|
+
}
|
|
507
|
+
if (unique) reversed.push(frame);
|
|
508
|
+
}
|
|
509
|
+
reversed.reverse();
|
|
510
|
+
return reversed;
|
|
387
511
|
}
|
|
388
512
|
/**
|
|
389
513
|
* Execute `fn()` with a previously captured context stack active.
|
|
@@ -400,12 +524,16 @@ function captureContextStack() {
|
|
|
400
524
|
*/
|
|
401
525
|
function restoreContextStack(snapshot, fn) {
|
|
402
526
|
const stack = getStack();
|
|
403
|
-
const insertIndex = stack.length;
|
|
404
527
|
for (const frame of snapshot) stack.push(frame);
|
|
405
528
|
try {
|
|
406
529
|
return fn();
|
|
407
530
|
} finally {
|
|
408
|
-
|
|
531
|
+
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
532
|
+
const frame = snapshot[i];
|
|
533
|
+
if (!frame) continue;
|
|
534
|
+
const idx = stack.lastIndexOf(frame);
|
|
535
|
+
if (idx !== -1) stack.splice(idx, 1);
|
|
536
|
+
}
|
|
409
537
|
}
|
|
410
538
|
}
|
|
411
539
|
setSnapshotCapture({
|
|
@@ -413,52 +541,6 @@ setSnapshotCapture({
|
|
|
413
541
|
restore: (snap, fn) => restoreContextStack(snap, fn)
|
|
414
542
|
});
|
|
415
543
|
|
|
416
|
-
//#endregion
|
|
417
|
-
//#region src/h.ts
|
|
418
|
-
/**
|
|
419
|
-
* Marker for fragment nodes — renders children without a wrapper element.
|
|
420
|
-
*
|
|
421
|
-
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
422
|
-
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
423
|
-
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
424
|
-
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
425
|
-
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
426
|
-
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
427
|
-
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
428
|
-
* fell through to `renderElement` and crashed SSG with
|
|
429
|
-
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
430
|
-
* `Symbol.for()` keys by string in a global registry shared across all
|
|
431
|
-
* bundle evaluations — same identity everywhere.
|
|
432
|
-
*/
|
|
433
|
-
const Fragment = Symbol.for("Pyreon.Fragment");
|
|
434
|
-
/**
|
|
435
|
-
* Hyperscript function — the compiled output of JSX.
|
|
436
|
-
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
437
|
-
*
|
|
438
|
-
* Generic on P so TypeScript validates props match the component's signature
|
|
439
|
-
* at the call site, then stores the result in the loosely-typed VNode.
|
|
440
|
-
*/
|
|
441
|
-
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
442
|
-
const EMPTY_PROPS = {};
|
|
443
|
-
function h(type, props, ...children) {
|
|
444
|
-
return {
|
|
445
|
-
type,
|
|
446
|
-
props: props ?? EMPTY_PROPS,
|
|
447
|
-
children: normalizeChildren(children),
|
|
448
|
-
key: props?.key ?? null
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
function normalizeChildren(children) {
|
|
452
|
-
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
453
|
-
return children;
|
|
454
|
-
}
|
|
455
|
-
function flattenChildren(children) {
|
|
456
|
-
const result = [];
|
|
457
|
-
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
458
|
-
else result.push(child);
|
|
459
|
-
return result;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
544
|
//#endregion
|
|
463
545
|
//#region src/dynamic.ts
|
|
464
546
|
const __DEV__$4 = process.env.NODE_ENV !== "production";
|
|
@@ -577,7 +659,7 @@ function ErrorBoundary(props) {
|
|
|
577
659
|
return true;
|
|
578
660
|
};
|
|
579
661
|
pushErrorBoundary(handler);
|
|
580
|
-
onUnmount(() => popErrorBoundary());
|
|
662
|
+
onUnmount(() => popErrorBoundary(handler));
|
|
581
663
|
return () => {
|
|
582
664
|
const err = error();
|
|
583
665
|
if (err != null) return props.fallback(err, reset);
|
|
@@ -1198,5 +1280,5 @@ function Suspense(props) {
|
|
|
1198
1280
|
}
|
|
1199
1281
|
|
|
1200
1282
|
//#endregion
|
|
1201
|
-
export { CSS_UNITLESS, Defer, Dynamic, EMPTY_PROPS, ErrorBoundary, For, ForSymbol, Fragment, Match, MatchSymbol, NATIVE_COMPAT_MARKER, Portal, PortalSymbol, REACTIVE_PROP, Show, Suspense, Switch, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mapCompatDomProps, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, reportError, restoreContextStack, runWithHooks, setContextStackProvider, shallowEqualProps, splitProps, toKebabCase, useContext, withContext };
|
|
1283
|
+
export { CSS_UNITLESS, Defer, Dynamic, EMPTY_PROPS, ErrorBoundary, For, ForSymbol, Fragment, Match, MatchSymbol, NATIVE_COMPAT_MARKER, Portal, PortalSymbol, REACTIVE_PROP, Show, Suspense, Switch, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, getContextStackLength, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mapCompatDomProps, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, removeContextFrame, reportError, restoreContextStack, runWithHooks, setContextStackProvider, shallowEqualProps, splitProps, toKebabCase, useContext, withContext };
|
|
1202
1284
|
//# sourceMappingURL=index.js.map
|
package/lib/jsx-dev-runtime.js
CHANGED
|
@@ -1,98 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Marker for fragment nodes — renders children without a wrapper element.
|
|
4
|
-
*
|
|
5
|
-
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
6
|
-
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
7
|
-
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
8
|
-
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
9
|
-
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
10
|
-
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
11
|
-
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
12
|
-
* fell through to `renderElement` and crashed SSG with
|
|
13
|
-
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
14
|
-
* `Symbol.for()` keys by string in a global registry shared across all
|
|
15
|
-
* bundle evaluations — same identity everywhere.
|
|
16
|
-
*/
|
|
17
|
-
const Fragment = Symbol.for("Pyreon.Fragment");
|
|
18
|
-
/**
|
|
19
|
-
* Hyperscript function — the compiled output of JSX.
|
|
20
|
-
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
21
|
-
*
|
|
22
|
-
* Generic on P so TypeScript validates props match the component's signature
|
|
23
|
-
* at the call site, then stores the result in the loosely-typed VNode.
|
|
24
|
-
*/
|
|
25
|
-
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
26
|
-
const EMPTY_PROPS = {};
|
|
27
|
-
function h(type, props, ...children) {
|
|
28
|
-
return {
|
|
29
|
-
type,
|
|
30
|
-
props: props ?? EMPTY_PROPS,
|
|
31
|
-
children: normalizeChildren(children),
|
|
32
|
-
key: props?.key ?? null
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function normalizeChildren(children) {
|
|
36
|
-
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
37
|
-
return children;
|
|
38
|
-
}
|
|
39
|
-
function flattenChildren(children) {
|
|
40
|
-
const result = [];
|
|
41
|
-
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
42
|
-
else result.push(child);
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
1
|
+
import { n as Fragment } from "./_chunks/h-CYSD6aBx.js";
|
|
2
|
+
import { jsx, jsxs } from "./jsx-runtime.js";
|
|
45
3
|
|
|
46
|
-
|
|
47
|
-
//#region src/jsx-runtime.ts
|
|
48
|
-
/**
|
|
49
|
-
* JSX automatic runtime.
|
|
50
|
-
*
|
|
51
|
-
* When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
|
|
52
|
-
* rewrites JSX to imports from this file automatically:
|
|
53
|
-
* <div class="x" /> → jsx("div", { class: "x" })
|
|
54
|
-
*
|
|
55
|
-
* The triple-slash reference above makes this file self-declare its DOM-lib
|
|
56
|
-
* dependency. Without it, any consumer whose tsconfig has `lib: ["ESNext"]`
|
|
57
|
-
* (no DOM) — e.g. backend-only packages like @pyreon/cli — fails to typecheck
|
|
58
|
-
* once `@pyreon/core` becomes resolvable from their dependency graph (e.g. via
|
|
59
|
-
* a transitive devDep), because tsc auto-resolves jsxImportSource and pulls
|
|
60
|
-
* jsx-runtime.ts into the consumer's compilation unit.
|
|
61
|
-
*/
|
|
62
|
-
function jsx(type, props, key) {
|
|
63
|
-
const descriptors = Object.getOwnPropertyDescriptors(props);
|
|
64
|
-
let hasGetter = false;
|
|
65
|
-
for (const k in descriptors) if (descriptors[k].get) {
|
|
66
|
-
hasGetter = true;
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
const children = props.children;
|
|
70
|
-
if (!hasGetter) {
|
|
71
|
-
const { children: _ignored, ...rest } = props;
|
|
72
|
-
const propsWithKey = key != null ? {
|
|
73
|
-
...rest,
|
|
74
|
-
key
|
|
75
|
-
} : rest;
|
|
76
|
-
if (typeof type === "function") return h(type, children !== void 0 ? {
|
|
77
|
-
...propsWithKey,
|
|
78
|
-
children
|
|
79
|
-
} : propsWithKey);
|
|
80
|
-
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
81
|
-
}
|
|
82
|
-
const propsWithKey = {};
|
|
83
|
-
for (const k in descriptors) {
|
|
84
|
-
if (k === "children") continue;
|
|
85
|
-
Object.defineProperty(propsWithKey, k, descriptors[k]);
|
|
86
|
-
}
|
|
87
|
-
if (key != null) propsWithKey.key = key;
|
|
88
|
-
if (typeof type === "function") {
|
|
89
|
-
if (children !== void 0) propsWithKey.children = children;
|
|
90
|
-
return h(type, propsWithKey);
|
|
91
|
-
}
|
|
92
|
-
return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
|
|
93
|
-
}
|
|
94
|
-
const jsxs = jsx;
|
|
95
|
-
|
|
96
|
-
//#endregion
|
|
97
|
-
export { Fragment, jsx as jsxDEV, jsxs };
|
|
98
|
-
//# sourceMappingURL=jsx-dev-runtime.js.map
|
|
4
|
+
export { Fragment, jsx as jsxDEV, jsxs };
|
package/lib/jsx-runtime.js
CHANGED
|
@@ -1,49 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Marker for fragment nodes — renders children without a wrapper element.
|
|
4
|
-
*
|
|
5
|
-
* MUST use `Symbol.for(...)` (global registry, keyed by string), NOT
|
|
6
|
-
* `Symbol(...)` (fresh per evaluation). `h.ts` is inlined into BOTH the
|
|
7
|
-
* main `lib/index.js` and the `lib/jsx-runtime.js` published bundles —
|
|
8
|
-
* each bundle's evaluation of a bare `Symbol(...)` would produce a
|
|
9
|
-
* DISTINCT Symbol identity. JSX `<>` compiles to `jsx(Fragment, ...)` and
|
|
10
|
-
* resolves to jsx-runtime's identity; `runtime-server` checks
|
|
11
|
-
* `vnode.type === Fragment` against the main-entry identity. Mismatch
|
|
12
|
-
* fell through to `renderElement` and crashed SSG with
|
|
13
|
-
* `TypeError: Cannot convert a Symbol value to a string`.
|
|
14
|
-
* `Symbol.for()` keys by string in a global registry shared across all
|
|
15
|
-
* bundle evaluations — same identity everywhere.
|
|
16
|
-
*/
|
|
17
|
-
const Fragment = Symbol.for("Pyreon.Fragment");
|
|
18
|
-
/**
|
|
19
|
-
* Hyperscript function — the compiled output of JSX.
|
|
20
|
-
* `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
|
|
21
|
-
*
|
|
22
|
-
* Generic on P so TypeScript validates props match the component's signature
|
|
23
|
-
* at the call site, then stores the result in the loosely-typed VNode.
|
|
24
|
-
*/
|
|
25
|
-
/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
|
|
26
|
-
const EMPTY_PROPS = {};
|
|
27
|
-
function h(type, props, ...children) {
|
|
28
|
-
return {
|
|
29
|
-
type,
|
|
30
|
-
props: props ?? EMPTY_PROPS,
|
|
31
|
-
children: normalizeChildren(children),
|
|
32
|
-
key: props?.key ?? null
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function normalizeChildren(children) {
|
|
36
|
-
for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
|
|
37
|
-
return children;
|
|
38
|
-
}
|
|
39
|
-
function flattenChildren(children) {
|
|
40
|
-
const result = [];
|
|
41
|
-
for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
|
|
42
|
-
else result.push(child);
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
1
|
+
import { n as Fragment, r as h } from "./_chunks/h-CYSD6aBx.js";
|
|
45
2
|
|
|
46
|
-
//#endregion
|
|
47
3
|
//#region src/jsx-runtime.ts
|
|
48
4
|
/**
|
|
49
5
|
* JSX automatic runtime.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -247,7 +247,49 @@ declare function createReactiveContext<T>(defaultValue: T): ReactiveContext<T>;
|
|
|
247
247
|
*/
|
|
248
248
|
declare function setContextStackProvider(fn: () => Map<symbol, unknown>[]): void;
|
|
249
249
|
declare function pushContext(values: Map<symbol, unknown>): void;
|
|
250
|
+
/**
|
|
251
|
+
* Pop the LAST frame from the context stack.
|
|
252
|
+
*
|
|
253
|
+
* NOTE: position-based pop. Safe ONLY when the caller can guarantee that the
|
|
254
|
+
* top of the stack is the frame they want to remove (the strict LIFO contract).
|
|
255
|
+
* The `provide()` helper does NOT use this — it uses identity-based removal
|
|
256
|
+
* via `removeContextFrame` because reactive boundaries can push snapshot
|
|
257
|
+
* frames between a component's `provide(ctx, value)` and its eventual
|
|
258
|
+
* unmount, making the top-of-stack unsafe to assume.
|
|
259
|
+
*/
|
|
250
260
|
declare function popContext(): void;
|
|
261
|
+
/**
|
|
262
|
+
* Read the current live stack length WITHOUT allocating a snapshot.
|
|
263
|
+
*
|
|
264
|
+
* SSR cleanup uses this as a position marker: capture the live length
|
|
265
|
+
* before a component renders, pop the live stack back to that length
|
|
266
|
+
* after. Previously these sites called `captureContextStack().length`,
|
|
267
|
+
* which allocated a full snapshot array (potentially 40k+ entries
|
|
268
|
+
* under deeply-nested reactive boundaries — the same allocation the
|
|
269
|
+
* `captureContextStack` dedup work is designed to shrink) just to
|
|
270
|
+
* read its length. This helper avoids the allocation entirely AND
|
|
271
|
+
* decouples SSR cleanup from `captureContextStack`'s snapshot shape,
|
|
272
|
+
* so dedup at capture time can never silently break SSR length
|
|
273
|
+
* bookkeeping.
|
|
274
|
+
*/
|
|
275
|
+
declare function getContextStackLength(): number;
|
|
276
|
+
/**
|
|
277
|
+
* Remove a SPECIFIC frame from the context stack by reference identity.
|
|
278
|
+
*
|
|
279
|
+
* Internal — used by `provide()` and `withContext()` to safely clean up
|
|
280
|
+
* their pushed frame on unmount even when other frames have been pushed
|
|
281
|
+
* between push and pop (e.g. a reactive boundary's `restoreContextStack`
|
|
282
|
+
* pushing snapshot frames during the descendant's lifecycle). The
|
|
283
|
+
* symmetric position-based `popContext()` would pop the wrong frame in
|
|
284
|
+
* that case and orphan the descendant's provider frame on the live stack
|
|
285
|
+
* — the root cause of the 321k-entry context-stack leak under repeated
|
|
286
|
+
* reactive remounts.
|
|
287
|
+
*
|
|
288
|
+
* Uses `lastIndexOf` (LIFO match) — picks the most-recently-pushed frame
|
|
289
|
+
* with that exact reference, so `provide(ctx, a); provide(ctx, b)` followed
|
|
290
|
+
* by two unmounts removes them in reverse order.
|
|
291
|
+
*/
|
|
292
|
+
declare function removeContextFrame(frame: Map<symbol, unknown>): void;
|
|
251
293
|
/**
|
|
252
294
|
* Read the nearest provided value for a context.
|
|
253
295
|
* Falls back to `context.defaultValue` if none found.
|
|
@@ -276,12 +318,59 @@ declare function provide<T>(context: Context<T>, value: T): void;
|
|
|
276
318
|
declare function withContext<T>(context: Context<T>, value: T, fn: () => void): void;
|
|
277
319
|
type ContextSnapshot = Map<symbol, unknown>[];
|
|
278
320
|
/**
|
|
279
|
-
* Capture a snapshot of the current context stack
|
|
321
|
+
* Capture a snapshot of the current context stack, **deduplicated** so
|
|
322
|
+
* only the topmost frame for each context-id is retained.
|
|
280
323
|
*
|
|
281
324
|
* Used by `mountReactive` to preserve the context that was active when a
|
|
282
325
|
* reactive boundary (e.g. `<Show>`, `<For>`) was set up. When the boundary
|
|
283
326
|
* later mounts new children inside an effect, the snapshot is restored so
|
|
284
327
|
* those children can see ancestor providers via `useContext()`.
|
|
328
|
+
*
|
|
329
|
+
* **Why dedup is semantically equivalent to a full snapshot:**
|
|
330
|
+
* `useContext()` walks the stack in reverse and returns the first frame
|
|
331
|
+
* matching the requested context-id (`for (let i = stack.length - 1; i >= 0; i--)`
|
|
332
|
+
* — see implementation below in this file). Any frame deeper in the
|
|
333
|
+
* stack that ALSO provides the same id is unreachable by definition —
|
|
334
|
+
* the reverse walk stops at the first match. Those shadowed frames are
|
|
335
|
+
* dead weight in the snapshot: they carry no observable value, they
|
|
336
|
+
* cost memory, and they can NEVER affect program behavior.
|
|
337
|
+
*
|
|
338
|
+
* The dedup walks frames from top to bottom keeping a `seen` set of
|
|
339
|
+
* already-resolved context ids. A frame is kept iff at least one of
|
|
340
|
+
* its keys is NOT in `seen` (i.e. it's the topmost provider for at
|
|
341
|
+
* least one id). All of a frame's keys are added to `seen` regardless
|
|
342
|
+
* of whether the frame is kept — `seen` represents "ids that are
|
|
343
|
+
* already provided by a more-recent frame".
|
|
344
|
+
*
|
|
345
|
+
* **Why this is safe for `restoreContextStack`:**
|
|
346
|
+
* `restoreContextStack` pushes the snapshot's frames onto the live
|
|
347
|
+
* stack, runs `fn()`, then removes those frames by **reference
|
|
348
|
+
* identity** (`stack.lastIndexOf(frame)`) — NOT by position or count
|
|
349
|
+
* of the snapshot. A deduped snapshot pushes fewer frames; the same
|
|
350
|
+
* reference-identity cleanup removes exactly those frames. No
|
|
351
|
+
* bookkeeping invariant breaks.
|
|
352
|
+
*
|
|
353
|
+
* **Why this is safe for the live stack length invariant:**
|
|
354
|
+
* SSR cleanup uses `getContextStackLength()` (a sibling helper) for
|
|
355
|
+
* position-marker bookkeeping. That helper reads the LIVE stack
|
|
356
|
+
* length, NOT the snapshot length, so dedup at capture time has zero
|
|
357
|
+
* effect on SSR cleanup behavior.
|
|
358
|
+
*
|
|
359
|
+
* **Why this is needed:**
|
|
360
|
+
* Under deeply-nested reactive boundaries (a `<Show>` inside a `<For>`
|
|
361
|
+
* inside a `<Suspense>`, each effect capturing its own snapshot at
|
|
362
|
+
* setup time), the live stack temporarily holds the same context-id
|
|
363
|
+
* pushed multiple times during nested `restoreContextStack` windows.
|
|
364
|
+
* The pre-dedup `[...getStack()]` snapshot baked those duplicates in
|
|
365
|
+
* permanently — each effect's closure retained an O(stack-depth)
|
|
366
|
+
* array for its lifetime. Reported heap snapshots from 0.21.x showed
|
|
367
|
+
* 1.22 MB / 321k-entry arrays from this pattern. The 0.23.0
|
|
368
|
+
* restoreContextStack reference-identity fix cleaned the LIVE stack
|
|
369
|
+
* but left the residual snapshot-amplification — observable as 20
|
|
370
|
+
* arrays at 157 KB each (40k entries) retained by effect closures.
|
|
371
|
+
* This dedup collapses each captured snapshot to ~N entries, where
|
|
372
|
+
* N is the number of DISTINCT context ids in scope (typically 2-10
|
|
373
|
+
* in real apps).
|
|
285
374
|
*/
|
|
286
375
|
declare function captureContextStack(): ContextSnapshot;
|
|
287
376
|
/**
|
|
@@ -1416,5 +1505,5 @@ declare function registerErrorHandler(handler: ErrorHandler): () => void;
|
|
|
1416
1505
|
*/
|
|
1417
1506
|
declare function reportError(ctx: ErrorContext): void;
|
|
1418
1507
|
//#endregion
|
|
1419
|
-
export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, Defer, type DeferProps, Dynamic, type DynamicProps, EMPTY_PROPS, ErrorBoundary, type ErrorContext, type ErrorHandler, type ExtractProps, For, type ForProps, ForSymbol, type FormAttributes, Fragment, type HigherOrderComponent, type ImgAttributes, type InputAttributes, type LazyComponent, type LifecycleHooks, Match, type MatchProps, MatchSymbol, NATIVE_COMPAT_MARKER, type NativeItem, Portal, type PortalProps, PortalSymbol, type Props, type PyreonHTMLAttributes, REACTIVE_PROP, type ReactiveContext, type ReactiveTraceEntry, type Ref, type RefCallback, type RefProp, type SelectAttributes, Show, type ShowProps, type StyleValue, Suspense, type SvgAttributes, Switch, type SwitchProps, type TargetedEvent, type TextareaAttributes, type VNode, type VNodeChild, type VNodeChildAccessor, type VNodeChildAtom, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mapCompatDomProps, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, reportError, restoreContextStack, runWithHooks, setContextStackProvider, shallowEqualProps, splitProps, toKebabCase, useContext, withContext };
|
|
1508
|
+
export { type AnchorAttributes, type ButtonAttributes, type CSSProperties, CSS_UNITLESS, type ClassValue, type CleanupFn, type ComponentFn, type ComponentInstance, type Context, type ContextSnapshot, Defer, type DeferProps, Dynamic, type DynamicProps, EMPTY_PROPS, ErrorBoundary, type ErrorContext, type ErrorHandler, type ExtractProps, For, type ForProps, ForSymbol, type FormAttributes, Fragment, type HigherOrderComponent, type ImgAttributes, type InputAttributes, type LazyComponent, type LifecycleHooks, Match, type MatchProps, MatchSymbol, NATIVE_COMPAT_MARKER, type NativeItem, Portal, type PortalProps, PortalSymbol, type Props, type PyreonHTMLAttributes, REACTIVE_PROP, type ReactiveContext, type ReactiveTraceEntry, type Ref, type RefCallback, type RefProp, type SelectAttributes, Show, type ShowProps, type StyleValue, Suspense, type SvgAttributes, Switch, type SwitchProps, type TargetedEvent, type TextareaAttributes, type VNode, type VNodeChild, type VNodeChildAccessor, type VNodeChildAtom, _rp, _wrapSpread, captureContextStack, createContext, createReactiveContext, createRef, createUniqueId, cx, defineComponent, dispatchToErrorBoundary, getContextStackLength, h, isNativeCompat, lazy, makeReactiveProps, mapArray, mapCompatDomProps, mergeProps, nativeCompat, normalizeStyleValue, onErrorCaptured, onMount, onUnmount, onUpdate, popContext, propagateError, provide, pushContext, registerErrorHandler, removeContextFrame, reportError, restoreContextStack, runWithHooks, setContextStackProvider, shallowEqualProps, splitProps, toKebabCase, useContext, withContext };
|
|
1420
1509
|
//# sourceMappingURL=index2.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Core component model and lifecycle for Pyreon",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/core#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"prepublishOnly": "bun run build"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@pyreon/reactivity": "^0.
|
|
56
|
+
"@pyreon/reactivity": "^0.24.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@pyreon/manifest": "0.13.1"
|
package/src/component.ts
CHANGED
|
@@ -46,6 +46,22 @@ export function propagateError(err: unknown, hooks: LifecycleHooks): boolean {
|
|
|
46
46
|
// Module-level stack of active ErrorBoundary handlers (innermost last).
|
|
47
47
|
// ErrorBoundary pushes during its own setup (before children mount) so that
|
|
48
48
|
// any child mountComponent error can dispatch up to the nearest boundary.
|
|
49
|
+
//
|
|
50
|
+
// Mutation contract: removal is IDENTITY-based (`lastIndexOf + splice`), not
|
|
51
|
+
// position-based (`pop`). Sibling boundaries unmount in an order that's
|
|
52
|
+
// driven by the renderer (keyed `<For>` reconciliation, `<Show>` flips,
|
|
53
|
+
// route nav), NOT in strict LIFO push order. A position-based `pop()` would
|
|
54
|
+
// remove the wrong frame whenever the unmount order diverges from the push
|
|
55
|
+
// order — the first boundary's `onUnmount` would pop the last boundary's
|
|
56
|
+
// handler, orphaning the first boundary's handler on the stack and removing
|
|
57
|
+
// the surviving boundary's handler from it. Subsequent errors would then
|
|
58
|
+
// route to the orphan (whose owning boundary's signal is already disposed,
|
|
59
|
+
// so the error vanishes silently) and the surviving boundary's children's
|
|
60
|
+
// errors would fall through to whichever boundary happens to sit at
|
|
61
|
+
// `stack[length-1]`. Same root-cause shape as the `popContext()` bug
|
|
62
|
+
// fixed in #725 for `provide()` — see
|
|
63
|
+
// `.claude/rules/anti-patterns.md` "Position-based pop for stack frames
|
|
64
|
+
// that may be pushed by reactive boundaries".
|
|
49
65
|
|
|
50
66
|
const _errorBoundaryStack: ((err: unknown) => boolean)[] = []
|
|
51
67
|
|
|
@@ -53,8 +69,23 @@ export function pushErrorBoundary(handler: (err: unknown) => boolean): void {
|
|
|
53
69
|
_errorBoundaryStack.push(handler)
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Remove a SPECIFIC handler from the error-boundary stack by reference
|
|
74
|
+
* identity. Each `ErrorBoundary` registers `onUnmount(() => popErrorBoundary(handler))`
|
|
75
|
+
* with its OWN handler — so unmount in any order (LIFO, FIFO, middle-out)
|
|
76
|
+
* correctly removes the right handler.
|
|
77
|
+
*/
|
|
78
|
+
export function popErrorBoundary(handler?: (err: unknown) => boolean): void {
|
|
79
|
+
if (handler === undefined) {
|
|
80
|
+
// Back-compat: legacy callers that don't pass a handler get the old
|
|
81
|
+
// pop-last behaviour. Internal `ErrorBoundary` setup always passes
|
|
82
|
+
// its handler now; any external direct callers (tests, advanced
|
|
83
|
+
// consumers) keep working with no-arg form.
|
|
84
|
+
_errorBoundaryStack.pop()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
const idx = _errorBoundaryStack.lastIndexOf(handler)
|
|
88
|
+
if (idx !== -1) _errorBoundaryStack.splice(idx, 1)
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
/**
|