@webjsdev/core 0.7.37 → 0.7.39
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/dist/webjs-core-browser.js +3 -3
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core.js +3 -3
- package/dist/webjs-core.js.map +2 -2
- package/package.json +1 -1
- package/src/metadata.d.ts +4 -3
- package/src/router-client.js +146 -12
package/package.json
CHANGED
package/src/metadata.d.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* export const metadata: Metadata = { title: 'Home', description: '…' };
|
|
12
12
|
* export async function generateMetadata(ctx: MetadataContext): Promise<Metadata> { … }
|
|
13
13
|
*
|
|
14
|
-
* The shape mirrors what ssr.js actually consumes (and
|
|
15
|
-
* documents), NOT Next.js's full superset.
|
|
14
|
+
* The shape mirrors what ssr.js actually consumes (and the skill's
|
|
15
|
+
* references/routing-and-pages.md documents), NOT Next.js's full superset.
|
|
16
|
+
* Every field is optional. Zero
|
|
16
17
|
* runtime cost: nothing in this file ships to the browser.
|
|
17
18
|
*/
|
|
18
19
|
|
|
@@ -177,7 +178,7 @@ export interface PreloadDescriptor {
|
|
|
177
178
|
* optional. Where the framework accepts a string OR an object (`title`,
|
|
178
179
|
* `viewport`, `robots`, `appleWebApp`, `icons`), the type is a union.
|
|
179
180
|
*
|
|
180
|
-
* @see
|
|
181
|
+
* @see the skill's references/routing-and-pages.md for the field-by-field emission reference.
|
|
181
182
|
*/
|
|
182
183
|
export interface Metadata {
|
|
183
184
|
/** -> `<title>`. Plain string or `{ template, default, absolute }`. */
|
package/src/router-client.js
CHANGED
|
@@ -718,11 +718,30 @@ function setNavigating(on) {
|
|
|
718
718
|
* appear multiple times in a document only if a layout transitively
|
|
719
719
|
* includes itself (pathological; we take the outermost).
|
|
720
720
|
*
|
|
721
|
+
* `recoverOrphans` (#994): register an OPEN marker whose closing
|
|
722
|
+
* `<!--/wj:children-->` comment is missing, using `end: null` (meaning "the
|
|
723
|
+
* children run to the end of the containing element"). Some browsers
|
|
724
|
+
* intermittently DROP the trailing close comment while parsing a soft-nav
|
|
725
|
+
* response, a parse/timing race that surfaced first on Android Chrome (a soft
|
|
726
|
+
* nav to `/blog` randomly dropped the navbar; #939/#940 confirmed the OPEN
|
|
727
|
+
* marker survived, `markers:1`, yet the router still fell to the destructive
|
|
728
|
+
* full-body swap) and was later reproduced on DESKTOP Chromium too, so this is
|
|
729
|
+
* NOT browser-specific, only rarer on faster hardware. Without both comments
|
|
730
|
+
* this walk registers no slot, so `longestSharedPath` finds nothing and
|
|
731
|
+
* `applySwap` wipes the outer layout (navbar). Recovering the orphaned open lets
|
|
732
|
+
* the correct scoped swap run and keeps the navbar (it sits BEFORE the open
|
|
733
|
+
* marker, so it is never in the swap range). The recovery keys off the SYMPTOM
|
|
734
|
+
* (a missing close), never any user-agent check, so it fixes every engine
|
|
735
|
+
* uniformly. Off by default so the exported helper's strict pairing is
|
|
736
|
+
* unchanged; the swap-decision and `X-Webjs-Have` call sites opt in.
|
|
737
|
+
*
|
|
721
738
|
* @param {ParentNode} root
|
|
722
|
-
* @
|
|
739
|
+
* @param {{ recoverOrphans?: boolean }} [options]
|
|
740
|
+
* @returns {Map<string, { start: Comment, end: Comment | null }>}
|
|
723
741
|
*/
|
|
724
|
-
export function collectChildrenSlots(root) {
|
|
725
|
-
|
|
742
|
+
export function collectChildrenSlots(root, options) {
|
|
743
|
+
const recoverOrphans = !!(options && options.recoverOrphans);
|
|
744
|
+
/** @type {Map<string, { start: Comment, end: Comment | null }>} */
|
|
726
745
|
const slots = new Map();
|
|
727
746
|
/** @type {{ path: string, start: Comment }[]} */
|
|
728
747
|
const stack = [];
|
|
@@ -756,6 +775,18 @@ export function collectChildrenSlots(root) {
|
|
|
756
775
|
}
|
|
757
776
|
}
|
|
758
777
|
visit(/** @type {Node} */ (root));
|
|
778
|
+
|
|
779
|
+
// #994: any open marker still on the stack was never closed (its
|
|
780
|
+
// `<!--/wj:children-->` was dropped by the browser's parser, any engine).
|
|
781
|
+
// Register it with a null end so the shared-path match succeeds and the scoped
|
|
782
|
+
// swap preserves the outer layout, instead of the destructive full-body
|
|
783
|
+
// fallback. Outermost first (stack order), never overwriting a slot a proper
|
|
784
|
+
// close already paired.
|
|
785
|
+
if (recoverOrphans) {
|
|
786
|
+
for (const frame of stack) {
|
|
787
|
+
if (!slots.has(frame.path)) slots.set(frame.path, { start: frame.start, end: null });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
759
790
|
return slots;
|
|
760
791
|
}
|
|
761
792
|
|
|
@@ -1047,6 +1078,18 @@ async function performSubmission(href, method, body, frameId, form) {
|
|
|
1047
1078
|
* @returns {string}
|
|
1048
1079
|
*/
|
|
1049
1080
|
function buildHaveHeader() {
|
|
1081
|
+
// Strict pairing here, deliberately (#994): if the SOLE or OUTERMOST close
|
|
1082
|
+
// marker was dropped, its orphaned open is NOT reported, so `have` omits that
|
|
1083
|
+
// layout and the server returns the FULL page (with the outer layout's trailing
|
|
1084
|
+
// chrome) rather than a reduced marker-pair-only fragment. `applySwap` then
|
|
1085
|
+
// recovers the orphan and bounds the swap against that full page's
|
|
1086
|
+
// trailing-sibling count, preserving the navbar and the footer. Recovering here
|
|
1087
|
+
// would send `have=/`, get back a reduced fragment with no trailing chrome
|
|
1088
|
+
// (`tail === 0`), and sweep an unwrapped layout's footer. The extra bytes of a
|
|
1089
|
+
// full page on the rare orphaned nav are the price of that correctness. A
|
|
1090
|
+
// dropped INNER close in a nested layout is a separate, unfixed mispairing
|
|
1091
|
+
// limitation (the surviving outer close pairs with the inner open); the wrapped
|
|
1092
|
+
// `${children}` idiom keeps it harmless.
|
|
1050
1093
|
const slots = collectChildrenSlots(document.body);
|
|
1051
1094
|
return [...slots.keys()].join(',');
|
|
1052
1095
|
}
|
|
@@ -2299,7 +2342,7 @@ function findInSlice(slice, id) {
|
|
|
2299
2342
|
* swap settles. The View Transitions API snapshots and replaces DOM, so
|
|
2300
2343
|
* elements can need a re-upgrade once the animation finishes.
|
|
2301
2344
|
*
|
|
2302
|
-
* @param {{ start: Comment, end: Comment } | undefined} range
|
|
2345
|
+
* @param {{ start: Comment, end: Comment | null } | undefined} range
|
|
2303
2346
|
*/
|
|
2304
2347
|
function upgradeCustomElementsInRange(range) {
|
|
2305
2348
|
if (!range || !range.start) return;
|
|
@@ -2493,19 +2536,33 @@ function applySwap(doc, frameId, revalidating, href, incomingBuild, incomingSrc)
|
|
|
2493
2536
|
return;
|
|
2494
2537
|
}
|
|
2495
2538
|
|
|
2496
|
-
// 2. Auto-derived layout-marker swap.
|
|
2497
|
-
|
|
2498
|
-
|
|
2539
|
+
// 2. Auto-derived layout-marker swap. Recover an orphaned open marker on
|
|
2540
|
+
// either side (#994): a dropped close comment must not force the destructive
|
|
2541
|
+
// path-3 fallback that wipes the outer layout (navbar).
|
|
2542
|
+
const here = collectChildrenSlots(document.body, { recoverOrphans: true });
|
|
2543
|
+
const there = collectChildrenSlots(doc.body, { recoverOrphans: true });
|
|
2499
2544
|
const sharedPath = longestSharedPath(here, there);
|
|
2500
2545
|
|
|
2501
2546
|
if (sharedPath) {
|
|
2547
|
+
const target = here.get(sharedPath);
|
|
2548
|
+
const source = there.get(sharedPath);
|
|
2549
|
+
// #994: a recovered orphan carries `end: null` (children run to the marker's
|
|
2550
|
+
// PARENT end). That is exactly right when the children fill their parent (the
|
|
2551
|
+
// shipped idiom wraps `${children}` in a `<main>`, so the close marker is the
|
|
2552
|
+
// parent's last child and trailing chrome like `<footer>` is a sibling
|
|
2553
|
+
// OUTSIDE it). But a layout that puts trailing content in the marker's OWN
|
|
2554
|
+
// parent would have that content swept (a live orphan) or duplicated (an
|
|
2555
|
+
// incoming orphan). When exactly one side is orphaned, bound it against the
|
|
2556
|
+
// well-formed side's trailing-sibling count so the trailing chrome is
|
|
2557
|
+
// preserved either way.
|
|
2558
|
+
boundRecoveredEnds(target, source);
|
|
2502
2559
|
// ADD-ONLY head merge for the same reason: outer layout stays
|
|
2503
2560
|
// mounted, its head-bound runtime state must not be invalidated.
|
|
2504
2561
|
addNewHeadElements(doc.head);
|
|
2505
2562
|
runWithTransition(() => {
|
|
2506
|
-
swapMarkerRange(
|
|
2563
|
+
swapMarkerRange(target, source, doc);
|
|
2507
2564
|
blurOutgoingFocus();
|
|
2508
|
-
}, () => upgradeCustomElementsInRange(
|
|
2565
|
+
}, () => upgradeCustomElementsInRange(target));
|
|
2509
2566
|
forwardSuspenseResolvers(doc.body);
|
|
2510
2567
|
return;
|
|
2511
2568
|
}
|
|
@@ -2562,6 +2619,78 @@ function blurOutgoingFocus() {
|
|
|
2562
2619
|
/** @type any */ (a).blur();
|
|
2563
2620
|
}
|
|
2564
2621
|
|
|
2622
|
+
/**
|
|
2623
|
+
* Count the siblings after `marker` up to the end of its parent. Used to
|
|
2624
|
+
* measure how much trailing outer-layout chrome sits after a close marker
|
|
2625
|
+
* WITHIN the marker's own parent (#994).
|
|
2626
|
+
*
|
|
2627
|
+
* @param {Comment} marker
|
|
2628
|
+
* @returns {number}
|
|
2629
|
+
*/
|
|
2630
|
+
function trailingSiblingCount(marker) {
|
|
2631
|
+
let n = 0;
|
|
2632
|
+
for (let s = marker.nextSibling; s; s = s.nextSibling) n++;
|
|
2633
|
+
return n;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
/**
|
|
2637
|
+
* Give a recovered orphan (`end: null`, #994) a concrete bounded end so trailing
|
|
2638
|
+
* outer-layout chrome in the marker's own parent is preserved. Runs only when
|
|
2639
|
+
* EXACTLY one side is orphaned: the well-formed side's close marker reveals how
|
|
2640
|
+
* many trailing siblings sit after the children within the parent (`tail`), and
|
|
2641
|
+
* a same-layout nav shares that outer-layout structure, so the orphan's children
|
|
2642
|
+
* end `tail` nodes before its parent's end. A `tail` of zero (the shipped
|
|
2643
|
+
* wrap-`${children}`-in-`<main>` idiom, where the close is the parent's last
|
|
2644
|
+
* child) leaves `end: null` and the sweep-to-parent-end stays correct. Both-side
|
|
2645
|
+
* orphans keep `null` on both (symmetric sweep, nothing to align against).
|
|
2646
|
+
*
|
|
2647
|
+
* This works for a SOLE or OUTERMOST dropped close because `buildHaveHeader`
|
|
2648
|
+
* reports STRICTLY paired markers, so an orphaned live page omits the layout from
|
|
2649
|
+
* `have` and the server returns the FULL page (trailing chrome included,
|
|
2650
|
+
* `tail > 0`) rather than a reduced marker-pair-only fragment. Two cases it does
|
|
2651
|
+
* NOT cover: an incoming side whose close was lost during parse (rare: the
|
|
2652
|
+
* response is parsed from a complete string, not streamed), and a dropped INNER
|
|
2653
|
+
* close in a nested layout (the surviving outer close mispairs with the inner
|
|
2654
|
+
* open, so both slot ends are non-null and this bounding is skipped). The wrapped
|
|
2655
|
+
* `${children}` idiom keeps both harmless (the marker's parent holds only the
|
|
2656
|
+
* children, so any sweep stays inside it).
|
|
2657
|
+
*
|
|
2658
|
+
* @param {{ start: Comment, end: Comment | null } | undefined} target
|
|
2659
|
+
* @param {{ start: Comment, end: Comment | null } | undefined} source
|
|
2660
|
+
*/
|
|
2661
|
+
function boundRecoveredEnds(target, source) {
|
|
2662
|
+
if (!target || !source) return;
|
|
2663
|
+
if (target.end === null && source.end !== null) target.end = boundOrphanEnd(target.start, source.end);
|
|
2664
|
+
else if (source.end === null && target.end !== null) source.end = boundOrphanEnd(source.start, target.end);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
/**
|
|
2668
|
+
* Compute the bounded end sentinel (exclusive) for an orphaned open marker,
|
|
2669
|
+
* preserving `tail` trailing siblings at the end of the marker's parent, where
|
|
2670
|
+
* `tail` is the well-formed counterpart's trailing-sibling count (#994).
|
|
2671
|
+
*
|
|
2672
|
+
* @param {Comment} orphanStart The orphan's open marker.
|
|
2673
|
+
* @param {Comment} wellFormedEnd The counterpart's real close marker.
|
|
2674
|
+
* @returns {Comment | Node | null} A node to stop before, or null (sweep to end).
|
|
2675
|
+
*/
|
|
2676
|
+
function boundOrphanEnd(orphanStart, wellFormedEnd) {
|
|
2677
|
+
const tail = trailingSiblingCount(wellFormedEnd);
|
|
2678
|
+
if (tail === 0) return null;
|
|
2679
|
+
// Collect the orphan's children region (start.nextSibling to parent end).
|
|
2680
|
+
/** @type {Node[]} */
|
|
2681
|
+
const nodes = [];
|
|
2682
|
+
for (let n = orphanStart.nextSibling; n; n = n.nextSibling) nodes.push(n);
|
|
2683
|
+
// Preserve the last `tail` nodes by stopping before index `cut`. A `cut` at or
|
|
2684
|
+
// below zero is a degenerate mismatch (the well-formed side claims more
|
|
2685
|
+
// trailing siblings than the orphan has nodes at all, which a same-layout nav
|
|
2686
|
+
// should never produce): fall back to `null` (sweep to the parent end, a normal
|
|
2687
|
+
// full-region swap) rather than an empty range, which would duplicate or blank
|
|
2688
|
+
// the children.
|
|
2689
|
+
const cut = nodes.length - tail;
|
|
2690
|
+
if (cut <= 0) return null;
|
|
2691
|
+
return nodes[cut];
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2565
2694
|
/**
|
|
2566
2695
|
* Replace nodes between `target.start` and `target.end` (exclusive) in the
|
|
2567
2696
|
* live document with the nodes between `source.start` and `source.end` in
|
|
@@ -2569,8 +2698,13 @@ function blurOutgoingFocus() {
|
|
|
2569
2698
|
* identity for matched elements + their live attributes (scroll, value,
|
|
2570
2699
|
* etc.).
|
|
2571
2700
|
*
|
|
2572
|
-
*
|
|
2573
|
-
*
|
|
2701
|
+
* A null `end` (an orphaned open marker recovered per #994) means "run to the
|
|
2702
|
+
* end of the containing element": the slice-collection loops stop at the natural
|
|
2703
|
+
* sibling end and `reconcileSiblings` appends before `null` (end of parent), so
|
|
2704
|
+
* a dropped close comment swaps the whole children region without a close marker.
|
|
2705
|
+
*
|
|
2706
|
+
* @param {{ start: Comment, end: Comment | null } | undefined} target
|
|
2707
|
+
* @param {{ start: Comment, end: Comment | null } | undefined} source
|
|
2574
2708
|
* @param {Document} _doc
|
|
2575
2709
|
*/
|
|
2576
2710
|
function swapMarkerRange(target, source, _doc) {
|
|
@@ -2634,7 +2768,7 @@ function swapMarkerRange(target, source, _doc) {
|
|
|
2634
2768
|
*
|
|
2635
2769
|
* @param {Node} parent
|
|
2636
2770
|
* @param {Comment} startMarker
|
|
2637
|
-
* @param {Comment} endMarker
|
|
2771
|
+
* @param {Comment | null} endMarker Null (recovered orphan, #994) appends at the parent end.
|
|
2638
2772
|
* @param {Node[]} live
|
|
2639
2773
|
* @param {Node[]} incoming
|
|
2640
2774
|
*/
|