@rsc-kit/core 0.20.6 → 0.20.8
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/cache.d.ts +11 -0
- package/dist/cache.js +13 -0
- package/dist/cache.js.map +1 -1
- package/dist/clientEntries.js +4 -1
- package/dist/clientEntries.js.map +1 -1
- package/dist/files.d.ts +38 -0
- package/dist/files.js +109 -0
- package/dist/files.js.map +1 -1
- package/dist/host.d.ts +11 -2
- package/dist/host.js +37 -6
- package/dist/host.js.map +1 -1
- package/dist/js/ActivityRouter.d.ts +11 -0
- package/dist/js/ActivityRouter.js +24 -4
- package/dist/js/ActivityRouter.js.map +1 -1
- package/dist/js/Form.js +24 -2
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +1 -1
- package/dist/js/Link.js +25 -9
- package/dist/js/Link.js.map +1 -1
- package/dist/js/PageTransition.d.ts +27 -0
- package/dist/js/PageTransition.js +57 -0
- package/dist/js/PageTransition.js.map +1 -0
- package/dist/js/PathnameProvider.d.ts +15 -2
- package/dist/js/PathnameProvider.js +17 -2
- package/dist/js/PathnameProvider.js.map +1 -1
- package/dist/js/SegmentBoundary.js +27 -3
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/activityMarkers.d.ts +7 -0
- package/dist/js/activityMarkers.js +18 -0
- package/dist/js/activityMarkers.js.map +1 -0
- package/dist/js/createViteRscApp.js +122 -76
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/earlyClicks.d.ts +3 -1
- package/dist/js/earlyClicks.js +28 -5
- package/dist/js/earlyClicks.js.map +1 -1
- package/dist/js/errors.d.ts +2 -0
- package/dist/js/errors.js +11 -0
- package/dist/js/errors.js.map +1 -1
- package/dist/js/imagePreload.d.ts +37 -0
- package/dist/js/imagePreload.js +116 -0
- package/dist/js/imagePreload.js.map +1 -0
- package/dist/js/navigate.d.ts +47 -1
- package/dist/js/navigate.js +249 -10
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/perf.d.ts +25 -0
- package/dist/js/perf.js +57 -0
- package/dist/js/perf.js.map +1 -0
- package/dist/js/router.d.ts +10 -1
- package/dist/js/router.js +9 -2
- package/dist/js/router.js.map +1 -1
- package/dist/js/segmentStore.d.ts +52 -0
- package/dist/js/segmentStore.js +123 -2
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/js/staleAssets.d.ts +10 -0
- package/dist/js/staleAssets.js +23 -2
- package/dist/js/staleAssets.js.map +1 -1
- package/dist/js/viewportPrefetch.d.ts +19 -10
- package/dist/js/viewportPrefetch.js +50 -15
- package/dist/js/viewportPrefetch.js.map +1 -1
- package/dist/shellHead.d.ts +22 -0
- package/dist/shellHead.js +43 -0
- package/dist/shellHead.js.map +1 -0
- package/dist/vite.js +250 -55
- package/dist/vite.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Whether this browser rasterises a transition snapshot at full element
|
|
4
|
+
* height. Apple's vendor string is WebKit's, wherever WebKit runs: Safari,
|
|
5
|
+
* every iOS browser, and WebKitGTK (Epiphany) too - all of them charge by
|
|
6
|
+
* snapshot size, so all of them are right to skip. Not a bug to "fix".
|
|
7
|
+
*/
|
|
8
|
+
export declare function snapshotsWholePage(): boolean;
|
|
9
|
+
/** Whether the visitor asked for no motion; a transition then costs a snapshot for nothing. */
|
|
10
|
+
export declare function prefersReducedMotion(): boolean;
|
|
11
|
+
/** The rules a page fade needs, for the class the navigation carries. */
|
|
12
|
+
export declare function pageTransitionCss(transition: string, durationMs: number): string;
|
|
13
|
+
export declare function PageTransition({ children, className, transition, duration, webkit, }: {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/** On the wrapper element the pair is made of; carry the body's layout here. */
|
|
16
|
+
className?: string;
|
|
17
|
+
/** The view-transition class a navigation gets, for CSS of your own on top. */
|
|
18
|
+
transition?: string;
|
|
19
|
+
/** How long the fade runs, in milliseconds. The group and root pairs match it. */
|
|
20
|
+
duration?: number;
|
|
21
|
+
/**
|
|
22
|
+
* What WebKit gets. `skip` for an app with long pages - the default,
|
|
23
|
+
* since a landing page usually is one; `run` for an app whose pages are
|
|
24
|
+
* a screen tall, where the snapshot is small and the fade is fine.
|
|
25
|
+
*/
|
|
26
|
+
webkit?: "skip" | "run";
|
|
27
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The page-level view transition, with WebKit left out of it.
|
|
5
|
+
*
|
|
6
|
+
* WebKit - Safari, and every browser on iOS, Chrome included - rasterises
|
|
7
|
+
* the outgoing snapshot at the element's full height rather than the
|
|
8
|
+
* viewport's. A landing page 12,000 px tall is 44 megapixels at 3×: a port
|
|
9
|
+
* measured 600-900 ms with the screen frozen on every navigation away from
|
|
10
|
+
* it, and the memory can have iOS discard the tab, which comes back as a
|
|
11
|
+
* reload nobody asked for. Chromium caps the snapshot, so a desktop never
|
|
12
|
+
* shows it. Other sites' transitions work on the same phone because they
|
|
13
|
+
* name the root, which the spec clips to the viewport; React's
|
|
14
|
+
* <ViewTransition> names the element it wraps, and the page is tall.
|
|
15
|
+
*
|
|
16
|
+
* So the cross-fade runs where it is cheap and is skipped where it is not.
|
|
17
|
+
* Decided after mount, from navigator.vendor - a server render has no
|
|
18
|
+
* navigator, and a transition class only matters on an update, so the
|
|
19
|
+
* first render agrees with the server either way. Skipped, React starts
|
|
20
|
+
* no transition at all: no snapshot, no overlay, nothing to wait for.
|
|
21
|
+
*
|
|
22
|
+
* The CSS the fade needs travels with it, because every app rediscovered
|
|
23
|
+
* the same three lines on a phone: the group and root pairs keep the
|
|
24
|
+
* browser's 250 ms whatever the page pair is set to, so a "120 ms" fade
|
|
25
|
+
* ran 290; the pseudo-element tree sits over the document while it runs
|
|
26
|
+
* and swallows a second tap; and reduced motion should mean none.
|
|
27
|
+
*/
|
|
28
|
+
import { ViewTransition, useEffect, useState } from "react";
|
|
29
|
+
/**
|
|
30
|
+
* Whether this browser rasterises a transition snapshot at full element
|
|
31
|
+
* height. Apple's vendor string is WebKit's, wherever WebKit runs: Safari,
|
|
32
|
+
* every iOS browser, and WebKitGTK (Epiphany) too - all of them charge by
|
|
33
|
+
* snapshot size, so all of them are right to skip. Not a bug to "fix".
|
|
34
|
+
*/
|
|
35
|
+
export function snapshotsWholePage() {
|
|
36
|
+
return typeof navigator !== "undefined" && navigator.vendor === "Apple Computer, Inc.";
|
|
37
|
+
}
|
|
38
|
+
/** Whether the visitor asked for no motion; a transition then costs a snapshot for nothing. */
|
|
39
|
+
export function prefersReducedMotion() {
|
|
40
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
41
|
+
}
|
|
42
|
+
/** The rules a page fade needs, for the class the navigation carries. */
|
|
43
|
+
export function pageTransitionCss(transition, durationMs) {
|
|
44
|
+
return (`::view-transition-old(.${transition}),::view-transition-new(.${transition}),` +
|
|
45
|
+
`::view-transition-group(*),::view-transition-old(root),::view-transition-new(root){animation-duration:${durationMs}ms}` +
|
|
46
|
+
`::view-transition{pointer-events:none}` +
|
|
47
|
+
`@media (prefers-reduced-motion:reduce){::view-transition-old(*),::view-transition-new(*),::view-transition-group(*){animation:none}}`);
|
|
48
|
+
}
|
|
49
|
+
export function PageTransition({ children, className, transition = "page", duration = 120, webkit = "skip", }) {
|
|
50
|
+
const [skip, setSkip] = useState(false);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if ((webkit === "skip" && snapshotsWholePage()) || prefersReducedMotion())
|
|
53
|
+
setSkip(true);
|
|
54
|
+
}, [webkit]);
|
|
55
|
+
return (_jsx(ViewTransition, { default: { "rsc-navigation": skip ? "none" : transition, default: "none" }, children: _jsxs("div", { className: className, children: [_jsx("style", { href: `rsc-kit-page-transition-${transition}`, precedence: "rsc-kit", children: pageTransitionCss(transition, duration) }), children] }) }));
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=PageTransition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PageTransition.js","sourceRoot":"","sources":["../../src/js/PageTransition.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG5D;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,CAAC,MAAM,KAAK,sBAAsB,CAAC;AACzF,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU,IAAI,MAAM,CAAC,UAAU,CAAC,kCAAkC,CAAC,CAAC,OAAO,CAAC;AACnJ,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,UAAkB;IACtE,OAAO,CACL,0BAA0B,UAAU,4BAA4B,UAAU,IAAI;QAC9E,yGAAyG,UAAU,KAAK;QACxH,wCAAwC;QACxC,sIAAsI,CACvI,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAC7B,QAAQ,EACR,SAAS,EACT,UAAU,GAAG,MAAM,EACnB,QAAQ,GAAG,GAAG,EACd,MAAM,GAAG,MAAM,GAehB;IACC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAExC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,kBAAkB,EAAE,CAAC,IAAI,oBAAoB,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3F,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,OAAO,CACL,KAAC,cAAc,IAAC,OAAO,EAAE,EAAE,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,YACxF,eAAK,SAAS,EAAE,SAAS,aACvB,gBAAO,IAAI,EAAE,2BAA2B,UAAU,EAAE,EAAE,UAAU,EAAC,SAAS,YACvE,iBAAiB,CAAC,UAAU,EAAE,QAAQ,CAAC,GAClC,EACP,QAAQ,IACL,GACS,CAClB,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\n/**\n * The page-level view transition, with WebKit left out of it.\n *\n * WebKit - Safari, and every browser on iOS, Chrome included - rasterises\n * the outgoing snapshot at the element's full height rather than the\n * viewport's. A landing page 12,000 px tall is 44 megapixels at 3×: a port\n * measured 600-900 ms with the screen frozen on every navigation away from\n * it, and the memory can have iOS discard the tab, which comes back as a\n * reload nobody asked for. Chromium caps the snapshot, so a desktop never\n * shows it. Other sites' transitions work on the same phone because they\n * name the root, which the spec clips to the viewport; React's\n * <ViewTransition> names the element it wraps, and the page is tall.\n *\n * So the cross-fade runs where it is cheap and is skipped where it is not.\n * Decided after mount, from navigator.vendor - a server render has no\n * navigator, and a transition class only matters on an update, so the\n * first render agrees with the server either way. Skipped, React starts\n * no transition at all: no snapshot, no overlay, nothing to wait for.\n *\n * The CSS the fade needs travels with it, because every app rediscovered\n * the same three lines on a phone: the group and root pairs keep the\n * browser's 250 ms whatever the page pair is set to, so a \"120 ms\" fade\n * ran 290; the pseudo-element tree sits over the document while it runs\n * and swallows a second tap; and reduced motion should mean none.\n */\n\nimport { ViewTransition, useEffect, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Whether this browser rasterises a transition snapshot at full element\n * height. Apple's vendor string is WebKit's, wherever WebKit runs: Safari,\n * every iOS browser, and WebKitGTK (Epiphany) too - all of them charge by\n * snapshot size, so all of them are right to skip. Not a bug to \"fix\".\n */\nexport function snapshotsWholePage(): boolean {\n return typeof navigator !== \"undefined\" && navigator.vendor === \"Apple Computer, Inc.\";\n}\n\n/** Whether the visitor asked for no motion; a transition then costs a snapshot for nothing. */\nexport function prefersReducedMotion(): boolean {\n return typeof window !== \"undefined\" && typeof window.matchMedia === \"function\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n/** The rules a page fade needs, for the class the navigation carries. */\nexport function pageTransitionCss(transition: string, durationMs: number): string {\n return (\n `::view-transition-old(.${transition}),::view-transition-new(.${transition}),` +\n `::view-transition-group(*),::view-transition-old(root),::view-transition-new(root){animation-duration:${durationMs}ms}` +\n `::view-transition{pointer-events:none}` +\n `@media (prefers-reduced-motion:reduce){::view-transition-old(*),::view-transition-new(*),::view-transition-group(*){animation:none}}`\n );\n}\n\nexport function PageTransition({\n children,\n className,\n transition = \"page\",\n duration = 120,\n webkit = \"skip\",\n}: {\n children: ReactNode;\n /** On the wrapper element the pair is made of; carry the body's layout here. */\n className?: string;\n /** The view-transition class a navigation gets, for CSS of your own on top. */\n transition?: string;\n /** How long the fade runs, in milliseconds. The group and root pairs match it. */\n duration?: number;\n /**\n * What WebKit gets. `skip` for an app with long pages - the default,\n * since a landing page usually is one; `run` for an app whose pages are\n * a screen tall, where the snapshot is small and the fade is fine.\n */\n webkit?: \"skip\" | \"run\";\n}) {\n const [skip, setSkip] = useState(false);\n\n useEffect(() => {\n if ((webkit === \"skip\" && snapshotsWholePage()) || prefersReducedMotion()) setSkip(true);\n }, [webkit]);\n\n return (\n <ViewTransition default={{ \"rsc-navigation\": skip ? \"none\" : transition, default: \"none\" }}>\n <div className={className}>\n <style href={`rsc-kit-page-transition-${transition}`} precedence=\"rsc-kit\">\n {pageTransitionCss(transition, duration)}\n </style>\n {children}\n </div>\n </ViewTransition>\n );\n}\n"]}
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* `value` is null where the url is not known: the shell of a route that
|
|
4
|
+
* listed no urls, rendered once for every url it matches, the holes that
|
|
5
|
+
* resume it, and the payload a document served from that shell boots from.
|
|
6
|
+
* The hooks answer "" then - no link active, no breadcrumb - and
|
|
7
|
+
* usePathname, a useSyncExternalStore, moves to the browser's url the
|
|
8
|
+
* moment hydration is done. The three renders agree with each other, which
|
|
9
|
+
* is what hydration needs; the browser knows the rest.
|
|
10
|
+
*/
|
|
2
11
|
export declare function PathnameProvider({ value, children }: {
|
|
3
|
-
value: string;
|
|
12
|
+
value: string | null;
|
|
4
13
|
children: ReactNode;
|
|
5
14
|
}): import("react").JSX.Element;
|
|
6
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* What the server rendered, or "" where it could not know. On the client
|
|
17
|
+
* this is what the payload said, so hydration matches the document; the
|
|
18
|
+
* live url is usePathname's, from the browser.
|
|
19
|
+
*/
|
|
7
20
|
export declare function useRenderedPathname(): string;
|
|
@@ -13,7 +13,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
13
13
|
* in the same process, and a variable would hand one page the other's url.
|
|
14
14
|
*/
|
|
15
15
|
import { createContext, useContext, useEffect } from "react";
|
|
16
|
+
// "/" with no provider at all - a component rendered outside the tree, a test.
|
|
17
|
+
// null from a provider means the url is not known: see below.
|
|
16
18
|
const PathnameContext = createContext("/");
|
|
19
|
+
/**
|
|
20
|
+
* `value` is null where the url is not known: the shell of a route that
|
|
21
|
+
* listed no urls, rendered once for every url it matches, the holes that
|
|
22
|
+
* resume it, and the payload a document served from that shell boots from.
|
|
23
|
+
* The hooks answer "" then - no link active, no breadcrumb - and
|
|
24
|
+
* usePathname, a useSyncExternalStore, moves to the browser's url the
|
|
25
|
+
* moment hydration is done. The three renders agree with each other, which
|
|
26
|
+
* is what hydration needs; the browser knows the rest.
|
|
27
|
+
*/
|
|
17
28
|
export function PathnameProvider({ value, children }) {
|
|
18
29
|
// The outermost client component on every page with a runtime, so its
|
|
19
30
|
// first effect is the moment hydration has committed - the moment a
|
|
@@ -30,8 +41,12 @@ export function PathnameProvider({ value, children }) {
|
|
|
30
41
|
}, []);
|
|
31
42
|
return _jsx(PathnameContext.Provider, { value: value, children: children });
|
|
32
43
|
}
|
|
33
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* What the server rendered, or "" where it could not know. On the client
|
|
46
|
+
* this is what the payload said, so hydration matches the document; the
|
|
47
|
+
* live url is usePathname's, from the browser.
|
|
48
|
+
*/
|
|
34
49
|
export function useRenderedPathname() {
|
|
35
|
-
return useContext(PathnameContext);
|
|
50
|
+
return useContext(PathnameContext) ?? "";
|
|
36
51
|
}
|
|
37
52
|
//# sourceMappingURL=PathnameProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PathnameProvider.js","sourceRoot":"","sources":["../../src/js/PathnameProvider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAG7D,MAAM,eAAe,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"PathnameProvider.js","sourceRoot":"","sources":["../../src/js/PathnameProvider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAG7D,+EAA+E;AAC/E,8DAA8D;AAC9D,MAAM,eAAe,GAAG,aAAa,CAAgB,GAAG,CAAC,CAAC;AAE1D;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAiD;IACjG,sEAAsE;IACtE,oEAAoE;IACpE,mEAAmE;IACnE,yEAAyE;IACzE,sEAAsE;IACtE,qBAAqB;IACrB,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,GAAG,MAAsE,CAAC;QAEjF,IAAI,CAAC,CAAC,cAAc;YAAE,OAAO;QAE7B,CAAC,CAAC,cAAc,GAAG,IAAI,CAAC;QACxB,CAAC,CAAC,iBAAiB,EAAE,EAAE,CAAC;IAC1B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,KAAC,eAAe,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA4B,CAAC;AACvF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,UAAU,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;AAC3C,CAAC","sourcesContent":["\"use client\";\n\n/**\n * The url the server rendered, handed to the client hooks.\n *\n * usePathname used to answer \"/\" during a server render, because a hook has no\n * props and the module had nothing else to go on. Every prerendered page\n * therefore shipped with the wrong link marked active, corrected only once\n * hydration ran — a visible flash, and simply wrong on a route that ships no\n * client runtime at all.\n *\n * A context rather than a module-level variable: two requests render at once\n * in the same process, and a variable would hand one page the other's url.\n */\n\nimport { createContext, useContext, useEffect } from \"react\";\nimport type { ReactNode } from \"react\";\n\n// \"/\" with no provider at all - a component rendered outside the tree, a test.\n// null from a provider means the url is not known: see below.\nconst PathnameContext = createContext<string | null>(\"/\");\n\n/**\n * `value` is null where the url is not known: the shell of a route that\n * listed no urls, rendered once for every url it matches, the holes that\n * resume it, and the payload a document served from that shell boots from.\n * The hooks answer \"\" then - no link active, no breadcrumb - and\n * usePathname, a useSyncExternalStore, moves to the browser's url the\n * moment hydration is done. The three renders agree with each other, which\n * is what hydration needs; the browser knows the rest.\n */\nexport function PathnameProvider({ value, children }: { value: string | null; children: ReactNode }) {\n // The outermost client component on every page with a runtime, so its\n // first effect is the moment hydration has committed - the moment a\n // click on a <Link> is React's to handle. Until then the bootstrap\n // script's listener holds the taps; see earlyClicks. Announced through a\n // global rather than a module, because this component and the runtime\n // are bundled apart.\n useEffect(() => {\n const w = window as { __rsc_hydrated?: boolean; __rsc_on_hydrated?: () => void };\n\n if (w.__rsc_hydrated) return;\n\n w.__rsc_hydrated = true;\n w.__rsc_on_hydrated?.();\n }, []);\n\n return <PathnameContext.Provider value={value}>{children}</PathnameContext.Provider>;\n}\n\n/**\n * What the server rendered, or \"\" where it could not know. On the client\n * this is what the payload said, so hydration matches the document; the\n * live url is usePathname's, from the browser.\n */\nexport function useRenderedPathname(): string {\n return useContext(PathnameContext) ?? \"\";\n}\n"]}
|
|
@@ -44,7 +44,8 @@ const addTransitionType = React.addTransitionType ??
|
|
|
44
44
|
React
|
|
45
45
|
.unstable_addTransitionType;
|
|
46
46
|
import { RedirectBoundary } from "./RedirectBoundary";
|
|
47
|
-
import {
|
|
47
|
+
import { navigationCommitted } from "./perf";
|
|
48
|
+
import { getSegmentState, replaceActive, navigatedOnce, seedSegment, subscribeToSegment, } from "./segmentStore";
|
|
48
49
|
export function SegmentBoundary({ depth, pageKey, children, }) {
|
|
49
50
|
// The store still addresses the boundary; the render reads React state, so
|
|
50
51
|
// the update can be a transition. Initialised from the store on the client
|
|
@@ -60,10 +61,22 @@ export function SegmentBoundary({ depth, pageKey, children, }) {
|
|
|
60
61
|
setState(getSegmentState(depth));
|
|
61
62
|
});
|
|
62
63
|
}), [depth]);
|
|
64
|
+
// The commit that put a navigated segment on screen: the end of the
|
|
65
|
+
// navigation's measure, see perf.ts. Not on the first render, which is the
|
|
66
|
+
// seed.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (state !== null)
|
|
69
|
+
navigationCommitted();
|
|
70
|
+
}, [state]);
|
|
63
71
|
// Record the page we arrived on, so a later navigation away and back can
|
|
64
72
|
// return to it. Without this the first page is the one page you cannot keep.
|
|
73
|
+
// New children while the store already holds this depth is the document
|
|
74
|
+
// rendered again in place - revalidate("all") - and the page on screen
|
|
75
|
+
// takes them under the key it has; see replaceActive.
|
|
65
76
|
useEffect(() => {
|
|
66
|
-
if (
|
|
77
|
+
if (getSegmentState(depth))
|
|
78
|
+
replaceActive(depth, children);
|
|
79
|
+
else if (pageKey)
|
|
67
80
|
seedSegment(depth, pageKey, children);
|
|
68
81
|
}, [depth, pageKey, children]);
|
|
69
82
|
// Wrapped here rather than around the whole app because this is the closest
|
|
@@ -82,8 +95,19 @@ export function SegmentBoundary({ depth, pageKey, children, }) {
|
|
|
82
95
|
// wrapper, keyed by the page, so when the store takes over after hydration
|
|
83
96
|
// the tree changes state but not shape - and React keeps the DOM instead of
|
|
84
97
|
// remounting the page, which was blank, then content, on every load.
|
|
98
|
+
//
|
|
99
|
+
// The Activity is there whether or not there is a page key to hang it on.
|
|
100
|
+
// A parameterised route's PPR shell is rendered for every url it matches
|
|
101
|
+
// and so carries no key - and the boundary used to leave the Activity out
|
|
102
|
+
// then, so the shell had no <!--&--> markers where the client, hydrating
|
|
103
|
+
// from the payload for the real url, rendered one. React 19.2 does not
|
|
104
|
+
// recover from a hydration mismatch at an Activity: the boundary retries
|
|
105
|
+
// hydrating, mismatches again, and the main thread never returns. A port
|
|
106
|
+
// found /agent/tools/3 freezing the tab on every document load. The
|
|
107
|
+
// markup of an Activity does not depend on its key, so the shell and the
|
|
108
|
+
// client agree whenever both render one.
|
|
85
109
|
if (!state) {
|
|
86
|
-
return
|
|
110
|
+
return (_jsx(Activity, { mode: "visible", children: _jsx(RedirectBoundary, { children: children }) }, pageKey));
|
|
87
111
|
}
|
|
88
112
|
return (_jsx(_Fragment, { children: state.entries.map((entry) => (_jsx(Activity, { mode: entry.key === state.activeKey ? "visible" : "hidden", children: _jsx(RedirectBoundary, { active: entry.key === state.activeKey, children: entry.tree }) }, entry.key))) }));
|
|
89
113
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentBoundary.js","sourceRoot":"","sources":["../../src/js/SegmentBoundary.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,gBAAgB,CAAC;AAE3D,4EAA4E;AAC5E,yEAAyE;AACzE,uDAAuD;AACvD,MAAM,iBAAiB,GACpB,KAAwD,CAAC,iBAAiB;IAC1E,KAAiE;SAC/D,0BAA0B,CAAC;AAEhC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,eAAe,EACf,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,MAAM,UAAU,eAAe,CAAC,EAC9B,KAAK,EACL,OAAO,EACP,QAAQ,GAMT;IACC,2EAA2E;IAC3E,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAqC,GAAG,EAAE,CAC1E,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAC9D,CAAC;IAEF,SAAS,CACP,GAAG,EAAE,CACH,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAE;QAC7B,eAAe,CAAC,GAAG,EAAE;YACnB,+DAA+D;YAC/D,gEAAgE;YAChE,sBAAsB;YACtB,IAAI,aAAa,EAAE;gBAAE,iBAAiB,EAAE,CAAC,0BAA0B,CAAC,CAAC;YAErE,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,EACJ,CAAC,KAAK,CAAC,CACR,CAAC;IAEF,yEAAyE;IACzE,6EAA6E;IAC7E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO;YAAE,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"SegmentBoundary.js","sourceRoot":"","sources":["../../src/js/SegmentBoundary.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,gBAAgB,CAAC;AAE3D,4EAA4E;AAC5E,yEAAyE;AACzE,uDAAuD;AACvD,MAAM,iBAAiB,GACpB,KAAwD,CAAC,iBAAiB;IAC1E,KAAiE;SAC/D,0BAA0B,CAAC;AAEhC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,EACL,eAAe,EACf,aAAa,EACb,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,MAAM,UAAU,eAAe,CAAC,EAC9B,KAAK,EACL,OAAO,EACP,QAAQ,GAMT;IACC,2EAA2E;IAC3E,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAqC,GAAG,EAAE,CAC1E,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAC9D,CAAC;IAEF,SAAS,CACP,GAAG,EAAE,CACH,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAE;QAC7B,eAAe,CAAC,GAAG,EAAE;YACnB,+DAA+D;YAC/D,gEAAgE;YAChE,sBAAsB;YACtB,IAAI,aAAa,EAAE;gBAAE,iBAAiB,EAAE,CAAC,0BAA0B,CAAC,CAAC;YAErE,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,EACJ,CAAC,KAAK,CAAC,CACR,CAAC;IAEF,oEAAoE;IACpE,2EAA2E;IAC3E,QAAQ;IACR,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,KAAK,KAAK,IAAI;YAAE,mBAAmB,EAAE,CAAC;IAC5C,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,yEAAyE;IACzE,6EAA6E;IAC7E,wEAAwE;IACxE,uEAAuE;IACvE,sDAAsD;IACtD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,eAAe,CAAC,KAAK,CAAC;YAAE,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;aACtD,IAAI,OAAO;YAAE,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC1D,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IAE/B,4EAA4E;IAC5E,yEAAyE;IACzE,4EAA4E;IAC5E,uEAAuE;IACvE,qEAAqE;IACrE,uEAAuE;IACvE,uEAAuE;IACvE,uEAAuE;IACvE,yEAAyE;IACzE,0EAA0E;IAC1E,QAAQ;IACR,wEAAwE;IACxE,sEAAsE;IACtE,2EAA2E;IAC3E,4EAA4E;IAC5E,qEAAqE;IACrE,EAAE;IACF,0EAA0E;IAC1E,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,uEAAuE;IACvE,yEAAyE;IACzE,yEAAyE;IACzE,oEAAoE;IACpE,yEAAyE;IACzE,yCAAyC;IACzC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CACL,KAAC,QAAQ,IAAe,IAAI,EAAC,SAAS,YACpC,KAAC,gBAAgB,cAAE,QAAQ,GAAoB,IADlC,OAAO,CAEX,CACZ,CAAC;IACJ,CAAC;IAED,OAAO,CACL,4BACG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAC5B,KAAC,QAAQ,IAEP,IAAI,EAAE,KAAK,CAAC,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,YAE1D,KAAC,gBAAgB,IAAC,MAAM,EAAE,KAAK,CAAC,GAAG,KAAK,KAAK,CAAC,SAAS,YACpD,KAAK,CAAC,IAAiB,GACP,IALd,KAAK,CAAC,GAAG,CAML,CACZ,CAAC,GACD,CACJ,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\n/**\n * The point in the tree a navigation can replace on its own.\n *\n * Sits between a layout and its children, so showing a different segment\n * re-renders from here down and leaves the layouts above it mounted. Server\n * components cannot be re-rendered on the client, which is why the seam has to\n * be a client component reading from a store rather than the layout itself.\n *\n * Pages it has already shown stay mounted behind <Activity mode=\"hidden\">.\n * Hidden is not unmounted: effects are torn down but state survives, so\n * returning to a page brings back the form you were filling in. Rendering only\n * the current one would throw that away, which is what replacing the root did.\n */\n\nimport * as React from \"react\";\nimport { Activity, startTransition, useEffect, useState } from \"react\";\n\n/**\n * The transition type a navigation carries.\n *\n * The engine does not animate a navigation itself; React's own\n * <ViewTransition> does, wherever an app puts one. What the app cannot do\n * from outside is tell a navigation from the seed after hydration - the\n * boundary taking over its own server-rendered children, which animated\n * would be the page fading into itself on every load. So a navigation's\n * update carries this type, and the seed's does not: a boundary keyed on it,\n * `default={{ \"rsc-navigation\": \"page\", default: \"none\" }}`, animates\n * navigations and nothing else.\n *\n * An earlier shape wrapped the segment in a boundary of the engine's own,\n * behind a flag. React names every top-level element under a boundary and\n * morphs each from where it was to where it is, so the stylesheet pulled\n * them back into one root fade - and fought React over the root, which it\n * cancels when nothing outside a boundary changed: a captured root with a\n * hidden group, a dark viewport on the first navigation. One wrapper element\n * under the app's own boundary is one snapshot pair, which is the dissolve\n * wanted, with nothing to fight.\n */\nexport const NAVIGATION_TRANSITION_TYPE = \"rsc-navigation\";\n\n// Exported by React 19.3; `unstable_` before it. Read at call time from the\n// namespace rather than imported by name, so a React that lacks it still\n// loads this module and simply does not mark the type.\nconst addTransitionType: ((type: string) => void) | undefined =\n (React as { addTransitionType?: (type: string) => void }).addTransitionType ??\n (React as { unstable_addTransitionType?: (type: string) => void })\n .unstable_addTransitionType;\nimport type { ReactNode } from \"react\";\nimport { RedirectBoundary } from \"./RedirectBoundary\";\nimport { navigationCommitted } from \"./perf\";\nimport {\n getSegmentState,\n replaceActive,\n navigatedOnce,\n seedSegment,\n subscribeToSegment,\n} from \"./segmentStore\";\n\nexport function SegmentBoundary({\n depth,\n pageKey,\n children,\n}: {\n depth: number;\n /** The page these server-rendered children belong to. */\n pageKey?: string;\n children: ReactNode;\n}) {\n // The store still addresses the boundary; the render reads React state, so\n // the update can be a transition. Initialised from the store on the client\n // and null on the server — exactly what getServerSnapshot did.\n const [state, setState] = useState<ReturnType<typeof getSegmentState>>(() =>\n typeof window === \"undefined\" ? null : getSegmentState(depth),\n );\n\n useEffect(\n () =>\n subscribeToSegment(depth, () => {\n startTransition(() => {\n // A navigation, not the seed: the seed is the page taking over\n // its own server-rendered children, and fading that is the page\n // fading into itself.\n if (navigatedOnce()) addTransitionType?.(NAVIGATION_TRANSITION_TYPE);\n\n setState(getSegmentState(depth));\n });\n }),\n [depth],\n );\n\n // The commit that put a navigated segment on screen: the end of the\n // navigation's measure, see perf.ts. Not on the first render, which is the\n // seed.\n useEffect(() => {\n if (state !== null) navigationCommitted();\n }, [state]);\n\n // Record the page we arrived on, so a later navigation away and back can\n // return to it. Without this the first page is the one page you cannot keep.\n // New children while the store already holds this depth is the document\n // rendered again in place - revalidate(\"all\") - and the page on screen\n // takes them under the key it has; see replaceActive.\n useEffect(() => {\n if (getSegmentState(depth)) replaceActive(depth, children);\n else if (pageKey) seedSegment(depth, pageKey, children);\n }, [depth, pageKey, children]);\n\n // Wrapped here rather than around the whole app because this is the closest\n // client component above a page: a redirect thrown inside the page's own\n // Suspense boundary surfaces at the nearest error boundary, and catching it\n // here leaves the layouts above mounted while the navigation runs. One\n // boundary per page, inside its Activity, not one around them all: a\n // boundary that catches renders its fallback in place of everything it\n // wraps, and one around them all unmounted every page being kept alive\n // behind the one redirecting - the destination among them, when it was\n // held - and then kept showing the fallback over the destination once it\n // arrived. The port's report was a sidebar and a breadcrumb over an empty\n // main.\n // The same shape before and after the seed. The server's render and the\n // first client render already place the children inside the retention\n // wrapper, keyed by the page, so when the store takes over after hydration\n // the tree changes state but not shape - and React keeps the DOM instead of\n // remounting the page, which was blank, then content, on every load.\n //\n // The Activity is there whether or not there is a page key to hang it on.\n // A parameterised route's PPR shell is rendered for every url it matches\n // and so carries no key - and the boundary used to leave the Activity out\n // then, so the shell had no <!--&--> markers where the client, hydrating\n // from the payload for the real url, rendered one. React 19.2 does not\n // recover from a hydration mismatch at an Activity: the boundary retries\n // hydrating, mismatches again, and the main thread never returns. A port\n // found /agent/tools/3 freezing the tab on every document load. The\n // markup of an Activity does not depend on its key, so the shell and the\n // client agree whenever both render one.\n if (!state) {\n return (\n <Activity key={pageKey} mode=\"visible\">\n <RedirectBoundary>{children}</RedirectBoundary>\n </Activity>\n );\n }\n\n return (\n <>\n {state.entries.map((entry) => (\n <Activity\n key={entry.key}\n mode={entry.key === state.activeKey ? \"visible\" : \"hidden\"}\n >\n <RedirectBoundary active={entry.key === state.activeKey}>\n {entry.tree as ReactNode}\n </RedirectBoundary>\n </Activity>\n ))}\n </>\n );\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many <Activity> boundaries the server put in this document.
|
|
3
|
+
*
|
|
4
|
+
* React marks each with a comment, <!--&--> to <!--/&-->; the opening ones
|
|
5
|
+
* are counted.
|
|
6
|
+
*/
|
|
7
|
+
export function activityMarkersIn(container) {
|
|
8
|
+
if (typeof document === "undefined" || typeof document.createTreeWalker !== "function")
|
|
9
|
+
return 1;
|
|
10
|
+
const walker = document.createTreeWalker(container, 128 /* NodeFilter.SHOW_COMMENT */);
|
|
11
|
+
let count = 0;
|
|
12
|
+
while (walker.nextNode()) {
|
|
13
|
+
if (walker.currentNode.data === "&")
|
|
14
|
+
count++;
|
|
15
|
+
}
|
|
16
|
+
return count;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=activityMarkers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activityMarkers.js","sourceRoot":"","sources":["../../src/js/activityMarkers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAA6B;IAC7D,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,OAAO,QAAQ,CAAC,gBAAgB,KAAK,UAAU;QAAE,OAAO,CAAC,CAAC;IAEjG,MAAM,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,GAAG,CAAC,6BAA6B,CAAC,CAAC;IACvF,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACzB,IAAK,MAAM,CAAC,WAAuB,CAAC,IAAI,KAAK,GAAG;YAAE,KAAK,EAAE,CAAC;IAC5D,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * How many <Activity> boundaries the server put in this document.\n *\n * React marks each with a comment, <!--&--> to <!--/&-->; the opening ones\n * are counted.\n */\nexport function activityMarkersIn(container: Element | Document): number {\n if (typeof document === \"undefined\" || typeof document.createTreeWalker !== \"function\") return 1;\n\n const walker = document.createTreeWalker(container, 128 /* NodeFilter.SHOW_COMMENT */);\n let count = 0;\n\n while (walker.nextNode()) {\n if ((walker.currentNode as Comment).data === \"&\") count++;\n }\n\n return count;\n}\n"]}
|
|
@@ -12,14 +12,15 @@ import { caughtByLoading } from "./fallbackReport";
|
|
|
12
12
|
import { noteNavigation } from "./segmentStore";
|
|
13
13
|
import { isSafeRedirect } from "../safeUrl.js";
|
|
14
14
|
import { createFromReadableStream, encodeReply, setServerCallback, } from "@vitejs/plugin-rsc/browser";
|
|
15
|
-
import { createElement } from "react";
|
|
16
|
-
import { hydrateRoot } from "react-dom/client";
|
|
15
|
+
import { createElement, startTransition } from "react";
|
|
16
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
17
|
+
import { activityMarkersIn } from "./activityMarkers";
|
|
17
18
|
import { ActivityRoot } from "./ActivityRouter";
|
|
18
19
|
import { ServerRedirectError, noteRedirected, throwForFailedAction } from "./errors";
|
|
19
20
|
import { fetchPagePayload } from "./pagePayload";
|
|
20
21
|
import { claimRead, setQueryCodec } from "./queryClient";
|
|
21
|
-
import { clearSegments, restoreSegments, setSegment } from "./segmentStore";
|
|
22
|
-
import { cancelPrefetch, isPrefetched, navigate, setApiRoutes, refresh,
|
|
22
|
+
import { clearSegments, dropHidden, isHeld, prerenderSegment, restoreSegments, setSegment } from "./segmentStore";
|
|
23
|
+
import { cancelPrefetch, isPrefetched, navigate, setApiRoutes, refresh, applyRevalidations, prefetch, retentionKey, setCallServer, setHeldLayouts, setStaticPayloads, setStaticRoutes, seedStaticChain, payloadUrl, setDeserializer, setInterceptManifest, getHeldLayouts, setNavigateHandler, setHeldHandlers, setPrerenderHandler, setReplaceRootHandler, setRestoreHandler, setVersion, } from "./navigate";
|
|
23
24
|
/**
|
|
24
25
|
* A redirect that travelled as an error: the digest of the error itself, or
|
|
25
26
|
* of the cause React wraps it in when it reports client-rendering a boundary
|
|
@@ -72,15 +73,17 @@ export async function createViteRscApp(container = document, interceptEntries =
|
|
|
72
73
|
body = encoded;
|
|
73
74
|
realContentType = "text/plain;charset=UTF-8";
|
|
74
75
|
}
|
|
76
|
+
// Where the action was invoked from. The host resolves it to the
|
|
77
|
+
// components that render the page, so anything the action says it
|
|
78
|
+
// invalidated can come back with the answer instead of being fetched
|
|
79
|
+
// afterwards - and what comes back is for this page, not whichever is
|
|
80
|
+
// showing when it lands.
|
|
81
|
+
const from = window.location.pathname + window.location.search;
|
|
75
82
|
const res = await fetch("/_rsc/action", {
|
|
76
83
|
method: "POST",
|
|
77
84
|
headers: {
|
|
78
85
|
"X-RSC-Action": id,
|
|
79
|
-
|
|
80
|
-
// components that render the page, so anything the action says it
|
|
81
|
-
// invalidated can come back with the answer instead of being fetched
|
|
82
|
-
// afterwards.
|
|
83
|
-
"X-RSC-Referer": window.location.pathname + window.location.search,
|
|
86
|
+
"X-RSC-Referer": from,
|
|
84
87
|
"X-RSC-Content-Type": realContentType,
|
|
85
88
|
"Content-Type": "application/octet-stream",
|
|
86
89
|
"X-XSRF-TOKEN": decodeURIComponent(document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? ""),
|
|
@@ -120,7 +123,7 @@ export async function createViteRscApp(container = document, interceptEntries =
|
|
|
120
123
|
throw err;
|
|
121
124
|
}
|
|
122
125
|
const answer = await createFromReadableStream(res.body, { callServer });
|
|
123
|
-
return unwrapRevalidated(answer);
|
|
126
|
+
return unwrapRevalidated(answer, from);
|
|
124
127
|
}
|
|
125
128
|
/**
|
|
126
129
|
* Put anything the action re-rendered on screen, and hand back its result.
|
|
@@ -129,16 +132,14 @@ export async function createViteRscApp(container = document, interceptEntries =
|
|
|
129
132
|
* the caller sees only what its action returned and never knows the page
|
|
130
133
|
* was updated around it.
|
|
131
134
|
*/
|
|
132
|
-
function unwrapRevalidated(answer) {
|
|
135
|
+
function unwrapRevalidated(answer, from) {
|
|
133
136
|
if (answer === null ||
|
|
134
137
|
typeof answer !== "object" ||
|
|
135
138
|
!("__rscRevalidated" in answer)) {
|
|
136
139
|
return answer;
|
|
137
140
|
}
|
|
138
141
|
const envelope = answer;
|
|
139
|
-
|
|
140
|
-
applyRevalidated(target, tree);
|
|
141
|
-
}
|
|
142
|
+
applyRevalidations(from, envelope.__rscRevalidated);
|
|
142
143
|
return envelope.result;
|
|
143
144
|
}
|
|
144
145
|
setDeserializer(createFromReadableStream);
|
|
@@ -215,68 +216,90 @@ export async function createViteRscApp(container = document, interceptEntries =
|
|
|
215
216
|
initialTree: tree,
|
|
216
217
|
})
|
|
217
218
|
: tree;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
219
|
+
// A document that cannot be hydrated by this tree is client-rendered
|
|
220
|
+
// instead - a repaint, not a freeze. React 19.2 does not recover from a
|
|
221
|
+
// hydration mismatch at an <Activity>: it retries hydrating the boundary,
|
|
222
|
+
// mismatches again, and never returns to the main thread. Every segment
|
|
223
|
+
// boundary is an Activity, one per layout, so a document with none where
|
|
224
|
+
// the page has layouts is that mismatch, guaranteed, before React starts.
|
|
225
|
+
// What produced one: a parameterised route's PPR shell, rendered without
|
|
226
|
+
// a page key and, by a build before this one, without the Activities too.
|
|
227
|
+
// None rather than fewer: a boundary inside a Suspense hole the shell left
|
|
228
|
+
// unfinished has no marker and is client-rendered by React on its own,
|
|
229
|
+
// which is not a mismatch. The stale-asset reload does not apply - the
|
|
230
|
+
// document would come back the same.
|
|
231
|
+
const missing = retains && getHeldLayouts().length > 0 && activityMarkersIn(container) === 0;
|
|
232
|
+
if (missing) {
|
|
233
|
+
console.warn("[rsc-kit] The document has no <Activity> boundaries where the page has layouts, so it cannot be hydrated; rendering the page instead. A PPR shell built by an earlier release lacks them - rebuild.");
|
|
234
|
+
}
|
|
235
|
+
// The payload answered with an error status - the not-found page for a
|
|
236
|
+
// url the shell was served for anyway. It is not the document on screen,
|
|
237
|
+
// so it is rendered, not hydrated: a mismatch at an Activity is the loop
|
|
238
|
+
// above, and this one is certain.
|
|
239
|
+
const disagrees = !res.ok;
|
|
240
|
+
const onRecoverableError = (error, errorInfo) => {
|
|
241
|
+
// A client component whose chunk the server no longer has fails while
|
|
242
|
+
// hydrating, and React reports that as recoverable: the page is loaded
|
|
243
|
+
// again, with the current names.
|
|
244
|
+
if (recoverFromStaleAssets(error))
|
|
245
|
+
return;
|
|
246
|
+
// A redirect decided inside a boundary: the server's row carried it as
|
|
247
|
+
// the digest, React client-renders the boundary and reports that it
|
|
248
|
+
// did, and the RedirectBoundary is already performing it. Nothing to
|
|
249
|
+
// read in the console about a page doing what it was told.
|
|
250
|
+
if (isRedirect(error))
|
|
251
|
+
return;
|
|
252
|
+
// A PPR shell is served with its Suspense boundaries deliberately
|
|
253
|
+
// unfinished — the build aborts the render once the static part is out.
|
|
254
|
+
// React reports that as #419 and client-renders the boundary from the
|
|
255
|
+
// Flight payload, which is the intended path, not a fault to report.
|
|
256
|
+
const message = String(error?.message ?? error);
|
|
257
|
+
if (message.includes("419") ||
|
|
258
|
+
message.includes("did not finish this Suspense boundary")) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// A view transition the browser cancelled because the tab was not
|
|
262
|
+
// visible. React reports it here; the update still landed, and a tab
|
|
263
|
+
// in the background is not a fault in the page.
|
|
264
|
+
if (message.includes("Transition was aborted"))
|
|
265
|
+
return;
|
|
266
|
+
// A navigation another one overtook: its request was aborted, and a
|
|
267
|
+
// row of its payload that a component was still reading rejects with
|
|
268
|
+
// the abort. React recovers by rendering the root again synchronously
|
|
269
|
+
// and reports the recovery here as #520, with the abort as the cause.
|
|
270
|
+
// The page it recovered to is the one the second navigation asked for;
|
|
271
|
+
// the abort was the design, not a fault.
|
|
272
|
+
const cause = error?.cause;
|
|
273
|
+
if (cause?.name === "AbortError" ||
|
|
274
|
+
/\baborted\b/i.test(cause?.message ?? "") ||
|
|
275
|
+
/\baborted\b/i.test(message)) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// A page whose query string was read under a boundary carries the
|
|
279
|
+
// fallback there; React reports the recovery on hydration. Under a
|
|
280
|
+
// boundary the developer wrote, that is the designed path and nothing
|
|
281
|
+
// is said. With nothing closer than a loading.tsx, the whole segment
|
|
282
|
+
// showed the fallback until the query arrived - said here, in the
|
|
283
|
+
// page and the console, for whoever is looking at either. Production
|
|
284
|
+
// has no stack to tell the two apart; the build's note on the route is
|
|
285
|
+
// the record there.
|
|
286
|
+
if (error?.digest === SEARCH_PARAMS_FALLBACK ||
|
|
287
|
+
message.includes("useSearchParams()")) {
|
|
288
|
+
const stack = errorInfo
|
|
289
|
+
?.componentStack;
|
|
290
|
+
if (!caughtByLoading(stack))
|
|
276
291
|
return;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
const hint = "useSearchParams() was read on the server with nothing closer than a loading.tsx, so the " +
|
|
293
|
+
"whole segment showed that fallback until the query arrived. A <Suspense> around the " +
|
|
294
|
+
"component that reads keeps the rest of the page painted.";
|
|
295
|
+
showDevNotice(hint, stack);
|
|
296
|
+
console.error(new Error(hint), errorInfo);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.error(error, errorInfo);
|
|
300
|
+
};
|
|
301
|
+
const rootOptions = {
|
|
302
|
+
onRecoverableError,
|
|
280
303
|
// A chunk the deploy no longer serves is not a fault in the page; the
|
|
281
304
|
// document is loaded again, and the new names come with it. Anything
|
|
282
305
|
// else is reported the way React would have.
|
|
@@ -293,7 +316,14 @@ export async function createViteRscApp(container = document, interceptEntries =
|
|
|
293
316
|
return;
|
|
294
317
|
console.error(error, errorInfo);
|
|
295
318
|
},
|
|
296
|
-
}
|
|
319
|
+
};
|
|
320
|
+
const root = missing || disagrees
|
|
321
|
+
? (() => {
|
|
322
|
+
const created = createRoot(container, rootOptions);
|
|
323
|
+
created.render(shell);
|
|
324
|
+
return created;
|
|
325
|
+
})()
|
|
326
|
+
: hydrateRoot(container, shell, rootOptions);
|
|
297
327
|
// Depth 0 is a whole document and replaces the root. Anything deeper is one
|
|
298
328
|
// segment: handing it to the boundary at that depth leaves the layouts above
|
|
299
329
|
// it mounted, which is the point of asking for a partial render at all.
|
|
@@ -308,9 +338,25 @@ export async function createViteRscApp(container = document, interceptEntries =
|
|
|
308
338
|
clearSegments();
|
|
309
339
|
root.render(newTree);
|
|
310
340
|
});
|
|
341
|
+
// The whole document again, in place - revalidate("all"). Not through the
|
|
342
|
+
// handler above: that clears the boundaries and renders the tree as a new
|
|
343
|
+
// page, and a boundary emptied re-keys its Activity to the current url and
|
|
344
|
+
// remounts everything under it. A port's "added to cart" went with the
|
|
345
|
+
// form's state that way, on every product reached by a link. Here the root
|
|
346
|
+
// takes the tree in a transition and each boundary hands its page the new
|
|
347
|
+
// children under the key it has - see replaceActive.
|
|
348
|
+
setReplaceRootHandler((tree) => {
|
|
349
|
+
startTransition(() => root.render(tree));
|
|
350
|
+
});
|
|
311
351
|
// Back and forward reveal a page the boundaries are still holding, with the
|
|
312
352
|
// form you were filling in still filled in, and without asking the server.
|
|
313
353
|
setRestoreHandler((key, maxAge) => restoreSegments(key, maxAge));
|
|
354
|
+
setHeldHandlers(isHeld, dropHidden);
|
|
355
|
+
// A touch or a settled hover: the page is rendered hidden now, and the
|
|
356
|
+
// click reveals it. See warm() in navigate.ts and prerenderSegment.
|
|
357
|
+
setPrerenderHandler((tree, key, segmentDepth) => {
|
|
358
|
+
prerenderSegment(segmentDepth, key, tree);
|
|
359
|
+
});
|
|
314
360
|
window.addEventListener("popstate", () => {
|
|
315
361
|
// restore: back and forward reveal the page you were on, with its state.
|
|
316
362
|
// Wherever the browser just went; not a literal this app wrote.
|