@richie-router/react 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/history.cjs +150 -0
- package/dist/cjs/index.cjs +48 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/router.cjs +887 -0
- package/dist/esm/history.mjs +110 -0
- package/dist/esm/index.mjs +3 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/router.mjs +841 -0
- package/dist/types/history.d.ts +19 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/router.d.ts +263 -0
- package/package.json +46 -7
- package/README.md +0 -45
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
// packages/react/src/router.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {
|
|
4
|
+
buildPath,
|
|
5
|
+
collectBranches,
|
|
6
|
+
collectRoutes,
|
|
7
|
+
createParsedLocation,
|
|
8
|
+
createRouteNode,
|
|
9
|
+
defaultParseSearch,
|
|
10
|
+
defaultStringifySearch,
|
|
11
|
+
isNotFound,
|
|
12
|
+
isRedirect,
|
|
13
|
+
matchRouteTree,
|
|
14
|
+
notFound,
|
|
15
|
+
redirect,
|
|
16
|
+
resolveHeadConfig
|
|
17
|
+
} from "@richie-router/core";
|
|
18
|
+
import {
|
|
19
|
+
createBrowserHistory,
|
|
20
|
+
createHashHistory,
|
|
21
|
+
createMemoryHistory
|
|
22
|
+
} from "./history.mjs";
|
|
23
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
24
|
+
var RouterContext = React.createContext(null);
|
|
25
|
+
var RouterStateContext = React.createContext(null);
|
|
26
|
+
var OutletContext = React.createContext(null);
|
|
27
|
+
var MatchContext = React.createContext(null);
|
|
28
|
+
var MANAGED_HEAD_ATTRIBUTE = "data-richie-router-head";
|
|
29
|
+
var EMPTY_HEAD = { meta: [], links: [], scripts: [], styles: [] };
|
|
30
|
+
function routeHasRecord(value) {
|
|
31
|
+
return typeof value === "object" && value !== null;
|
|
32
|
+
}
|
|
33
|
+
function resolveParamsInput(input, previous) {
|
|
34
|
+
if (input === undefined) {
|
|
35
|
+
return previous;
|
|
36
|
+
}
|
|
37
|
+
return typeof input === "function" ? input(previous) : input;
|
|
38
|
+
}
|
|
39
|
+
function resolveSearchInput(input, previous) {
|
|
40
|
+
if (input === true) {
|
|
41
|
+
return previous;
|
|
42
|
+
}
|
|
43
|
+
if (input === undefined) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
return typeof input === "function" ? input(previous) : input;
|
|
47
|
+
}
|
|
48
|
+
function attachRouteApi(route) {
|
|
49
|
+
const api = route;
|
|
50
|
+
api.useParams = () => useRouteMatchByFullPath(route.fullPath).params;
|
|
51
|
+
api.useSearch = () => useRouteMatchByFullPath(route.fullPath).search;
|
|
52
|
+
api.useNavigate = () => useNavigate();
|
|
53
|
+
api.useMatch = () => useRouteMatchByFullPath(route.fullPath);
|
|
54
|
+
return api;
|
|
55
|
+
}
|
|
56
|
+
function createFileRoute(path) {
|
|
57
|
+
return function(options) {
|
|
58
|
+
const route = createRouteNode(path, options);
|
|
59
|
+
return attachRouteApi(route);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function createRootRoute(options) {
|
|
63
|
+
const route = createRouteNode("__root__", options, { isRoot: true });
|
|
64
|
+
return attachRouteApi(route);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class Router {
|
|
68
|
+
routeTree;
|
|
69
|
+
history;
|
|
70
|
+
options;
|
|
71
|
+
state;
|
|
72
|
+
routesByFullPath = new Map;
|
|
73
|
+
routesByTo = new Map;
|
|
74
|
+
listeners = new Set;
|
|
75
|
+
headCache = new Map;
|
|
76
|
+
parseSearch;
|
|
77
|
+
stringifySearch;
|
|
78
|
+
started = false;
|
|
79
|
+
unsubscribeHistory;
|
|
80
|
+
constructor(options) {
|
|
81
|
+
this.routeTree = options.routeTree;
|
|
82
|
+
this.options = options;
|
|
83
|
+
this.history = options.history ?? (typeof window === "undefined" ? createMemoryHistory() : createBrowserHistory());
|
|
84
|
+
this.parseSearch = options.parseSearch ?? defaultParseSearch;
|
|
85
|
+
this.stringifySearch = options.stringifySearch ?? defaultStringifySearch;
|
|
86
|
+
for (const route of collectRoutes(this.routeTree)) {
|
|
87
|
+
this.routesByFullPath.set(route.fullPath, route);
|
|
88
|
+
}
|
|
89
|
+
for (const branch of collectBranches(this.routeTree)) {
|
|
90
|
+
this.routesByTo.set(branch.leaf.to, branch.leaf);
|
|
91
|
+
}
|
|
92
|
+
const location = this.readLocation();
|
|
93
|
+
const initialHeadSnapshot = typeof window !== "undefined" ? window.__RICHIE_ROUTER_HEAD__ : undefined;
|
|
94
|
+
const initialHead = initialHeadSnapshot && initialHeadSnapshot.href === location.href ? initialHeadSnapshot.head : EMPTY_HEAD;
|
|
95
|
+
if (typeof window !== "undefined" && initialHeadSnapshot !== undefined) {
|
|
96
|
+
delete window.__RICHIE_ROUTER_HEAD__;
|
|
97
|
+
}
|
|
98
|
+
this.state = {
|
|
99
|
+
status: "loading",
|
|
100
|
+
location,
|
|
101
|
+
matches: this.buildMatches(location),
|
|
102
|
+
head: initialHead,
|
|
103
|
+
error: null
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
getSnapshot = () => this.state;
|
|
107
|
+
subscribe = (listener) => {
|
|
108
|
+
this.listeners.add(listener);
|
|
109
|
+
return () => {
|
|
110
|
+
this.listeners.delete(listener);
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
start() {
|
|
114
|
+
if (this.started) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.started = true;
|
|
118
|
+
this.unsubscribeHistory = this.history.listen(() => {
|
|
119
|
+
this.handleHistoryChange();
|
|
120
|
+
});
|
|
121
|
+
if (this.state.status === "loading") {
|
|
122
|
+
this.load();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
dispose() {
|
|
126
|
+
this.unsubscribeHistory?.();
|
|
127
|
+
this.unsubscribeHistory = undefined;
|
|
128
|
+
this.started = false;
|
|
129
|
+
}
|
|
130
|
+
async load(options) {
|
|
131
|
+
const nextLocation = this.readLocation();
|
|
132
|
+
await this.commitLocation(nextLocation, {
|
|
133
|
+
request: options?.request,
|
|
134
|
+
replace: true,
|
|
135
|
+
writeHistory: false
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async navigate(options) {
|
|
139
|
+
const href = this.buildHref(options);
|
|
140
|
+
const location = createParsedLocation(href, options.state ?? null, this.parseSearch);
|
|
141
|
+
await this.commitLocation(location, {
|
|
142
|
+
replace: options.replace ?? false,
|
|
143
|
+
writeHistory: true,
|
|
144
|
+
resetScroll: options.resetScroll
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async preloadRoute(options) {
|
|
148
|
+
const href = this.buildHref(options);
|
|
149
|
+
const location = createParsedLocation(href, options.state ?? null, this.parseSearch);
|
|
150
|
+
try {
|
|
151
|
+
await this.resolveLocation(location);
|
|
152
|
+
} catch {}
|
|
153
|
+
}
|
|
154
|
+
async invalidate() {
|
|
155
|
+
this.headCache.clear();
|
|
156
|
+
await this.load();
|
|
157
|
+
}
|
|
158
|
+
buildHref(options) {
|
|
159
|
+
const targetRoute = this.routesByTo.get(options.to) ?? null;
|
|
160
|
+
const fromMatch = options.from ? this.findMatchByTo(options.from) : null;
|
|
161
|
+
const previousParams = fromMatch?.params ?? {};
|
|
162
|
+
const previousSearch = fromMatch?.search ?? this.state.location.search;
|
|
163
|
+
const params = resolveParamsInput(options.params, previousParams);
|
|
164
|
+
const path = buildPath(options.to, params);
|
|
165
|
+
const searchValue = resolveSearchInput(options.search, previousSearch);
|
|
166
|
+
const search = this.stringifySearch(routeHasRecord(searchValue) ? searchValue : {});
|
|
167
|
+
const hash = options.hash ? `#${options.hash.replace(/^#/, "")}` : "";
|
|
168
|
+
const normalizedPath = this.applyTrailingSlash(path, targetRoute);
|
|
169
|
+
return `${normalizedPath}${search}${hash}`;
|
|
170
|
+
}
|
|
171
|
+
readLocation() {
|
|
172
|
+
const location = this.history.location;
|
|
173
|
+
return createParsedLocation(location.href, location.state, this.parseSearch);
|
|
174
|
+
}
|
|
175
|
+
applyTrailingSlash(pathname, route) {
|
|
176
|
+
const trailingSlash = this.options.trailingSlash ?? "preserve";
|
|
177
|
+
if (trailingSlash === "preserve") {
|
|
178
|
+
return pathname;
|
|
179
|
+
}
|
|
180
|
+
if (pathname === "/") {
|
|
181
|
+
return "/";
|
|
182
|
+
}
|
|
183
|
+
if (trailingSlash === "always") {
|
|
184
|
+
return pathname.endsWith("/") ? pathname : `${pathname}/`;
|
|
185
|
+
}
|
|
186
|
+
if (route && route.fullPath.endsWith("/") && route.to === pathname) {
|
|
187
|
+
return pathname;
|
|
188
|
+
}
|
|
189
|
+
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
190
|
+
}
|
|
191
|
+
notify() {
|
|
192
|
+
for (const listener of this.listeners) {
|
|
193
|
+
listener();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
findMatchByTo(to) {
|
|
197
|
+
const route = this.routesByTo.get(to);
|
|
198
|
+
if (!route) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return this.state.matches.find((match) => match.route.fullPath === route.fullPath) ?? null;
|
|
202
|
+
}
|
|
203
|
+
buildMatches(location) {
|
|
204
|
+
const matched = matchRouteTree(this.routeTree, location.pathname) ?? [];
|
|
205
|
+
const rawSearch = location.search;
|
|
206
|
+
let accumulatedSearch = { ...rawSearch };
|
|
207
|
+
return matched.map(({ route, params }) => {
|
|
208
|
+
const nextSearch = this.resolveSearch(route, rawSearch);
|
|
209
|
+
if (routeHasRecord(nextSearch)) {
|
|
210
|
+
accumulatedSearch = {
|
|
211
|
+
...accumulatedSearch,
|
|
212
|
+
...nextSearch
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
id: route.fullPath,
|
|
217
|
+
pathname: location.pathname,
|
|
218
|
+
params,
|
|
219
|
+
route,
|
|
220
|
+
search: accumulatedSearch,
|
|
221
|
+
to: route.to
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
resolveSearch(route, rawSearch) {
|
|
226
|
+
const fromSchema = route.searchSchema ? route.searchSchema.parse(rawSearch) : {};
|
|
227
|
+
if (routeHasRecord(fromSchema)) {
|
|
228
|
+
return fromSchema;
|
|
229
|
+
}
|
|
230
|
+
return rawSearch;
|
|
231
|
+
}
|
|
232
|
+
async resolveLocation(location, options) {
|
|
233
|
+
const matched = matchRouteTree(this.routeTree, location.pathname);
|
|
234
|
+
if (!matched) {
|
|
235
|
+
throw notFound();
|
|
236
|
+
}
|
|
237
|
+
const rawSearch = location.search;
|
|
238
|
+
let accumulatedSearch = { ...rawSearch };
|
|
239
|
+
const matches = [];
|
|
240
|
+
for (const { route, params } of matched) {
|
|
241
|
+
const nextSearch = this.resolveSearch(route, rawSearch);
|
|
242
|
+
if (routeHasRecord(nextSearch)) {
|
|
243
|
+
accumulatedSearch = {
|
|
244
|
+
...accumulatedSearch,
|
|
245
|
+
...nextSearch
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (route.options.beforeLoad) {
|
|
249
|
+
await route.options.beforeLoad({
|
|
250
|
+
location,
|
|
251
|
+
params,
|
|
252
|
+
search: accumulatedSearch,
|
|
253
|
+
navigate: async (navigateOptions) => {
|
|
254
|
+
await this.navigate(navigateOptions);
|
|
255
|
+
},
|
|
256
|
+
cause: this.state.location.pathname === location.pathname ? "stay" : "enter"
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
matches.push({
|
|
260
|
+
id: route.fullPath,
|
|
261
|
+
pathname: location.pathname,
|
|
262
|
+
params,
|
|
263
|
+
route,
|
|
264
|
+
search: accumulatedSearch,
|
|
265
|
+
to: route.to
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const head = await this.resolveLocationHead(matches, location, options?.request);
|
|
269
|
+
return { matches, head, error: null };
|
|
270
|
+
}
|
|
271
|
+
async resolveLocationHead(matches, location, request) {
|
|
272
|
+
const resolvedHeadByRoute = new Map;
|
|
273
|
+
for (const match of matches) {
|
|
274
|
+
if (!match.route.serverHead) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
resolvedHeadByRoute.set(match.route.fullPath, await this.loadRouteHead(match.route, match.params, match.search, location, request));
|
|
278
|
+
}
|
|
279
|
+
return resolveHeadConfig(matches, resolvedHeadByRoute);
|
|
280
|
+
}
|
|
281
|
+
async loadRouteHead(route, params, search, location, request) {
|
|
282
|
+
const cacheKey = JSON.stringify({
|
|
283
|
+
routeId: route.fullPath,
|
|
284
|
+
params,
|
|
285
|
+
search
|
|
286
|
+
});
|
|
287
|
+
const cached = this.headCache.get(cacheKey);
|
|
288
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
289
|
+
return cached.head;
|
|
290
|
+
}
|
|
291
|
+
const response = this.options.loadRouteHead !== undefined ? await this.options.loadRouteHead({
|
|
292
|
+
route,
|
|
293
|
+
routeId: route.fullPath,
|
|
294
|
+
params,
|
|
295
|
+
search,
|
|
296
|
+
location,
|
|
297
|
+
request
|
|
298
|
+
}) : await this.fetchRouteHead(route, params, search);
|
|
299
|
+
this.headCache.set(cacheKey, {
|
|
300
|
+
head: response.head,
|
|
301
|
+
expiresAt: Date.now() + (response.staleTime ?? 0)
|
|
302
|
+
});
|
|
303
|
+
return response.head;
|
|
304
|
+
}
|
|
305
|
+
async fetchRouteHead(route, params, search) {
|
|
306
|
+
const basePath = this.options.headBasePath ?? "/head-api";
|
|
307
|
+
const searchParams = new URLSearchParams({
|
|
308
|
+
routeId: route.fullPath,
|
|
309
|
+
params: JSON.stringify(params),
|
|
310
|
+
search: JSON.stringify(search)
|
|
311
|
+
});
|
|
312
|
+
const response = await fetch(`${basePath}?${searchParams.toString()}`);
|
|
313
|
+
if (!response.ok) {
|
|
314
|
+
if (response.status === 404) {
|
|
315
|
+
throw notFound();
|
|
316
|
+
}
|
|
317
|
+
throw new Error(`Failed to resolve server head for route "${route.fullPath}"`);
|
|
318
|
+
}
|
|
319
|
+
return await response.json();
|
|
320
|
+
}
|
|
321
|
+
async commitLocation(location, options) {
|
|
322
|
+
this.state = {
|
|
323
|
+
...this.state,
|
|
324
|
+
status: "loading",
|
|
325
|
+
location
|
|
326
|
+
};
|
|
327
|
+
this.notify();
|
|
328
|
+
try {
|
|
329
|
+
const resolved = await this.resolveLocation(location, {
|
|
330
|
+
request: options.request
|
|
331
|
+
});
|
|
332
|
+
if (options.writeHistory) {
|
|
333
|
+
if (options.replace) {
|
|
334
|
+
this.history.replace(location.href, location.state);
|
|
335
|
+
} else {
|
|
336
|
+
this.history.push(location.href, location.state);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.state = {
|
|
340
|
+
status: "idle",
|
|
341
|
+
location,
|
|
342
|
+
matches: resolved.matches,
|
|
343
|
+
head: resolved.head,
|
|
344
|
+
error: resolved.error
|
|
345
|
+
};
|
|
346
|
+
this.notify();
|
|
347
|
+
this.restoreScroll(options.resetScroll);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (isRedirect(error)) {
|
|
350
|
+
await this.navigate({
|
|
351
|
+
...error.options,
|
|
352
|
+
replace: error.options.replace ?? true
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const errorMatches = this.buildMatches(location);
|
|
357
|
+
if (options.writeHistory) {
|
|
358
|
+
if (options.replace) {
|
|
359
|
+
this.history.replace(location.href, location.state);
|
|
360
|
+
} else {
|
|
361
|
+
this.history.push(location.href, location.state);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this.state = {
|
|
365
|
+
status: "idle",
|
|
366
|
+
location,
|
|
367
|
+
matches: errorMatches,
|
|
368
|
+
head: resolveHeadConfig(errorMatches),
|
|
369
|
+
error
|
|
370
|
+
};
|
|
371
|
+
this.notify();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
restoreScroll(resetScroll) {
|
|
375
|
+
if (typeof window === "undefined") {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!this.options.scrollRestoration || resetScroll === false) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const selectors = this.options.scrollToTopSelectors ?? [];
|
|
382
|
+
if (selectors.length === 0) {
|
|
383
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
for (const selector of selectors) {
|
|
387
|
+
const element = document.querySelector(selector);
|
|
388
|
+
if (element instanceof HTMLElement) {
|
|
389
|
+
element.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async handleHistoryChange() {
|
|
394
|
+
const nextLocation = this.readLocation();
|
|
395
|
+
await this.commitLocation(nextLocation, {
|
|
396
|
+
replace: true,
|
|
397
|
+
writeHistory: false
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function createRouter(options) {
|
|
402
|
+
return new Router(options);
|
|
403
|
+
}
|
|
404
|
+
function useRouterContext() {
|
|
405
|
+
const router = React.useContext(RouterContext);
|
|
406
|
+
if (!router) {
|
|
407
|
+
throw new Error("Richie Router hooks must be used inside <RouterProvider>.");
|
|
408
|
+
}
|
|
409
|
+
return router;
|
|
410
|
+
}
|
|
411
|
+
function useRouterStateContext() {
|
|
412
|
+
const state = React.useContext(RouterStateContext);
|
|
413
|
+
if (!state) {
|
|
414
|
+
throw new Error("Richie Router hooks must be used inside <RouterProvider>.");
|
|
415
|
+
}
|
|
416
|
+
return state;
|
|
417
|
+
}
|
|
418
|
+
function useRouteMatchByFullPath(fullPath) {
|
|
419
|
+
const state = useRouterStateContext();
|
|
420
|
+
const currentRenderedMatch = React.useContext(MatchContext);
|
|
421
|
+
return state.matches.find((match) => match.route.fullPath === fullPath) ?? (currentRenderedMatch?.route.fullPath === fullPath ? currentRenderedMatch : null) ?? (() => {
|
|
422
|
+
throw new Error(`No active match found for "${fullPath}".`);
|
|
423
|
+
})();
|
|
424
|
+
}
|
|
425
|
+
function resolveHookMatch(from) {
|
|
426
|
+
const router = useRouterContext();
|
|
427
|
+
const state = useRouterStateContext();
|
|
428
|
+
const currentRenderedMatch = React.useContext(MatchContext);
|
|
429
|
+
if (!from) {
|
|
430
|
+
return currentRenderedMatch ?? state.matches.at(-1) ?? (() => {
|
|
431
|
+
throw new Error("No active route match is available.");
|
|
432
|
+
})();
|
|
433
|
+
}
|
|
434
|
+
const targetRoute = router.routesByTo.get(from);
|
|
435
|
+
if (!targetRoute) {
|
|
436
|
+
throw new Error(`Unknown route path "${from}".`);
|
|
437
|
+
}
|
|
438
|
+
return state.matches.find((match) => match.route.fullPath === targetRoute.fullPath) ?? (() => {
|
|
439
|
+
throw new Error(`The route "${from}" is not part of the current match set.`);
|
|
440
|
+
})();
|
|
441
|
+
}
|
|
442
|
+
function createManagedHeadElements(head) {
|
|
443
|
+
if (typeof document === "undefined") {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
const elements = [];
|
|
447
|
+
const managed = (element) => {
|
|
448
|
+
element.setAttribute(MANAGED_HEAD_ATTRIBUTE, "true");
|
|
449
|
+
return element;
|
|
450
|
+
};
|
|
451
|
+
for (const meta of head.meta ?? []) {
|
|
452
|
+
if ("title" in meta) {
|
|
453
|
+
const title = managed(document.createElement("title"));
|
|
454
|
+
title.textContent = meta.title;
|
|
455
|
+
elements.push(title);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const tag = managed(document.createElement("meta"));
|
|
459
|
+
if ("charset" in meta) {
|
|
460
|
+
tag.setAttribute("charset", meta.charset);
|
|
461
|
+
} else if ("name" in meta) {
|
|
462
|
+
tag.setAttribute("name", meta.name);
|
|
463
|
+
tag.setAttribute("content", meta.content);
|
|
464
|
+
} else if ("property" in meta) {
|
|
465
|
+
tag.setAttribute("property", meta.property);
|
|
466
|
+
tag.setAttribute("content", meta.content);
|
|
467
|
+
} else {
|
|
468
|
+
tag.setAttribute("http-equiv", meta.httpEquiv);
|
|
469
|
+
tag.setAttribute("content", meta.content);
|
|
470
|
+
}
|
|
471
|
+
elements.push(tag);
|
|
472
|
+
}
|
|
473
|
+
for (const link of head.links ?? []) {
|
|
474
|
+
const tag = managed(document.createElement("link"));
|
|
475
|
+
tag.setAttribute("rel", link.rel);
|
|
476
|
+
tag.setAttribute("href", link.href);
|
|
477
|
+
if (link.type)
|
|
478
|
+
tag.setAttribute("type", link.type);
|
|
479
|
+
if (link.media)
|
|
480
|
+
tag.setAttribute("media", link.media);
|
|
481
|
+
if (link.sizes)
|
|
482
|
+
tag.setAttribute("sizes", link.sizes);
|
|
483
|
+
if (link.crossorigin)
|
|
484
|
+
tag.setAttribute("crossorigin", link.crossorigin);
|
|
485
|
+
elements.push(tag);
|
|
486
|
+
}
|
|
487
|
+
for (const style of head.styles ?? []) {
|
|
488
|
+
const tag = managed(document.createElement("style"));
|
|
489
|
+
if (style.media)
|
|
490
|
+
tag.setAttribute("media", style.media);
|
|
491
|
+
tag.textContent = style.children;
|
|
492
|
+
elements.push(tag);
|
|
493
|
+
}
|
|
494
|
+
for (const script of head.scripts ?? []) {
|
|
495
|
+
const tag = managed(document.createElement("script"));
|
|
496
|
+
if (script.src)
|
|
497
|
+
tag.setAttribute("src", script.src);
|
|
498
|
+
if (script.type)
|
|
499
|
+
tag.setAttribute("type", script.type);
|
|
500
|
+
if (script.async)
|
|
501
|
+
tag.async = true;
|
|
502
|
+
if (script.defer)
|
|
503
|
+
tag.defer = true;
|
|
504
|
+
if (script.children)
|
|
505
|
+
tag.textContent = script.children;
|
|
506
|
+
elements.push(tag);
|
|
507
|
+
}
|
|
508
|
+
return elements;
|
|
509
|
+
}
|
|
510
|
+
function reconcileDocumentHead(head) {
|
|
511
|
+
if (typeof document === "undefined") {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
for (const element of Array.from(document.head.querySelectorAll(`[${MANAGED_HEAD_ATTRIBUTE}]`))) {
|
|
515
|
+
element.remove();
|
|
516
|
+
}
|
|
517
|
+
const elements = createManagedHeadElements(head);
|
|
518
|
+
for (const element of elements) {
|
|
519
|
+
document.head.appendChild(element);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function RenderMatches({ matches, index }) {
|
|
523
|
+
const match = matches[index];
|
|
524
|
+
if (!match) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
const Component = match.route.options.component;
|
|
528
|
+
const outlet = /* @__PURE__ */ jsxDEV(RenderMatches, {
|
|
529
|
+
matches,
|
|
530
|
+
index: index + 1
|
|
531
|
+
}, undefined, false, undefined, this);
|
|
532
|
+
return /* @__PURE__ */ jsxDEV(MatchContext.Provider, {
|
|
533
|
+
value: match,
|
|
534
|
+
children: /* @__PURE__ */ jsxDEV(OutletContext.Provider, {
|
|
535
|
+
value: outlet,
|
|
536
|
+
children: Component ? /* @__PURE__ */ jsxDEV(Component, {}, undefined, false, undefined, this) : outlet
|
|
537
|
+
}, undefined, false, undefined, this)
|
|
538
|
+
}, undefined, false, undefined, this);
|
|
539
|
+
}
|
|
540
|
+
function renderError(error, matches, router) {
|
|
541
|
+
const reversed = [...matches].reverse();
|
|
542
|
+
if (isNotFound(error)) {
|
|
543
|
+
const NotFoundComponent = reversed.find((match) => match.route.options.notFoundComponent)?.route.options.notFoundComponent ?? router.options.defaultNotFoundComponent;
|
|
544
|
+
if (NotFoundComponent) {
|
|
545
|
+
return React.createElement(NotFoundComponent);
|
|
546
|
+
}
|
|
547
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
548
|
+
children: "Not Found"
|
|
549
|
+
}, undefined, false, undefined, this);
|
|
550
|
+
}
|
|
551
|
+
const ErrorComponent = reversed.find((match) => match.route.options.errorComponent)?.route.options.errorComponent ?? router.options.defaultErrorComponent;
|
|
552
|
+
if (ErrorComponent) {
|
|
553
|
+
return React.createElement(ErrorComponent, {
|
|
554
|
+
error,
|
|
555
|
+
reset: () => {
|
|
556
|
+
router.invalidate();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return /* @__PURE__ */ jsxDEV("pre", {
|
|
561
|
+
children: error instanceof Error ? error.message : "Unknown routing error"
|
|
562
|
+
}, undefined, false, undefined, this);
|
|
563
|
+
}
|
|
564
|
+
function RouterProvider({ router }) {
|
|
565
|
+
const snapshot = React.useSyncExternalStore(router.subscribe, router.getSnapshot, router.getSnapshot);
|
|
566
|
+
React.useEffect(() => {
|
|
567
|
+
router.start();
|
|
568
|
+
return () => {
|
|
569
|
+
router.dispose();
|
|
570
|
+
};
|
|
571
|
+
}, [router]);
|
|
572
|
+
React.useEffect(() => {
|
|
573
|
+
reconcileDocumentHead(snapshot.head);
|
|
574
|
+
}, [snapshot.head]);
|
|
575
|
+
const content = snapshot.error ? renderError(snapshot.error, snapshot.matches, router) : /* @__PURE__ */ jsxDEV(RenderMatches, {
|
|
576
|
+
matches: snapshot.matches,
|
|
577
|
+
index: 0
|
|
578
|
+
}, undefined, false, undefined, this);
|
|
579
|
+
return /* @__PURE__ */ jsxDEV(RouterContext.Provider, {
|
|
580
|
+
value: router,
|
|
581
|
+
children: /* @__PURE__ */ jsxDEV(RouterStateContext.Provider, {
|
|
582
|
+
value: snapshot,
|
|
583
|
+
children: content
|
|
584
|
+
}, undefined, false, undefined, this)
|
|
585
|
+
}, undefined, false, undefined, this);
|
|
586
|
+
}
|
|
587
|
+
function Outlet() {
|
|
588
|
+
return React.useContext(OutletContext);
|
|
589
|
+
}
|
|
590
|
+
function useRouter() {
|
|
591
|
+
return useRouterContext();
|
|
592
|
+
}
|
|
593
|
+
function useMatches() {
|
|
594
|
+
return useRouterStateContext().matches;
|
|
595
|
+
}
|
|
596
|
+
function useMatch(options) {
|
|
597
|
+
return resolveHookMatch(options?.from);
|
|
598
|
+
}
|
|
599
|
+
function useParams(options) {
|
|
600
|
+
return resolveHookMatch(options?.from).params;
|
|
601
|
+
}
|
|
602
|
+
function useSearch(options) {
|
|
603
|
+
return resolveHookMatch(options?.from).search;
|
|
604
|
+
}
|
|
605
|
+
function useNavigate() {
|
|
606
|
+
const router = useRouterContext();
|
|
607
|
+
return React.useCallback(async (options) => {
|
|
608
|
+
await router.navigate(options);
|
|
609
|
+
}, [router]);
|
|
610
|
+
}
|
|
611
|
+
function useLocation() {
|
|
612
|
+
return useRouterStateContext().location;
|
|
613
|
+
}
|
|
614
|
+
function useRouterState(options) {
|
|
615
|
+
const router = useRouterContext();
|
|
616
|
+
return React.useSyncExternalStore(router.subscribe, () => options.select(router.getSnapshot()), () => options.select(router.getSnapshot()));
|
|
617
|
+
}
|
|
618
|
+
function useBlocker(options) {
|
|
619
|
+
const current = useLocation();
|
|
620
|
+
const [next, setNext] = React.useState(null);
|
|
621
|
+
React.useEffect(() => {
|
|
622
|
+
if (!options.enableBeforeUnload || typeof window === "undefined") {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const listener = (event) => {
|
|
626
|
+
if (!options.shouldBlockFn?.({ current, next })) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
event.preventDefault();
|
|
630
|
+
event.returnValue = "";
|
|
631
|
+
};
|
|
632
|
+
window.addEventListener("beforeunload", listener);
|
|
633
|
+
return () => window.removeEventListener("beforeunload", listener);
|
|
634
|
+
}, [current, next, options]);
|
|
635
|
+
return {
|
|
636
|
+
status: next ? "blocked" : "idle",
|
|
637
|
+
next,
|
|
638
|
+
proceed() {
|
|
639
|
+
setNext(null);
|
|
640
|
+
},
|
|
641
|
+
reset() {
|
|
642
|
+
setNext(null);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function Block() {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
function useElementScrollRestoration() {
|
|
650
|
+
return {
|
|
651
|
+
ref: () => {}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function useResolvedLink(props) {
|
|
655
|
+
const router = useRouterContext();
|
|
656
|
+
const href = router.buildHref(props);
|
|
657
|
+
const location = useLocation();
|
|
658
|
+
const pathOnly = href.split(/[?#]/u)[0] ?? href;
|
|
659
|
+
const isActive = pathOnly === location.pathname;
|
|
660
|
+
return { href, isActive, router };
|
|
661
|
+
}
|
|
662
|
+
var LinkComponent = React.forwardRef(function LinkInner(props, ref) {
|
|
663
|
+
const allProps = props;
|
|
664
|
+
const {
|
|
665
|
+
to,
|
|
666
|
+
from,
|
|
667
|
+
params,
|
|
668
|
+
search,
|
|
669
|
+
hash,
|
|
670
|
+
replace,
|
|
671
|
+
resetScroll,
|
|
672
|
+
state,
|
|
673
|
+
mask,
|
|
674
|
+
ignoreBlocker,
|
|
675
|
+
activeProps,
|
|
676
|
+
children,
|
|
677
|
+
onClick,
|
|
678
|
+
onMouseEnter,
|
|
679
|
+
onFocus,
|
|
680
|
+
preload,
|
|
681
|
+
...anchorProps
|
|
682
|
+
} = allProps;
|
|
683
|
+
const navigation = {
|
|
684
|
+
to,
|
|
685
|
+
from,
|
|
686
|
+
params,
|
|
687
|
+
search,
|
|
688
|
+
hash,
|
|
689
|
+
replace,
|
|
690
|
+
resetScroll,
|
|
691
|
+
state,
|
|
692
|
+
mask,
|
|
693
|
+
ignoreBlocker
|
|
694
|
+
};
|
|
695
|
+
const { href, isActive, router } = useResolvedLink(navigation);
|
|
696
|
+
const preloadMode = preload ?? router.options.defaultPreload;
|
|
697
|
+
const preloadDelay = router.options.defaultPreloadDelay ?? 50;
|
|
698
|
+
const preloadTimeout = React.useRef(null);
|
|
699
|
+
React.useEffect(() => {
|
|
700
|
+
if (preloadMode !== "render") {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
router.preloadRoute(navigation);
|
|
704
|
+
}, [navigation, preloadMode, router]);
|
|
705
|
+
const schedulePreload = React.useCallback(() => {
|
|
706
|
+
if (preloadMode !== "intent") {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
preloadTimeout.current = window.setTimeout(() => {
|
|
710
|
+
router.preloadRoute(navigation);
|
|
711
|
+
}, preloadDelay);
|
|
712
|
+
}, [navigation, preloadDelay, preloadMode, router]);
|
|
713
|
+
const cancelPreload = React.useCallback(() => {
|
|
714
|
+
if (preloadTimeout.current !== null) {
|
|
715
|
+
window.clearTimeout(preloadTimeout.current);
|
|
716
|
+
preloadTimeout.current = null;
|
|
717
|
+
}
|
|
718
|
+
}, []);
|
|
719
|
+
const renderedChildren = typeof children === "function" ? children({ isActive }) : children;
|
|
720
|
+
return /* @__PURE__ */ jsxDEV("a", {
|
|
721
|
+
...anchorProps,
|
|
722
|
+
...isActive ? activeProps : undefined,
|
|
723
|
+
ref,
|
|
724
|
+
href,
|
|
725
|
+
onClick: (event) => {
|
|
726
|
+
onClick?.(event);
|
|
727
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
event.preventDefault();
|
|
731
|
+
router.navigate(navigation);
|
|
732
|
+
},
|
|
733
|
+
onMouseEnter: (event) => {
|
|
734
|
+
onMouseEnter?.(event);
|
|
735
|
+
if (typeof window !== "undefined") {
|
|
736
|
+
schedulePreload();
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
onMouseLeave: cancelPreload,
|
|
740
|
+
onFocus: (event) => {
|
|
741
|
+
onFocus?.(event);
|
|
742
|
+
if (typeof window !== "undefined") {
|
|
743
|
+
schedulePreload();
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
onBlur: cancelPreload,
|
|
747
|
+
children: renderedChildren
|
|
748
|
+
}, undefined, false, undefined, this);
|
|
749
|
+
});
|
|
750
|
+
var Link = LinkComponent;
|
|
751
|
+
function createLink(Component) {
|
|
752
|
+
return function CreatedLink(props) {
|
|
753
|
+
const allProps = props;
|
|
754
|
+
const {
|
|
755
|
+
to,
|
|
756
|
+
from,
|
|
757
|
+
params,
|
|
758
|
+
search,
|
|
759
|
+
hash,
|
|
760
|
+
replace,
|
|
761
|
+
resetScroll,
|
|
762
|
+
state,
|
|
763
|
+
mask,
|
|
764
|
+
ignoreBlocker,
|
|
765
|
+
activeProps,
|
|
766
|
+
children,
|
|
767
|
+
preload,
|
|
768
|
+
...componentProps
|
|
769
|
+
} = allProps;
|
|
770
|
+
const navigation = {
|
|
771
|
+
to,
|
|
772
|
+
from,
|
|
773
|
+
params,
|
|
774
|
+
search,
|
|
775
|
+
hash,
|
|
776
|
+
replace,
|
|
777
|
+
resetScroll,
|
|
778
|
+
state,
|
|
779
|
+
mask,
|
|
780
|
+
ignoreBlocker
|
|
781
|
+
};
|
|
782
|
+
const { href, isActive, router } = useResolvedLink(navigation);
|
|
783
|
+
const renderedChildren = typeof children === "function" ? children({ isActive }) : children;
|
|
784
|
+
React.useEffect(() => {
|
|
785
|
+
if (preload !== "render") {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
router.preloadRoute(navigation);
|
|
789
|
+
}, [navigation, preload, router]);
|
|
790
|
+
return /* @__PURE__ */ jsxDEV(Component, {
|
|
791
|
+
...componentProps,
|
|
792
|
+
...isActive ? activeProps : undefined,
|
|
793
|
+
href,
|
|
794
|
+
children: renderedChildren
|
|
795
|
+
}, undefined, false, undefined, this);
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function linkOptions(options) {
|
|
799
|
+
return options;
|
|
800
|
+
}
|
|
801
|
+
function getRouteApi(to) {
|
|
802
|
+
return {
|
|
803
|
+
useParams: () => useParams({ from: to }),
|
|
804
|
+
useSearch: () => useSearch({ from: to }),
|
|
805
|
+
useMatch: () => useMatch({ from: to })
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function createRouteMask(mask) {
|
|
809
|
+
return mask;
|
|
810
|
+
}
|
|
811
|
+
export {
|
|
812
|
+
useSearch,
|
|
813
|
+
useRouterState,
|
|
814
|
+
useRouter,
|
|
815
|
+
useParams,
|
|
816
|
+
useNavigate,
|
|
817
|
+
useMatches,
|
|
818
|
+
useMatch,
|
|
819
|
+
useLocation,
|
|
820
|
+
useElementScrollRestoration,
|
|
821
|
+
useBlocker,
|
|
822
|
+
redirect,
|
|
823
|
+
notFound,
|
|
824
|
+
linkOptions,
|
|
825
|
+
isRedirect,
|
|
826
|
+
isNotFound,
|
|
827
|
+
getRouteApi,
|
|
828
|
+
createRouter,
|
|
829
|
+
createRouteMask,
|
|
830
|
+
createRootRoute,
|
|
831
|
+
createMemoryHistory,
|
|
832
|
+
createLink,
|
|
833
|
+
createHashHistory,
|
|
834
|
+
createFileRoute,
|
|
835
|
+
createBrowserHistory,
|
|
836
|
+
RouterProvider,
|
|
837
|
+
Router,
|
|
838
|
+
Outlet,
|
|
839
|
+
Link,
|
|
840
|
+
Block
|
|
841
|
+
};
|