toiljs 0.0.10 → 0.0.12
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 +315 -1
- package/assets/logo.svg +37 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/configure.js +10 -4
- package/build/cli/create.js +60 -32
- package/build/cli/diagnostics.d.ts +55 -0
- package/build/cli/diagnostics.js +333 -0
- package/build/cli/doctor.d.ts +6 -0
- package/build/cli/doctor.js +249 -0
- package/build/cli/index.js +26 -0
- package/build/cli/proc.d.ts +5 -0
- package/build/cli/proc.js +20 -0
- package/build/cli/ui.d.ts +1 -0
- package/build/cli/ui.js +1 -0
- package/build/cli/update.d.ts +7 -0
- package/build/cli/update.js +117 -0
- package/build/cli/updates.d.ts +10 -0
- package/build/cli/updates.js +45 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/head/metadata.js +3 -1
- package/build/client/index.d.ts +5 -1
- package/build/client/index.js +2 -0
- package/build/client/navigation/navigation.js +1 -1
- package/build/client/routing/Router.js +2 -2
- package/build/client/search/search.d.ts +26 -0
- package/build/client/search/search.js +101 -0
- package/build/client/search/use-page-search.d.ts +8 -0
- package/build/client/search/use-page-search.js +21 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +35 -26
- package/build/compiler/index.d.ts +2 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/pages.d.ts +8 -0
- package/build/compiler/pages.js +37 -0
- package/build/compiler/plugin.js +3 -1
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +11 -5
- package/build/compiler/seo.js +10 -3
- package/build/compiler/vite.js +7 -0
- package/build/io/.tsbuildinfo +1 -1
- package/examples/basic/client/components/Header.tsx +43 -38
- package/examples/basic/client/components/HoneycombBackground.tsx +223 -230
- package/examples/basic/client/layout.tsx +4 -1
- package/examples/basic/client/public/index.html +18 -16
- package/examples/basic/client/routes/(legal)/privacy.tsx +18 -0
- package/examples/basic/client/routes/(legal)/terms.tsx +15 -0
- package/examples/basic/client/routes/about.tsx +21 -19
- package/examples/basic/client/routes/blog/[id].tsx +26 -12
- package/examples/basic/client/routes/features/actions.tsx +67 -0
- package/examples/basic/client/routes/features/error/error.tsx +16 -0
- package/examples/basic/client/routes/features/error/index.tsx +27 -0
- package/examples/basic/client/routes/features/head.tsx +38 -0
- package/examples/basic/client/routes/features/index.tsx +83 -0
- package/examples/basic/client/routes/features/realtime.tsx +34 -0
- package/examples/basic/client/routes/features/script.tsx +31 -0
- package/examples/basic/client/routes/features/seo.tsx +39 -0
- package/examples/basic/client/routes/features/template/b.tsx +14 -0
- package/examples/basic/client/routes/features/template/index.tsx +20 -0
- package/examples/basic/client/routes/features/template/template.tsx +16 -0
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -0
- package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -0
- package/examples/basic/client/routes/gallery/index.tsx +42 -0
- package/examples/basic/client/routes/gallery/layout.tsx +13 -0
- package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -0
- package/examples/basic/client/routes/get-started.tsx +157 -84
- package/examples/basic/client/routes/index.tsx +137 -87
- package/examples/basic/client/routes/loader-demo/index.tsx +59 -50
- package/examples/basic/client/routes/search.tsx +61 -0
- package/examples/basic/client/routes/test.tsx +7 -8
- package/examples/basic/client/styles/main.css +624 -552
- package/examples/basic/client/toil.tsx +2 -4
- package/package.json +3 -2
- package/presets/eslint.js +10 -3
- package/src/cli/configure.ts +363 -353
- package/src/cli/create.ts +563 -530
- package/src/cli/diagnostics.ts +421 -0
- package/src/cli/doctor.ts +318 -0
- package/src/cli/features.ts +166 -160
- package/src/cli/index.ts +242 -211
- package/src/cli/proc.ts +30 -0
- package/src/cli/ui.ts +111 -103
- package/src/cli/update.ts +150 -0
- package/src/cli/updates.ts +69 -0
- package/src/client/components/Image.tsx +91 -89
- package/src/client/dev/error-overlay.tsx +193 -197
- package/src/client/head/metadata.ts +94 -92
- package/src/client/index.ts +79 -64
- package/src/client/navigation/Link.tsx +94 -100
- package/src/client/navigation/navigation.ts +215 -218
- package/src/client/routing/Router.tsx +210 -193
- package/src/client/routing/hooks.ts +110 -114
- package/src/client/routing/lazy.ts +77 -81
- package/src/client/search/search.ts +189 -0
- package/src/client/search/use-page-search.ts +73 -0
- package/src/compiler/config.ts +173 -171
- package/src/compiler/fonts.ts +89 -87
- package/src/compiler/generate.ts +378 -364
- package/src/compiler/image-report.ts +88 -85
- package/src/compiler/index.ts +2 -0
- package/src/compiler/pages.ts +70 -0
- package/src/compiler/plugin.ts +51 -47
- package/src/compiler/prerender.ts +152 -130
- package/src/compiler/routes.ts +132 -131
- package/src/compiler/seo.ts +381 -356
- package/src/compiler/vite.ts +155 -130
- package/src/io/FastSet.ts +99 -96
- package/test/configure.test.ts +94 -90
- package/test/doctor.test.ts +140 -0
- package/test/dom/Image.test.tsx +73 -46
- package/test/dom/Script.test.tsx +48 -45
- package/test/dom/action.test.tsx +146 -129
- package/test/dom/error-overlay.test.tsx +44 -44
- package/test/dom/loader.test.tsx +2 -2
- package/test/dom/revalidate.test.tsx +1 -1
- package/test/dom/route-head.test.tsx +35 -2
- package/test/dom/slot.test.tsx +131 -109
- package/test/dom/view-transitions.test.tsx +53 -51
- package/test/features.test.ts +149 -142
- package/test/fonts.test.ts +28 -26
- package/test/head.test.ts +45 -35
- package/test/metadata.test.ts +42 -41
- package/test/pages.test.ts +105 -0
- package/test/prerender.test.ts +54 -46
- package/test/search.test.ts +114 -0
- package/test/seo.test.ts +164 -142
- package/test/slot-layouts.test.ts +69 -0
- 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(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
+
}
|