@richie-router/react 0.1.2 → 0.1.4

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.
@@ -10,6 +10,7 @@ import {
10
10
  defaultStringifySearch,
11
11
  isNotFound,
12
12
  isRedirect,
13
+ matchPathname,
13
14
  matchRouteTree,
14
15
  notFound,
15
16
  redirect,
@@ -20,16 +21,89 @@ import {
20
21
  createHashHistory,
21
22
  createMemoryHistory
22
23
  } from "./history.mjs";
23
- import { jsxDEV } from "react/jsx-dev-runtime";
24
+ import { jsx } from "react/jsx-runtime";
24
25
  var RouterContext = React.createContext(null);
25
26
  var RouterStateContext = React.createContext(null);
26
27
  var OutletContext = React.createContext(null);
27
28
  var MatchContext = React.createContext(null);
28
29
  var MANAGED_HEAD_ATTRIBUTE = "data-richie-router-head";
29
- var EMPTY_HEAD = { meta: [], links: [], scripts: [], styles: [] };
30
+ var EMPTY_HEAD = [];
31
+ function ensureLeadingSlash(value) {
32
+ return value.startsWith("/") ? value : `/${value}`;
33
+ }
34
+ function normalizeBasePath(basePath) {
35
+ if (!basePath) {
36
+ return "";
37
+ }
38
+ const trimmed = basePath.trim();
39
+ if (trimmed === "" || trimmed === "/") {
40
+ return "";
41
+ }
42
+ const normalized = ensureLeadingSlash(trimmed).replace(/\/+$/u, "");
43
+ return normalized === "/" ? "" : normalized;
44
+ }
45
+ function parseHref(href) {
46
+ if (href.startsWith("http://") || href.startsWith("https://")) {
47
+ return new URL(href);
48
+ }
49
+ return new URL(ensureLeadingSlash(href), "http://richie-router.local");
50
+ }
51
+ function stripBasePathFromPathname(pathname, basePath) {
52
+ if (!basePath) {
53
+ return pathname;
54
+ }
55
+ if (pathname === basePath) {
56
+ return "/";
57
+ }
58
+ return pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) || "/" : pathname;
59
+ }
60
+ function stripBasePathFromHref(href, basePath) {
61
+ const normalizedBasePath = normalizeBasePath(basePath);
62
+ if (!normalizedBasePath) {
63
+ return href;
64
+ }
65
+ const url = parseHref(href);
66
+ return `${stripBasePathFromPathname(url.pathname, normalizedBasePath)}${url.search}${url.hash}`;
67
+ }
68
+ function prependBasePathToPathname(pathname, basePath) {
69
+ if (!basePath) {
70
+ return pathname;
71
+ }
72
+ return pathname === "/" ? basePath : `${basePath}${ensureLeadingSlash(pathname)}`;
73
+ }
74
+ function prependBasePathToHref(href, basePath) {
75
+ const normalizedBasePath = normalizeBasePath(basePath);
76
+ if (!normalizedBasePath) {
77
+ return href;
78
+ }
79
+ const url = parseHref(href);
80
+ return `${prependBasePathToPathname(url.pathname, normalizedBasePath)}${url.search}${url.hash}`;
81
+ }
30
82
  function routeHasRecord(value) {
31
83
  return typeof value === "object" && value !== null;
32
84
  }
85
+ function isDeepInclusiveMatch(expected, actual) {
86
+ if (Array.isArray(expected)) {
87
+ if (!Array.isArray(actual) || expected.length !== actual.length) {
88
+ return false;
89
+ }
90
+ return expected.every((value, index) => isDeepInclusiveMatch(value, actual[index]));
91
+ }
92
+ if (routeHasRecord(expected)) {
93
+ if (!routeHasRecord(actual)) {
94
+ return false;
95
+ }
96
+ return Object.entries(expected).every(([key, value]) => (key in actual) && isDeepInclusiveMatch(value, actual[key]));
97
+ }
98
+ return Object.is(expected, actual);
99
+ }
100
+ function routeHasInlineHead(route) {
101
+ const headOption = route.options.head;
102
+ return Boolean(headOption && typeof headOption !== "string");
103
+ }
104
+ function matchesHaveInlineHead(matches) {
105
+ return matches.some((match) => routeHasInlineHead(match.route));
106
+ }
33
107
  function resolveParamsInput(input, previous) {
34
108
  if (input === undefined) {
35
109
  return previous;
@@ -75,6 +149,8 @@ class Router {
75
149
  headCache = new Map;
76
150
  parseSearch;
77
151
  stringifySearch;
152
+ basePath;
153
+ initialHeadSnapshot;
78
154
  started = false;
79
155
  unsubscribeHistory;
80
156
  constructor(options) {
@@ -83,6 +159,7 @@ class Router {
83
159
  this.history = options.history ?? (typeof window === "undefined" ? createMemoryHistory() : createBrowserHistory());
84
160
  this.parseSearch = options.parseSearch ?? defaultParseSearch;
85
161
  this.stringifySearch = options.stringifySearch ?? defaultStringifySearch;
162
+ this.basePath = normalizeBasePath(options.basePath);
86
163
  for (const route of collectRoutes(this.routeTree)) {
87
164
  this.routesByFullPath.set(route.fullPath, route);
88
165
  }
@@ -90,15 +167,22 @@ class Router {
90
167
  this.routesByTo.set(branch.leaf.to, branch.leaf);
91
168
  }
92
169
  const location = this.readLocation();
170
+ const initialMatches = this.buildMatches(location);
171
+ const rawHistoryHref = this.history.location.href;
93
172
  const initialHeadSnapshot = typeof window !== "undefined" ? window.__RICHIE_ROUTER_HEAD__ : undefined;
94
- const initialHead = initialHeadSnapshot && initialHeadSnapshot.href === location.href ? initialHeadSnapshot.head : EMPTY_HEAD;
173
+ const hasMatchingInitialHeadSnapshot = Boolean(initialHeadSnapshot && (initialHeadSnapshot.href === location.href || initialHeadSnapshot.href === rawHistoryHref));
174
+ const initialHead = hasMatchingInitialHeadSnapshot && initialHeadSnapshot ? initialHeadSnapshot.head : EMPTY_HEAD;
175
+ if (hasMatchingInitialHeadSnapshot && this.options.loadRouteHead === undefined && initialHeadSnapshot?.routeHeads !== undefined) {
176
+ this.seedHeadCacheFromRouteHeads(initialMatches, initialHeadSnapshot.routeHeads);
177
+ }
95
178
  if (typeof window !== "undefined" && initialHeadSnapshot !== undefined) {
96
179
  delete window.__RICHIE_ROUTER_HEAD__;
97
180
  }
181
+ this.initialHeadSnapshot = hasMatchingInitialHeadSnapshot ? initialHeadSnapshot : undefined;
98
182
  this.state = {
99
183
  status: "loading",
100
184
  location,
101
- matches: this.buildMatches(location),
185
+ matches: initialMatches,
102
186
  head: initialHead,
103
187
  error: null
104
188
  };
@@ -129,15 +213,17 @@ class Router {
129
213
  }
130
214
  async load(options) {
131
215
  const nextLocation = this.readLocation();
216
+ const initialHeadSnapshot = this.initialHeadSnapshot?.href === nextLocation.href ? this.initialHeadSnapshot : undefined;
217
+ this.initialHeadSnapshot = undefined;
132
218
  await this.commitLocation(nextLocation, {
133
219
  request: options?.request,
134
220
  replace: true,
135
- writeHistory: false
221
+ writeHistory: false,
222
+ initialHeadSnapshot
136
223
  });
137
224
  }
138
225
  async navigate(options) {
139
- const href = this.buildHref(options);
140
- const location = createParsedLocation(href, options.state ?? null, this.parseSearch);
226
+ const location = this.buildLocation(options);
141
227
  await this.commitLocation(location, {
142
228
  replace: options.replace ?? false,
143
229
  writeHistory: true,
@@ -145,8 +231,7 @@ class Router {
145
231
  });
146
232
  }
147
233
  async preloadRoute(options) {
148
- const href = this.buildHref(options);
149
- const location = createParsedLocation(href, options.state ?? null, this.parseSearch);
234
+ const location = this.buildLocation(options);
150
235
  try {
151
236
  await this.resolveLocation(location);
152
237
  } catch {}
@@ -156,6 +241,12 @@ class Router {
156
241
  await this.load();
157
242
  }
158
243
  buildHref(options) {
244
+ return prependBasePathToHref(this.buildLocationHref(options), this.basePath);
245
+ }
246
+ buildLocation(options) {
247
+ return createParsedLocation(this.buildLocationHref(options), options.state ?? null, this.parseSearch);
248
+ }
249
+ buildLocationHref(options) {
159
250
  const targetRoute = this.routesByTo.get(options.to) ?? null;
160
251
  const fromMatch = options.from ? this.findMatchByTo(options.from) : null;
161
252
  const previousParams = fromMatch?.params ?? {};
@@ -170,7 +261,7 @@ class Router {
170
261
  }
171
262
  readLocation() {
172
263
  const location = this.history.location;
173
- return createParsedLocation(location.href, location.state, this.parseSearch);
264
+ return createParsedLocation(stripBasePathFromHref(location.href, this.basePath), location.state, this.parseSearch);
174
265
  }
175
266
  applyTrailingSlash(pathname, route) {
176
267
  const trailingSlash = this.options.trailingSlash ?? "preserve";
@@ -265,29 +356,102 @@ class Router {
265
356
  to: route.to
266
357
  });
267
358
  }
268
- const head = await this.resolveLocationHead(matches, location, options?.request);
359
+ const head = await this.resolveLocationHead(matches, location, options?.request, options?.initialHeadSnapshot);
269
360
  return { matches, head, error: null };
270
361
  }
271
- async resolveLocationHead(matches, location, request) {
362
+ async resolveLocationHead(matches, location, request, initialHeadSnapshot) {
272
363
  const resolvedHeadByRoute = new Map;
273
- for (const match of matches) {
274
- if (!match.route.serverHead) {
364
+ const serverMatches = matches.filter((match) => match.route.serverHead);
365
+ if (serverMatches.length === 0) {
366
+ return resolveHeadConfig(matches, resolvedHeadByRoute);
367
+ }
368
+ if (this.options.loadRouteHead !== undefined) {
369
+ for (const match of serverMatches) {
370
+ resolvedHeadByRoute.set(match.route.fullPath, await this.loadRouteHead(match.route, match.params, match.search, location, request));
371
+ }
372
+ return resolveHeadConfig(matches, resolvedHeadByRoute);
373
+ }
374
+ if (initialHeadSnapshot?.href === location.href && initialHeadSnapshot.routeHeads === undefined && !matchesHaveInlineHead(matches)) {
375
+ return initialHeadSnapshot.head;
376
+ }
377
+ let needsDocumentHeadFetch = false;
378
+ for (const match of serverMatches) {
379
+ const cachedHead = this.getCachedRouteHead(match.route.fullPath, match.params, match.search);
380
+ if (cachedHead) {
381
+ resolvedHeadByRoute.set(match.route.fullPath, cachedHead);
275
382
  continue;
276
383
  }
277
- resolvedHeadByRoute.set(match.route.fullPath, await this.loadRouteHead(match.route, match.params, match.search, location, request));
384
+ needsDocumentHeadFetch = true;
385
+ }
386
+ if (!needsDocumentHeadFetch) {
387
+ return resolveHeadConfig(matches, resolvedHeadByRoute);
388
+ }
389
+ const documentHead = await this.fetchDocumentHead(location);
390
+ if ((documentHead.routeHeads?.length ?? 0) === 0 && !matchesHaveInlineHead(matches)) {
391
+ return documentHead.head;
392
+ }
393
+ const routeHeadsById = this.cacheRouteHeadsFromDocument(matches, documentHead.routeHeads ?? []);
394
+ for (const match of serverMatches) {
395
+ const responseHead = routeHeadsById.get(match.route.fullPath);
396
+ if (responseHead) {
397
+ resolvedHeadByRoute.set(match.route.fullPath, responseHead);
398
+ continue;
399
+ }
400
+ const cachedHead = this.getCachedRouteHead(match.route.fullPath, match.params, match.search);
401
+ if (cachedHead) {
402
+ resolvedHeadByRoute.set(match.route.fullPath, cachedHead);
403
+ continue;
404
+ }
405
+ const response = await this.fetchRouteHead(match.route, match.params, match.search);
406
+ this.setRouteHeadCache(match.route.fullPath, match.params, match.search, response);
407
+ resolvedHeadByRoute.set(match.route.fullPath, response.head);
278
408
  }
279
409
  return resolveHeadConfig(matches, resolvedHeadByRoute);
280
410
  }
281
- async loadRouteHead(route, params, search, location, request) {
282
- const cacheKey = JSON.stringify({
283
- routeId: route.fullPath,
411
+ getRouteHeadCacheKey(routeId, params, search) {
412
+ return JSON.stringify({
413
+ routeId,
284
414
  params,
285
415
  search
286
416
  });
287
- const cached = this.headCache.get(cacheKey);
417
+ }
418
+ getCachedRouteHead(routeId, params, search) {
419
+ const cached = this.headCache.get(this.getRouteHeadCacheKey(routeId, params, search));
288
420
  if (cached && cached.expiresAt > Date.now()) {
289
421
  return cached.head;
290
422
  }
423
+ return null;
424
+ }
425
+ setRouteHeadCache(routeId, params, search, response) {
426
+ this.headCache.set(this.getRouteHeadCacheKey(routeId, params, search), {
427
+ head: response.head,
428
+ expiresAt: Date.now() + (response.staleTime ?? 0)
429
+ });
430
+ }
431
+ seedHeadCacheFromRouteHeads(matches, routeHeads) {
432
+ this.cacheRouteHeadsFromDocument(matches, routeHeads);
433
+ }
434
+ cacheRouteHeadsFromDocument(matches, routeHeads) {
435
+ const routeHeadsById = new Map(routeHeads.map((entry) => [entry.routeId, entry]));
436
+ const resolvedHeadByRoute = new Map;
437
+ for (const match of matches) {
438
+ if (!match.route.serverHead) {
439
+ continue;
440
+ }
441
+ const entry = routeHeadsById.get(match.route.fullPath);
442
+ if (!entry) {
443
+ continue;
444
+ }
445
+ this.setRouteHeadCache(match.route.fullPath, match.params, match.search, entry);
446
+ resolvedHeadByRoute.set(match.route.fullPath, entry.head);
447
+ }
448
+ return resolvedHeadByRoute;
449
+ }
450
+ async loadRouteHead(route, params, search, location, request) {
451
+ const cachedHead = this.getCachedRouteHead(route.fullPath, params, search);
452
+ if (cachedHead) {
453
+ return cachedHead;
454
+ }
291
455
  const response = this.options.loadRouteHead !== undefined ? await this.options.loadRouteHead({
292
456
  route,
293
457
  routeId: route.fullPath,
@@ -296,14 +460,11 @@ class Router {
296
460
  location,
297
461
  request
298
462
  }) : await this.fetchRouteHead(route, params, search);
299
- this.headCache.set(cacheKey, {
300
- head: response.head,
301
- expiresAt: Date.now() + (response.staleTime ?? 0)
302
- });
463
+ this.setRouteHeadCache(route.fullPath, params, search, response);
303
464
  return response.head;
304
465
  }
305
466
  async fetchRouteHead(route, params, search) {
306
- const basePath = this.options.headBasePath ?? "/head-api";
467
+ const basePath = this.options.headBasePath ?? prependBasePathToHref("/head-api", this.basePath);
307
468
  const searchParams = new URLSearchParams({
308
469
  routeId: route.fullPath,
309
470
  params: JSON.stringify(params),
@@ -318,6 +479,20 @@ class Router {
318
479
  }
319
480
  return await response.json();
320
481
  }
482
+ async fetchDocumentHead(location) {
483
+ const basePath = this.options.headBasePath ?? prependBasePathToHref("/head-api", this.basePath);
484
+ const searchParams = new URLSearchParams({
485
+ href: prependBasePathToHref(location.href, this.basePath)
486
+ });
487
+ const response = await fetch(`${basePath}?${searchParams.toString()}`);
488
+ if (!response.ok) {
489
+ if (response.status === 404) {
490
+ throw notFound();
491
+ }
492
+ throw new Error(`Failed to resolve server head for location "${location.href}"`);
493
+ }
494
+ return await response.json();
495
+ }
321
496
  async commitLocation(location, options) {
322
497
  this.state = {
323
498
  ...this.state,
@@ -327,13 +502,15 @@ class Router {
327
502
  this.notify();
328
503
  try {
329
504
  const resolved = await this.resolveLocation(location, {
330
- request: options.request
505
+ request: options.request,
506
+ initialHeadSnapshot: options.initialHeadSnapshot
331
507
  });
508
+ const historyHref = prependBasePathToHref(location.href, this.basePath);
332
509
  if (options.writeHistory) {
333
510
  if (options.replace) {
334
- this.history.replace(location.href, location.state);
511
+ this.history.replace(historyHref, location.state);
335
512
  } else {
336
- this.history.push(location.href, location.state);
513
+ this.history.push(historyHref, location.state);
337
514
  }
338
515
  }
339
516
  this.state = {
@@ -354,11 +531,12 @@ class Router {
354
531
  return;
355
532
  }
356
533
  const errorMatches = this.buildMatches(location);
534
+ const historyHref = prependBasePathToHref(location.href, this.basePath);
357
535
  if (options.writeHistory) {
358
536
  if (options.replace) {
359
- this.history.replace(location.href, location.state);
537
+ this.history.replace(historyHref, location.state);
360
538
  } else {
361
- this.history.push(location.href, location.state);
539
+ this.history.push(historyHref, location.state);
362
540
  }
363
541
  }
364
542
  this.state = {
@@ -448,61 +626,95 @@ function createManagedHeadElements(head) {
448
626
  element.setAttribute(MANAGED_HEAD_ATTRIBUTE, "true");
449
627
  return element;
450
628
  };
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);
629
+ const setAttributes = (element, attributes) => {
630
+ for (const [key, value] of Object.entries(attributes)) {
631
+ if (value === undefined || value === false) {
632
+ continue;
633
+ }
634
+ if (value === true) {
635
+ element.setAttribute(key, "");
636
+ continue;
637
+ }
638
+ element.setAttribute(key, value);
639
+ }
640
+ };
641
+ for (const element of head) {
642
+ if (element.tag === "title") {
456
643
  continue;
457
644
  }
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);
645
+ if (element.tag === "meta") {
646
+ const tag2 = managed(document.createElement("meta"));
647
+ if ("charset" in element) {
648
+ tag2.setAttribute("charset", element.charset);
649
+ } else if ("name" in element) {
650
+ tag2.setAttribute("name", element.name);
651
+ tag2.setAttribute("content", element.content);
652
+ } else if ("property" in element) {
653
+ tag2.setAttribute("property", element.property);
654
+ tag2.setAttribute("content", element.content);
655
+ } else {
656
+ tag2.setAttribute("http-equiv", element.httpEquiv);
657
+ tag2.setAttribute("content", element.content);
658
+ }
659
+ elements.push(tag2);
660
+ continue;
661
+ }
662
+ if (element.tag === "link") {
663
+ const tag2 = managed(document.createElement("link"));
664
+ setAttributes(tag2, {
665
+ rel: element.rel,
666
+ href: element.href,
667
+ type: element.type,
668
+ media: element.media,
669
+ sizes: element.sizes,
670
+ crossorigin: element.crossorigin
671
+ });
672
+ elements.push(tag2);
673
+ continue;
674
+ }
675
+ if (element.tag === "style") {
676
+ const tag2 = managed(document.createElement("style"));
677
+ if (element.media) {
678
+ tag2.setAttribute("media", element.media);
679
+ }
680
+ tag2.textContent = element.children;
681
+ elements.push(tag2);
682
+ continue;
683
+ }
684
+ if (element.tag === "script") {
685
+ const tag2 = managed(document.createElement("script"));
686
+ if (element.src) {
687
+ tag2.setAttribute("src", element.src);
688
+ }
689
+ if (element.type) {
690
+ tag2.setAttribute("type", element.type);
691
+ }
692
+ if (element.async) {
693
+ tag2.async = true;
694
+ }
695
+ if (element.defer) {
696
+ tag2.defer = true;
697
+ }
698
+ if (element.children) {
699
+ tag2.textContent = element.children;
700
+ }
701
+ elements.push(tag2);
702
+ continue;
703
+ }
704
+ if (element.tag === "base") {
705
+ const tag2 = managed(document.createElement("base"));
706
+ tag2.setAttribute("href", element.href);
707
+ if (element.target) {
708
+ tag2.setAttribute("target", element.target);
709
+ }
710
+ elements.push(tag2);
711
+ continue;
712
+ }
713
+ const tag = managed(document.createElement(element.name));
714
+ setAttributes(tag, element.attrs ?? {});
715
+ if (element.children) {
716
+ tag.textContent = element.children;
470
717
  }
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
718
  elements.push(tag);
507
719
  }
508
720
  return elements;
@@ -511,6 +723,10 @@ function reconcileDocumentHead(head) {
511
723
  if (typeof document === "undefined") {
512
724
  return;
513
725
  }
726
+ const title = [...head].reverse().find((element) => element.tag === "title");
727
+ if (title && title.tag === "title") {
728
+ document.title = title.children;
729
+ }
514
730
  for (const element of Array.from(document.head.querySelectorAll(`[${MANAGED_HEAD_ATTRIBUTE}]`))) {
515
731
  element.remove();
516
732
  }
@@ -525,17 +741,17 @@ function RenderMatches({ matches, index }) {
525
741
  return null;
526
742
  }
527
743
  const Component = match.route.options.component;
528
- const outlet = /* @__PURE__ */ jsxDEV(RenderMatches, {
744
+ const outlet = /* @__PURE__ */ jsx(RenderMatches, {
529
745
  matches,
530
746
  index: index + 1
531
- }, undefined, false, undefined, this);
532
- return /* @__PURE__ */ jsxDEV(MatchContext.Provider, {
747
+ });
748
+ return /* @__PURE__ */ jsx(MatchContext.Provider, {
533
749
  value: match,
534
- children: /* @__PURE__ */ jsxDEV(OutletContext.Provider, {
750
+ children: /* @__PURE__ */ jsx(OutletContext.Provider, {
535
751
  value: outlet,
536
- children: Component ? /* @__PURE__ */ jsxDEV(Component, {}, undefined, false, undefined, this) : outlet
537
- }, undefined, false, undefined, this)
538
- }, undefined, false, undefined, this);
752
+ children: Component ? /* @__PURE__ */ jsx(Component, {}) : outlet
753
+ })
754
+ });
539
755
  }
540
756
  function renderError(error, matches, router) {
541
757
  const reversed = [...matches].reverse();
@@ -544,9 +760,9 @@ function renderError(error, matches, router) {
544
760
  if (NotFoundComponent) {
545
761
  return React.createElement(NotFoundComponent);
546
762
  }
547
- return /* @__PURE__ */ jsxDEV("div", {
763
+ return /* @__PURE__ */ jsx("div", {
548
764
  children: "Not Found"
549
- }, undefined, false, undefined, this);
765
+ });
550
766
  }
551
767
  const ErrorComponent = reversed.find((match) => match.route.options.errorComponent)?.route.options.errorComponent ?? router.options.defaultErrorComponent;
552
768
  if (ErrorComponent) {
@@ -557,9 +773,9 @@ function renderError(error, matches, router) {
557
773
  }
558
774
  });
559
775
  }
560
- return /* @__PURE__ */ jsxDEV("pre", {
776
+ return /* @__PURE__ */ jsx("pre", {
561
777
  children: error instanceof Error ? error.message : "Unknown routing error"
562
- }, undefined, false, undefined, this);
778
+ });
563
779
  }
564
780
  function RouterProvider({ router }) {
565
781
  const snapshot = React.useSyncExternalStore(router.subscribe, router.getSnapshot, router.getSnapshot);
@@ -572,17 +788,17 @@ function RouterProvider({ router }) {
572
788
  React.useEffect(() => {
573
789
  reconcileDocumentHead(snapshot.head);
574
790
  }, [snapshot.head]);
575
- const content = snapshot.error ? renderError(snapshot.error, snapshot.matches, router) : /* @__PURE__ */ jsxDEV(RenderMatches, {
791
+ const content = snapshot.error ? renderError(snapshot.error, snapshot.matches, router) : /* @__PURE__ */ jsx(RenderMatches, {
576
792
  matches: snapshot.matches,
577
793
  index: 0
578
- }, undefined, false, undefined, this);
579
- return /* @__PURE__ */ jsxDEV(RouterContext.Provider, {
794
+ });
795
+ return /* @__PURE__ */ jsx(RouterContext.Provider, {
580
796
  value: router,
581
- children: /* @__PURE__ */ jsxDEV(RouterStateContext.Provider, {
797
+ children: /* @__PURE__ */ jsx(RouterStateContext.Provider, {
582
798
  value: snapshot,
583
799
  children: content
584
- }, undefined, false, undefined, this)
585
- }, undefined, false, undefined, this);
800
+ })
801
+ });
586
802
  }
587
803
  function Outlet() {
588
804
  return React.useContext(OutletContext);
@@ -608,6 +824,31 @@ function useNavigate() {
608
824
  await router.navigate(options);
609
825
  }, [router]);
610
826
  }
827
+ function useMatchRoute() {
828
+ const location = useLocation();
829
+ return React.useCallback((options) => {
830
+ const matched = matchPathname(options.to, location.pathname, {
831
+ partial: options.fuzzy === true
832
+ });
833
+ if (!matched) {
834
+ return false;
835
+ }
836
+ if (options.params) {
837
+ for (const [key, value] of Object.entries(options.params)) {
838
+ if (value !== undefined && matched.params[key] !== value) {
839
+ return false;
840
+ }
841
+ }
842
+ }
843
+ if (options.includeSearch) {
844
+ const expectedSearch = options.search ?? {};
845
+ if (!isDeepInclusiveMatch(expectedSearch, location.search)) {
846
+ return false;
847
+ }
848
+ }
849
+ return matched.params;
850
+ }, [location.pathname, location.search]);
851
+ }
611
852
  function useLocation() {
612
853
  return useRouterStateContext().location;
613
854
  }
@@ -651,12 +892,14 @@ function useElementScrollRestoration() {
651
892
  ref: () => {}
652
893
  };
653
894
  }
654
- function useResolvedLink(props) {
895
+ function useResolvedLink(props, activeOptions) {
655
896
  const router = useRouterContext();
656
897
  const href = router.buildHref(props);
657
898
  const location = useLocation();
658
- const pathOnly = href.split(/[?#]/u)[0] ?? href;
659
- const isActive = pathOnly === location.pathname;
899
+ const targetPathname = stripBasePathFromHref(href, router.options.basePath).split(/[?#]/u)[0] ?? href;
900
+ const isActive = matchPathname(targetPathname, location.pathname, {
901
+ partial: activeOptions?.exact !== true
902
+ }) !== null;
660
903
  return { href, isActive, router };
661
904
  }
662
905
  var LinkComponent = React.forwardRef(function LinkInner(props, ref) {
@@ -672,6 +915,7 @@ var LinkComponent = React.forwardRef(function LinkInner(props, ref) {
672
915
  state,
673
916
  mask,
674
917
  ignoreBlocker,
918
+ activeOptions,
675
919
  activeProps,
676
920
  children,
677
921
  onClick,
@@ -692,7 +936,7 @@ var LinkComponent = React.forwardRef(function LinkInner(props, ref) {
692
936
  mask,
693
937
  ignoreBlocker
694
938
  };
695
- const { href, isActive, router } = useResolvedLink(navigation);
939
+ const { href, isActive, router } = useResolvedLink(navigation, activeOptions);
696
940
  const preloadMode = preload ?? router.options.defaultPreload;
697
941
  const preloadDelay = router.options.defaultPreloadDelay ?? 50;
698
942
  const preloadTimeout = React.useRef(null);
@@ -717,7 +961,7 @@ var LinkComponent = React.forwardRef(function LinkInner(props, ref) {
717
961
  }
718
962
  }, []);
719
963
  const renderedChildren = typeof children === "function" ? children({ isActive }) : children;
720
- return /* @__PURE__ */ jsxDEV("a", {
964
+ return /* @__PURE__ */ jsx("a", {
721
965
  ...anchorProps,
722
966
  ...isActive ? activeProps : undefined,
723
967
  ref,
@@ -745,7 +989,7 @@ var LinkComponent = React.forwardRef(function LinkInner(props, ref) {
745
989
  },
746
990
  onBlur: cancelPreload,
747
991
  children: renderedChildren
748
- }, undefined, false, undefined, this);
992
+ });
749
993
  });
750
994
  var Link = LinkComponent;
751
995
  function createLink(Component) {
@@ -762,6 +1006,7 @@ function createLink(Component) {
762
1006
  state,
763
1007
  mask,
764
1008
  ignoreBlocker,
1009
+ activeOptions,
765
1010
  activeProps,
766
1011
  children,
767
1012
  preload,
@@ -779,7 +1024,7 @@ function createLink(Component) {
779
1024
  mask,
780
1025
  ignoreBlocker
781
1026
  };
782
- const { href, isActive, router } = useResolvedLink(navigation);
1027
+ const { href, isActive, router } = useResolvedLink(navigation, activeOptions);
783
1028
  const renderedChildren = typeof children === "function" ? children({ isActive }) : children;
784
1029
  React.useEffect(() => {
785
1030
  if (preload !== "render") {
@@ -787,12 +1032,12 @@ function createLink(Component) {
787
1032
  }
788
1033
  router.preloadRoute(navigation);
789
1034
  }, [navigation, preload, router]);
790
- return /* @__PURE__ */ jsxDEV(Component, {
1035
+ return /* @__PURE__ */ jsx(Component, {
791
1036
  ...componentProps,
792
1037
  ...isActive ? activeProps : undefined,
793
1038
  href,
794
1039
  children: renderedChildren
795
- }, undefined, false, undefined, this);
1040
+ });
796
1041
  };
797
1042
  }
798
1043
  function linkOptions(options) {
@@ -815,6 +1060,7 @@ export {
815
1060
  useParams,
816
1061
  useNavigate,
817
1062
  useMatches,
1063
+ useMatchRoute,
818
1064
  useMatch,
819
1065
  useLocation,
820
1066
  useElementScrollRestoration,