toiljs 0.0.11 → 0.0.14

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 (120) hide show
  1. package/README.md +3 -1
  2. package/build/cli/.tsbuildinfo +1 -1
  3. package/build/cli/configure.js +10 -4
  4. package/build/cli/create.js +58 -30
  5. package/build/cli/diagnostics.d.ts +55 -0
  6. package/build/cli/diagnostics.js +333 -0
  7. package/build/cli/doctor.d.ts +6 -0
  8. package/build/cli/doctor.js +249 -0
  9. package/build/cli/index.js +26 -0
  10. package/build/cli/proc.d.ts +5 -0
  11. package/build/cli/proc.js +20 -0
  12. package/build/cli/ui.d.ts +1 -0
  13. package/build/cli/ui.js +1 -0
  14. package/build/cli/update.d.ts +7 -0
  15. package/build/cli/update.js +117 -0
  16. package/build/cli/updates.d.ts +10 -0
  17. package/build/cli/updates.js +45 -0
  18. package/build/client/.tsbuildinfo +1 -1
  19. package/build/client/dev/error-overlay.js +1 -1
  20. package/build/client/head/metadata.js +3 -1
  21. package/build/client/index.d.ts +5 -1
  22. package/build/client/index.js +2 -0
  23. package/build/client/navigation/navigation.js +1 -1
  24. package/build/client/routing/Router.js +2 -2
  25. package/build/client/search/search.d.ts +26 -0
  26. package/build/client/search/search.js +101 -0
  27. package/build/client/search/use-page-search.d.ts +8 -0
  28. package/build/client/search/use-page-search.js +21 -0
  29. package/build/compiler/.tsbuildinfo +1 -1
  30. package/build/compiler/generate.js +33 -24
  31. package/build/compiler/index.d.ts +2 -0
  32. package/build/compiler/index.js +1 -0
  33. package/build/compiler/pages.d.ts +8 -0
  34. package/build/compiler/pages.js +37 -0
  35. package/build/compiler/plugin.js +3 -1
  36. package/build/compiler/prerender.d.ts +1 -0
  37. package/build/compiler/prerender.js +11 -5
  38. package/build/compiler/seo.js +10 -3
  39. package/build/io/.tsbuildinfo +1 -1
  40. package/examples/basic/client/components/Header.tsx +43 -41
  41. package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
  42. package/examples/basic/client/public/index.html +18 -16
  43. package/examples/basic/client/routes/(legal)/privacy.tsx +18 -19
  44. package/examples/basic/client/routes/(legal)/terms.tsx +15 -16
  45. package/examples/basic/client/routes/about.tsx +21 -22
  46. package/examples/basic/client/routes/blog/[id].tsx +26 -18
  47. package/examples/basic/client/routes/features/actions.tsx +67 -67
  48. package/examples/basic/client/routes/features/error/index.tsx +27 -27
  49. package/examples/basic/client/routes/features/head.tsx +38 -38
  50. package/examples/basic/client/routes/features/index.tsx +83 -75
  51. package/examples/basic/client/routes/features/realtime.tsx +34 -32
  52. package/examples/basic/client/routes/features/script.tsx +31 -31
  53. package/examples/basic/client/routes/features/seo.tsx +39 -39
  54. package/examples/basic/client/routes/features/template/index.tsx +20 -20
  55. package/examples/basic/client/routes/features/template/template.tsx +16 -18
  56. package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -23
  57. package/examples/basic/client/routes/gallery/index.tsx +42 -42
  58. package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -18
  59. package/examples/basic/client/routes/get-started.tsx +157 -84
  60. package/examples/basic/client/routes/index.tsx +137 -96
  61. package/examples/basic/client/routes/loader-demo/index.tsx +59 -52
  62. package/examples/basic/client/routes/search.tsx +61 -0
  63. package/examples/basic/client/routes/test.tsx +7 -8
  64. package/examples/basic/client/styles/main.css +624 -552
  65. package/package.json +2 -2
  66. package/presets/eslint.js +10 -3
  67. package/src/cli/configure.ts +363 -353
  68. package/src/cli/create.ts +563 -530
  69. package/src/cli/diagnostics.ts +421 -0
  70. package/src/cli/doctor.ts +318 -0
  71. package/src/cli/features.ts +166 -160
  72. package/src/cli/index.ts +242 -211
  73. package/src/cli/proc.ts +30 -0
  74. package/src/cli/ui.ts +111 -103
  75. package/src/cli/update.ts +150 -0
  76. package/src/cli/updates.ts +69 -0
  77. package/src/client/components/Image.tsx +91 -89
  78. package/src/client/dev/error-overlay.tsx +193 -197
  79. package/src/client/head/metadata.ts +94 -92
  80. package/src/client/index.ts +79 -64
  81. package/src/client/navigation/Link.tsx +94 -100
  82. package/src/client/navigation/navigation.ts +215 -218
  83. package/src/client/routing/Router.tsx +210 -193
  84. package/src/client/routing/hooks.ts +110 -114
  85. package/src/client/routing/lazy.ts +77 -81
  86. package/src/client/search/search.ts +189 -0
  87. package/src/client/search/use-page-search.ts +73 -0
  88. package/src/compiler/config.ts +173 -171
  89. package/src/compiler/fonts.ts +89 -87
  90. package/src/compiler/generate.ts +45 -27
  91. package/src/compiler/image-report.ts +88 -85
  92. package/src/compiler/index.ts +2 -0
  93. package/src/compiler/pages.ts +70 -0
  94. package/src/compiler/plugin.ts +51 -47
  95. package/src/compiler/prerender.ts +152 -130
  96. package/src/compiler/routes.ts +132 -131
  97. package/src/compiler/seo.ts +381 -356
  98. package/src/compiler/vite.ts +155 -145
  99. package/src/io/FastSet.ts +99 -96
  100. package/test/configure.test.ts +94 -90
  101. package/test/doctor.test.ts +140 -0
  102. package/test/dom/Image.test.tsx +73 -46
  103. package/test/dom/Script.test.tsx +48 -45
  104. package/test/dom/action.test.tsx +146 -129
  105. package/test/dom/error-overlay.test.tsx +1 -1
  106. package/test/dom/loader.test.tsx +2 -2
  107. package/test/dom/revalidate.test.tsx +1 -1
  108. package/test/dom/route-head.test.tsx +1 -2
  109. package/test/dom/router-loading.test.tsx +1 -1
  110. package/test/dom/slot.test.tsx +131 -109
  111. package/test/dom/view-transitions.test.tsx +53 -51
  112. package/test/features.test.ts +149 -142
  113. package/test/fonts.test.ts +28 -26
  114. package/test/head.test.ts +45 -35
  115. package/test/metadata.test.ts +42 -41
  116. package/test/pages.test.ts +105 -0
  117. package/test/prerender.test.ts +54 -46
  118. package/test/search.test.ts +114 -0
  119. package/test/seo.test.ts +30 -8
  120. package/test/update.test.ts +44 -0
@@ -1,193 +1,210 @@
1
- import { createElement, Suspense, useLayoutEffect, type ReactNode } from 'react';
2
-
3
- import { ErrorBoundary } from './error-boundary.js';
4
- import { useLocation } from './hooks.js';
5
- import {
6
- errorComponent,
7
- loadingComponent,
8
- nestedLayout,
9
- resolveLayout,
10
- resolveNotFound,
11
- } from './lazy.js';
12
- import { loaderKey, LoaderDataContext, readRouteData } from './loader.js';
13
- import { matchRoute, type RouteParams } from './match.js';
14
- import { useRouteHead } from '../head/head.js';
15
- import { ParamsContext } from './params-context.js';
16
- import { SlotContext } from './slot-context.js';
17
- import {
18
- isSoftNavigation,
19
- navigationEpoch,
20
- previousPathname,
21
- settleNavigation,
22
- } from '../navigation/navigation.js';
23
- import { applyScroll } from '../navigation/scroll.js';
24
- import type { ErrorComponentLoader, LayoutLoader, NotFoundLoader, RouteDef } from '../types.js';
25
-
26
- /** Loads a matched route's module + loader data (suspending), then renders it with the data in context. */
27
- function RoutePage(props: {
28
- route: RouteDef;
29
- params: RouteParams;
30
- dataKey: string;
31
- epoch: number;
32
- }): ReactNode {
33
- const { Component, data, head } = readRouteData(props.route, props.params, props.dataKey, props.epoch);
34
- useRouteHead(head);
35
- return <LoaderDataContext.Provider value={data}>{createElement(Component)}</LoaderDataContext.Provider>;
36
- }
37
-
38
- /**
39
- * Wraps a matched route's page in its loading boundary, templates, nested layouts, and error
40
- * boundary. `keyPrefix` namespaces the loader-cache key and boundary keys so a parallel slot and the
41
- * main route can match the same URL without colliding.
42
- */
43
- function renderMatched(
44
- matched: RouteDef,
45
- params: RouteParams,
46
- pathname: string,
47
- epoch: number,
48
- keyPrefix: string,
49
- ): ReactNode {
50
- const search = typeof window === 'undefined' ? '' : window.location.search;
51
- const dataKey = keyPrefix + loaderKey(pathname, search);
52
- const fallback: ReactNode = matched.loading
53
- ? createElement(Suspense, { fallback: null }, createElement(loadingComponent(matched.loading)))
54
- : null;
55
-
56
- // A route with a `loading.tsx` keys its boundary per URL *and* navigation epoch, so its fallback
57
- // shows even inside the transition, on first nav and on an in-place revalidate of the same URL
58
- // (the epoch bumps). A route without one keeps a stable boundary so the transition holds the old
59
- // page. The loader cache key stays the bare URL, so the boundary remount still reuses cached data.
60
- let content: ReactNode = (
61
- <Suspense
62
- key={matched.loading ? `${dataKey}:${String(epoch)}` : undefined}
63
- fallback={fallback}>
64
- <RoutePage
65
- route={matched}
66
- params={params}
67
- dataKey={dataKey}
68
- epoch={epoch}
69
- />
70
- </Suspense>
71
- );
72
- // Templates wrap inside the layouts and re-mount on every navigation (keyed by URL).
73
- const templates = matched.templates ?? [];
74
- for (let i = templates.length - 1; i >= 0; i--) {
75
- const Template = nestedLayout(templates[i]);
76
- content = (
77
- <Suspense
78
- key={`${keyPrefix}${pathname}:${String(i)}`}
79
- fallback={null}>
80
- <Template>{content}</Template>
81
- </Suspense>
82
- );
83
- }
84
- // Nested layouts, deepest first so the shallowest ends up outermost.
85
- const chain = matched.layouts ?? [];
86
- for (let i = chain.length - 1; i >= 0; i--) {
87
- const NestedLayout = nestedLayout(chain[i]);
88
- content = (
89
- <Suspense fallback={null}>
90
- <NestedLayout>{content}</NestedLayout>
91
- </Suspense>
92
- );
93
- }
94
- if (matched.errorComponent) {
95
- content = <ErrorBoundary fallback={errorComponent(matched.errorComponent)}>{content}</ErrorBoundary>;
96
- }
97
- return content;
98
- }
99
-
100
- /**
101
- * Finds the first route (already specificity-sorted) matching `pathname`. Intercepting routes are
102
- * skipped unless `allowIntercept`, they only apply on soft navigation.
103
- */
104
- function match(
105
- routes: RouteDef[],
106
- pathname: string,
107
- allowIntercept = true,
108
- ): { route: RouteDef; params: RouteParams } | null {
109
- for (const route of routes) {
110
- if (route.intercept && !allowIntercept) continue;
111
- const params = matchRoute(route.pattern, pathname);
112
- if (params) return { route, params };
113
- }
114
- return null;
115
- }
116
-
117
- /** Matches the current location to a route and renders it, optionally wrapped in the root layout. */
118
- export function Router(props: {
119
- routes: RouteDef[];
120
- layout?: LayoutLoader;
121
- notFound?: NotFoundLoader;
122
- globalError?: ErrorComponentLoader;
123
- slots?: Record<string, RouteDef[]>;
124
- }): ReactNode {
125
- const { routes, layout = null, notFound = null, globalError = null, slots = {} } = props;
126
- const pathname = useLocation();
127
-
128
- // After each navigation commits, apply the planned scroll (top / restore / #hash) and mark the
129
- // navigation settled. A layout effect runs before paint, so the scroll lands without a flash.
130
- useLayoutEffect(() => {
131
- applyScroll();
132
- settleNavigation();
133
- });
134
-
135
- const epoch = navigationEpoch();
136
- const soft = isSoftNavigation();
137
-
138
- // Parallel slots: each `@slot` tree matches the current URL independently (intercepting routes
139
- // only on soft navigation). Each match is exposed by name via SlotContext and rendered wherever a
140
- // layout/page places a `Slot`. If an intercepting route matches, the main view holds the previous
141
- // page (the backdrop) while the slot shows the intercepted route, i.e. a modal overlay.
142
- const slotElements: Record<string, ReactNode> = {};
143
- let intercepting = false;
144
- for (const [name, defs] of Object.entries(slots)) {
145
- const slotMatch = match(defs, pathname, soft);
146
- if (!slotMatch) continue;
147
- if (slotMatch.route.intercept) intercepting = true;
148
- slotElements[name] = (
149
- <ParamsContext.Provider value={slotMatch.params}>
150
- {renderMatched(slotMatch.route, slotMatch.params, pathname, epoch, `@${name} `)}
151
- </ParamsContext.Provider>
152
- );
153
- }
154
-
155
- const mainPath = intercepting ? previousPathname() : pathname;
156
- const matched = match(routes, mainPath);
157
- const params: RouteParams = matched?.params ?? {};
158
-
159
- let content: ReactNode;
160
- if (matched) {
161
- content = renderMatched(matched.route, matched.params, mainPath, epoch, '');
162
- } else if (notFound) {
163
- const NotFound = resolveNotFound(notFound);
164
- content = (
165
- <Suspense fallback={null}>
166
- <NotFound />
167
- </Suspense>
168
- );
169
- } else {
170
- content = <div style={{ padding: 24, fontFamily: 'system-ui' }}>404, Not found</div>;
171
- }
172
-
173
- if (layout) {
174
- const Layout = resolveLayout(layout);
175
- content = (
176
- <Suspense fallback={null}>
177
- <Layout>{content}</Layout>
178
- </Suspense>
179
- );
180
- }
181
-
182
- // The root error boundary (global-error.tsx) sits outside the root layout, so it catches
183
- // errors thrown by the layout itself, the last line of defense before a blank screen.
184
- if (globalError) {
185
- content = <ErrorBoundary fallback={errorComponent(globalError)}>{content}</ErrorBoundary>;
186
- }
187
-
188
- return (
189
- <ParamsContext.Provider value={params}>
190
- <SlotContext.Provider value={slotElements}>{content}</SlotContext.Provider>
191
- </ParamsContext.Provider>
192
- );
193
- }
1
+ import { createElement, Suspense, useLayoutEffect, type ReactNode } from 'react';
2
+
3
+ import { ErrorBoundary } from './error-boundary.js';
4
+ import { useLocation } from './hooks.js';
5
+ import {
6
+ errorComponent,
7
+ loadingComponent,
8
+ nestedLayout,
9
+ resolveLayout,
10
+ resolveNotFound,
11
+ } from './lazy.js';
12
+ import { loaderKey, LoaderDataContext, readRouteData } from './loader.js';
13
+ import { matchRoute, type RouteParams } from './match.js';
14
+ import { useRouteHead } from '../head/head.js';
15
+ import { ParamsContext } from './params-context.js';
16
+ import { SlotContext } from './slot-context.js';
17
+ import {
18
+ isSoftNavigation,
19
+ navigationEpoch,
20
+ previousPathname,
21
+ settleNavigation,
22
+ } from '../navigation/navigation.js';
23
+ import { applyScroll } from '../navigation/scroll.js';
24
+ import type { ErrorComponentLoader, LayoutLoader, NotFoundLoader, RouteDef } from '../types.js';
25
+
26
+ /** Loads a matched route's module + loader data (suspending), then renders it with the data in context. */
27
+ function RoutePage(props: {
28
+ route: RouteDef;
29
+ params: RouteParams;
30
+ dataKey: string;
31
+ epoch: number;
32
+ }): ReactNode {
33
+ const { Component, data, head } = readRouteData(
34
+ props.route,
35
+ props.params,
36
+ props.dataKey,
37
+ props.epoch,
38
+ );
39
+ useRouteHead(head);
40
+ return (
41
+ <LoaderDataContext.Provider value={data}>
42
+ {createElement(Component)}
43
+ </LoaderDataContext.Provider>
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Wraps a matched route's page in its loading boundary, templates, nested layouts, and error
49
+ * boundary. `keyPrefix` namespaces the loader-cache key and boundary keys so a parallel slot and the
50
+ * main route can match the same URL without colliding.
51
+ */
52
+ function renderMatched(
53
+ matched: RouteDef,
54
+ params: RouteParams,
55
+ pathname: string,
56
+ epoch: number,
57
+ keyPrefix: string,
58
+ ): ReactNode {
59
+ const search = typeof window === 'undefined' ? '' : window.location.search;
60
+ const dataKey = keyPrefix + loaderKey(pathname, search);
61
+ const fallback: ReactNode = matched.loading
62
+ ? createElement(
63
+ Suspense,
64
+ { fallback: null },
65
+ createElement(loadingComponent(matched.loading)),
66
+ )
67
+ : null;
68
+
69
+ // A route with a `loading.tsx` keys its boundary per URL *and* navigation epoch, so its fallback
70
+ // shows even inside the transition, on first nav and on an in-place revalidate of the same URL
71
+ // (the epoch bumps). A route without one keeps a stable boundary so the transition holds the old
72
+ // page. The loader cache key stays the bare URL, so the boundary remount still reuses cached data.
73
+ let content: ReactNode = (
74
+ <Suspense
75
+ key={matched.loading ? `${dataKey}:${String(epoch)}` : undefined}
76
+ fallback={fallback}>
77
+ <RoutePage
78
+ route={matched}
79
+ params={params}
80
+ dataKey={dataKey}
81
+ epoch={epoch}
82
+ />
83
+ </Suspense>
84
+ );
85
+ // Templates wrap inside the layouts and re-mount on every navigation (keyed by URL).
86
+ const templates = matched.templates ?? [];
87
+ for (let i = templates.length - 1; i >= 0; i--) {
88
+ const Template = nestedLayout(templates[i]);
89
+ content = (
90
+ <Suspense
91
+ key={`${keyPrefix}${pathname}:${String(i)}`}
92
+ fallback={null}>
93
+ <Template>{content}</Template>
94
+ </Suspense>
95
+ );
96
+ }
97
+ // Nested layouts, deepest first so the shallowest ends up outermost.
98
+ const chain = matched.layouts ?? [];
99
+ for (let i = chain.length - 1; i >= 0; i--) {
100
+ const NestedLayout = nestedLayout(chain[i]);
101
+ content = (
102
+ <Suspense fallback={null}>
103
+ <NestedLayout>{content}</NestedLayout>
104
+ </Suspense>
105
+ );
106
+ }
107
+ if (matched.errorComponent) {
108
+ content = (
109
+ <ErrorBoundary fallback={errorComponent(matched.errorComponent)}>
110
+ {content}
111
+ </ErrorBoundary>
112
+ );
113
+ }
114
+ return content;
115
+ }
116
+
117
+ /**
118
+ * Finds the first route (already specificity-sorted) matching `pathname`. Intercepting routes are
119
+ * skipped unless `allowIntercept`, they only apply on soft navigation.
120
+ */
121
+ function match(
122
+ routes: RouteDef[],
123
+ pathname: string,
124
+ allowIntercept = true,
125
+ ): { route: RouteDef; params: RouteParams } | null {
126
+ for (const route of routes) {
127
+ if (route.intercept && !allowIntercept) continue;
128
+ const params = matchRoute(route.pattern, pathname);
129
+ if (params) return { route, params };
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /** Matches the current location to a route and renders it, optionally wrapped in the root layout. */
135
+ export function Router(props: {
136
+ routes: RouteDef[];
137
+ layout?: LayoutLoader;
138
+ notFound?: NotFoundLoader;
139
+ globalError?: ErrorComponentLoader;
140
+ slots?: Record<string, RouteDef[]>;
141
+ }): ReactNode {
142
+ const { routes, layout = null, notFound = null, globalError = null, slots = {} } = props;
143
+ const pathname = useLocation();
144
+
145
+ // After each navigation commits, apply the planned scroll (top / restore / #hash) and mark the
146
+ // navigation settled. A layout effect runs before paint, so the scroll lands without a flash.
147
+ useLayoutEffect(() => {
148
+ applyScroll();
149
+ settleNavigation();
150
+ });
151
+
152
+ const epoch = navigationEpoch();
153
+ const soft = isSoftNavigation();
154
+
155
+ // Parallel slots: each `@slot` tree matches the current URL independently (intercepting routes
156
+ // only on soft navigation). Each match is exposed by name via SlotContext and rendered wherever a
157
+ // layout/page places a `Slot`. If an intercepting route matches, the main view holds the previous
158
+ // page (the backdrop) while the slot shows the intercepted route, i.e. a modal overlay.
159
+ const slotElements: Record<string, ReactNode> = {};
160
+ let intercepting = false;
161
+ for (const [name, defs] of Object.entries(slots)) {
162
+ const slotMatch = match(defs, pathname, soft);
163
+ if (!slotMatch) continue;
164
+ if (slotMatch.route.intercept) intercepting = true;
165
+ slotElements[name] = (
166
+ <ParamsContext.Provider value={slotMatch.params}>
167
+ {renderMatched(slotMatch.route, slotMatch.params, pathname, epoch, `@${name} `)}
168
+ </ParamsContext.Provider>
169
+ );
170
+ }
171
+
172
+ const mainPath = intercepting ? previousPathname() : pathname;
173
+ const matched = match(routes, mainPath);
174
+ const params: RouteParams = matched?.params ?? {};
175
+
176
+ let content: ReactNode;
177
+ if (matched) {
178
+ content = renderMatched(matched.route, matched.params, mainPath, epoch, '');
179
+ } else if (notFound) {
180
+ const NotFound = resolveNotFound(notFound);
181
+ content = (
182
+ <Suspense fallback={null}>
183
+ <NotFound />
184
+ </Suspense>
185
+ );
186
+ } else {
187
+ content = <div style={{ padding: 24, fontFamily: 'system-ui' }}>404, Not found</div>;
188
+ }
189
+
190
+ if (layout) {
191
+ const Layout = resolveLayout(layout);
192
+ content = (
193
+ <Suspense fallback={null}>
194
+ <Layout>{content}</Layout>
195
+ </Suspense>
196
+ );
197
+ }
198
+
199
+ // The root error boundary (global-error.tsx) sits outside the root layout, so it catches
200
+ // errors thrown by the layout itself, the last line of defense before a blank screen.
201
+ if (globalError) {
202
+ content = <ErrorBoundary fallback={errorComponent(globalError)}>{content}</ErrorBoundary>;
203
+ }
204
+
205
+ return (
206
+ <ParamsContext.Provider value={params}>
207
+ <SlotContext.Provider value={slotElements}>{content}</SlotContext.Provider>
208
+ </ParamsContext.Provider>
209
+ );
210
+ }
@@ -1,114 +1,110 @@
1
- /**
2
- * Router hooks for user route components: read the params / pathname / search params, navigate
3
- * imperatively, and grab a router handle.
4
- */
5
- import { useContext, useEffect, useMemo, useReducer, useSyncExternalStore } from 'react';
6
-
7
- import type { RouteParams } from './match.js';
8
- import {
9
- back,
10
- forward,
11
- isNavigationPending,
12
- navigate,
13
- refresh,
14
- subscribeLocation,
15
- subscribePending,
16
- type NavigateOptions,
17
- } from '../navigation/navigation.js';
18
- import { clearLoaderData, revalidate as revalidateData } from './loader.js';
19
- import { ParamsContext } from './params-context.js';
20
- import { prefetch } from '../navigation/prefetch.js';
21
- import type { Href } from '../types.js';
22
-
23
- /** Imperative router handle returned by {@link useRouter}. */
24
- export interface RouterInstance {
25
- /** Navigate to `href`, pushing a new history entry (or replacing with `{ replace: true }`). */
26
- push(href: Href, options?: NavigateOptions): void;
27
- /** Navigate to `href`, replacing the current history entry. */
28
- replace(href: Href): void;
29
- /** Go back one history entry. */
30
- back(): void;
31
- /** Go forward one history entry. */
32
- forward(): void;
33
- /** Re-render the current route and re-run its loader (clears all cached loader data). */
34
- refresh(): void;
35
- /**
36
- * Invalidate cached loader data and re-render so it refetches. No argument refetches the active
37
- * route; pass an `href` to target a specific route. Use after a mutation.
38
- */
39
- revalidate(href?: Href): void;
40
- /** Prefetch a route's chunk ahead of navigation. */
41
- prefetch(href: Href): void;
42
- }
43
-
44
- const ROUTER: RouterInstance = {
45
- push: (href, options) => {
46
- navigate(href, options);
47
- },
48
- replace: (href) => {
49
- navigate(href, { replace: true });
50
- },
51
- back,
52
- forward,
53
- refresh: () => {
54
- clearLoaderData();
55
- refresh();
56
- },
57
- revalidate: (href) => {
58
- revalidateData(href);
59
- },
60
- prefetch,
61
- };
62
-
63
- /** Current dynamic route params, e.g. `{ id }` inside `/blog/:id`. Pass a shape: `useParams<{ id: string }>()`. */
64
- export function useParams<T extends RouteParams = RouteParams>(): T {
65
- return useContext(ParamsContext) as T;
66
- }
67
-
68
- /** Returns the imperative `navigate(href, { replace })` function. */
69
- export function useNavigate(): typeof navigate {
70
- return navigate;
71
- }
72
-
73
- /** Returns the router handle (`push` / `replace` / `back` / `forward` / `refresh` / `prefetch`). */
74
- export function useRouter(): RouterInstance {
75
- return ROUTER;
76
- }
77
-
78
- /**
79
- * Subscribes to location changes and reads the live `window.location` on render. Re-renders on any
80
- * pathname, search, or hash change. The re-render is orchestrated by `navigate`/`notify` (wrapped in
81
- * `startTransition` for smooth nav, or `document.startViewTransition` when enabled), so the listener
82
- * itself is a plain force-update.
83
- */
84
- function useLocationSubscription(): void {
85
- const [, forceUpdate] = useReducer((n: number): number => n + 1, 0);
86
- useEffect(() => subscribeLocation(forceUpdate), []);
87
- }
88
-
89
- /** Subscribes to and returns the current `location.pathname`. */
90
- export function useLocation(): string {
91
- useLocationSubscription();
92
- return window.location.pathname;
93
- }
94
-
95
- /** Alias of {@link useLocation}: the current `location.pathname`. */
96
- export function usePathname(): string {
97
- return useLocation();
98
- }
99
-
100
- /** The current query string as a `URLSearchParams`, re-read on every navigation. */
101
- export function useSearchParams(): URLSearchParams {
102
- useLocationSubscription();
103
- const search = window.location.search;
104
- return useMemo(() => new URLSearchParams(search), [search]);
105
- }
106
-
107
- /** True while a navigation is in flight (started but not yet committed), e.g. for a loading bar. */
108
- export function useNavigationPending(): boolean {
109
- return useSyncExternalStore(
110
- subscribePending,
111
- isNavigationPending,
112
- () => false,
113
- );
114
- }
1
+ /**
2
+ * Router hooks for user route components: read the params / pathname / search params, navigate
3
+ * imperatively, and grab a router handle.
4
+ */
5
+ import { useContext, useEffect, useMemo, useReducer, useSyncExternalStore } from 'react';
6
+
7
+ import type { RouteParams } from './match.js';
8
+ import {
9
+ back,
10
+ forward,
11
+ isNavigationPending,
12
+ navigate,
13
+ type NavigateOptions,
14
+ refresh,
15
+ subscribeLocation,
16
+ subscribePending,
17
+ } from '../navigation/navigation.js';
18
+ import { clearLoaderData, revalidate as revalidateData } from './loader.js';
19
+ import { ParamsContext } from './params-context.js';
20
+ import { prefetch } from '../navigation/prefetch.js';
21
+ import type { Href } from '../types.js';
22
+
23
+ /** Imperative router handle returned by {@link useRouter}. */
24
+ export interface RouterInstance {
25
+ /** Navigate to `href`, pushing a new history entry (or replacing with `{ replace: true }`). */
26
+ push(href: Href, options?: NavigateOptions): void;
27
+ /** Navigate to `href`, replacing the current history entry. */
28
+ replace(href: Href): void;
29
+ /** Go back one history entry. */
30
+ back(): void;
31
+ /** Go forward one history entry. */
32
+ forward(): void;
33
+ /** Re-render the current route and re-run its loader (clears all cached loader data). */
34
+ refresh(): void;
35
+ /**
36
+ * Invalidate cached loader data and re-render so it refetches. No argument refetches the active
37
+ * route; pass an `href` to target a specific route. Use after a mutation.
38
+ */
39
+ revalidate(href?: Href): void;
40
+ /** Prefetch a route's chunk ahead of navigation. */
41
+ prefetch(href: Href): void;
42
+ }
43
+
44
+ const ROUTER: RouterInstance = {
45
+ push: (href, options) => {
46
+ navigate(href, options);
47
+ },
48
+ replace: (href) => {
49
+ navigate(href, { replace: true });
50
+ },
51
+ back,
52
+ forward,
53
+ refresh: () => {
54
+ clearLoaderData();
55
+ refresh();
56
+ },
57
+ revalidate: (href) => {
58
+ revalidateData(href);
59
+ },
60
+ prefetch,
61
+ };
62
+
63
+ /** Current dynamic route params, e.g. `{ id }` inside `/blog/:id`. Pass a shape: `useParams<{ id: string }>()`. */
64
+ export function useParams<T extends RouteParams = RouteParams>(): T {
65
+ return useContext(ParamsContext) as T;
66
+ }
67
+
68
+ /** Returns the imperative `navigate(href, { replace })` function. */
69
+ export function useNavigate(): typeof navigate {
70
+ return navigate;
71
+ }
72
+
73
+ /** Returns the router handle (`push` / `replace` / `back` / `forward` / `refresh` / `prefetch`). */
74
+ export function useRouter(): RouterInstance {
75
+ return ROUTER;
76
+ }
77
+
78
+ /**
79
+ * Subscribes to location changes and reads the live `window.location` on render. Re-renders on any
80
+ * pathname, search, or hash change. The re-render is orchestrated by `navigate`/`notify` (wrapped in
81
+ * `startTransition` for smooth nav, or `document.startViewTransition` when enabled), so the listener
82
+ * itself is a plain force-update.
83
+ */
84
+ function useLocationSubscription(): void {
85
+ const [, forceUpdate] = useReducer((n: number): number => n + 1, 0);
86
+ useEffect(() => subscribeLocation(forceUpdate), []);
87
+ }
88
+
89
+ /** Subscribes to and returns the current `location.pathname`. */
90
+ export function useLocation(): string {
91
+ useLocationSubscription();
92
+ return window.location.pathname;
93
+ }
94
+
95
+ /** Alias of {@link useLocation}: the current `location.pathname`. */
96
+ export function usePathname(): string {
97
+ return useLocation();
98
+ }
99
+
100
+ /** The current query string as a `URLSearchParams`, re-read on every navigation. */
101
+ export function useSearchParams(): URLSearchParams {
102
+ useLocationSubscription();
103
+ const search = window.location.search;
104
+ return useMemo(() => new URLSearchParams(search), [search]);
105
+ }
106
+
107
+ /** True while a navigation is in flight (started but not yet committed), e.g. for a loading bar. */
108
+ export function useNavigationPending(): boolean {
109
+ return useSyncExternalStore(subscribePending, isNavigationPending, () => false);
110
+ }