toiljs 0.0.15 → 0.0.16

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.
Files changed (217) hide show
  1. package/.babelrc +13 -13
  2. package/.gitattributes +2 -2
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
  4. package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -90
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
  7. package/.github/PULL_REQUEST_TEMPLATE.md +43 -43
  8. package/.github/changelog-config.json +45 -45
  9. package/.github/dependabot.yml +27 -27
  10. package/.github/workflows/ci.yml +191 -191
  11. package/.prettierrc.json +11 -11
  12. package/.vscode/settings.json +9 -9
  13. package/CHANGELOG.md +5 -5
  14. package/LICENSE +187 -187
  15. package/README.md +339 -315
  16. package/as-pect.asconfig.json +34 -34
  17. package/as-pect.config.js +65 -65
  18. package/assets/logo.svg +36 -36
  19. package/build/backend/.tsbuildinfo +1 -1
  20. package/build/cli/.tsbuildinfo +1 -1
  21. package/build/cli/index.js +0 -0
  22. package/build/client/.tsbuildinfo +1 -1
  23. package/build/client/dev/devtools.d.ts +6 -0
  24. package/build/client/dev/devtools.js +442 -0
  25. package/build/client/dev/error-overlay.d.ts +9 -0
  26. package/build/client/dev/error-overlay.js +19 -4
  27. package/build/client/navigation/prefetch.d.ts +1 -0
  28. package/build/client/navigation/prefetch.js +35 -0
  29. package/build/client/routing/Router.js +1 -1
  30. package/build/client/routing/hooks.js +6 -2
  31. package/build/client/routing/loader.d.ts +23 -0
  32. package/build/client/routing/loader.js +53 -7
  33. package/build/client/routing/mount.js +4 -3
  34. package/build/compiler/.tsbuildinfo +1 -1
  35. package/build/compiler/config.d.ts +16 -0
  36. package/build/compiler/config.js +7 -0
  37. package/build/compiler/docs.js +16 -16
  38. package/build/compiler/index.d.ts +2 -2
  39. package/build/compiler/index.js +1 -1
  40. package/build/compiler/plugin.js +156 -0
  41. package/build/compiler/prerender.d.ts +1 -0
  42. package/build/compiler/prerender.js +1 -1
  43. package/build/compiler/seo.d.ts +1 -1
  44. package/build/compiler/seo.js +5 -4
  45. package/build/compiler/ssg.js +32 -1
  46. package/build/io/.tsbuildinfo +1 -1
  47. package/build/logger/.tsbuildinfo +1 -1
  48. package/build/shared/.tsbuildinfo +1 -1
  49. package/eslint.config.js +48 -48
  50. package/examples/basic/client/404.tsx +11 -11
  51. package/examples/basic/client/components/.gitkeep +1 -1
  52. package/examples/basic/client/global-error.tsx +13 -13
  53. package/examples/basic/client/layout.tsx +25 -25
  54. package/examples/basic/client/public/images/.gitkeep +1 -1
  55. package/examples/basic/client/public/images/logo.svg +36 -36
  56. package/examples/basic/client/public/robots.txt +2 -2
  57. package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
  58. package/examples/basic/client/routes/features/error/error.tsx +16 -16
  59. package/examples/basic/client/routes/features/template/b.tsx +14 -14
  60. package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
  61. package/examples/basic/client/routes/gallery/layout.tsx +13 -13
  62. package/examples/basic/client/routes/io.tsx +24 -24
  63. package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
  64. package/examples/basic/client/routes/search.tsx +61 -61
  65. package/examples/basic/client/toil.tsx +5 -5
  66. package/package.json +155 -148
  67. package/presets/eslint.js +88 -88
  68. package/presets/no-uint8array-tostring.js +200 -200
  69. package/presets/prettier.json +18 -18
  70. package/presets/tsconfig.json +37 -37
  71. package/src/backend/index.ts +160 -160
  72. package/src/cli/proc.ts +50 -50
  73. package/src/cli/updates.ts +69 -69
  74. package/src/cli/validate.ts +31 -31
  75. package/src/client/channel/channel.ts +146 -146
  76. package/src/client/components/Form.tsx +65 -65
  77. package/src/client/components/Script.tsx +113 -113
  78. package/src/client/components/Slot.tsx +21 -21
  79. package/src/client/dev/devtools.tsx +973 -0
  80. package/src/client/dev/error-overlay.tsx +30 -4
  81. package/src/client/head/head.ts +167 -167
  82. package/src/client/head/metadata.ts +112 -112
  83. package/src/client/index.ts +89 -89
  84. package/src/client/navigation/NavLink.tsx +86 -86
  85. package/src/client/navigation/navigation.ts +235 -235
  86. package/src/client/navigation/prefetch.ts +169 -130
  87. package/src/client/navigation/scroll.ts +53 -53
  88. package/src/client/routing/Router.tsx +8 -2
  89. package/src/client/routing/action.ts +122 -122
  90. package/src/client/routing/error-boundary.tsx +43 -43
  91. package/src/client/routing/hooks.ts +21 -6
  92. package/src/client/routing/loader.ts +325 -235
  93. package/src/client/routing/match.ts +47 -47
  94. package/src/client/routing/mount.tsx +54 -52
  95. package/src/client/routing/params-context.ts +10 -10
  96. package/src/client/routing/slot-context.ts +7 -7
  97. package/src/client/search/search.ts +189 -189
  98. package/src/client/search/use-page-search.ts +73 -73
  99. package/src/client/types.ts +73 -73
  100. package/src/compiler/config.ts +219 -182
  101. package/src/compiler/docs.ts +228 -228
  102. package/src/compiler/generate.ts +394 -394
  103. package/src/compiler/index.ts +64 -57
  104. package/src/compiler/pages.ts +70 -70
  105. package/src/compiler/plugin.ts +170 -2
  106. package/src/compiler/prerender.ts +156 -156
  107. package/src/compiler/seo.ts +397 -390
  108. package/src/compiler/ssg.ts +162 -126
  109. package/src/io/BinaryReader.ts +340 -340
  110. package/src/io/BinaryWriter.ts +385 -385
  111. package/src/io/FastMap.ts +127 -127
  112. package/src/io/index.ts +11 -11
  113. package/src/io/lengths.ts +14 -14
  114. package/src/io/types.ts +18 -18
  115. package/src/logger/index.ts +22 -22
  116. package/src/server/index.ts +10 -10
  117. package/src/server/main.ts +13 -13
  118. package/src/server/tsconfig.json +4 -4
  119. package/src/shared/index.ts +10 -10
  120. package/std/client/index.d.ts +15 -15
  121. package/std/client/package.json +3 -3
  122. package/test/assembly/example.spec.ts +7 -7
  123. package/test/channel.test.ts +21 -21
  124. package/test/dom/Link.test.tsx +47 -47
  125. package/test/dom/NavLink.test.tsx +37 -37
  126. package/test/dom/error-overlay.test.tsx +44 -44
  127. package/test/dom/loader.test.tsx +121 -121
  128. package/test/dom/navigation.test.ts +59 -59
  129. package/test/dom/revalidate.test.tsx +38 -38
  130. package/test/dom/route-head.test.tsx +78 -78
  131. package/test/dom/router-loading.test.tsx +44 -44
  132. package/test/dom/scroll.test.ts +56 -56
  133. package/test/dom/use-metadata.test.tsx +58 -58
  134. package/test/io.test.ts +93 -93
  135. package/test/navlink.test.ts +28 -28
  136. package/test/placeholder.test.ts +9 -9
  137. package/test/routes.test.ts +76 -76
  138. package/test/seo.test.ts +175 -164
  139. package/test/slot-layouts.test.ts +69 -69
  140. package/test/ssg.test.ts +36 -36
  141. package/test/update.test.ts +44 -44
  142. package/test/validate.test.ts +42 -42
  143. package/toil-routes.d.ts +7 -0
  144. package/toilconfig.json +30 -30
  145. package/tsconfig.backend.json +13 -13
  146. package/tsconfig.base.json +35 -35
  147. package/tsconfig.cli.json +13 -13
  148. package/tsconfig.client.json +14 -14
  149. package/tsconfig.compiler.json +13 -13
  150. package/tsconfig.io.json +12 -12
  151. package/tsconfig.json +22 -22
  152. package/tsconfig.logger.json +12 -12
  153. package/tsconfig.server.json +10 -10
  154. package/tsconfig.shared.json +12 -12
  155. package/vitest.config.ts +26 -26
  156. package/.idea/codeStyles/Project.xml +0 -54
  157. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  158. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  159. package/.idea/modules.xml +0 -8
  160. package/.idea/prettier.xml +0 -7
  161. package/.idea/toiljs.iml +0 -8
  162. package/.idea/vcs.xml +0 -6
  163. package/.toil/entry.tsx +0 -9
  164. package/.toil/index.html +0 -12
  165. package/.toil/routes.ts +0 -9
  166. package/build/cli/configure.d.ts +0 -16
  167. package/build/cli/configure.js +0 -272
  168. package/build/cli/create.d.ts +0 -16
  169. package/build/cli/create.js +0 -420
  170. package/build/cli/diagnostics.d.ts +0 -55
  171. package/build/cli/diagnostics.js +0 -333
  172. package/build/cli/doctor.d.ts +0 -6
  173. package/build/cli/doctor.js +0 -249
  174. package/build/cli/features.d.ts +0 -25
  175. package/build/cli/features.js +0 -107
  176. package/build/cli/index.d.ts +0 -2
  177. package/build/cli/proc.d.ts +0 -6
  178. package/build/cli/proc.js +0 -31
  179. package/build/cli/ui.d.ts +0 -9
  180. package/build/cli/ui.js +0 -75
  181. package/build/cli/update.d.ts +0 -7
  182. package/build/cli/update.js +0 -117
  183. package/build/cli/updates.d.ts +0 -10
  184. package/build/cli/updates.js +0 -45
  185. package/build/cli/validate.d.ts +0 -4
  186. package/build/cli/validate.js +0 -19
  187. package/build/client/Link.d.ts +0 -8
  188. package/build/client/Link.js +0 -44
  189. package/build/client/NavLink.d.ts +0 -14
  190. package/build/client/NavLink.js +0 -37
  191. package/build/client/Router.d.ts +0 -7
  192. package/build/client/Router.js +0 -55
  193. package/build/client/channel.d.ts +0 -23
  194. package/build/client/channel.js +0 -94
  195. package/build/client/error-boundary.d.ts +0 -16
  196. package/build/client/error-boundary.js +0 -19
  197. package/build/client/head.d.ts +0 -26
  198. package/build/client/head.js +0 -87
  199. package/build/client/hooks.d.ts +0 -17
  200. package/build/client/hooks.js +0 -48
  201. package/build/client/lazy.d.ts +0 -16
  202. package/build/client/lazy.js +0 -53
  203. package/build/client/match.d.ts +0 -2
  204. package/build/client/match.js +0 -32
  205. package/build/client/mount.d.ts +0 -2
  206. package/build/client/mount.js +0 -13
  207. package/build/client/navigation.d.ts +0 -13
  208. package/build/client/navigation.js +0 -97
  209. package/build/client/params-context.d.ts +0 -2
  210. package/build/client/params-context.js +0 -2
  211. package/build/client/prefetch.d.ts +0 -11
  212. package/build/client/prefetch.js +0 -100
  213. package/build/client/runtime.d.ts +0 -31
  214. package/build/client/runtime.js +0 -112
  215. package/build/client/scroll.d.ts +0 -8
  216. package/build/client/scroll.js +0 -36
  217. package/toil-env.d.ts +0 -16
@@ -1,235 +1,325 @@
1
- /**
2
- * Route data loaders. A route file may export a `loader` alongside its default component; the
3
- * loader runs on navigation, in parallel with the route's chunk load, and the page suspends until
4
- * it resolves (so `loading.tsx` shows and `useNavigationPending` is true). The page reads the
5
- * result with `useLoaderData<typeof loader>()`.
6
- *
7
- * Caching: results are cached by URL (`pathname + search`). Re-renders reuse the cached result (the
8
- * loader does not re-run). Across navigations, the cache policy comes from the route's optional
9
- * `export const revalidate` ({@link Revalidate}): by default the loader re-runs on every navigation;
10
- * a number keeps data fresh for that many seconds; `false` caches until manual invalidation.
11
- * `revalidate()` / `router.refresh()` bust the cache to force a refetch.
12
- */
13
- import { createContext, useContext, type ComponentType } from 'react';
14
-
15
- import type { HeadSpec } from '../head/head.js';
16
- import { resolveMetadata, type GenerateMetadata, type Metadata } from '../head/metadata.js';
17
- import { refresh as rerender } from '../navigation/navigation.js';
18
- import type { RouteDef } from '../types.js';
19
- import type { RouteParams } from './match.js';
20
-
21
- /** Argument passed to a route `loader`. */
22
- export interface LoaderArgs {
23
- readonly params: RouteParams;
24
- readonly searchParams: URLSearchParams;
25
- }
26
-
27
- /** A route `loader`: `export const loader = ({ params }) => …` (sync or async). */
28
- export type LoaderFunction<T = unknown> = (args: LoaderArgs) => T | Promise<T>;
29
-
30
- /** One concrete set of route params (a dynamic segment maps to a string, a catch-all to a string[]). */
31
- export type StaticParams = Record<string, string | string[]>;
32
-
33
- /**
34
- * A route's `export const generateStaticParams`: returns the concrete param sets to pre-render at
35
- * build time (SSG). toil enumerates them, runs the route's `generateMetadata` per set, and bakes a
36
- * `<url>/index.html` + sitemap entry for each, so dynamic routes get build-time SEO. Build-only.
37
- */
38
- export type GenerateStaticParams = () => StaticParams[] | Promise<StaticParams[]>;
39
-
40
- /**
41
- * Per-route cache policy, set with `export const revalidate` in a route file:
42
- * - `0` (default): re-run the loader on every navigation to the route.
43
- * - a positive number: reuse cached data across navigations for that many **seconds**, then refetch.
44
- * - `false`: cache indefinitely until manually invalidated (`revalidate()` / `router.refresh()`).
45
- */
46
- export type Revalidate = number | false;
47
-
48
- /** Resolves the data type for {@link useLoaderData}: `typeof loader` → its (awaited) return, else `T`. */
49
- export type LoaderData<T> = T extends (...args: never[]) => infer R ? Awaited<R> : T;
50
-
51
- interface RouteModule {
52
- default: ComponentType;
53
- loader?: LoaderFunction;
54
- revalidate?: Revalidate;
55
- metadata?: Metadata;
56
- generateMetadata?: GenerateMetadata;
57
- }
58
- interface RouteData {
59
- Component: ComponentType;
60
- data: unknown;
61
- /** Resolved baseline head from the route's `metadata` / `generateMetadata`, if any. */
62
- head?: HeadSpec;
63
- }
64
- interface Entry {
65
- status: 'pending' | 'done' | 'error';
66
- promise: Promise<void>;
67
- value?: RouteData;
68
- error?: unknown;
69
- /** `Date.now()` when the fetch settled (`0` while pending). */
70
- loadedAt: number;
71
- /** Cache policy captured from the route module once loaded. */
72
- revalidate: Revalidate;
73
- /** Navigation epoch at which this entry was (re)fetched. */
74
- epoch: number;
75
- /** Whether the route exports a `loader`, a route without one has no data that can change. */
76
- hasLoader: boolean;
77
- }
78
-
79
- const cache = new Map<string, Entry>();
80
- const MAX_ENTRIES = 32;
81
-
82
- /** Cache key for a URL: path + query (hash is ignored, it never changes loader data). */
83
- export function loaderKey(pathname: string, search: string): string {
84
- return `${pathname}${search}`;
85
- }
86
-
87
- /** Loads the route module and runs its loader (if any), in parallel where possible. */
88
- async function loadRoute(
89
- route: RouteDef,
90
- params: RouteParams,
91
- ): Promise<{ data: RouteData; revalidate: Revalidate; hasLoader: boolean }> {
92
- const mod: RouteModule = await route.load();
93
- const searchParams = new URLSearchParams(
94
- typeof window === 'undefined' ? '' : window.location.search,
95
- );
96
- const data = mod.loader ? await mod.loader({ params, searchParams }) : undefined;
97
- let head: HeadSpec | undefined;
98
- if (mod.generateMetadata) {
99
- head = resolveMetadata(await mod.generateMetadata({ params, searchParams, data }));
100
- } else if (mod.metadata) {
101
- head = resolveMetadata(mod.metadata);
102
- }
103
- return {
104
- data: { Component: mod.default, data, head },
105
- revalidate: mod.revalidate ?? 0,
106
- hasLoader: mod.loader != null,
107
- };
108
- }
109
-
110
- /** Whether a settled entry must be refetched for the current navigation. */
111
- function isStale(entry: Entry, epoch: number): boolean {
112
- if (entry.status === 'error') return true; // always retry a failed load
113
- // A route with no loader has no data that can change, keep it cached so repeat navigations
114
- // render synchronously (instant) instead of re-suspending and remounting on every switch.
115
- if (!entry.hasLoader) return false;
116
- if (entry.revalidate === false) return false; // cache forever
117
- if (entry.revalidate === 0) return entry.epoch !== epoch; // refetch once per navigation
118
- return Date.now() - entry.loadedAt >= entry.revalidate * 1000; // time-based staleness
119
- }
120
-
121
- /** Starts (and caches) a fresh fetch for `key`. */
122
- function startFetch(route: RouteDef, params: RouteParams, key: string, epoch: number): Entry {
123
- const created: Entry = {
124
- status: 'pending',
125
- promise: Promise.resolve(),
126
- loadedAt: 0,
127
- revalidate: 0,
128
- epoch,
129
- hasLoader: false,
130
- };
131
- created.promise = loadRoute(route, params).then(
132
- (result) => {
133
- created.value = result.data;
134
- created.revalidate = result.revalidate;
135
- created.hasLoader = result.hasLoader;
136
- created.loadedAt = Date.now();
137
- created.status = 'done';
138
- },
139
- (error: unknown) => {
140
- created.error = error;
141
- created.loadedAt = Date.now();
142
- created.status = 'error';
143
- },
144
- );
145
- cache.set(key, created);
146
- while (cache.size > MAX_ENTRIES) {
147
- const oldest = cache.keys().next().value;
148
- if (oldest === undefined || oldest === key) break;
149
- cache.delete(oldest);
150
- }
151
- return created;
152
- }
153
-
154
- /**
155
- * Reads (or starts) the route's module + loader data for the URL `key`, suspending until ready.
156
- * Throws the loader's error so the route's `error.tsx` boundary can catch it. `epoch` is the current
157
- * navigation epoch, used to refetch once-per-navigation under the default cache policy.
158
- */
159
- export function readRouteData(
160
- route: RouteDef,
161
- params: RouteParams,
162
- key: string,
163
- epoch: number,
164
- ): RouteData {
165
- let entry = cache.get(key);
166
- if (entry && entry.status !== 'pending' && isStale(entry, epoch)) {
167
- entry = undefined; // stale → drop and refetch below
168
- }
169
- entry ??= startFetch(route, params, key, epoch);
170
- if (entry.status === 'pending') throw entry.promise;
171
- if (entry.status === 'error') throw entry.error;
172
- if (!entry.value) throw entry.promise;
173
- return entry.value;
174
- }
175
-
176
- /** Clears all cached loader data, so the next render re-runs loaders (used by router.refresh). */
177
- export function clearLoaderData(): void {
178
- cache.clear();
179
- }
180
-
181
- /** Cache key for an href (relative or absolute), matching {@link loaderKey}. */
182
- function keyForHref(href: string): string | undefined {
183
- if (typeof window === 'undefined') return undefined;
184
- try {
185
- const url = new URL(href, window.location.href);
186
- return loaderKey(url.pathname, url.search);
187
- } catch {
188
- return undefined;
189
- }
190
- }
191
-
192
- /**
193
- * Invalidates cached loader data so it refetches on the next render. With no argument, clears every
194
- * route; with an `href`, clears just that route's entry. Pair with a re-render (the active route
195
- * refetches and suspends), see {@link revalidate}.
196
- */
197
- export function invalidateLoaderData(href?: string): void {
198
- if (href === undefined) {
199
- cache.clear();
200
- return;
201
- }
202
- const key = keyForHref(href);
203
- if (key !== undefined) cache.delete(key);
204
- }
205
-
206
- /**
207
- * Invalidates loader data and re-renders so the active route refetches. Call after a mutation to
208
- * refresh the current route (`revalidate()`), or target another route by href
209
- * (`revalidate('/posts')`). Usable outside React (e.g. in an event handler after a `fetch`).
210
- */
211
- export function revalidate(href?: string): void {
212
- invalidateLoaderData(href);
213
- rerender();
214
- }
215
-
216
- /** Holds the active route's loader data; provided by the Router, read by {@link useLoaderData}. */
217
- export const LoaderDataContext = createContext<unknown>(undefined);
218
-
219
- /**
220
- * The data returned by the active route's `loader`. Three ways to type it, easiest first:
221
- *
222
- * 1. **Pass the loader**, zero generics, fully inferred from your loader's return:
223
- * `const data = useLoaderData(loader);`
224
- * 2. Pass `typeof loader` as a type argument: `useLoaderData<typeof loader>();`
225
- * 3. Pass an explicit shape: `useLoaderData<Post>();`
226
- *
227
- * With no argument and no type, it returns `unknown` (never `any`), so the data is there at runtime,
228
- * but you must annotate or narrow before using it. There's no way to infer the type from a bare call:
229
- * TypeScript can't tell which file (and so which `loader`) the call belongs to, hence option 1.
230
- */
231
- export function useLoaderData<L extends LoaderFunction>(loader: L): Awaited<ReturnType<L>>;
232
- export function useLoaderData<T = unknown>(): LoaderData<T>;
233
- export function useLoaderData(_loader?: LoaderFunction): unknown {
234
- return useContext(LoaderDataContext);
235
- }
1
+ /**
2
+ * Route data loaders. A route file may export a `loader` alongside its default component; the
3
+ * loader runs on navigation, in parallel with the route's chunk load, and the page suspends until
4
+ * it resolves (so `loading.tsx` shows and `useNavigationPending` is true). The page reads the
5
+ * result with `useLoaderData<typeof loader>()`.
6
+ *
7
+ * Caching: results are cached by URL (`pathname + search`). Re-renders reuse the cached result (the
8
+ * loader does not re-run). Across navigations, the cache policy comes from the route's optional
9
+ * `export const revalidate` ({@link Revalidate}): by default the loader re-runs on every navigation;
10
+ * a number keeps data fresh for that many seconds; `false` caches until manual invalidation.
11
+ * `revalidate()` / `router.refresh()` bust the cache to force a refetch.
12
+ */
13
+ import { createContext, useContext, type ComponentType } from 'react';
14
+
15
+ import type { HeadSpec } from '../head/head.js';
16
+ import { resolveMetadata, type GenerateMetadata, type Metadata } from '../head/metadata.js';
17
+ import { navigationEpoch, refresh as rerender } from '../navigation/navigation.js';
18
+ import type { RouteDef } from '../types.js';
19
+ import type { RouteParams } from './match.js';
20
+
21
+ /** Argument passed to a route `loader`. */
22
+ export interface LoaderArgs {
23
+ readonly params: RouteParams;
24
+ readonly searchParams: URLSearchParams;
25
+ }
26
+
27
+ /** A route `loader`: `export const loader = ({ params }) => …` (sync or async). */
28
+ export type LoaderFunction<T = unknown> = (args: LoaderArgs) => T | Promise<T>;
29
+
30
+ /** One concrete set of route params (a dynamic segment maps to a string, a catch-all to a string[]). */
31
+ export type StaticParams = Record<string, string | string[]>;
32
+
33
+ /**
34
+ * A route's `export const generateStaticParams`: returns the concrete param sets to pre-render at
35
+ * build time (SSG). toil enumerates them, runs the route's `generateMetadata` per set, and bakes a
36
+ * `<url>/index.html` + sitemap entry for each, so dynamic routes get build-time SEO. Build-only.
37
+ */
38
+ export type GenerateStaticParams = () => StaticParams[] | Promise<StaticParams[]>;
39
+
40
+ /**
41
+ * Per-route cache policy, set with `export const revalidate` in a route file:
42
+ * - `0` (default): re-run the loader on every navigation to the route.
43
+ * - a positive number: reuse cached data across navigations for that many **seconds**, then refetch.
44
+ * - `false`: cache indefinitely until manually invalidated (`revalidate()` / `router.refresh()`).
45
+ */
46
+ export type Revalidate = number | false;
47
+
48
+ /** Resolves the data type for {@link useLoaderData}: `typeof loader` → its (awaited) return, else `T`. */
49
+ export type LoaderData<T> = T extends (...args: never[]) => infer R ? Awaited<R> : T;
50
+
51
+ interface RouteModule {
52
+ default: ComponentType;
53
+ loader?: LoaderFunction;
54
+ revalidate?: Revalidate;
55
+ metadata?: Metadata;
56
+ generateMetadata?: GenerateMetadata;
57
+ }
58
+ interface RouteData {
59
+ Component: ComponentType;
60
+ data: unknown;
61
+ /** Resolved baseline head from the route's `metadata` / `generateMetadata`, if any. */
62
+ head?: HeadSpec;
63
+ }
64
+ interface Entry {
65
+ status: 'pending' | 'done' | 'error';
66
+ promise: Promise<void>;
67
+ value?: RouteData;
68
+ error?: unknown;
69
+ /** `Date.now()` when the fetch settled (`0` while pending). */
70
+ loadedAt: number;
71
+ /** Cache policy captured from the route module once loaded. */
72
+ revalidate: Revalidate;
73
+ /** Navigation epoch at which this entry was (re)fetched. */
74
+ epoch: number;
75
+ /** Whether the route exports a `loader`, a route without one has no data that can change. */
76
+ hasLoader: boolean;
77
+ /**
78
+ * Fetched ahead of navigation (hover/visible prefetch), not yet consumed by a navigation. Such an
79
+ * entry is reused once by the next navigation even under `revalidate: 0`, then behaves normally.
80
+ */
81
+ prefetched: boolean;
82
+ }
83
+
84
+ const cache = new Map<string, Entry>();
85
+ const MAX_ENTRIES = 32;
86
+
87
+ // Dev-cache observers (the dev toolbar's Data tab). The emit calls are no-ops in production builds,
88
+ // where the toolbar (the only subscriber) is dead-code-eliminated.
89
+ const cacheListeners = new Set<() => void>();
90
+ // Cached snapshot for useSyncExternalStore: getSnapshot must return a stable reference between
91
+ // changes, so we recompute only when the cache mutates (in emitCache), not on every read. Returning
92
+ // a fresh array per call makes React think the store changed every render -> infinite loop.
93
+ let cacheSnapshot: LoaderCacheSnapshot[] = [];
94
+ function emitCache(): void {
95
+ cacheSnapshot = [...cache.entries()].map(([key, e]) => ({
96
+ key,
97
+ status: e.status,
98
+ hasLoader: e.hasLoader,
99
+ revalidate: e.revalidate,
100
+ loadedAt: e.loadedAt,
101
+ epoch: e.epoch,
102
+ data: e.value?.data,
103
+ }));
104
+ for (const l of cacheListeners) l();
105
+ }
106
+ /** Subscribes to loader-cache changes (dev toolbar). Returns an unsubscribe. */
107
+ export function subscribeLoaderCache(listener: () => void): () => void {
108
+ cacheListeners.add(listener);
109
+ return () => {
110
+ cacheListeners.delete(listener);
111
+ };
112
+ }
113
+ /** A read-only snapshot of one cached loader entry (dev toolbar). */
114
+ export interface LoaderCacheSnapshot {
115
+ readonly key: string;
116
+ readonly status: Entry['status'];
117
+ readonly hasLoader: boolean;
118
+ readonly revalidate: Revalidate;
119
+ readonly loadedAt: number;
120
+ readonly epoch: number;
121
+ readonly data: unknown;
122
+ }
123
+ /** Snapshots the live loader cache (dev toolbar). Returns a stable reference between changes. */
124
+ export function inspectLoaderCache(): LoaderCacheSnapshot[] {
125
+ return cacheSnapshot;
126
+ }
127
+
128
+ /** Cache key for a URL: path + query (hash is ignored, it never changes loader data). */
129
+ export function loaderKey(pathname: string, search: string): string {
130
+ return `${pathname}${search}`;
131
+ }
132
+
133
+ /**
134
+ * Warms a route's chunk *and* loader data for a URL ahead of navigation (the prefetcher calls this on
135
+ * link hover/focus), so the eventual click commits synchronously instead of suspending. The entry is
136
+ * flagged `prefetched` so the next navigation reuses it once even under the default `revalidate: 0`.
137
+ * No-op when the URL is already cached or its fetch is already in flight. Errors are swallowed into
138
+ * the entry (the real navigation will retry and surface them via the route's `error.tsx`).
139
+ */
140
+ export function prefetchRouteData(
141
+ route: RouteDef,
142
+ params: RouteParams,
143
+ pathname: string,
144
+ search: string,
145
+ ): void {
146
+ const key = loaderKey(pathname, search);
147
+ const existing = cache.get(key);
148
+ if (existing && existing.status !== 'error') return;
149
+ startFetch(route, params, key, navigationEpoch(), search, true);
150
+ }
151
+
152
+ /** Loads the route module and runs its loader (if any), in parallel where possible. */
153
+ async function loadRoute(
154
+ route: RouteDef,
155
+ params: RouteParams,
156
+ search: string,
157
+ ): Promise<{ data: RouteData; revalidate: Revalidate; hasLoader: boolean }> {
158
+ const mod: RouteModule = await route.load();
159
+ const searchParams = new URLSearchParams(search);
160
+ const data = mod.loader ? await mod.loader({ params, searchParams }) : undefined;
161
+ let head: HeadSpec | undefined;
162
+ if (mod.generateMetadata) {
163
+ head = resolveMetadata(await mod.generateMetadata({ params, searchParams, data }));
164
+ } else if (mod.metadata) {
165
+ head = resolveMetadata(mod.metadata);
166
+ }
167
+ return {
168
+ data: { Component: mod.default, data, head },
169
+ revalidate: mod.revalidate ?? 0,
170
+ hasLoader: mod.loader != null,
171
+ };
172
+ }
173
+
174
+ /** Whether a settled entry must be refetched for the current navigation. */
175
+ function isStale(entry: Entry, epoch: number): boolean {
176
+ if (entry.status === 'error') return true; // always retry a failed load
177
+ // A route with no loader has no data that can change, keep it cached so repeat navigations
178
+ // render synchronously (instant) instead of re-suspending and remounting on every switch.
179
+ if (!entry.hasLoader) return false;
180
+ if (entry.revalidate === false) return false; // cache forever
181
+ if (entry.revalidate === 0) {
182
+ // A just-prefetched entry is the fetch for this very navigation, reuse it once (consumed in
183
+ // readRouteData, so the *next* navigation refetches as `revalidate: 0` normally requires).
184
+ if (entry.prefetched) return false;
185
+ return entry.epoch !== epoch; // otherwise refetch once per navigation
186
+ }
187
+ return Date.now() - entry.loadedAt >= entry.revalidate * 1000; // time-based staleness
188
+ }
189
+
190
+ /** Starts (and caches) a fresh fetch for `key`. */
191
+ function startFetch(
192
+ route: RouteDef,
193
+ params: RouteParams,
194
+ key: string,
195
+ epoch: number,
196
+ search: string,
197
+ prefetched = false,
198
+ ): Entry {
199
+ const created: Entry = {
200
+ status: 'pending',
201
+ promise: Promise.resolve(),
202
+ loadedAt: 0,
203
+ revalidate: 0,
204
+ epoch,
205
+ hasLoader: false,
206
+ prefetched,
207
+ };
208
+ created.promise = loadRoute(route, params, search).then(
209
+ (result) => {
210
+ created.value = result.data;
211
+ created.revalidate = result.revalidate;
212
+ created.hasLoader = result.hasLoader;
213
+ created.loadedAt = Date.now();
214
+ created.status = 'done';
215
+ emitCache();
216
+ },
217
+ (error: unknown) => {
218
+ created.error = error;
219
+ created.loadedAt = Date.now();
220
+ created.status = 'error';
221
+ emitCache();
222
+ },
223
+ );
224
+ cache.set(key, created);
225
+ while (cache.size > MAX_ENTRIES) {
226
+ const oldest = cache.keys().next().value;
227
+ if (oldest === undefined || oldest === key) break;
228
+ cache.delete(oldest);
229
+ }
230
+ emitCache();
231
+ return created;
232
+ }
233
+
234
+ /**
235
+ * Reads (or starts) the route's module + loader data for the URL `key`, suspending until ready.
236
+ * Throws the loader's error so the route's `error.tsx` boundary can catch it. `epoch` is the current
237
+ * navigation epoch, used to refetch once-per-navigation under the default cache policy.
238
+ */
239
+ export function readRouteData(
240
+ route: RouteDef,
241
+ params: RouteParams,
242
+ key: string,
243
+ epoch: number,
244
+ ): RouteData {
245
+ const search = typeof window === 'undefined' ? '' : window.location.search;
246
+ let entry = cache.get(key);
247
+ if (entry && entry.status !== 'pending' && isStale(entry, epoch)) {
248
+ entry = undefined; // stale → drop and refetch below
249
+ }
250
+ entry ??= startFetch(route, params, key, epoch, search);
251
+ if (entry.status === 'pending') throw entry.promise;
252
+ if (entry.status === 'error') throw entry.error;
253
+ if (!entry.value) throw entry.promise;
254
+ // Claim a prefetched entry for this navigation: clear the flag and stamp the current epoch so the
255
+ // next navigation re-evaluates its revalidate policy (a `revalidate: 0` route refetches again).
256
+ if (entry.prefetched) {
257
+ entry.prefetched = false;
258
+ entry.epoch = epoch;
259
+ }
260
+ return entry.value;
261
+ }
262
+
263
+ /** Clears all cached loader data, so the next render re-runs loaders (used by router.refresh). */
264
+ export function clearLoaderData(): void {
265
+ cache.clear();
266
+ emitCache();
267
+ }
268
+
269
+ /** Cache key for an href (relative or absolute), matching {@link loaderKey}. */
270
+ function keyForHref(href: string): string | undefined {
271
+ if (typeof window === 'undefined') return undefined;
272
+ try {
273
+ const url = new URL(href, window.location.href);
274
+ return loaderKey(url.pathname, url.search);
275
+ } catch {
276
+ return undefined;
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Invalidates cached loader data so it refetches on the next render. With no argument, clears every
282
+ * route; with an `href`, clears just that route's entry. Pair with a re-render (the active route
283
+ * refetches and suspends), see {@link revalidate}.
284
+ */
285
+ export function invalidateLoaderData(href?: string): void {
286
+ if (href === undefined) {
287
+ cache.clear();
288
+ emitCache();
289
+ return;
290
+ }
291
+ const key = keyForHref(href);
292
+ if (key !== undefined) cache.delete(key);
293
+ emitCache();
294
+ }
295
+
296
+ /**
297
+ * Invalidates loader data and re-renders so the active route refetches. Call after a mutation to
298
+ * refresh the current route (`revalidate()`), or target another route by href
299
+ * (`revalidate('/posts')`). Usable outside React (e.g. in an event handler after a `fetch`).
300
+ */
301
+ export function revalidate(href?: string): void {
302
+ invalidateLoaderData(href);
303
+ rerender();
304
+ }
305
+
306
+ /** Holds the active route's loader data; provided by the Router, read by {@link useLoaderData}. */
307
+ export const LoaderDataContext = createContext<unknown>(undefined);
308
+
309
+ /**
310
+ * The data returned by the active route's `loader`. Three ways to type it, easiest first:
311
+ *
312
+ * 1. **Pass the loader**, zero generics, fully inferred from your loader's return:
313
+ * `const data = useLoaderData(loader);`
314
+ * 2. Pass `typeof loader` as a type argument: `useLoaderData<typeof loader>();`
315
+ * 3. Pass an explicit shape: `useLoaderData<Post>();`
316
+ *
317
+ * With no argument and no type, it returns `unknown` (never `any`), so the data is there at runtime,
318
+ * but you must annotate or narrow before using it. There's no way to infer the type from a bare call:
319
+ * TypeScript can't tell which file (and so which `loader`) the call belongs to, hence option 1.
320
+ */
321
+ export function useLoaderData<L extends LoaderFunction>(loader: L): Awaited<ReturnType<L>>;
322
+ export function useLoaderData<T = unknown>(): LoaderData<T>;
323
+ export function useLoaderData(_loader?: LoaderFunction): unknown {
324
+ return useContext(LoaderDataContext);
325
+ }