@rsc-kit/core 0.1.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 +95 -0
- package/SECURITY.md +60 -0
- package/dist/action.d.ts +90 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +137 -0
- package/dist/action.js.map +1 -0
- package/dist/build-rsc-vite.d.ts +39 -0
- package/dist/build-rsc-vite.d.ts.map +1 -0
- package/dist/build-rsc-vite.js +172 -0
- package/dist/build-rsc-vite.js.map +1 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +108 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +85 -0
- package/dist/cli.js.map +1 -0
- package/dist/compile.d.ts +55 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +141 -0
- package/dist/compile.js.map +1 -0
- package/dist/devServer.d.ts +18 -0
- package/dist/devServer.d.ts.map +1 -0
- package/dist/devServer.js +70 -0
- package/dist/devServer.js.map +1 -0
- package/dist/devUrls.d.ts +11 -0
- package/dist/devUrls.d.ts.map +1 -0
- package/dist/devUrls.js +68 -0
- package/dist/devUrls.js.map +1 -0
- package/dist/embed.d.ts +21 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +119 -0
- package/dist/embed.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/export.d.ts +41 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +100 -0
- package/dist/export.js.map +1 -0
- package/dist/files.d.ts +39 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +146 -0
- package/dist/files.js.map +1 -0
- package/dist/headers.d.ts +81 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +96 -0
- package/dist/headers.js.map +1 -0
- package/dist/host.d.ts +86 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +590 -0
- package/dist/host.js.map +1 -0
- package/dist/js/ActivityRouter.d.ts +47 -0
- package/dist/js/ActivityRouter.d.ts.map +1 -0
- package/dist/js/ActivityRouter.js +87 -0
- package/dist/js/ActivityRouter.js.map +1 -0
- package/dist/js/DocumentTitle.d.ts +22 -0
- package/dist/js/DocumentTitle.d.ts.map +1 -0
- package/dist/js/DocumentTitle.js +28 -0
- package/dist/js/DocumentTitle.js.map +1 -0
- package/dist/js/Form.d.ts +54 -0
- package/dist/js/Form.d.ts.map +1 -0
- package/dist/js/Form.js +225 -0
- package/dist/js/Form.js.map +1 -0
- package/dist/js/Link.d.ts +29 -0
- package/dist/js/Link.d.ts.map +1 -0
- package/dist/js/Link.js +112 -0
- package/dist/js/Link.js.map +1 -0
- package/dist/js/PathnameProvider.d.ts +8 -0
- package/dist/js/PathnameProvider.d.ts.map +1 -0
- package/dist/js/PathnameProvider.js +24 -0
- package/dist/js/PathnameProvider.js.map +1 -0
- package/dist/js/RedirectBoundary.d.ts +18 -0
- package/dist/js/RedirectBoundary.d.ts.map +1 -0
- package/dist/js/RedirectBoundary.js +34 -0
- package/dist/js/RedirectBoundary.js.map +1 -0
- package/dist/js/SegmentBoundary.d.ts +8 -0
- package/dist/js/SegmentBoundary.d.ts.map +1 -0
- package/dist/js/SegmentBoundary.js +38 -0
- package/dist/js/SegmentBoundary.js.map +1 -0
- package/dist/js/SlotBoundary.d.ts +15 -0
- package/dist/js/SlotBoundary.d.ts.map +1 -0
- package/dist/js/SlotBoundary.js +17 -0
- package/dist/js/SlotBoundary.js.map +1 -0
- package/dist/js/createViteRscApp.d.ts +8 -0
- package/dist/js/createViteRscApp.d.ts.map +1 -0
- package/dist/js/createViteRscApp.js +185 -0
- package/dist/js/createViteRscApp.js.map +1 -0
- package/dist/js/errors.d.ts +51 -0
- package/dist/js/errors.d.ts.map +1 -0
- package/dist/js/errors.js +93 -0
- package/dist/js/errors.js.map +1 -0
- package/dist/js/navigate.d.ts +118 -0
- package/dist/js/navigate.d.ts.map +1 -0
- package/dist/js/navigate.js +780 -0
- package/dist/js/navigate.js.map +1 -0
- package/dist/js/onlineStore.d.ts +23 -0
- package/dist/js/onlineStore.d.ts.map +1 -0
- package/dist/js/onlineStore.js +42 -0
- package/dist/js/onlineStore.js.map +1 -0
- package/dist/js/pagePayload.d.ts +15 -0
- package/dist/js/pagePayload.d.ts.map +1 -0
- package/dist/js/pagePayload.js +39 -0
- package/dist/js/pagePayload.js.map +1 -0
- package/dist/js/router.d.ts +29 -0
- package/dist/js/router.d.ts.map +1 -0
- package/dist/js/router.js +40 -0
- package/dist/js/router.js.map +1 -0
- package/dist/js/section.d.ts +16 -0
- package/dist/js/section.d.ts.map +1 -0
- package/dist/js/section.js +55 -0
- package/dist/js/section.js.map +1 -0
- package/dist/js/segmentStore.d.ts +69 -0
- package/dist/js/segmentStore.d.ts.map +1 -0
- package/dist/js/segmentStore.js +127 -0
- package/dist/js/segmentStore.js.map +1 -0
- package/dist/js/slotStore.d.ts +35 -0
- package/dist/js/slotStore.d.ts.map +1 -0
- package/dist/js/slotStore.js +46 -0
- package/dist/js/slotStore.js.map +1 -0
- package/dist/js/standardSchema.d.ts +47 -0
- package/dist/js/standardSchema.d.ts.map +1 -0
- package/dist/js/standardSchema.js +41 -0
- package/dist/js/standardSchema.js.map +1 -0
- package/dist/js/useForm.d.ts +43 -0
- package/dist/js/useForm.d.ts.map +1 -0
- package/dist/js/useForm.js +164 -0
- package/dist/js/useForm.js.map +1 -0
- package/dist/js/useLinkStatus.d.ts +2 -0
- package/dist/js/useLinkStatus.d.ts.map +1 -0
- package/dist/js/useLinkStatus.js +3 -0
- package/dist/js/useLinkStatus.js.map +1 -0
- package/dist/js/useOnline.d.ts +8 -0
- package/dist/js/useOnline.d.ts.map +1 -0
- package/dist/js/useOnline.js +20 -0
- package/dist/js/useOnline.js.map +1 -0
- package/dist/js/usePathname.d.ts +9 -0
- package/dist/js/usePathname.d.ts.map +1 -0
- package/dist/js/usePathname.js +32 -0
- package/dist/js/usePathname.js.map +1 -0
- package/dist/js/useSearchParams.d.ts +2 -0
- package/dist/js/useSearchParams.d.ts.map +1 -0
- package/dist/js/useSearchParams.js +60 -0
- package/dist/js/useSearchParams.js.map +1 -0
- package/dist/manifest.d.ts +66 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +13 -0
- package/dist/manifest.js.map +1 -0
- package/dist/prerender.d.ts +138 -0
- package/dist/prerender.d.ts.map +1 -0
- package/dist/prerender.js +387 -0
- package/dist/prerender.js.map +1 -0
- package/dist/redirect.d.ts +34 -0
- package/dist/redirect.d.ts.map +1 -0
- package/dist/redirect.js +108 -0
- package/dist/redirect.js.map +1 -0
- package/dist/redirectDigest.d.ts +20 -0
- package/dist/redirectDigest.d.ts.map +1 -0
- package/dist/redirectDigest.js +66 -0
- package/dist/redirectDigest.js.map +1 -0
- package/dist/request.d.ts +93 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +308 -0
- package/dist/request.js.map +1 -0
- package/dist/revalidate.d.ts +43 -0
- package/dist/revalidate.d.ts.map +1 -0
- package/dist/revalidate.js +99 -0
- package/dist/revalidate.js.map +1 -0
- package/dist/routes.d.ts +37 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +28 -0
- package/dist/routes.js.map +1 -0
- package/dist/routing.d.ts +52 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +146 -0
- package/dist/routing.js.map +1 -0
- package/dist/runtime.d.ts +36 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +86 -0
- package/dist/runtime.js.map +1 -0
- package/dist/safeUrl.d.ts +10 -0
- package/dist/safeUrl.d.ts.map +1 -0
- package/dist/safeUrl.js +40 -0
- package/dist/safeUrl.js.map +1 -0
- package/dist/streaming.d.ts +62 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +108 -0
- package/dist/streaming.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/vite.d.ts +109 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +1909 -0
- package/dist/vite.js.map +1 -0
- package/dist/worker.d.ts +2 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +863 -0
- package/dist/worker.js.map +1 -0
- package/package.json +223 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps recently visited pages mounted but hidden, so going back restores them
|
|
3
|
+
* rather than rebuilding them.
|
|
4
|
+
*
|
|
5
|
+
* Rendering each navigation with root.render() unmounts the previous tree, and
|
|
6
|
+
* unmounting destroys client state: a half-typed form, an open disclosure, a
|
|
7
|
+
* scrolled list. React's <Activity mode="hidden"> hides a tree without
|
|
8
|
+
* unmounting it — effects are torn down, but state survives — which is what
|
|
9
|
+
* makes returning feel like the browser's own back button rather than a reload.
|
|
10
|
+
*
|
|
11
|
+
* Retention is bounded. Hidden trees keep their DOM, so an unbounded history
|
|
12
|
+
* would grow the document for the length of the session; the least recently
|
|
13
|
+
* visited entry is dropped past the limit.
|
|
14
|
+
*/
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
16
|
+
export interface RouterEntry {
|
|
17
|
+
/** Identifies the page: its URL, or the intercept variant of one. */
|
|
18
|
+
key: string;
|
|
19
|
+
tree: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export interface ActivityRouterHandle {
|
|
22
|
+
/** Show `tree` for `key`, retaining whatever was visible before. */
|
|
23
|
+
show(key: string, tree: ReactNode): void;
|
|
24
|
+
/** Reveal a retained page without refetching. False when not retained. */
|
|
25
|
+
restore(key: string): boolean;
|
|
26
|
+
/** Drop every retained page but the visible one. */
|
|
27
|
+
clear(): void;
|
|
28
|
+
}
|
|
29
|
+
/** Pages kept alive behind the visible one. Four covers ordinary back-and-forth. */
|
|
30
|
+
export declare const DEFAULT_RETENTION = 4;
|
|
31
|
+
export declare function useActivityRouter(initial: RouterEntry, retention?: number): {
|
|
32
|
+
entries: RouterEntry[];
|
|
33
|
+
activeKey: string;
|
|
34
|
+
handle: ActivityRouterHandle;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Root of the hydrated app.
|
|
38
|
+
*
|
|
39
|
+
* Registers itself with the navigation engine so a navigation becomes a state
|
|
40
|
+
* update here rather than a fresh root.render(), which is what allows the
|
|
41
|
+
* previous page to stay mounted.
|
|
42
|
+
*/
|
|
43
|
+
export declare function ActivityRoot({ initialKey, initialTree }: {
|
|
44
|
+
initialKey: string;
|
|
45
|
+
initialTree: ReactNode;
|
|
46
|
+
}): import("react").JSX.Element;
|
|
47
|
+
//# sourceMappingURL=ActivityRouter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActivityRouter.d.ts","sourceRoot":"","sources":["../../src/js/ActivityRouter.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,SAAS,CAAA;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IACxC,0EAA0E;IAC1E,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IAC7B,oDAAoD;IACpD,KAAK,IAAI,IAAI,CAAA;CACd;AAED,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAElC,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,EACpB,SAAS,GAAE,MAA0B,GACpC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CA8D7E;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,SAAS,CAAA;CAAE,+BA6BvG"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Keeps recently visited pages mounted but hidden, so going back restores them
|
|
4
|
+
* rather than rebuilding them.
|
|
5
|
+
*
|
|
6
|
+
* Rendering each navigation with root.render() unmounts the previous tree, and
|
|
7
|
+
* unmounting destroys client state: a half-typed form, an open disclosure, a
|
|
8
|
+
* scrolled list. React's <Activity mode="hidden"> hides a tree without
|
|
9
|
+
* unmounting it — effects are torn down, but state survives — which is what
|
|
10
|
+
* makes returning feel like the browser's own back button rather than a reload.
|
|
11
|
+
*
|
|
12
|
+
* Retention is bounded. Hidden trees keep their DOM, so an unbounded history
|
|
13
|
+
* would grow the document for the length of the session; the least recently
|
|
14
|
+
* visited entry is dropped past the limit.
|
|
15
|
+
*/
|
|
16
|
+
import { Activity, useCallback, useEffect, useRef, useState } from 'react';
|
|
17
|
+
import { setNavigateHandler, setRestoreHandler } from './navigate';
|
|
18
|
+
import { clearSegments, setSegment } from './segmentStore';
|
|
19
|
+
/** Pages kept alive behind the visible one. Four covers ordinary back-and-forth. */
|
|
20
|
+
export const DEFAULT_RETENTION = 4;
|
|
21
|
+
export function useActivityRouter(initial, retention = DEFAULT_RETENTION) {
|
|
22
|
+
const [entries, setEntries] = useState([initial]);
|
|
23
|
+
const [activeKey, setActiveKey] = useState(initial.key);
|
|
24
|
+
// The router calls into this from outside React, where the values captured by
|
|
25
|
+
// the last render may already be stale. Refs mirror state so those calls read
|
|
26
|
+
// what is actually mounted.
|
|
27
|
+
const entriesRef = useRef(entries);
|
|
28
|
+
const orderRef = useRef([initial.key]);
|
|
29
|
+
const commit = useCallback((next) => {
|
|
30
|
+
entriesRef.current = next;
|
|
31
|
+
setEntries(next);
|
|
32
|
+
}, []);
|
|
33
|
+
// Recency, most recent last. Retention is by visit order rather than
|
|
34
|
+
// insertion, so bouncing between two pages evicts neither.
|
|
35
|
+
const touch = useCallback((key) => {
|
|
36
|
+
const next = orderRef.current.filter((k) => k !== key);
|
|
37
|
+
next.push(key);
|
|
38
|
+
orderRef.current = next.slice(-Math.max(1, retention));
|
|
39
|
+
return orderRef.current;
|
|
40
|
+
}, [retention]);
|
|
41
|
+
const show = useCallback((key, tree) => {
|
|
42
|
+
const kept = touch(key);
|
|
43
|
+
const others = entriesRef.current.filter((entry) => entry.key !== key && kept.includes(entry.key));
|
|
44
|
+
commit([...others, { key, tree }]);
|
|
45
|
+
setActiveKey(key);
|
|
46
|
+
}, [commit, touch]);
|
|
47
|
+
const restore = useCallback((key) => {
|
|
48
|
+
if (!entriesRef.current.some((entry) => entry.key === key))
|
|
49
|
+
return false;
|
|
50
|
+
touch(key);
|
|
51
|
+
setActiveKey(key);
|
|
52
|
+
return true;
|
|
53
|
+
}, [touch]);
|
|
54
|
+
const clear = useCallback(() => {
|
|
55
|
+
const kept = entriesRef.current.filter((entry) => entry.key === activeKey);
|
|
56
|
+
commit(kept);
|
|
57
|
+
orderRef.current = [activeKey];
|
|
58
|
+
}, [activeKey, commit]);
|
|
59
|
+
const handleRef = useRef({ show, restore, clear });
|
|
60
|
+
handleRef.current = { show, restore, clear };
|
|
61
|
+
return { entries, activeKey, handle: handleRef.current };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Root of the hydrated app.
|
|
65
|
+
*
|
|
66
|
+
* Registers itself with the navigation engine so a navigation becomes a state
|
|
67
|
+
* update here rather than a fresh root.render(), which is what allows the
|
|
68
|
+
* previous page to stay mounted.
|
|
69
|
+
*/
|
|
70
|
+
export function ActivityRoot({ initialKey, initialTree }) {
|
|
71
|
+
const { entries, activeKey, handle } = useActivityRouter({ key: initialKey, tree: initialTree });
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
setNavigateHandler((tree, key, segmentDepth) => {
|
|
74
|
+
// A partial payload replaces one segment inside the page that is already
|
|
75
|
+
// showing; only a whole document is a new page to retain.
|
|
76
|
+
if (segmentDepth > 0) {
|
|
77
|
+
setSegment(segmentDepth, key, tree);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
clearSegments();
|
|
81
|
+
handle.show(key, tree);
|
|
82
|
+
});
|
|
83
|
+
setRestoreHandler((key) => handle.restore(key));
|
|
84
|
+
}, [handle]);
|
|
85
|
+
return (_jsx(_Fragment, { children: entries.map((entry) => (_jsx(Activity, { mode: entry.key === activeKey ? 'visible' : 'hidden', children: entry.tree }, entry.key))) }));
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=ActivityRouter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActivityRouter.js","sourceRoot":"","sources":["../../src/js/ActivityRouter.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps document.title on the page you are actually looking at.
|
|
3
|
+
*
|
|
4
|
+
* A page's <title> is rendered inside its own tree so React 19 hoists it into
|
|
5
|
+
* <head>, which is right for a server render and wrong the moment a boundary
|
|
6
|
+
* starts retaining pages. A retained page is still mounted — that is what makes
|
|
7
|
+
* returning to it restore its state — so its <title> is still in the tree, React
|
|
8
|
+
* hoists that one too, and <head> ends up holding one per retained page.
|
|
9
|
+
* `document.title` reads the first, which is the oldest of them: the title lags
|
|
10
|
+
* a navigation behind, deterministically, and looks like a race because it
|
|
11
|
+
* depends on which pages happen to be retained.
|
|
12
|
+
*
|
|
13
|
+
* An effect settles it, because `<Activity mode="hidden">` tears effects down
|
|
14
|
+
* and re-runs them on show. Exactly one page's effect is live at a time — the
|
|
15
|
+
* visible one — so this needs no notion of navigation, history or which tree is
|
|
16
|
+
* current. It is only rendered on a route that ships a runtime; one that does
|
|
17
|
+
* not has no router, no retention, and a single <title> that was always right.
|
|
18
|
+
*/
|
|
19
|
+
export declare function DocumentTitle({ title }: {
|
|
20
|
+
title: string;
|
|
21
|
+
}): null;
|
|
22
|
+
//# sourceMappingURL=DocumentTitle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocumentTitle.d.ts","sourceRoot":"","sources":["../../src/js/DocumentTitle.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,QAMzD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Keeps document.title on the page you are actually looking at.
|
|
5
|
+
*
|
|
6
|
+
* A page's <title> is rendered inside its own tree so React 19 hoists it into
|
|
7
|
+
* <head>, which is right for a server render and wrong the moment a boundary
|
|
8
|
+
* starts retaining pages. A retained page is still mounted — that is what makes
|
|
9
|
+
* returning to it restore its state — so its <title> is still in the tree, React
|
|
10
|
+
* hoists that one too, and <head> ends up holding one per retained page.
|
|
11
|
+
* `document.title` reads the first, which is the oldest of them: the title lags
|
|
12
|
+
* a navigation behind, deterministically, and looks like a race because it
|
|
13
|
+
* depends on which pages happen to be retained.
|
|
14
|
+
*
|
|
15
|
+
* An effect settles it, because `<Activity mode="hidden">` tears effects down
|
|
16
|
+
* and re-runs them on show. Exactly one page's effect is live at a time — the
|
|
17
|
+
* visible one — so this needs no notion of navigation, history or which tree is
|
|
18
|
+
* current. It is only rendered on a route that ships a runtime; one that does
|
|
19
|
+
* not has no router, no retention, and a single <title> that was always right.
|
|
20
|
+
*/
|
|
21
|
+
export function DocumentTitle({ title }) {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (document.title !== title)
|
|
24
|
+
document.title = title;
|
|
25
|
+
}, [title]);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=DocumentTitle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocumentTitle.js","sourceRoot":"","sources":["../../src/js/DocumentTitle.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAqB;IACxD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ,CAAC,KAAK,KAAK,KAAK;YAAE,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC;IACvD,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Href } from "../routes.js";
|
|
2
|
+
import { type FormHTMLAttributes, type ReactNode } from "react";
|
|
3
|
+
import type { StandardSchemaV1 } from "./standardSchema";
|
|
4
|
+
type PrefetchStrategy = "hover" | "mount" | "none";
|
|
5
|
+
interface FormRenderProps<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
6
|
+
pending: boolean;
|
|
7
|
+
data: T;
|
|
8
|
+
errors: Record<string, string[]>;
|
|
9
|
+
error: (field: keyof T & string) => string | undefined;
|
|
10
|
+
clearErrors: (...fields: (keyof T & string)[]) => void;
|
|
11
|
+
reset: () => void;
|
|
12
|
+
}
|
|
13
|
+
interface FormProps<T extends Record<string, unknown> = Record<string, unknown>> extends Omit<FormHTMLAttributes<HTMLFormElement>, "action" | "method" | "children" | "onSubmit" | "onError"> {
|
|
14
|
+
action: Href | ((formData: FormData) => Promise<unknown>);
|
|
15
|
+
method?: "get" | "post";
|
|
16
|
+
prefetch?: PrefetchStrategy;
|
|
17
|
+
cacheFor?: number;
|
|
18
|
+
replace?: boolean;
|
|
19
|
+
preserveScroll?: boolean;
|
|
20
|
+
resetOnSuccess?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Check the fields before submitting, with any Standard Schema — Zod,
|
|
23
|
+
* Valibot, ArkType.
|
|
24
|
+
*
|
|
25
|
+
* A failure fills `errors` and the action is never called, so a mistake
|
|
26
|
+
* costs no round trip. It is a courtesy and not a control: the same action
|
|
27
|
+
* is reachable without this form, so the server still has to check.
|
|
28
|
+
*/
|
|
29
|
+
schema?: StandardSchemaV1;
|
|
30
|
+
/** Transform form data before submitting to the server action. */
|
|
31
|
+
transform?: (data: T) => Record<string, unknown>;
|
|
32
|
+
/** Called inside the transition with typed form data. Use it to call your useOptimistic setter. */
|
|
33
|
+
optimistic?: (data: T) => void;
|
|
34
|
+
onSuccess?: (result: unknown) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Called when a submit does not succeed.
|
|
37
|
+
*
|
|
38
|
+
* Validation failures arrive as field errors, with no second argument.
|
|
39
|
+
* Anything else arrives as an empty error map and the thrown value — there
|
|
40
|
+
* are no field errors to report, but the form still has to say so.
|
|
41
|
+
*/
|
|
42
|
+
onError?: (errors: Record<string, string[]>, error?: unknown) => void;
|
|
43
|
+
onSubmit?: (formData: FormData) => void | false;
|
|
44
|
+
children: ReactNode | ((form: FormRenderProps<T>) => ReactNode);
|
|
45
|
+
}
|
|
46
|
+
export declare function useFormStatus<T extends Record<string, unknown> = Record<string, unknown>>(): FormRenderProps<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Also exported by name, and re-exported below, because both spellings are in
|
|
49
|
+
* use: `import Form from` and `import { Form } from`.
|
|
50
|
+
*/
|
|
51
|
+
export default function Form<T extends Record<string, unknown> = Record<string, unknown>>({ action, method: methodProp, prefetch, cacheFor, replace, preserveScroll, resetOnSuccess, schema, transform, optimistic, onSuccess, onError, onSubmit, children, ...rest }: FormProps<T>): import("react").JSX.Element;
|
|
52
|
+
export { Form };
|
|
53
|
+
export { useForm } from "./useForm";
|
|
54
|
+
//# sourceMappingURL=Form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Form.d.ts","sourceRoot":"","sources":["../../src/js/Form.tsx"],"names":[],"mappings":"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"}
|
package/dist/js/Form.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState, useTransition, } from "react";
|
|
4
|
+
import { ServerValidationError, ServerDumpError } from "./errors";
|
|
5
|
+
import { validateWith } from "./standardSchema";
|
|
6
|
+
/**
|
|
7
|
+
* What an action built with createActionClient answers with.
|
|
8
|
+
*
|
|
9
|
+
* Recognised rather than thrown, because React serialises a rejected server
|
|
10
|
+
* action opaquely: production strips the message and the fields it named are
|
|
11
|
+
* gone. A returned object crosses intact, so a form reads it.
|
|
12
|
+
*/
|
|
13
|
+
function resultOf(value) {
|
|
14
|
+
if (typeof value !== 'object' || value === null)
|
|
15
|
+
return null;
|
|
16
|
+
const result = value;
|
|
17
|
+
if (result.validationErrors)
|
|
18
|
+
return { errors: result.validationErrors };
|
|
19
|
+
if (result.serverError)
|
|
20
|
+
return { serverError: result.serverError };
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const FormStatusContext = createContext({
|
|
24
|
+
pending: false,
|
|
25
|
+
data: {},
|
|
26
|
+
errors: {},
|
|
27
|
+
error: () => undefined,
|
|
28
|
+
clearErrors: () => { },
|
|
29
|
+
reset: () => { },
|
|
30
|
+
});
|
|
31
|
+
export function useFormStatus() {
|
|
32
|
+
return useContext(FormStatusContext);
|
|
33
|
+
}
|
|
34
|
+
function formDataToObject(formData) {
|
|
35
|
+
const obj = {};
|
|
36
|
+
for (const [key, value] of formData.entries()) {
|
|
37
|
+
if (typeof value === "string") {
|
|
38
|
+
obj[key] = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return obj;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Also exported by name, and re-exported below, because both spellings are in
|
|
45
|
+
* use: `import Form from` and `import { Form } from`.
|
|
46
|
+
*/
|
|
47
|
+
export default function Form({ action, method: methodProp, prefetch = "hover", cacheFor, replace = false, preserveScroll = false, resetOnSuccess = true, schema, transform, optimistic, onSuccess, onError, onSubmit, children, ...rest }) {
|
|
48
|
+
const isGetForm = typeof action === "string";
|
|
49
|
+
const method = methodProp ?? (isGetForm ? "get" : "post");
|
|
50
|
+
const [errors, setErrors] = useState({});
|
|
51
|
+
const [currentData, setCurrentData] = useState({});
|
|
52
|
+
const [isPending, startTransition] = useTransition();
|
|
53
|
+
const formRef = useRef(null);
|
|
54
|
+
const error = useCallback((field) => errors[field]?.[0], [errors]);
|
|
55
|
+
const clearErrors = useCallback((...fields) => {
|
|
56
|
+
if (fields.length === 0) {
|
|
57
|
+
setErrors({});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
setErrors((prev) => {
|
|
61
|
+
const next = { ...prev };
|
|
62
|
+
for (const f of fields) {
|
|
63
|
+
delete next[f];
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
|
+
const resetForm = useCallback(() => {
|
|
70
|
+
formRef.current?.reset();
|
|
71
|
+
setErrors({});
|
|
72
|
+
setCurrentData({});
|
|
73
|
+
}, []);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (isGetForm && prefetch === "mount") {
|
|
76
|
+
const fn = window.__rsc_prefetch;
|
|
77
|
+
fn?.(action, cacheFor);
|
|
78
|
+
}
|
|
79
|
+
}, [isGetForm, prefetch, action, cacheFor]);
|
|
80
|
+
const doPrefetch = useCallback(() => {
|
|
81
|
+
if (!isGetForm)
|
|
82
|
+
return;
|
|
83
|
+
const fn = window.__rsc_prefetch;
|
|
84
|
+
fn?.(action, cacheFor);
|
|
85
|
+
}, [isGetForm, action, cacheFor]);
|
|
86
|
+
const handleSubmit = useCallback(async (e) => {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const formData = new FormData(e.currentTarget);
|
|
89
|
+
const data = formDataToObject(formData);
|
|
90
|
+
setCurrentData(data);
|
|
91
|
+
if (onSubmit?.(formData) === false) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (isGetForm && method === "get") {
|
|
95
|
+
const url = new URL(action, window.location.origin);
|
|
96
|
+
for (const [key, value] of formData.entries()) {
|
|
97
|
+
if (typeof value === "string" && value !== "") {
|
|
98
|
+
url.searchParams.set(key, value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const path = url.pathname + url.search;
|
|
102
|
+
const shell = action;
|
|
103
|
+
const nav = window.__rsc_navigate;
|
|
104
|
+
const prefetched = window.__rsc_is_prefetched;
|
|
105
|
+
// The route without its query is that route's shell: the layout, the
|
|
106
|
+
// chrome, and whatever it renders with nothing to show yet. Prefetching
|
|
107
|
+
// on hover put it in the cache, so going there first costs no request
|
|
108
|
+
// and puts the page on screen while the real query is still running.
|
|
109
|
+
//
|
|
110
|
+
// The query itself is never prefetched. It is the expensive half, and
|
|
111
|
+
// hovering a search button is not a reason to run someone's search.
|
|
112
|
+
if (path !== shell && prefetched?.(shell)) {
|
|
113
|
+
// Awaited rather than raced: navigate() aborts whatever is in flight,
|
|
114
|
+
// so starting the real one first would cancel the shell before it
|
|
115
|
+
// could render. It is a cache hit, so this is a render, not a wait.
|
|
116
|
+
Promise.resolve(nav?.(shell, { replace, preserveScroll })).then(() =>
|
|
117
|
+
// Replaces, so the shell does not become a back-button stop of its
|
|
118
|
+
// own — the pair leaves exactly one entry behind.
|
|
119
|
+
nav?.(path, { replace: true, preserveScroll }));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
nav?.(path, { replace, preserveScroll });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const serverAction = action;
|
|
126
|
+
// Apply transform — rebuild FormData from transformed values
|
|
127
|
+
if (transform) {
|
|
128
|
+
const transformed = transform(data);
|
|
129
|
+
for (const key of [...formData.keys()]) {
|
|
130
|
+
formData.delete(key);
|
|
131
|
+
}
|
|
132
|
+
for (const [key, val] of Object.entries(transformed)) {
|
|
133
|
+
if (val === null || val === undefined)
|
|
134
|
+
continue;
|
|
135
|
+
if (val instanceof File) {
|
|
136
|
+
formData.append(key, val);
|
|
137
|
+
}
|
|
138
|
+
else if (typeof val === "boolean") {
|
|
139
|
+
formData.append(key, val ? "1" : "0");
|
|
140
|
+
}
|
|
141
|
+
else if (Array.isArray(val)) {
|
|
142
|
+
for (const item of val) {
|
|
143
|
+
formData.append(`${key}[]`, String(item));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
formData.append(key, String(val));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
setErrors({});
|
|
152
|
+
// Before the transition, so a failure neither runs the optimistic
|
|
153
|
+
// update nor leaves the form looking like it is submitting.
|
|
154
|
+
const invalid = await validateWith(schema, data);
|
|
155
|
+
if (invalid) {
|
|
156
|
+
setErrors(invalid);
|
|
157
|
+
onError?.(invalid);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
startTransition(async () => {
|
|
161
|
+
try {
|
|
162
|
+
// Call optimistic updater inside the transition so React's
|
|
163
|
+
// useOptimistic picks it up and auto-reverts on settle.
|
|
164
|
+
optimistic?.(data);
|
|
165
|
+
const result = await serverAction(formData);
|
|
166
|
+
const refused = resultOf(result);
|
|
167
|
+
if (refused) {
|
|
168
|
+
if (refused.errors) {
|
|
169
|
+
setErrors(refused.errors);
|
|
170
|
+
onError?.(refused.errors);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
onError?.({}, new Error(refused.serverError));
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (resetOnSuccess) {
|
|
178
|
+
formRef.current?.reset();
|
|
179
|
+
setCurrentData({});
|
|
180
|
+
}
|
|
181
|
+
setErrors({});
|
|
182
|
+
onSuccess?.(result);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
if (err instanceof ServerValidationError) {
|
|
186
|
+
setErrors(err.errors);
|
|
187
|
+
onError?.(err.errors);
|
|
188
|
+
}
|
|
189
|
+
else if (err instanceof ServerDumpError) {
|
|
190
|
+
// Dump overlay is already shown — silently swallow
|
|
191
|
+
}
|
|
192
|
+
else if (onError) {
|
|
193
|
+
// Not rethrown. A rejected action never settles, and until it
|
|
194
|
+
// settles React keeps the optimistic update on screen — so an
|
|
195
|
+
// unexpected failure left the row showing as though the write had
|
|
196
|
+
// worked. Settling is what takes it back.
|
|
197
|
+
onError({}, err);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Nothing is handling it, and swallowing here would lose it
|
|
201
|
+
// entirely — the reason this used to rethrow.
|
|
202
|
+
console.error('[rsc-kit] form submit failed', err);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}, [action, isGetForm, method, replace, preserveScroll, resetOnSuccess, schema, transform, optimistic, onSubmit, onSuccess, onError]);
|
|
207
|
+
const formStatus = {
|
|
208
|
+
pending: isPending,
|
|
209
|
+
data: currentData,
|
|
210
|
+
errors,
|
|
211
|
+
error,
|
|
212
|
+
clearErrors,
|
|
213
|
+
reset: resetForm,
|
|
214
|
+
};
|
|
215
|
+
return (_jsx(FormStatusContext.Provider, { value: formStatus, children: _jsx("form", { ref: formRef, onSubmit: handleSubmit, onMouseEnter: prefetch === "hover" ? doPrefetch : undefined, "data-pending": isPending ? "" : undefined, ...rest, children: typeof children === "function" ? children(formStatus) : children }) }));
|
|
216
|
+
}
|
|
217
|
+
// Also by name, because both spellings are in use: `import Form` and
|
|
218
|
+
// `import { Form }`. This was a re-export of "./Form" — from inside Form.tsx,
|
|
219
|
+
// so the module named itself. It survived on the bundler tolerating a circular
|
|
220
|
+
// self-reference, left over from when a separate barrel re-exported a
|
|
221
|
+
// FormComponent.tsx that a case-insensitive filesystem would not let be called
|
|
222
|
+
// Form.ts. There is one file now, and it can just export what it declares.
|
|
223
|
+
export { Form };
|
|
224
|
+
export { useForm } from "./useForm";
|
|
225
|
+
//# sourceMappingURL=Form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Form.js","sourceRoot":"","sources":["../../src/js/Form.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Href } from "../routes.js";
|
|
2
|
+
import { type AnchorHTMLAttributes, type Ref } from "react";
|
|
3
|
+
type PrefetchStrategy = "hover" | "mount" | "click" | "none" | boolean;
|
|
4
|
+
interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
5
|
+
/**
|
|
6
|
+
* Where this goes. Typed to the routes the build found, so a link to a page
|
|
7
|
+
* that does not exist stops compiling; `path as Href` when it is computed.
|
|
8
|
+
*/
|
|
9
|
+
href: Href;
|
|
10
|
+
/**
|
|
11
|
+
* The underlying anchor.
|
|
12
|
+
*
|
|
13
|
+
* Spread onto it like any other prop — React 19 passes ref through a
|
|
14
|
+
* function component without forwardRef — but declared here because it is
|
|
15
|
+
* not part of AnchorHTMLAttributes. A link that cannot be focused is a link
|
|
16
|
+
* a dialog cannot move focus to when it opens.
|
|
17
|
+
*/
|
|
18
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
19
|
+
prefetch?: PrefetchStrategy;
|
|
20
|
+
cacheFor?: number;
|
|
21
|
+
replace?: boolean;
|
|
22
|
+
preserveScroll?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function useLinkStatus(): {
|
|
25
|
+
pending: boolean;
|
|
26
|
+
};
|
|
27
|
+
export default function Link({ href, prefetch: prefetchProp, cacheFor, replace, preserveScroll, children, onClick, onMouseEnter, onMouseLeave, ...rest }: LinkProps): import("react").JSX.Element;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=Link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../src/js/Link.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EACL,KAAK,oBAAoB,EAEzB,KAAK,GAAG,EAOT,MAAM,OAAO,CAAC;AAEf,KAAK,gBAAgB,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEvE,UAAU,SAAU,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC/E;;;OAGG;IACH,IAAI,EAAE,IAAI,CAAC;IACX;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID,wBAAgB,aAAa,IAAI;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAEpD;AAgCD,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,EAC3B,IAAI,EACJ,QAAQ,EAAE,YAAsB,EAChC,QAAQ,EACR,OAAe,EACf,cAAsB,EACtB,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,GAAG,IAAI,EACR,EAAE,SAAS,+BAiHX"}
|
package/dist/js/Link.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from "react";
|
|
4
|
+
const LinkStatusContext = createContext({ pending: false });
|
|
5
|
+
export function useLinkStatus() {
|
|
6
|
+
return useContext(LinkStatusContext);
|
|
7
|
+
}
|
|
8
|
+
function isExternalUrl(url) {
|
|
9
|
+
try {
|
|
10
|
+
return new URL(url, window.location.origin).origin !== window.location.origin;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function shouldInterceptClick(e) {
|
|
17
|
+
return (!e.defaultPrevented &&
|
|
18
|
+
e.button === 0 &&
|
|
19
|
+
!e.metaKey &&
|
|
20
|
+
!e.ctrlKey &&
|
|
21
|
+
!e.shiftKey &&
|
|
22
|
+
!e.altKey);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* How long a pointer has to settle on a link before it prefetches.
|
|
26
|
+
*
|
|
27
|
+
* A pointer crossing a nav bar enters every link on the way, and each one used
|
|
28
|
+
* to fire a request immediately — enough to fill the browser's per-origin
|
|
29
|
+
* connection limit with pages the user never meant to visit. Waiting is close
|
|
30
|
+
* to free: the prefetch only has to beat the click, and a click that follows a
|
|
31
|
+
* hover this short would not have had its payload back anyway.
|
|
32
|
+
*/
|
|
33
|
+
const HOVER_PREFETCH_DELAY_MS = 100;
|
|
34
|
+
export default function Link({ href, prefetch: prefetchProp = "hover", cacheFor, replace = false, preserveScroll = false, children, onClick, onMouseEnter, onMouseLeave, ...rest }) {
|
|
35
|
+
const [pending, setPending] = useState(false);
|
|
36
|
+
const hoverTimer = useRef(null);
|
|
37
|
+
const prefetchStrategy = prefetchProp === true
|
|
38
|
+
? "hover"
|
|
39
|
+
: prefetchProp === false
|
|
40
|
+
? "none"
|
|
41
|
+
: prefetchProp;
|
|
42
|
+
const doPrefetch = useCallback(() => {
|
|
43
|
+
if (isExternalUrl(href))
|
|
44
|
+
return;
|
|
45
|
+
const fn = window.__rsc_prefetch;
|
|
46
|
+
fn?.(href, cacheFor);
|
|
47
|
+
}, [href, cacheFor]);
|
|
48
|
+
// Only useEffect needed: prefetch on mount strategy
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (prefetchStrategy === "mount") {
|
|
51
|
+
doPrefetch();
|
|
52
|
+
}
|
|
53
|
+
}, [prefetchStrategy, doPrefetch]);
|
|
54
|
+
const handleClick = useCallback((e) => {
|
|
55
|
+
onClick?.(e);
|
|
56
|
+
if (e.defaultPrevented)
|
|
57
|
+
return;
|
|
58
|
+
const target = e.currentTarget.target;
|
|
59
|
+
if (target && target !== "_self")
|
|
60
|
+
return;
|
|
61
|
+
if (!shouldInterceptClick(e) || isExternalUrl(href))
|
|
62
|
+
return;
|
|
63
|
+
// Hash-only links (#section) — let the browser scroll natively
|
|
64
|
+
if (href.startsWith("#"))
|
|
65
|
+
return;
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
setPending(true);
|
|
68
|
+
// navigate() returns a Promise — clear pending when it resolves or rejects
|
|
69
|
+
const nav = window.__rsc_navigate;
|
|
70
|
+
const promise = nav?.(href, { replace, preserveScroll });
|
|
71
|
+
promise?.then(() => setPending(false), () => setPending(false));
|
|
72
|
+
}, [href, replace, preserveScroll, onClick]);
|
|
73
|
+
const handleMouseEnter = useCallback((e) => {
|
|
74
|
+
onMouseEnter?.(e);
|
|
75
|
+
if (prefetchStrategy !== "hover" && prefetchStrategy !== "click")
|
|
76
|
+
return;
|
|
77
|
+
if (hoverTimer.current !== null)
|
|
78
|
+
clearTimeout(hoverTimer.current);
|
|
79
|
+
hoverTimer.current = setTimeout(() => {
|
|
80
|
+
hoverTimer.current = null;
|
|
81
|
+
doPrefetch();
|
|
82
|
+
}, HOVER_PREFETCH_DELAY_MS);
|
|
83
|
+
}, [prefetchStrategy, doPrefetch, onMouseEnter]);
|
|
84
|
+
const handleMouseLeave = useCallback((e) => {
|
|
85
|
+
onMouseLeave?.(e);
|
|
86
|
+
// Never fired: the pointer passed over on its way somewhere else.
|
|
87
|
+
if (hoverTimer.current !== null) {
|
|
88
|
+
clearTimeout(hoverTimer.current);
|
|
89
|
+
hoverTimer.current = null;
|
|
90
|
+
}
|
|
91
|
+
// Already in flight: give the connection back to whatever the pointer is
|
|
92
|
+
// heading for. A prefetch that has landed is kept — see cancelPrefetch.
|
|
93
|
+
if (isExternalUrl(href))
|
|
94
|
+
return;
|
|
95
|
+
window.__rsc_cancel_prefetch?.(href);
|
|
96
|
+
}, [href, onMouseLeave]);
|
|
97
|
+
// Touch gets no delay. There is no hovering to disambiguate — a touch is
|
|
98
|
+
// already the start of a tap — and touchstart leads the click by little
|
|
99
|
+
// enough that spending any of it waiting would waste the head start.
|
|
100
|
+
const handleTouchStart = useCallback(() => {
|
|
101
|
+
if (prefetchStrategy === "hover" || prefetchStrategy === "click") {
|
|
102
|
+
doPrefetch();
|
|
103
|
+
}
|
|
104
|
+
}, [prefetchStrategy, doPrefetch]);
|
|
105
|
+
// A link unmounted mid-hover (navigating away) must not prefetch afterwards.
|
|
106
|
+
useEffect(() => () => {
|
|
107
|
+
if (hoverTimer.current !== null)
|
|
108
|
+
clearTimeout(hoverTimer.current);
|
|
109
|
+
}, []);
|
|
110
|
+
return (_jsx(LinkStatusContext.Provider, { value: { pending }, children: _jsx("a", { href: href, onClick: handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onTouchStart: handleTouchStart, "data-pending": pending ? "" : undefined, ...rest, children: children }) }));
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=Link.js.map
|