@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.
@@ -0,0 +1,574 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ function __accessProp(key) {
7
+ return this[key];
8
+ }
9
+ var __toESMCache_node;
10
+ var __toESMCache_esm;
11
+ var __toESM = (mod, isNodeMode, target) => {
12
+ var canCache = mod != null && typeof mod === "object";
13
+ if (canCache) {
14
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
15
+ var cached = cache.get(mod);
16
+ if (cached)
17
+ return cached;
18
+ }
19
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
20
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
21
+ for (let key of __getOwnPropNames(mod))
22
+ if (!__hasOwnProp.call(to, key))
23
+ __defProp(to, key, {
24
+ get: __accessProp.bind(mod, key),
25
+ enumerable: true
26
+ });
27
+ if (canCache)
28
+ cache.set(mod, to);
29
+ return to;
30
+ };
31
+
32
+ // packages/react/src/router.test.ts
33
+ var import_bun_test = require("bun:test");
34
+ var import_react = __toESM(require("react"));
35
+ var import_server = require("react-dom/server");
36
+ var import_router = require("./router.cjs");
37
+ function createTestRouteTree(options) {
38
+ const rootRoute = import_router.createRootRoute({
39
+ component: () => null
40
+ });
41
+ const indexRoute = import_router.createFileRoute("/")({
42
+ component: () => null
43
+ });
44
+ const aboutRoute = import_router.createFileRoute("/about")({
45
+ component: () => null
46
+ });
47
+ aboutRoute._setServerHead(options?.serverHead);
48
+ return rootRoute._addFileChildren({
49
+ index: indexRoute,
50
+ about: aboutRoute
51
+ });
52
+ }
53
+ function createNestedHeadRouteTree() {
54
+ const rootRoute = import_router.createRootRoute({
55
+ component: () => null
56
+ });
57
+ const postsRoute = import_router.createFileRoute("/posts")({
58
+ component: () => null,
59
+ head: [
60
+ { tag: "title", children: "Posts" }
61
+ ]
62
+ });
63
+ const postRoute = import_router.createFileRoute("/posts/$postId")({
64
+ component: () => null
65
+ });
66
+ rootRoute._setServerHead(true);
67
+ postRoute._setServerHead(true);
68
+ postsRoute._addFileChildren({
69
+ post: postRoute
70
+ });
71
+ return rootRoute._addFileChildren({
72
+ posts: postsRoute
73
+ });
74
+ }
75
+ function createRootServerHeadTree() {
76
+ const rootRoute = import_router.createRootRoute({
77
+ component: () => null
78
+ });
79
+ const indexRoute = import_router.createFileRoute("/")({
80
+ component: () => null
81
+ });
82
+ const aboutRoute = import_router.createFileRoute("/about")({
83
+ component: () => null
84
+ });
85
+ rootRoute._setServerHead(true);
86
+ return rootRoute._addFileChildren({
87
+ index: indexRoute,
88
+ about: aboutRoute
89
+ });
90
+ }
91
+ function createNestedServerHeadTree() {
92
+ const rootRoute = import_router.createRootRoute({
93
+ component: () => null
94
+ });
95
+ const postsRoute = import_router.createFileRoute("/posts")({
96
+ component: () => null
97
+ });
98
+ const postRoute = import_router.createFileRoute("/posts/$postId")({
99
+ component: () => null
100
+ });
101
+ rootRoute._setServerHead(true);
102
+ postsRoute._setServerHead(true);
103
+ postRoute._setServerHead(true);
104
+ postsRoute._addFileChildren({
105
+ post: postRoute
106
+ });
107
+ return rootRoute._addFileChildren({
108
+ posts: postsRoute
109
+ });
110
+ }
111
+ function createLinkTestRouteTree(component) {
112
+ const rootRoute = import_router.createRootRoute({
113
+ component
114
+ });
115
+ const postRoute = import_router.createFileRoute("/post")({
116
+ component: () => null
117
+ });
118
+ const postsRoute = import_router.createFileRoute("/posts")({
119
+ component: () => null
120
+ });
121
+ const postDetailRoute = import_router.createFileRoute("/posts/$postId")({
122
+ component: () => null
123
+ });
124
+ postsRoute._addFileChildren({
125
+ detail: postDetailRoute
126
+ });
127
+ return rootRoute._addFileChildren({
128
+ post: postRoute,
129
+ posts: postsRoute
130
+ });
131
+ }
132
+ function renderLinkMarkup(initialEntry, component) {
133
+ const history = import_router.createMemoryHistory({
134
+ initialEntries: [initialEntry]
135
+ });
136
+ const router = import_router.createRouter({
137
+ routeTree: createLinkTestRouteTree(component),
138
+ history
139
+ });
140
+ return import_server.renderToStaticMarkup(import_react.default.createElement(import_router.RouterProvider, { router }));
141
+ }
142
+ import_bun_test.describe("createRouter basePath", () => {
143
+ import_bun_test.test('treats "/" as the root basePath', () => {
144
+ const history = import_router.createMemoryHistory({
145
+ initialEntries: ["/about?tab=team#bio"]
146
+ });
147
+ const router = import_router.createRouter({
148
+ routeTree: createTestRouteTree(),
149
+ history,
150
+ basePath: "/"
151
+ });
152
+ import_bun_test.expect(router.state.location.pathname).toBe("/about");
153
+ import_bun_test.expect(router.state.location.href).toBe("/about?tab=team#bio");
154
+ import_bun_test.expect(router.buildHref({ to: "/about" })).toBe("/about");
155
+ });
156
+ import_bun_test.test("normalizes a trailing slash in the basePath", async () => {
157
+ const history = import_router.createMemoryHistory({
158
+ initialEntries: ["/project/about?tab=team#bio"]
159
+ });
160
+ const router = import_router.createRouter({
161
+ routeTree: createTestRouteTree(),
162
+ history,
163
+ basePath: "/project/"
164
+ });
165
+ import_bun_test.expect(router.state.location.pathname).toBe("/about");
166
+ import_bun_test.expect(router.buildHref({ to: "/about" })).toBe("/project/about");
167
+ await router.navigate({
168
+ to: "/about"
169
+ });
170
+ import_bun_test.expect(history.location.href).toBe("/project/about");
171
+ });
172
+ import_bun_test.test("strips the basePath from the current history location", () => {
173
+ const history = import_router.createMemoryHistory({
174
+ initialEntries: ["/project/about?tab=team#bio"]
175
+ });
176
+ const router = import_router.createRouter({
177
+ routeTree: createTestRouteTree(),
178
+ history,
179
+ basePath: "/project"
180
+ });
181
+ import_bun_test.expect(router.state.location.pathname).toBe("/about");
182
+ import_bun_test.expect(router.state.location.href).toBe("/about?tab=team#bio");
183
+ import_bun_test.expect(router.state.matches.at(-1)?.route.fullPath).toBe("/about");
184
+ });
185
+ import_bun_test.test("prefixes generated hrefs and history writes with the basePath", async () => {
186
+ const history = import_router.createMemoryHistory({
187
+ initialEntries: ["/project"]
188
+ });
189
+ const router = import_router.createRouter({
190
+ routeTree: createTestRouteTree(),
191
+ history,
192
+ basePath: "/project"
193
+ });
194
+ import_bun_test.expect(router.buildHref({ to: "/about" })).toBe("/project/about");
195
+ await router.navigate({
196
+ to: "/about",
197
+ search: {
198
+ tab: "team"
199
+ }
200
+ });
201
+ import_bun_test.expect(history.location.href).toBe("/project/about?tab=team");
202
+ import_bun_test.expect(router.state.location.href).toBe("/about?tab=team");
203
+ });
204
+ import_bun_test.test("uses the basePath for the default head API endpoint", async () => {
205
+ const history = import_router.createMemoryHistory({
206
+ initialEntries: ["/project/about"]
207
+ });
208
+ const fetchCalls = [];
209
+ const originalFetch = globalThis.fetch;
210
+ globalThis.fetch = async (input) => {
211
+ fetchCalls.push(typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url);
212
+ return new Response(JSON.stringify({
213
+ head: [],
214
+ routeHeads: [
215
+ { routeId: "/about", head: [] }
216
+ ]
217
+ }), {
218
+ status: 200,
219
+ headers: {
220
+ "content-type": "application/json"
221
+ }
222
+ });
223
+ };
224
+ try {
225
+ const router = import_router.createRouter({
226
+ routeTree: createTestRouteTree({ serverHead: true }),
227
+ history,
228
+ basePath: "/project"
229
+ });
230
+ await router.load();
231
+ import_bun_test.expect(fetchCalls).toHaveLength(1);
232
+ import_bun_test.expect(fetchCalls[0]).toBe("/project/head-api?href=%2Fproject%2Fabout");
233
+ } finally {
234
+ globalThis.fetch = originalFetch;
235
+ }
236
+ });
237
+ import_bun_test.test("uses one document head request and preserves inline head precedence", async () => {
238
+ const history = import_router.createMemoryHistory({
239
+ initialEntries: ["/posts/alpha"]
240
+ });
241
+ const fetchCalls = [];
242
+ const originalFetch = globalThis.fetch;
243
+ globalThis.fetch = async (input) => {
244
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
245
+ fetchCalls.push(url);
246
+ return new Response(JSON.stringify({
247
+ head: [
248
+ { tag: "title", children: "Site" },
249
+ { tag: "title", children: "Alpha" }
250
+ ],
251
+ routeHeads: [
252
+ {
253
+ routeId: "__root__",
254
+ head: [{ tag: "title", children: "Site" }],
255
+ staleTime: 60000
256
+ },
257
+ {
258
+ routeId: "/posts/$postId",
259
+ head: [{ tag: "title", children: "Alpha" }],
260
+ staleTime: 1e4
261
+ }
262
+ ],
263
+ staleTime: 1e4
264
+ }), {
265
+ status: 200,
266
+ headers: {
267
+ "content-type": "application/json"
268
+ }
269
+ });
270
+ };
271
+ try {
272
+ const router = import_router.createRouter({
273
+ routeTree: createNestedHeadRouteTree(),
274
+ history
275
+ });
276
+ await router.load();
277
+ import_bun_test.expect(fetchCalls).toEqual(["/head-api?href=%2Fposts%2Falpha"]);
278
+ import_bun_test.expect(router.state.head).toEqual([
279
+ { tag: "title", children: "Alpha" }
280
+ ]);
281
+ } finally {
282
+ globalThis.fetch = originalFetch;
283
+ }
284
+ });
285
+ import_bun_test.test("reuses a fresh root server head across navigations without refetching", async () => {
286
+ const history = import_router.createMemoryHistory({
287
+ initialEntries: ["/"]
288
+ });
289
+ const fetchCalls = [];
290
+ const originalFetch = globalThis.fetch;
291
+ globalThis.fetch = async (input) => {
292
+ fetchCalls.push(typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url);
293
+ return new Response(JSON.stringify({
294
+ head: [
295
+ { tag: "title", children: "Site" }
296
+ ],
297
+ routeHeads: [
298
+ {
299
+ routeId: "__root__",
300
+ head: [{ tag: "title", children: "Site" }],
301
+ staleTime: 60000
302
+ }
303
+ ],
304
+ staleTime: 60000
305
+ }), {
306
+ status: 200,
307
+ headers: {
308
+ "content-type": "application/json"
309
+ }
310
+ });
311
+ };
312
+ try {
313
+ const router = import_router.createRouter({
314
+ routeTree: createRootServerHeadTree(),
315
+ history
316
+ });
317
+ await router.load();
318
+ await router.navigate({
319
+ to: "/about"
320
+ });
321
+ import_bun_test.expect(fetchCalls).toEqual(["/head-api?href=%2F"]);
322
+ } finally {
323
+ globalThis.fetch = originalFetch;
324
+ }
325
+ });
326
+ import_bun_test.test("seeds the route head cache from the dehydrated snapshot", async () => {
327
+ const history = import_router.createMemoryHistory({
328
+ initialEntries: ["/about"]
329
+ });
330
+ const fetchCalls = [];
331
+ const originalFetch = globalThis.fetch;
332
+ const globalWithWindow = globalThis;
333
+ const originalWindow = globalWithWindow.window;
334
+ globalThis.fetch = async (input) => {
335
+ fetchCalls.push(typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url);
336
+ return new Response(JSON.stringify({
337
+ head: [
338
+ { tag: "title", children: "Site" }
339
+ ],
340
+ routeHeads: [
341
+ {
342
+ routeId: "__root__",
343
+ head: [{ tag: "title", children: "Site" }],
344
+ staleTime: 60000
345
+ }
346
+ ],
347
+ staleTime: 60000
348
+ }), {
349
+ status: 200,
350
+ headers: {
351
+ "content-type": "application/json"
352
+ }
353
+ });
354
+ };
355
+ globalWithWindow.window = {
356
+ __RICHIE_ROUTER_HEAD__: {
357
+ href: "/about",
358
+ head: [
359
+ { tag: "title", children: "Site" }
360
+ ],
361
+ routeHeads: [
362
+ {
363
+ routeId: "__root__",
364
+ head: [{ tag: "title", children: "Site" }],
365
+ staleTime: 60000
366
+ }
367
+ ]
368
+ }
369
+ };
370
+ try {
371
+ const router = import_router.createRouter({
372
+ routeTree: createRootServerHeadTree(),
373
+ history
374
+ });
375
+ await router.load();
376
+ import_bun_test.expect(fetchCalls).toHaveLength(0);
377
+ import_bun_test.expect(router.state.head).toEqual([
378
+ { tag: "title", children: "Site" }
379
+ ]);
380
+ } finally {
381
+ globalThis.fetch = originalFetch;
382
+ if (originalWindow === undefined) {
383
+ Reflect.deleteProperty(globalWithWindow, "window");
384
+ } else {
385
+ originalWindow.__RICHIE_ROUTER_HEAD__ = undefined;
386
+ globalWithWindow.window = originalWindow;
387
+ }
388
+ }
389
+ });
390
+ import_bun_test.test("reuses the initial merged head snapshot without fetching when the branch has no inline head", async () => {
391
+ const history = import_router.createMemoryHistory({
392
+ initialEntries: ["/about"]
393
+ });
394
+ const fetchCalls = [];
395
+ const originalFetch = globalThis.fetch;
396
+ const globalWithWindow = globalThis;
397
+ const originalWindow = globalWithWindow.window;
398
+ globalThis.fetch = async (input) => {
399
+ fetchCalls.push(typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url);
400
+ return new Response("{}", {
401
+ status: 500
402
+ });
403
+ };
404
+ globalWithWindow.window = {
405
+ __RICHIE_ROUTER_HEAD__: {
406
+ href: "/about",
407
+ head: [
408
+ { tag: "title", children: "About from SSR" }
409
+ ]
410
+ }
411
+ };
412
+ try {
413
+ const router = import_router.createRouter({
414
+ routeTree: createTestRouteTree({ serverHead: true }),
415
+ history
416
+ });
417
+ await router.load();
418
+ import_bun_test.expect(fetchCalls).toHaveLength(0);
419
+ import_bun_test.expect(router.state.head).toEqual([
420
+ { tag: "title", children: "About from SSR" }
421
+ ]);
422
+ } finally {
423
+ globalThis.fetch = originalFetch;
424
+ if (originalWindow === undefined) {
425
+ Reflect.deleteProperty(globalWithWindow, "window");
426
+ } else {
427
+ originalWindow.__RICHIE_ROUTER_HEAD__ = undefined;
428
+ globalWithWindow.window = originalWindow;
429
+ }
430
+ }
431
+ });
432
+ import_bun_test.test("uses the merged document head without route fallback requests when the branch has no inline head", async () => {
433
+ const history = import_router.createMemoryHistory({
434
+ initialEntries: ["/posts/alpha"]
435
+ });
436
+ const fetchCalls = [];
437
+ const originalFetch = globalThis.fetch;
438
+ globalThis.fetch = async (input) => {
439
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
440
+ fetchCalls.push(url);
441
+ return new Response(JSON.stringify({
442
+ head: [
443
+ { tag: "meta", name: "description", content: "Nested server head" },
444
+ { tag: "title", children: "Alpha" }
445
+ ],
446
+ staleTime: 1e4
447
+ }), {
448
+ status: 200,
449
+ headers: {
450
+ "content-type": "application/json"
451
+ }
452
+ });
453
+ };
454
+ try {
455
+ const router = import_router.createRouter({
456
+ routeTree: createNestedServerHeadTree(),
457
+ history
458
+ });
459
+ await router.load();
460
+ import_bun_test.expect(fetchCalls).toEqual(["/head-api?href=%2Fposts%2Falpha"]);
461
+ import_bun_test.expect(router.state.head).toEqual([
462
+ { tag: "meta", name: "description", content: "Nested server head" },
463
+ { tag: "title", children: "Alpha" }
464
+ ]);
465
+ } finally {
466
+ globalThis.fetch = originalFetch;
467
+ }
468
+ });
469
+ import_bun_test.test("keeps loadRouteHead as the route-scoped override path", async () => {
470
+ const history = import_router.createMemoryHistory({
471
+ initialEntries: ["/about"]
472
+ });
473
+ const fetchCalls = [];
474
+ const loadRouteHeadCalls = [];
475
+ const originalFetch = globalThis.fetch;
476
+ globalThis.fetch = async (input) => {
477
+ fetchCalls.push(typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url);
478
+ return new Response("{}", {
479
+ status: 500
480
+ });
481
+ };
482
+ try {
483
+ const router = import_router.createRouter({
484
+ routeTree: createTestRouteTree({ serverHead: true }),
485
+ history,
486
+ loadRouteHead: async ({ routeId }) => {
487
+ loadRouteHeadCalls.push(routeId);
488
+ return {
489
+ head: [
490
+ { tag: "title", children: "About override" }
491
+ ],
492
+ staleTime: 1000
493
+ };
494
+ }
495
+ });
496
+ await router.load();
497
+ import_bun_test.expect(loadRouteHeadCalls).toEqual(["/about"]);
498
+ import_bun_test.expect(fetchCalls).toHaveLength(0);
499
+ import_bun_test.expect(router.state.head).toEqual([
500
+ { tag: "title", children: "About override" }
501
+ ]);
502
+ } finally {
503
+ globalThis.fetch = originalFetch;
504
+ }
505
+ });
506
+ });
507
+ import_bun_test.describe("Link active state", () => {
508
+ import_bun_test.test("keeps parent links active on child routes by default", () => {
509
+ const TestLink = import_router.Link;
510
+ const markup = renderLinkMarkup("/posts/alpha", () => import_react.default.createElement(TestLink, { to: "/posts", activeProps: { className: "active" } }, "Posts"));
511
+ import_bun_test.expect(markup).toContain('class="active"');
512
+ });
513
+ import_bun_test.test("supports exact-only active matching", () => {
514
+ const TestLink = import_router.Link;
515
+ const markup = renderLinkMarkup("/posts/alpha", () => import_react.default.createElement(TestLink, {
516
+ to: "/posts",
517
+ activeOptions: { exact: true },
518
+ activeProps: { className: "active" }
519
+ }, "Posts"));
520
+ import_bun_test.expect(markup).not.toContain('class="active"');
521
+ });
522
+ import_bun_test.test("matches path segments instead of raw string prefixes", () => {
523
+ const TestLink = import_router.Link;
524
+ const markup = renderLinkMarkup("/posts/alpha", () => import_react.default.createElement(TestLink, { to: "/post", activeProps: { className: "active" } }, "Post"));
525
+ import_bun_test.expect(markup).not.toContain('class="active"');
526
+ });
527
+ import_bun_test.test("applies activeOptions in custom links created with createLink", () => {
528
+ const AppLink = import_router.createLink((props) => import_react.default.createElement("a", props));
529
+ const markup = renderLinkMarkup("/posts/alpha", () => import_react.default.createElement(AppLink, {
530
+ to: "/posts",
531
+ activeOptions: { exact: true },
532
+ activeProps: { className: "active" }
533
+ }, "Posts"));
534
+ import_bun_test.expect(markup).not.toContain('class="active"');
535
+ });
536
+ });
537
+ import_bun_test.describe("useMatchRoute", () => {
538
+ import_bun_test.test("returns matched params for exact matches", () => {
539
+ const markup = renderLinkMarkup("/posts/alpha", () => {
540
+ const matchRoute = import_router.useMatchRoute();
541
+ const match = matchRoute({ to: "/posts/$postId" });
542
+ return import_react.default.createElement("pre", null, match === false ? "false" : JSON.stringify(match));
543
+ });
544
+ import_bun_test.expect(markup).toContain("{"postId":"alpha"}");
545
+ });
546
+ import_bun_test.test("supports fuzzy parent matching", () => {
547
+ const markup = renderLinkMarkup("/posts/alpha", () => {
548
+ const matchRoute = import_router.useMatchRoute();
549
+ const match = matchRoute({ to: "/posts", fuzzy: true });
550
+ return import_react.default.createElement("pre", null, match === false ? "false" : JSON.stringify(match));
551
+ });
552
+ import_bun_test.expect(markup).toContain("{}");
553
+ });
554
+ import_bun_test.test("supports partial param filters", () => {
555
+ const markup = renderLinkMarkup("/posts/alpha", () => {
556
+ const matchRoute = import_router.useMatchRoute();
557
+ const match = matchRoute({ to: "/posts/$postId", params: { postId: "beta" } });
558
+ return import_react.default.createElement("pre", null, match === false ? "false" : JSON.stringify(match));
559
+ });
560
+ import_bun_test.expect(markup).toContain("false");
561
+ });
562
+ import_bun_test.test("can include search params in the match", () => {
563
+ const markup = renderLinkMarkup("/posts/alpha?tab=details&count=2", () => {
564
+ const matchRoute = import_router.useMatchRoute();
565
+ const match = matchRoute({
566
+ to: "/posts/$postId",
567
+ includeSearch: true,
568
+ search: { tab: "details" }
569
+ });
570
+ return import_react.default.createElement("pre", null, match === false ? "false" : JSON.stringify(match));
571
+ });
572
+ import_bun_test.expect(markup).toContain("{"postId":"alpha"}");
573
+ });
574
+ });