litzjs 0.0.0

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,1625 @@
1
+ import { n as installClientBindings } from "./bindings-BDe-v5i6.mjs";
2
+ import { a as matchPathname, c as createFormDataPayload, i as extractRouteLikeParams, n as LITZ_RESULT_ACCEPT, o as sortByPathSpecificity, r as createInternalActionRequestInit } from "./internal-transport-dsMykcNK.mjs";
3
+ import * as React from "react";
4
+ import { routeManifest } from "virtual:litzjs:route-manifest";
5
+ import { jsx } from "react/jsx-runtime";
6
+ //#region src/client/navigation.ts
7
+ function isHashOnlyNavigation(currentUrl, nextUrl) {
8
+ return currentUrl.origin === nextUrl.origin && currentUrl.pathname === nextUrl.pathname && currentUrl.search === nextUrl.search && currentUrl.hash !== nextUrl.hash;
9
+ }
10
+ function shouldInterceptLinkNavigation(options) {
11
+ if (options.button !== 0 || options.metaKey || options.altKey || options.ctrlKey || options.shiftKey) return false;
12
+ if (options.target && options.target !== "_self") return false;
13
+ if (options.download) return false;
14
+ if (options.nextUrl.origin !== options.currentUrl.origin) return false;
15
+ if (isHashOnlyNavigation(options.currentUrl, options.nextUrl)) return false;
16
+ return true;
17
+ }
18
+ function toNavigationHref(url) {
19
+ return url.pathname + url.search + url.hash;
20
+ }
21
+ function shouldPrefetchLink(options) {
22
+ return shouldInterceptLinkNavigation({
23
+ button: 0,
24
+ metaKey: false,
25
+ altKey: false,
26
+ ctrlKey: false,
27
+ shiftKey: false,
28
+ target: options.target,
29
+ download: options.download,
30
+ currentUrl: options.currentUrl,
31
+ nextUrl: options.nextUrl
32
+ });
33
+ }
34
+ function applySearchParams(currentUrl, updates) {
35
+ const nextUrl = new URL(currentUrl.href);
36
+ const nextSearch = new URLSearchParams(currentUrl.search);
37
+ for (const [key, value] of Object.entries(updates)) {
38
+ nextSearch.delete(key);
39
+ if (value == null) continue;
40
+ if (Array.isArray(value)) {
41
+ for (const entry of value) nextSearch.append(key, entry);
42
+ continue;
43
+ }
44
+ nextSearch.set(key, value);
45
+ }
46
+ nextUrl.search = nextSearch.toString();
47
+ return {
48
+ changed: nextUrl.search !== currentUrl.search,
49
+ href: toNavigationHref(nextUrl)
50
+ };
51
+ }
52
+ //#endregion
53
+ //#region src/client/link.tsx
54
+ function createLinkComponent(dependencies) {
55
+ return function LitzLink(props) {
56
+ const navigate = dependencies.useNavigate();
57
+ const { href, replace = false, onClick, onMouseEnter, onFocus, onTouchStart, target, download, rel, ...rest } = props;
58
+ return React.createElement("a", {
59
+ ...rest,
60
+ href,
61
+ target,
62
+ download,
63
+ rel,
64
+ onMouseEnter(event) {
65
+ onMouseEnter?.(event);
66
+ if (event.defaultPrevented) return;
67
+ dependencies.prefetchRouteModuleForHref(href, target, download);
68
+ },
69
+ onFocus(event) {
70
+ onFocus?.(event);
71
+ if (event.defaultPrevented) return;
72
+ dependencies.prefetchRouteModuleForHref(href, target, download);
73
+ },
74
+ onTouchStart(event) {
75
+ onTouchStart?.(event);
76
+ if (event.defaultPrevented) return;
77
+ dependencies.prefetchRouteModuleForHref(href, target, download);
78
+ },
79
+ onClick(event) {
80
+ onClick?.(event);
81
+ if (event.defaultPrevented) return;
82
+ const nextUrl = new URL(href, window.location.href);
83
+ const currentUrl = new URL(window.location.href);
84
+ if (!shouldInterceptLinkNavigation({
85
+ button: event.button,
86
+ metaKey: event.metaKey,
87
+ altKey: event.altKey,
88
+ ctrlKey: event.ctrlKey,
89
+ shiftKey: event.shiftKey,
90
+ target,
91
+ download,
92
+ currentUrl,
93
+ nextUrl
94
+ })) return;
95
+ event.preventDefault();
96
+ navigate(toNavigationHref(nextUrl), { replace });
97
+ }
98
+ });
99
+ };
100
+ }
101
+ //#endregion
102
+ //#region src/client/result-headers.ts
103
+ const PUBLIC_RESULT_HEADER_NAMES = new Set([
104
+ "content-type",
105
+ "x-litzjs-kind",
106
+ "x-litzjs-revalidate",
107
+ "x-litzjs-status",
108
+ "x-litzjs-view-id"
109
+ ]);
110
+ function createPublicResultHeaders(headers) {
111
+ const publicHeaders = new Headers();
112
+ headers.forEach((value, key) => {
113
+ const normalizedKey = key.toLowerCase();
114
+ if (PUBLIC_RESULT_HEADER_NAMES.has(normalizedKey) || normalizedKey.startsWith("x-litzjs-public-")) publicHeaders.append(key, value);
115
+ });
116
+ return publicHeaders;
117
+ }
118
+ //#endregion
119
+ //#region src/client/transport.tsx
120
+ async function parseLoaderResponse(response) {
121
+ const contentType = response.headers.get("content-type") ?? "";
122
+ const publicHeaders = createPublicResultHeaders(response.headers);
123
+ if (contentType.includes("text/x-component")) return createViewResult(response, publicHeaders);
124
+ const body = await response.json();
125
+ if (body.kind === "data") return {
126
+ kind: "data",
127
+ status: response.status,
128
+ headers: publicHeaders,
129
+ stale: false,
130
+ data: body.data,
131
+ render() {
132
+ return null;
133
+ }
134
+ };
135
+ if (body.kind === "error") throw createRouteLikeError(response.status, publicHeaders, body);
136
+ if (body.kind === "fault") throw createRouteLikeError(response.status, publicHeaders, body);
137
+ if (body.kind === "redirect") throw createRedirectSignal(response.status, publicHeaders, body);
138
+ throw new Error(`Unsupported loader response kind "${body.kind}".`);
139
+ }
140
+ async function parseActionResponse(response) {
141
+ const contentType = response.headers.get("content-type") ?? "";
142
+ const publicHeaders = createPublicResultHeaders(response.headers);
143
+ if (contentType.includes("text/x-component")) return createViewResult(response, publicHeaders);
144
+ const body = await response.json();
145
+ switch (body.kind) {
146
+ case "data": return {
147
+ kind: "data",
148
+ status: response.status,
149
+ headers: publicHeaders,
150
+ data: body.data
151
+ };
152
+ case "invalid": return {
153
+ kind: "invalid",
154
+ status: response.status,
155
+ headers: publicHeaders,
156
+ fields: body.fields,
157
+ formError: body.formError,
158
+ data: body.data
159
+ };
160
+ case "redirect": return {
161
+ kind: "redirect",
162
+ status: response.status,
163
+ headers: publicHeaders,
164
+ location: body.location,
165
+ replace: body.replace ?? false
166
+ };
167
+ case "error": return {
168
+ kind: "error",
169
+ status: response.status,
170
+ headers: publicHeaders,
171
+ message: body.message,
172
+ code: body.code,
173
+ data: body.data
174
+ };
175
+ case "fault": return {
176
+ kind: "fault",
177
+ status: response.status,
178
+ headers: publicHeaders,
179
+ message: body.message,
180
+ digest: body.digest
181
+ };
182
+ default: throw new Error(`Unsupported action response kind "${String(body.kind)}".`);
183
+ }
184
+ }
185
+ async function createViewResult(response, publicHeaders = createPublicResultHeaders(response.headers)) {
186
+ if (!response.body) throw new Error("Flight response body is missing.");
187
+ const { createFromReadableStream } = await import("@vitejs/plugin-rsc/browser");
188
+ const node = await createFromReadableStream(response.body);
189
+ return {
190
+ kind: "view",
191
+ status: Number(response.headers.get("x-litz-status") ?? response.status),
192
+ headers: publicHeaders,
193
+ stale: false,
194
+ node,
195
+ render() {
196
+ return node;
197
+ }
198
+ };
199
+ }
200
+ function isRouteLikeError(value) {
201
+ return typeof value === "object" && value !== null && "kind" in value && (value.kind === "error" || value.kind === "fault");
202
+ }
203
+ function isRedirectSignal(value) {
204
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "redirect";
205
+ }
206
+ function getRevalidateTargets(headers) {
207
+ const value = headers.get("x-litz-revalidate");
208
+ if (!value) return [];
209
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
210
+ }
211
+ function createRouteLikeError(status, headers, body) {
212
+ return {
213
+ kind: body.kind,
214
+ status,
215
+ headers,
216
+ message: body.message,
217
+ code: "code" in body ? body.code : void 0,
218
+ digest: "digest" in body ? body.digest : void 0,
219
+ data: "data" in body ? body.data : void 0
220
+ };
221
+ }
222
+ function createRedirectSignal(status, headers, body) {
223
+ return {
224
+ kind: "redirect",
225
+ status,
226
+ headers,
227
+ location: body.location,
228
+ replace: body.replace ?? false
229
+ };
230
+ }
231
+ //#endregion
232
+ //#region src/client/resources.tsx
233
+ const RESOURCE_STORE_LIMIT = 200;
234
+ const resourceStore = /* @__PURE__ */ new Map();
235
+ const resourceFormComponentCache = /* @__PURE__ */ new Map();
236
+ const resourceComponentCache = /* @__PURE__ */ new Map();
237
+ let resourceLocationContext = null;
238
+ let resourceStatusContext = null;
239
+ let resourceDataContext = null;
240
+ let resourceActionsContext = null;
241
+ function createRuntimeContext$1(name) {
242
+ const createContext = React.createContext;
243
+ if (!createContext) throw new Error(`${name} is not available in this environment.`);
244
+ return createContext(null);
245
+ }
246
+ function getResourceLocationContext() {
247
+ resourceLocationContext ??= createRuntimeContext$1("Litz resource location");
248
+ return resourceLocationContext;
249
+ }
250
+ function getResourceStatusContext() {
251
+ resourceStatusContext ??= createRuntimeContext$1("Litz resource status");
252
+ return resourceStatusContext;
253
+ }
254
+ function getResourceDataContext() {
255
+ resourceDataContext ??= createRuntimeContext$1("Litz resource data");
256
+ return resourceDataContext;
257
+ }
258
+ function getResourceActionsContext() {
259
+ resourceActionsContext ??= createRuntimeContext$1("Litz resource actions");
260
+ return resourceActionsContext;
261
+ }
262
+ function requireActiveResourceSlice(resourcePath, value) {
263
+ if (!value) throw new Error(`Resource "${resourcePath}" is being used outside its resource component.`);
264
+ if (value.id !== resourcePath) throw new Error(`Resource "${resourcePath}" is not the active resource. Active resource is "${value.id}".`);
265
+ return value;
266
+ }
267
+ function ResourceRuntimeProvider(props) {
268
+ const ResourceLocationContext = getResourceLocationContext();
269
+ const ResourceStatusContext = getResourceStatusContext();
270
+ const ResourceDataContext = getResourceDataContext();
271
+ const ResourceActionsContext = getResourceActionsContext();
272
+ const locationValue = React.useMemo(() => ({
273
+ id: props.value.id,
274
+ params: props.value.params,
275
+ search: props.value.search,
276
+ setSearch: props.value.setSearch
277
+ }), [
278
+ props.value.id,
279
+ props.value.params,
280
+ props.value.search,
281
+ props.value.setSearch
282
+ ]);
283
+ const statusValue = React.useMemo(() => ({
284
+ id: props.value.id,
285
+ status: props.value.status,
286
+ pending: props.value.pending
287
+ }), [
288
+ props.value.id,
289
+ props.value.pending,
290
+ props.value.status
291
+ ]);
292
+ const dataValue = React.useMemo(() => ({
293
+ id: props.value.id,
294
+ loaderResult: props.value.loaderResult,
295
+ actionResult: props.value.actionResult,
296
+ data: props.value.data,
297
+ view: props.value.view
298
+ }), [
299
+ props.value.actionResult,
300
+ props.value.data,
301
+ props.value.id,
302
+ props.value.loaderResult,
303
+ props.value.view
304
+ ]);
305
+ const actionsValue = React.useMemo(() => ({
306
+ id: props.value.id,
307
+ submit: props.value.submit,
308
+ reload: props.value.reload,
309
+ retry: props.value.retry
310
+ }), [
311
+ props.value.id,
312
+ props.value.reload,
313
+ props.value.retry,
314
+ props.value.submit
315
+ ]);
316
+ return /* @__PURE__ */ jsx(ResourceLocationContext.Provider, {
317
+ value: locationValue,
318
+ children: /* @__PURE__ */ jsx(ResourceStatusContext.Provider, {
319
+ value: statusValue,
320
+ children: /* @__PURE__ */ jsx(ResourceDataContext.Provider, {
321
+ value: dataValue,
322
+ children: /* @__PURE__ */ jsx(ResourceActionsContext.Provider, {
323
+ value: actionsValue,
324
+ children: props.children
325
+ })
326
+ })
327
+ })
328
+ });
329
+ }
330
+ function useRequiredResourceLocation(resourcePath) {
331
+ return requireActiveResourceSlice(resourcePath, React.useContext(getResourceLocationContext()));
332
+ }
333
+ function useRequiredResourceStatus(resourcePath) {
334
+ return requireActiveResourceSlice(resourcePath, React.useContext(getResourceStatusContext()));
335
+ }
336
+ function useRequiredResourceData(resourcePath) {
337
+ return requireActiveResourceSlice(resourcePath, React.useContext(getResourceDataContext()));
338
+ }
339
+ function useRequiredResourceActions(resourcePath) {
340
+ return requireActiveResourceSlice(resourcePath, React.useContext(getResourceActionsContext()));
341
+ }
342
+ function createResourceFormComponent(resourcePath) {
343
+ const cached = resourceFormComponentCache.get(resourcePath);
344
+ if (cached) return cached;
345
+ const MemoizedLitzResourceForm = React.memo(function LitzResourceForm(props) {
346
+ const actions = useRequiredResourceActions(resourcePath);
347
+ const { children, onSubmit, replace, revalidate, ...rest } = props;
348
+ const submitRef = React.useRef((payload, options) => actions.submit(payload, options));
349
+ React.useEffect(() => {
350
+ submitRef.current = (payload, options) => actions.submit(payload, options);
351
+ }, [actions.submit]);
352
+ const action = React.useCallback(async (formData) => {
353
+ await submitRef.current(formData, {
354
+ replace,
355
+ revalidate
356
+ });
357
+ }, [replace, revalidate]);
358
+ return React.createElement("form", {
359
+ ...rest,
360
+ action,
361
+ onSubmit
362
+ }, children);
363
+ });
364
+ MemoizedLitzResourceForm.displayName = `LitzResourceForm(${resourcePath})`;
365
+ resourceFormComponentCache.set(resourcePath, MemoizedLitzResourceForm);
366
+ return MemoizedLitzResourceForm;
367
+ }
368
+ function createResourceComponent(resourcePath, Component) {
369
+ const cached = resourceComponentCache.get(resourcePath);
370
+ if (cached) return cached;
371
+ const MemoizedLitzResourceComponent = React.memo(function LitzResourceComponent(props) {
372
+ return /* @__PURE__ */ jsx(ResourceRuntimeProvider, {
373
+ value: useResourceRuntime(resourcePath, props),
374
+ children: /* @__PURE__ */ jsx(Component, { ...props })
375
+ });
376
+ });
377
+ MemoizedLitzResourceComponent.displayName = `LitzResource(${resourcePath})`;
378
+ resourceComponentCache.set(resourcePath, MemoizedLitzResourceComponent);
379
+ return MemoizedLitzResourceComponent;
380
+ }
381
+ function useResourceRuntime(resourcePath, request) {
382
+ const params = React.useMemo(() => request?.params ?? {}, [request?.params]);
383
+ const incomingSearchKey = React.useMemo(() => createUrlSearchParams(request?.search).toString(), [request?.search]);
384
+ const [searchState, setSearchState] = React.useState(() => createUrlSearchParams(request?.search));
385
+ React.useEffect(() => {
386
+ setSearchState((current) => current.toString() === incomingSearchKey ? current : createUrlSearchParams(request?.search));
387
+ }, [incomingSearchKey, request?.search]);
388
+ const preparedRequest = React.useMemo(() => prepareResourceRequest(resourcePath, {
389
+ params,
390
+ search: searchState
391
+ }), [
392
+ params,
393
+ resourcePath,
394
+ searchState
395
+ ]);
396
+ const snapshot = React.useSyncExternalStore(React.useCallback((listener) => subscribe(preparedRequest.key, listener), [preparedRequest.key]), React.useCallback(() => getEntry(preparedRequest.key).snapshot, [preparedRequest.key]), React.useCallback(() => getEntry(preparedRequest.key).snapshot, [preparedRequest.key]));
397
+ const reloadImpl = React.useCallback(async (mode = "loading") => {
398
+ await performPreparedResourceRequest(resourcePath, "loader", preparedRequest, void 0, mode);
399
+ }, [preparedRequest, resourcePath]);
400
+ React.useEffect(() => {
401
+ if (!snapshot.pending && !snapshot.loaderResult && !snapshot.failure) reloadImpl("loading");
402
+ }, [
403
+ reloadImpl,
404
+ snapshot.failure,
405
+ snapshot.loaderResult,
406
+ snapshot.pending
407
+ ]);
408
+ const setSearch = React.useCallback((updates) => {
409
+ const result = applySearchParams(new URL(`https://litz.local/?${searchState.toString()}`), updates);
410
+ if (!result.changed) return;
411
+ const nextUrl = new URL(result.href);
412
+ React.startTransition(() => {
413
+ setSearchState(new URLSearchParams(nextUrl.search));
414
+ });
415
+ }, [searchState]);
416
+ const submit = React.useCallback(async (payload, options) => {
417
+ const formData = createFormDataPayload(payload);
418
+ options?.onBeforeSubmit?.(formData);
419
+ const result = await performPreparedResourceRequest(resourcePath, "action", preparedRequest, formData, "submitting");
420
+ if (!result || !("kind" in result)) return;
421
+ if (result.kind === "error" || result.kind === "fault") {
422
+ options?.onError?.(result);
423
+ return;
424
+ }
425
+ options?.onSuccess?.(result);
426
+ }, [preparedRequest, resourcePath]);
427
+ const reload = React.useCallback(() => {
428
+ reloadImpl(snapshot.loaderResult ? "revalidating" : "loading");
429
+ }, [reloadImpl, snapshot.loaderResult]);
430
+ const retry = React.useCallback(() => {
431
+ reloadImpl(snapshot.loaderResult ? "revalidating" : "loading");
432
+ }, [reloadImpl, snapshot.loaderResult]);
433
+ if (snapshot.failure) throw snapshot.failure;
434
+ return React.useMemo(() => ({
435
+ id: resourcePath,
436
+ params,
437
+ search: searchState,
438
+ setSearch,
439
+ status: snapshot.status,
440
+ pending: snapshot.pending,
441
+ loaderResult: snapshot.loaderResult,
442
+ actionResult: snapshot.actionResult,
443
+ data: snapshot.data,
444
+ view: snapshot.view,
445
+ submit,
446
+ reload,
447
+ retry
448
+ }), [
449
+ params,
450
+ reload,
451
+ resourcePath,
452
+ retry,
453
+ searchState,
454
+ setSearch,
455
+ snapshot.actionResult,
456
+ snapshot.data,
457
+ snapshot.loaderResult,
458
+ snapshot.pending,
459
+ snapshot.status,
460
+ snapshot.view,
461
+ submit
462
+ ]);
463
+ }
464
+ async function performPreparedResourceRequest(resourcePath, operation, preparedRequest, payload, mode = "loading") {
465
+ const { key, normalizedRequest } = preparedRequest;
466
+ const entry = getEntry(key);
467
+ if (entry.inFlight) return entry.inFlight;
468
+ entry.snapshot = {
469
+ ...entry.snapshot,
470
+ status: mode,
471
+ pending: true,
472
+ failure: void 0
473
+ };
474
+ notify(entry);
475
+ entry.inFlight = (async () => {
476
+ try {
477
+ const response = operation === "action" ? await fetch("/_litz/resource", {
478
+ method: "POST",
479
+ ...createInternalActionRequestInit({
480
+ path: resourcePath,
481
+ operation,
482
+ request: {
483
+ params: normalizedRequest.params,
484
+ search: normalizedRequest.search
485
+ }
486
+ }, payload)
487
+ }) : await fetch("/_litz/resource", {
488
+ method: "POST",
489
+ headers: {
490
+ "content-type": "application/json",
491
+ accept: LITZ_RESULT_ACCEPT
492
+ },
493
+ body: JSON.stringify({
494
+ path: resourcePath,
495
+ operation,
496
+ request: {
497
+ params: normalizedRequest.params,
498
+ search: normalizedRequest.search
499
+ }
500
+ })
501
+ });
502
+ if (operation === "loader") {
503
+ const loaderResult = await parseLoaderResponse(response);
504
+ entry.snapshot = {
505
+ ...entry.snapshot,
506
+ loaderResult,
507
+ data: loaderResult.kind === "data" ? loaderResult.data : null,
508
+ view: loaderResult.kind === "view" ? loaderResult.node : null,
509
+ status: "idle",
510
+ pending: false,
511
+ failure: void 0
512
+ };
513
+ return loaderResult;
514
+ }
515
+ const actionResult = await parseActionResponse(response);
516
+ if (actionResult.kind === "redirect") {
517
+ entry.snapshot = {
518
+ ...entry.snapshot,
519
+ actionResult,
520
+ status: "idle",
521
+ pending: false,
522
+ failure: void 0
523
+ };
524
+ performClientRedirect(actionResult.location, actionResult.replace);
525
+ return actionResult;
526
+ }
527
+ if (actionResult.kind === "fault") {
528
+ entry.snapshot = {
529
+ ...entry.snapshot,
530
+ actionResult,
531
+ status: "error",
532
+ pending: false,
533
+ failure: actionResult
534
+ };
535
+ throw actionResult;
536
+ }
537
+ entry.snapshot = {
538
+ ...entry.snapshot,
539
+ actionResult,
540
+ data: actionResult.kind === "data" ? actionResult.data : null,
541
+ view: actionResult.kind === "view" ? actionResult.node : null,
542
+ status: actionResult.kind === "error" ? "error" : "idle",
543
+ pending: false,
544
+ failure: void 0
545
+ };
546
+ return actionResult;
547
+ } catch (error) {
548
+ if (isRedirectSignal(error)) {
549
+ performClientRedirect(error.location, error.replace);
550
+ entry.snapshot = {
551
+ ...entry.snapshot,
552
+ status: "idle",
553
+ pending: false,
554
+ failure: void 0
555
+ };
556
+ return;
557
+ }
558
+ entry.snapshot = {
559
+ ...entry.snapshot,
560
+ status: isRouteLikeError(error) ? "error" : "error",
561
+ pending: false,
562
+ failure: error
563
+ };
564
+ throw error;
565
+ } finally {
566
+ entry.inFlight = void 0;
567
+ notify(entry);
568
+ cleanupResourceEntry(key, entry);
569
+ }
570
+ })();
571
+ return entry.inFlight;
572
+ }
573
+ function createUrlSearchParams(search) {
574
+ if (!search) return new URLSearchParams();
575
+ if (search instanceof URLSearchParams) return new URLSearchParams(search);
576
+ return new URLSearchParams(search);
577
+ }
578
+ function normalizeResourceRequest(request) {
579
+ return {
580
+ params: request?.params ?? {},
581
+ search: Object.fromEntries(createUrlSearchParams(request?.search).entries())
582
+ };
583
+ }
584
+ function prepareResourceRequest(resourcePath, request) {
585
+ const normalizedRequest = normalizeResourceRequest(request);
586
+ return {
587
+ key: createResourceCacheKey(resourcePath, normalizedRequest),
588
+ normalizedRequest
589
+ };
590
+ }
591
+ function createResourceCacheKey(resourcePath, normalizedRequest) {
592
+ return JSON.stringify({
593
+ path: resourcePath,
594
+ params: sortRecord$1(normalizedRequest.params),
595
+ search: sortRecord$1(normalizedRequest.search)
596
+ });
597
+ }
598
+ function sortRecord$1(value) {
599
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
600
+ }
601
+ function subscribe(key, listener) {
602
+ const entry = getEntry(key);
603
+ entry.listeners.add(listener);
604
+ return () => {
605
+ entry.listeners.delete(listener);
606
+ cleanupResourceEntry(key, entry);
607
+ };
608
+ }
609
+ function getInitialSnapshot() {
610
+ return {
611
+ loaderResult: null,
612
+ actionResult: null,
613
+ data: null,
614
+ view: null,
615
+ status: "idle",
616
+ pending: false
617
+ };
618
+ }
619
+ function getEntry(key) {
620
+ let entry = resourceStore.get(key);
621
+ if (!entry) {
622
+ entry = {
623
+ snapshot: getInitialSnapshot(),
624
+ listeners: /* @__PURE__ */ new Set()
625
+ };
626
+ resourceStore.set(key, entry);
627
+ pruneResourceStore();
628
+ }
629
+ return entry;
630
+ }
631
+ function notify(entry) {
632
+ for (const listener of entry.listeners) listener();
633
+ }
634
+ function cleanupResourceEntry(key, entry) {
635
+ if (entry.listeners.size > 0 || entry.inFlight || entry.snapshot.pending) return;
636
+ resourceStore.delete(key);
637
+ }
638
+ function pruneResourceStore() {
639
+ if (resourceStore.size <= RESOURCE_STORE_LIMIT) return;
640
+ for (const [key, entry] of resourceStore) {
641
+ if (resourceStore.size <= RESOURCE_STORE_LIMIT) return;
642
+ cleanupResourceEntry(key, entry);
643
+ }
644
+ }
645
+ function performClientRedirect(href, replace) {
646
+ if (typeof window === "undefined") return;
647
+ if (replace) window.history.replaceState(null, "", href);
648
+ else window.history.pushState(null, "", href);
649
+ window.dispatchEvent(new PopStateEvent("popstate"));
650
+ }
651
+ //#endregion
652
+ //#region src/client/route-transition.ts
653
+ function resolveRouteModuleLoadState(options) {
654
+ if (!options.matched) return {
655
+ kind: "not-found",
656
+ loadedRoute: null,
657
+ displayLocation: options.nextLocation,
658
+ pageState: options.createEmptyPageState()
659
+ };
660
+ if (options.cachedRoute) return {
661
+ kind: "cached",
662
+ loadedRoute: options.cachedRoute,
663
+ displayLocation: options.nextLocation,
664
+ pageState: options.createBootstrapPageState(options.cachedRoute)
665
+ };
666
+ if (options.previousRoute) return { kind: "preserve-current" };
667
+ return {
668
+ kind: "reset-before-load",
669
+ loadedRoute: null,
670
+ displayLocation: options.nextLocation,
671
+ pageState: options.createEmptyPageState()
672
+ };
673
+ }
674
+ function resolveLoadedRouteState(options) {
675
+ return {
676
+ loadedRoute: options.loadedRoute,
677
+ displayLocation: options.nextLocation,
678
+ pageState: options.createBootstrapPageState(options.loadedRoute)
679
+ };
680
+ }
681
+ //#endregion
682
+ //#region src/client/route-host-state.tsx
683
+ function useResolvedRouteState(options) {
684
+ const createEmptyPageState = options.createEmptyPageState;
685
+ const createBootstrapPageState = options.createBootstrapPageState;
686
+ const getCachedRoute = options.getCachedRoute;
687
+ const setCachedRoute = options.setCachedRoute;
688
+ const [displayLocation, setDisplayLocation] = React.useState(() => options.location);
689
+ const [renderedRoute, setRenderedRoute] = React.useState(null);
690
+ const [pageState, setPageState] = React.useState(() => createEmptyPageState());
691
+ const renderedRouteRef = React.useRef(null);
692
+ React.useEffect(() => {
693
+ renderedRouteRef.current = renderedRoute;
694
+ }, [renderedRoute]);
695
+ React.useLayoutEffect(() => {
696
+ let cancelled = false;
697
+ async function loadRouteModule() {
698
+ const routeState = resolveRouteModuleLoadState({
699
+ matched: Boolean(options.matched),
700
+ cachedRoute: options.matched ? getCachedRoute(options.matched.entry.id) : null,
701
+ previousRoute: renderedRouteRef.current,
702
+ nextLocation: options.location,
703
+ createEmptyPageState: () => createEmptyPageState(),
704
+ createBootstrapPageState: (route) => createBootstrapPageState(route)
705
+ });
706
+ if (routeState.kind === "not-found") {
707
+ setRenderedRoute(routeState.loadedRoute);
708
+ setDisplayLocation(routeState.displayLocation);
709
+ setPageState(routeState.pageState);
710
+ return;
711
+ }
712
+ if (routeState.kind === "cached") {
713
+ setPageState(routeState.pageState);
714
+ setRenderedRoute(routeState.loadedRoute);
715
+ setDisplayLocation(routeState.displayLocation);
716
+ return;
717
+ }
718
+ if (routeState.kind === "reset-before-load") {
719
+ setRenderedRoute(routeState.loadedRoute);
720
+ setDisplayLocation(routeState.displayLocation);
721
+ setPageState(routeState.pageState);
722
+ }
723
+ const matchedEntry = options.matched?.entry;
724
+ if (!matchedEntry) return;
725
+ const loaded = await matchedEntry.load();
726
+ if (cancelled) return;
727
+ if (!loaded.route) throw new Error(`Route module "${matchedEntry.id}" does not export "route".`);
728
+ setCachedRoute(matchedEntry.id, loaded.route);
729
+ const loadedRouteState = resolveLoadedRouteState({
730
+ loadedRoute: loaded.route,
731
+ nextLocation: options.location,
732
+ createBootstrapPageState: (route) => createBootstrapPageState(route)
733
+ });
734
+ setPageState(loadedRouteState.pageState);
735
+ setRenderedRoute(loadedRouteState.loadedRoute);
736
+ setDisplayLocation(loadedRouteState.displayLocation);
737
+ }
738
+ loadRouteModule();
739
+ return () => {
740
+ cancelled = true;
741
+ };
742
+ }, [
743
+ createBootstrapPageState,
744
+ createEmptyPageState,
745
+ getCachedRoute,
746
+ options.location,
747
+ options.matched,
748
+ setCachedRoute
749
+ ]);
750
+ return {
751
+ displayLocation,
752
+ renderedRoute,
753
+ pageState,
754
+ setPageState
755
+ };
756
+ }
757
+ //#endregion
758
+ //#region src/client/route-runtime.tsx
759
+ let routeLocationContext = null;
760
+ let routeStatusContext = null;
761
+ let routeDataContext = null;
762
+ let routeActionsContext = null;
763
+ const routeFormComponentCache = /* @__PURE__ */ new Map();
764
+ function createRuntimeContext(name) {
765
+ const createContext = React.createContext;
766
+ if (!createContext) throw new Error(`${name} is not available in this environment.`);
767
+ return createContext(null);
768
+ }
769
+ function getRouteLocationContext() {
770
+ routeLocationContext ??= createRuntimeContext("Litz route location");
771
+ return routeLocationContext;
772
+ }
773
+ function getRouteStatusContext() {
774
+ routeStatusContext ??= createRuntimeContext("Litz route status");
775
+ return routeStatusContext;
776
+ }
777
+ function getRouteDataContext() {
778
+ routeDataContext ??= createRuntimeContext("Litz route data");
779
+ return routeDataContext;
780
+ }
781
+ function getRouteActionsContext() {
782
+ routeActionsContext ??= createRuntimeContext("Litz route actions");
783
+ return routeActionsContext;
784
+ }
785
+ function requireActiveRouteSlice(routeId, value) {
786
+ if (!value) throw new Error(`Route "${routeId}" is being used outside the Litz runtime.`);
787
+ if (value.id !== routeId) throw new Error(`Route "${routeId}" is not the active route. Active route is "${value.id}".`);
788
+ return value;
789
+ }
790
+ function RouteRuntimeProvider(props) {
791
+ const RouteLocationContext = getRouteLocationContext();
792
+ const RouteStatusContext = getRouteStatusContext();
793
+ const RouteDataContext = getRouteDataContext();
794
+ const RouteActionsContext = getRouteActionsContext();
795
+ const locationValue = React.useMemo(() => ({
796
+ id: props.value.id,
797
+ params: props.value.params,
798
+ search: props.value.search,
799
+ setSearch: props.value.setSearch
800
+ }), [
801
+ props.value.id,
802
+ props.value.params,
803
+ props.value.search,
804
+ props.value.setSearch
805
+ ]);
806
+ const statusValue = React.useMemo(() => ({
807
+ id: props.value.id,
808
+ status: props.value.status,
809
+ pending: props.value.pending
810
+ }), [
811
+ props.value.id,
812
+ props.value.pending,
813
+ props.value.status
814
+ ]);
815
+ const dataValue = React.useMemo(() => ({
816
+ id: props.value.id,
817
+ loaderResult: props.value.loaderResult,
818
+ actionResult: props.value.actionResult,
819
+ data: props.value.data,
820
+ view: props.value.view
821
+ }), [
822
+ props.value.actionResult,
823
+ props.value.data,
824
+ props.value.id,
825
+ props.value.loaderResult,
826
+ props.value.view
827
+ ]);
828
+ const actionsValue = React.useMemo(() => ({
829
+ id: props.value.id,
830
+ submit: props.value.submit,
831
+ reload: props.value.reload,
832
+ retry: props.value.retry
833
+ }), [
834
+ props.value.id,
835
+ props.value.reload,
836
+ props.value.retry,
837
+ props.value.submit
838
+ ]);
839
+ return /* @__PURE__ */ jsx(RouteLocationContext.Provider, {
840
+ value: locationValue,
841
+ children: /* @__PURE__ */ jsx(RouteStatusContext.Provider, {
842
+ value: statusValue,
843
+ children: /* @__PURE__ */ jsx(RouteDataContext.Provider, {
844
+ value: dataValue,
845
+ children: /* @__PURE__ */ jsx(RouteActionsContext.Provider, {
846
+ value: actionsValue,
847
+ children: props.children
848
+ })
849
+ })
850
+ })
851
+ });
852
+ }
853
+ function useRequiredRouteLocation(routeId) {
854
+ return requireActiveRouteSlice(routeId, React.useContext(getRouteLocationContext()));
855
+ }
856
+ function useRequiredRouteStatus(routeId) {
857
+ return requireActiveRouteSlice(routeId, React.useContext(getRouteStatusContext()));
858
+ }
859
+ function useRequiredRouteData(routeId) {
860
+ return requireActiveRouteSlice(routeId, React.useContext(getRouteDataContext()));
861
+ }
862
+ function useRequiredRouteActions(routeId) {
863
+ return requireActiveRouteSlice(routeId, React.useContext(getRouteActionsContext()));
864
+ }
865
+ function createRouteFormComponent(routeId) {
866
+ const cached = routeFormComponentCache.get(routeId);
867
+ if (cached) return cached;
868
+ const MemoizedLitzRouteForm = React.memo(function LitzRouteForm(props) {
869
+ const actions = useRequiredRouteActions(routeId);
870
+ const { children, onSubmit, replace, revalidate, ...rest } = props;
871
+ const submitRef = React.useRef((payload, options) => actions.submit(payload, options));
872
+ React.useEffect(() => {
873
+ submitRef.current = (payload, options) => actions.submit(payload, options);
874
+ }, [actions.submit]);
875
+ const action = React.useCallback(async (formData) => {
876
+ await submitRef.current(formData, {
877
+ replace,
878
+ revalidate
879
+ });
880
+ }, [replace, revalidate]);
881
+ return React.createElement("form", {
882
+ ...rest,
883
+ action,
884
+ onSubmit
885
+ }, children);
886
+ });
887
+ MemoizedLitzRouteForm.displayName = `LitzRouteForm(${routeId})`;
888
+ routeFormComponentCache.set(routeId, MemoizedLitzRouteForm);
889
+ return MemoizedLitzRouteForm;
890
+ }
891
+ //#endregion
892
+ //#region src/client/runtime.tsx
893
+ async function fetchRouteLoader(path, request, target) {
894
+ return parseLoaderResponse(await fetch("/_litz/route", {
895
+ method: "POST",
896
+ headers: {
897
+ "content-type": "application/json",
898
+ accept: LITZ_RESULT_ACCEPT
899
+ },
900
+ body: JSON.stringify({
901
+ path,
902
+ target,
903
+ operation: "loader",
904
+ request: normalizeRequest(request)
905
+ })
906
+ }));
907
+ }
908
+ async function fetchRouteAction(path, request, payload) {
909
+ const actionRequest = createInternalActionRequestInit({
910
+ path,
911
+ operation: "action",
912
+ request: normalizeRequest(request)
913
+ }, payload);
914
+ return parseActionResponse(await fetch("/_litz/action", {
915
+ method: "POST",
916
+ headers: actionRequest.headers,
917
+ body: actionRequest.body
918
+ }));
919
+ }
920
+ function normalizeRequest(request) {
921
+ return {
922
+ params: request.params ?? {},
923
+ search: request.search instanceof URLSearchParams ? Object.fromEntries(request.search.entries()) : request.search ?? {}
924
+ };
925
+ }
926
+ //#endregion
927
+ //#region src/client/index.ts
928
+ installClientBindings({
929
+ usePathname,
930
+ useLocation,
931
+ useRequiredRouteLocation,
932
+ useRequiredRouteStatus,
933
+ useRequiredRouteData,
934
+ useRequiredRouteActions,
935
+ useRequiredResourceLocation,
936
+ useRequiredResourceStatus,
937
+ useRequiredResourceData,
938
+ useRequiredResourceActions,
939
+ useMatches,
940
+ createRouteFormComponent,
941
+ createResourceFormComponent,
942
+ createResourceComponent
943
+ });
944
+ const manifest = sortByPathSpecificity(routeManifest);
945
+ const exactManifestEntries = /* @__PURE__ */ new Map();
946
+ const dynamicManifestEntries = [];
947
+ const ROUTE_CACHE_LIMIT = 200;
948
+ const ROUTE_MODULE_CACHE_LIMIT = Math.max(manifest.length, 1);
949
+ let navigationContext = null;
950
+ let locationContext = null;
951
+ let matchesContext = null;
952
+ const routeCache = /* @__PURE__ */ new Map();
953
+ const routeModuleCache = /* @__PURE__ */ new Map();
954
+ const routeModulePrefetchCache = /* @__PURE__ */ new Map();
955
+ const layoutChainCache = /* @__PURE__ */ new WeakMap();
956
+ const pathParamNamesCache = /* @__PURE__ */ new Map();
957
+ for (const entry of manifest) if (entry.path.includes(":")) dynamicManifestEntries.push(entry);
958
+ else exactManifestEntries.set(entry.path, entry);
959
+ function getNavigationContext() {
960
+ if (!navigationContext) {
961
+ const createContext = React.createContext;
962
+ if (!createContext) throw new Error("Litz client navigation is not available in this environment.");
963
+ navigationContext = createContext(null);
964
+ }
965
+ return navigationContext;
966
+ }
967
+ function getMatchesContext() {
968
+ if (!matchesContext) {
969
+ const createContext = React.createContext;
970
+ if (!createContext) throw new Error("Litz client matches are not available in this environment.");
971
+ matchesContext = createContext([]);
972
+ }
973
+ return matchesContext;
974
+ }
975
+ function getLocationContext() {
976
+ if (!locationContext) {
977
+ const createContext = React.createContext;
978
+ if (!createContext) throw new Error("Litz client location is not available in this environment.");
979
+ locationContext = createContext(null);
980
+ }
981
+ return locationContext;
982
+ }
983
+ function mountApp(element, options) {
984
+ import("react-dom/client").then(({ createRoot }) => {
985
+ createRoot(element).render(React.createElement(LitzApp, {
986
+ component: options?.component,
987
+ layout: options?.layout
988
+ }));
989
+ });
990
+ }
991
+ function useNavigate() {
992
+ const context = React.useContext(getNavigationContext());
993
+ if (!context) throw new Error("useNavigate() must be used inside the Litz client runtime.");
994
+ return (href, options) => context.navigate(href, options);
995
+ }
996
+ function useMatches() {
997
+ return React.useContext(getMatchesContext());
998
+ }
999
+ function usePathname() {
1000
+ const location = React.useContext(getLocationContext());
1001
+ if (!location) throw new Error("usePathname() must be used inside the Litz client runtime.");
1002
+ return location.pathname;
1003
+ }
1004
+ function useLocation() {
1005
+ const location = React.useContext(getLocationContext());
1006
+ if (!location) throw new Error("useLocation() must be used inside the Litz client runtime.");
1007
+ return location;
1008
+ }
1009
+ const Link = createLinkComponent({
1010
+ useNavigate,
1011
+ prefetchRouteModuleForHref
1012
+ });
1013
+ function LitzApp(props) {
1014
+ const [location, setLocation] = React.useState(() => window.location.href);
1015
+ React.useEffect(() => {
1016
+ function handlePopState() {
1017
+ React.startTransition(() => {
1018
+ setLocation(window.location.href);
1019
+ });
1020
+ }
1021
+ window.addEventListener("popstate", handlePopState);
1022
+ return () => window.removeEventListener("popstate", handlePopState);
1023
+ }, []);
1024
+ const navigate = React.useCallback((next, replace = false) => {
1025
+ if (replace) window.history.replaceState(null, "", next);
1026
+ else window.history.pushState(null, "", next);
1027
+ React.startTransition(() => {
1028
+ setLocation(window.location.href);
1029
+ });
1030
+ }, []);
1031
+ const navigationValue = React.useMemo(() => ({ navigate(href, options) {
1032
+ navigate(href, options?.replace);
1033
+ } }), [navigate]);
1034
+ const locationValue = React.useMemo(() => {
1035
+ const url = new URL(location);
1036
+ return {
1037
+ href: location,
1038
+ pathname: url.pathname,
1039
+ search: new URLSearchParams(url.search),
1040
+ hash: url.hash
1041
+ };
1042
+ }, [location]);
1043
+ const routeHost = React.createElement(RouteHost, {
1044
+ location,
1045
+ navigate,
1046
+ component: props.component
1047
+ });
1048
+ const content = props.layout ? React.createElement(props.layout.component, null, routeHost) : routeHost;
1049
+ return React.createElement(getNavigationContext().Provider, { value: navigationValue }, React.createElement(getLocationContext().Provider, { value: locationValue }, content));
1050
+ }
1051
+ function RouteHost(props) {
1052
+ const navigate = React.useCallback((next, replace) => {
1053
+ props.navigate(next, replace);
1054
+ }, [props.navigate]);
1055
+ const url = React.useMemo(() => new URL(props.location), [props.location]);
1056
+ const search = React.useMemo(() => new URLSearchParams(url.search), [url.search]);
1057
+ const matched = React.useMemo(() => findMatch(url.pathname), [url.pathname]);
1058
+ const createBootstrapPageStateForLocation = React.useCallback((route) => createBootstrapPageState(route, url.pathname, search), [search, url.pathname]);
1059
+ const { displayLocation, renderedRoute, pageState, setPageState } = useResolvedRouteState({
1060
+ matched,
1061
+ location: props.location,
1062
+ createEmptyPageState,
1063
+ createBootstrapPageState: createBootstrapPageStateForLocation,
1064
+ getCachedRoute: getCachedRouteModule,
1065
+ setCachedRoute: setCachedRouteModule
1066
+ });
1067
+ const displayedUrl = React.useMemo(() => new URL(displayLocation), [displayLocation]);
1068
+ const displayedSearch = React.useMemo(() => new URLSearchParams(displayedUrl.search), [displayedUrl.search]);
1069
+ const activeRouteState = React.useMemo(() => {
1070
+ if (!renderedRoute) return null;
1071
+ const activeMatches = buildActiveMatches(renderedRoute, displayedUrl.pathname, displayedSearch);
1072
+ return {
1073
+ activeMatches,
1074
+ loaderMatches: activeMatches.filter((entry) => Boolean(entry.options?.loader)),
1075
+ baseRequest: {
1076
+ params: extractRouteParams(renderedRoute.path, displayedUrl.pathname) ?? {},
1077
+ search: displayedSearch
1078
+ }
1079
+ };
1080
+ }, [
1081
+ displayedSearch,
1082
+ displayedUrl.pathname,
1083
+ renderedRoute
1084
+ ]);
1085
+ React.useEffect(() => {
1086
+ if (!renderedRoute || !activeRouteState) return;
1087
+ let cancelled = false;
1088
+ const { loaderMatches, baseRequest } = activeRouteState;
1089
+ const finalLoaderMatchId = loaderMatches[loaderMatches.length - 1]?.id;
1090
+ const reload = async (mode = "loading") => {
1091
+ if (loaderMatches.length === 0) {
1092
+ if (!cancelled) setPageState((current) => ({
1093
+ ...current,
1094
+ status: "idle",
1095
+ pending: false,
1096
+ errorInfo: void 0,
1097
+ errorTargetId: void 0
1098
+ }));
1099
+ return;
1100
+ }
1101
+ setPageState((current) => applyCachedLoaderStateToPageState(current, loaderMatches, mode));
1102
+ for (const entry of loaderMatches) try {
1103
+ const loaderResult = await fetchRouteLoader(renderedRoute.path, baseRequest, entry.id);
1104
+ if (cancelled) return;
1105
+ setCachedLoaderResult(entry.cacheKey, withLoaderStaleState(loaderResult, false));
1106
+ setPageState((current) => withMatchLoaderResult(current, entry.id, renderedRoute.id, loaderResult, entry.id === finalLoaderMatchId ? "idle" : current.status, entry.id === finalLoaderMatchId ? false : current.pending));
1107
+ } catch (error) {
1108
+ if (cancelled) return;
1109
+ if (isRedirectSignal(error)) {
1110
+ navigate(error.location, true);
1111
+ return;
1112
+ }
1113
+ if (isRouteLikeError(error)) {
1114
+ setPageState((current) => ({
1115
+ ...current,
1116
+ status: "error",
1117
+ pending: false,
1118
+ errorInfo: error,
1119
+ errorTargetId: entry.id
1120
+ }));
1121
+ return;
1122
+ }
1123
+ throw error;
1124
+ }
1125
+ };
1126
+ reload();
1127
+ return () => {
1128
+ cancelled = true;
1129
+ };
1130
+ }, [
1131
+ activeRouteState,
1132
+ navigate,
1133
+ renderedRoute
1134
+ ]);
1135
+ const activeMatches = activeRouteState?.activeMatches ?? [];
1136
+ const matchesValue = activeMatches;
1137
+ const reloadImpl = React.useCallback((mode) => renderedRoute ? reloadCurrentRoute({
1138
+ route: renderedRoute,
1139
+ pathname: displayedUrl.pathname,
1140
+ search: displayedSearch,
1141
+ navigate,
1142
+ setPageState,
1143
+ mode
1144
+ }) : Promise.resolve(), [
1145
+ displayedSearch,
1146
+ displayedUrl.pathname,
1147
+ navigate,
1148
+ renderedRoute,
1149
+ setPageState
1150
+ ]);
1151
+ if (!matched) return React.createElement(NotFoundPage);
1152
+ if (!renderedRoute) return React.createElement(React.Fragment);
1153
+ const content = renderMatchChain(renderedRoute, activeMatches, pageState, displayLocation, navigate, reloadImpl, setPageState);
1154
+ return React.createElement(getMatchesContext().Provider, { value: matchesValue }, props.component ? React.createElement(props.component, null, content) : content);
1155
+ }
1156
+ function findMatch(pathname) {
1157
+ const exactEntry = exactManifestEntries.get(pathname);
1158
+ if (exactEntry) return {
1159
+ entry: exactEntry,
1160
+ params: {}
1161
+ };
1162
+ for (const entry of dynamicManifestEntries) {
1163
+ const params = matchPathname(entry.path, pathname);
1164
+ if (params) return {
1165
+ entry,
1166
+ params
1167
+ };
1168
+ }
1169
+ return null;
1170
+ }
1171
+ function NotFoundPage() {
1172
+ return React.createElement("main", null, React.createElement("h1", null, "Not Found"));
1173
+ }
1174
+ function createEmptyPageState() {
1175
+ return {
1176
+ matchStates: {},
1177
+ actionResult: null,
1178
+ nextResultSequence: 1,
1179
+ latestDataResult: null,
1180
+ latestViewResult: null,
1181
+ status: "loading",
1182
+ pending: true
1183
+ };
1184
+ }
1185
+ function createBootstrapPageState(route, pathname, search) {
1186
+ const matches = buildActiveMatches(route, pathname, search);
1187
+ const matchStates = {};
1188
+ let hasAnyLoader = false;
1189
+ let hasCachedLoader = false;
1190
+ let missingLoader = false;
1191
+ let nextResultSequence = 1;
1192
+ let latestDataResult = null;
1193
+ let latestViewResult = null;
1194
+ for (const match of matches) {
1195
+ if (!match.options?.loader) continue;
1196
+ hasAnyLoader = true;
1197
+ const cached = getCachedLoaderResult(match.cacheKey);
1198
+ if (cached) {
1199
+ hasCachedLoader = true;
1200
+ const staleCached = withLoaderStaleState(cached, true);
1201
+ matchStates[match.id] = { loaderResult: staleCached };
1202
+ if (match.id === route.id) {
1203
+ if (staleCached.kind === "data") {
1204
+ latestDataResult = {
1205
+ sequence: nextResultSequence,
1206
+ result: staleCached
1207
+ };
1208
+ nextResultSequence += 1;
1209
+ } else if (staleCached.kind === "view") {
1210
+ latestViewResult = {
1211
+ sequence: nextResultSequence,
1212
+ result: staleCached
1213
+ };
1214
+ nextResultSequence += 1;
1215
+ }
1216
+ }
1217
+ } else {
1218
+ missingLoader = true;
1219
+ matchStates[match.id] = { loaderResult: null };
1220
+ }
1221
+ }
1222
+ if (!hasAnyLoader) return {
1223
+ matchStates,
1224
+ actionResult: null,
1225
+ nextResultSequence,
1226
+ latestDataResult,
1227
+ latestViewResult,
1228
+ status: "idle",
1229
+ pending: false
1230
+ };
1231
+ return {
1232
+ matchStates,
1233
+ actionResult: null,
1234
+ nextResultSequence,
1235
+ latestDataResult,
1236
+ latestViewResult,
1237
+ status: hasCachedLoader ? "revalidating" : "loading",
1238
+ pending: missingLoader || hasCachedLoader
1239
+ };
1240
+ }
1241
+ function withLatestDataResult(current, result) {
1242
+ const sequence = current.nextResultSequence;
1243
+ return {
1244
+ ...current,
1245
+ nextResultSequence: sequence + 1,
1246
+ latestDataResult: {
1247
+ sequence,
1248
+ result
1249
+ }
1250
+ };
1251
+ }
1252
+ function withLatestViewResult(current, result) {
1253
+ const sequence = current.nextResultSequence;
1254
+ return {
1255
+ ...current,
1256
+ nextResultSequence: sequence + 1,
1257
+ latestViewResult: {
1258
+ sequence,
1259
+ result
1260
+ }
1261
+ };
1262
+ }
1263
+ function applyCachedLoaderStateToPageState(current, matches, mode) {
1264
+ const matchStates = { ...current.matchStates };
1265
+ let hasResolvedLoader = false;
1266
+ for (const match of matches) {
1267
+ const cached = getCachedLoaderResult(match.cacheKey);
1268
+ if (cached) {
1269
+ matchStates[match.id] = { loaderResult: withLoaderStaleState(cached, true) };
1270
+ hasResolvedLoader = true;
1271
+ } else if (!matchStates[match.id]) matchStates[match.id] = { loaderResult: null };
1272
+ else if (matchStates[match.id]?.loaderResult) hasResolvedLoader = true;
1273
+ }
1274
+ return {
1275
+ ...current,
1276
+ matchStates,
1277
+ status: hasResolvedLoader ? "revalidating" : mode,
1278
+ pending: true,
1279
+ errorInfo: void 0,
1280
+ errorTargetId: void 0
1281
+ };
1282
+ }
1283
+ function withMatchLoaderResult(current, matchId, routeId, loaderResult, status, pending) {
1284
+ let nextState = {
1285
+ ...current,
1286
+ matchStates: {
1287
+ ...current.matchStates,
1288
+ [matchId]: { loaderResult }
1289
+ },
1290
+ status,
1291
+ pending,
1292
+ errorInfo: void 0,
1293
+ errorTargetId: void 0
1294
+ };
1295
+ if (matchId === routeId) {
1296
+ if (loaderResult.kind === "data") nextState = withLatestDataResult(nextState, loaderResult);
1297
+ else if (loaderResult.kind === "view") nextState = withLatestViewResult(nextState, loaderResult);
1298
+ }
1299
+ return nextState;
1300
+ }
1301
+ async function reloadCurrentRoute(options) {
1302
+ const loaderMatches = buildActiveMatches(options.route, options.pathname, options.search).filter((entry) => Boolean(entry.options?.loader));
1303
+ if (loaderMatches.length === 0) {
1304
+ options.setPageState((current) => ({
1305
+ ...current,
1306
+ status: "idle",
1307
+ pending: false,
1308
+ errorInfo: void 0,
1309
+ errorTargetId: void 0
1310
+ }));
1311
+ return;
1312
+ }
1313
+ options.setPageState((current) => applyCachedLoaderStateToPageState(current, loaderMatches, options.mode ?? "loading"));
1314
+ const baseRequest = {
1315
+ params: extractRouteParams(options.route.path, options.pathname) ?? {},
1316
+ search: options.search
1317
+ };
1318
+ for (const entry of loaderMatches) try {
1319
+ const loaderResult = await fetchRouteLoader(options.route.path, baseRequest, entry.id);
1320
+ setCachedLoaderResult(entry.cacheKey, withLoaderStaleState(loaderResult, false));
1321
+ options.setPageState((current) => withMatchLoaderResult(current, entry.id, options.route.id, loaderResult, entry === loaderMatches[loaderMatches.length - 1] ? "idle" : current.status, entry === loaderMatches[loaderMatches.length - 1] ? false : current.pending));
1322
+ } catch (error) {
1323
+ if (isRedirectSignal(error)) {
1324
+ options.navigate(error.location, true);
1325
+ return;
1326
+ }
1327
+ if (isRouteLikeError(error)) {
1328
+ options.setPageState((current) => ({
1329
+ ...current,
1330
+ status: "error",
1331
+ pending: false,
1332
+ errorInfo: error,
1333
+ errorTargetId: entry.id
1334
+ }));
1335
+ return;
1336
+ }
1337
+ throw error;
1338
+ }
1339
+ }
1340
+ function renderMatchChain(route, matches, pageState, location, navigate, reloadImpl, setPageState) {
1341
+ const errorBoundaryIndex = findErrorBoundaryIndex(matches, pageState);
1342
+ const pendingBoundaryIndex = errorBoundaryIndex === null ? findPendingBoundaryIndex(matches, pageState) : null;
1343
+ if (errorBoundaryIndex === null && pendingBoundaryIndex === null && matches.some((match) => Boolean(match.options?.loader) && !pageState.matchStates[match.id]?.loaderResult)) return React.createElement(React.Fragment);
1344
+ let node = null;
1345
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
1346
+ const match = matches[index];
1347
+ if (!match) continue;
1348
+ let content;
1349
+ if (errorBoundaryIndex !== null && index === errorBoundaryIndex) {
1350
+ const ErrorComponent = match.options?.errorComponent ?? DefaultRouteErrorPage;
1351
+ content = React.createElement(ErrorComponent, { error: pageState.errorInfo });
1352
+ } else if (pendingBoundaryIndex !== null && index === pendingBoundaryIndex) content = React.createElement(match.options?.pendingComponent);
1353
+ else if (errorBoundaryIndex !== null && index > errorBoundaryIndex) continue;
1354
+ else if (pendingBoundaryIndex !== null && index > pendingBoundaryIndex) continue;
1355
+ else if (match.kind === "route") content = React.createElement(match.component);
1356
+ else content = React.createElement(match.component, null, node);
1357
+ node = React.createElement(MatchRuntimeBoundary, {
1358
+ key: match.id,
1359
+ match,
1360
+ route,
1361
+ pageState,
1362
+ location,
1363
+ navigate,
1364
+ reloadImpl,
1365
+ setPageState
1366
+ }, content);
1367
+ }
1368
+ return node ?? React.createElement(React.Fragment);
1369
+ }
1370
+ function findErrorBoundaryIndex(matches, pageState) {
1371
+ if (!pageState.errorInfo || !pageState.errorTargetId) return null;
1372
+ const targetIndex = matches.findIndex((match) => match.id === pageState.errorTargetId);
1373
+ if (targetIndex === -1) return null;
1374
+ for (let index = targetIndex; index >= 0; index -= 1) {
1375
+ const match = matches[index];
1376
+ if (match && match.options?.errorComponent) return index;
1377
+ }
1378
+ return targetIndex;
1379
+ }
1380
+ function findPendingBoundaryIndex(matches, pageState) {
1381
+ if (!pageState.pending) return null;
1382
+ const targetIndex = matches.findIndex((match) => Boolean(match.options?.loader) && !pageState.matchStates[match.id]?.loaderResult);
1383
+ if (targetIndex === -1) return null;
1384
+ for (let index = targetIndex; index >= 0; index -= 1) {
1385
+ const match = matches[index];
1386
+ if (match && match.options?.pendingComponent) return index;
1387
+ }
1388
+ return null;
1389
+ }
1390
+ function DefaultRouteErrorPage(props) {
1391
+ return React.createElement("main", null, React.createElement("h1", null, "Route Error"), React.createElement("p", null, `${props.error.kind} ${props.error.status}: ${props.error.message}`));
1392
+ }
1393
+ function MatchRuntimeBoundary(props) {
1394
+ const runtime = useMatchRuntime(props);
1395
+ return React.createElement(RouteRuntimeProvider, { value: runtime }, props.children);
1396
+ }
1397
+ function useMatchRuntime(options) {
1398
+ const { match, route, pageState, location, navigate, reloadImpl, setPageState } = options;
1399
+ const loaderResult = pageState.matchStates[match.id]?.loaderResult ?? null;
1400
+ const actionResult = match.kind === "route" ? pageState.actionResult : null;
1401
+ const data = match.kind === "route" ? pageState.latestDataResult?.result.data ?? null : loaderResult?.kind === "data" ? loaderResult.data : null;
1402
+ const view = match.kind === "route" ? pageState.latestViewResult?.result.node ?? null : loaderResult?.kind === "view" ? loaderResult.node : null;
1403
+ const setSearch = React.useCallback((updates, submitOptions) => {
1404
+ const result = applySearchParams(new URL(location), updates);
1405
+ if (!result.changed) return;
1406
+ navigate(result.href, submitOptions?.replace);
1407
+ }, [location, navigate]);
1408
+ const submit = React.useCallback(async (payload, submitOptions) => {
1409
+ if (match.kind !== "route" || !route.options?.action) throw new Error(`Route "${match.path}" does not define an action.`);
1410
+ const formData = createFormDataPayload(payload);
1411
+ submitOptions?.onBeforeSubmit?.(formData);
1412
+ setPageState((current) => ({
1413
+ ...current,
1414
+ status: "submitting",
1415
+ pending: true
1416
+ }));
1417
+ const result = await fetchRouteAction(route.path, {
1418
+ params: match.params,
1419
+ search: match.search
1420
+ }, formData);
1421
+ if (result?.kind === "redirect") {
1422
+ navigate(result.location, submitOptions?.replace ?? result.replace);
1423
+ return;
1424
+ }
1425
+ if (result?.kind === "view") setPageState((current) => withLatestViewResult({
1426
+ ...current,
1427
+ actionResult: result,
1428
+ status: "idle",
1429
+ pending: false
1430
+ }, result));
1431
+ else if (result?.kind === "data") setPageState((current) => withLatestDataResult({
1432
+ ...current,
1433
+ actionResult: result,
1434
+ status: "idle",
1435
+ pending: false
1436
+ }, result));
1437
+ else if (result) setPageState((current) => ({
1438
+ ...current,
1439
+ actionResult: result,
1440
+ status: result.kind === "error" || result.kind === "fault" ? "error" : "idle",
1441
+ pending: false,
1442
+ errorInfo: result.kind === "error" || result.kind === "fault" ? result : current.errorInfo,
1443
+ errorTargetId: result.kind === "error" || result.kind === "fault" ? route.id : current.errorTargetId
1444
+ }));
1445
+ if (result && shouldRevalidateAfterSubmit(route, result.headers, submitOptions?.revalidate)) await reloadImpl("revalidating");
1446
+ if (result?.kind === "error" || result?.kind === "fault") submitOptions?.onError?.(result);
1447
+ else if (result) submitOptions?.onSuccess?.(result);
1448
+ }, [
1449
+ match,
1450
+ navigate,
1451
+ reloadImpl,
1452
+ route,
1453
+ setPageState
1454
+ ]);
1455
+ const reload = React.useCallback(() => {
1456
+ reloadImpl("revalidating");
1457
+ }, [reloadImpl]);
1458
+ const retry = React.useCallback(() => {
1459
+ reloadImpl("loading");
1460
+ }, [reloadImpl]);
1461
+ return React.useMemo(() => ({
1462
+ id: match.id,
1463
+ params: match.params,
1464
+ search: match.search,
1465
+ setSearch,
1466
+ status: pageState.status,
1467
+ pending: pageState.pending,
1468
+ loaderResult,
1469
+ actionResult,
1470
+ data,
1471
+ view,
1472
+ submit,
1473
+ reload,
1474
+ retry
1475
+ }), [
1476
+ actionResult,
1477
+ data,
1478
+ loaderResult,
1479
+ match.id,
1480
+ match.params,
1481
+ match.search,
1482
+ pageState.pending,
1483
+ pageState.status,
1484
+ reload,
1485
+ retry,
1486
+ setSearch,
1487
+ submit,
1488
+ view
1489
+ ]);
1490
+ }
1491
+ function getLayoutChain(layout) {
1492
+ if (!layout) return [];
1493
+ const cached = layoutChainCache.get(layout);
1494
+ if (cached) return cached;
1495
+ const chain = [...getLayoutChain(layout.options?.layout), layout];
1496
+ layoutChainCache.set(layout, chain);
1497
+ return chain;
1498
+ }
1499
+ function buildActiveMatches(route, pathname, search) {
1500
+ const routeParams = extractRouteParams(route.path, pathname) ?? {};
1501
+ const layouts = getLayoutChain(route.options?.layout);
1502
+ const sortedSearch = sortRecord(Object.fromEntries(search.entries()));
1503
+ return [...layouts.map((layout) => {
1504
+ const params = extractRouteParams(layout.path, pathname) ?? filterParamsForPath(routeParams, layout.path);
1505
+ return {
1506
+ id: layout.id,
1507
+ path: layout.path,
1508
+ cacheKey: createRouteCacheKey(layout.path, params, sortedSearch),
1509
+ kind: "layout",
1510
+ component: layout.component,
1511
+ options: layout.options,
1512
+ params,
1513
+ search
1514
+ };
1515
+ }), {
1516
+ id: route.id,
1517
+ path: route.path,
1518
+ cacheKey: createRouteCacheKey(route.path, routeParams, sortedSearch),
1519
+ kind: "route",
1520
+ component: route.component,
1521
+ options: route.options,
1522
+ params: routeParams,
1523
+ search
1524
+ }];
1525
+ }
1526
+ function shouldRevalidateAfterSubmit(route, headers, revalidate) {
1527
+ if (revalidate === false) return false;
1528
+ if (revalidate === true) return true;
1529
+ const targets = new Set(getRevalidateTargets(headers));
1530
+ if (Array.isArray(revalidate)) for (const target of revalidate) targets.add(target);
1531
+ if (targets.size === 0) return false;
1532
+ return getActiveMatchPaths(route).some((path) => targets.has(path));
1533
+ }
1534
+ function extractRouteParams(pathPattern, pathname) {
1535
+ return extractRouteLikeParams(pathPattern, pathname);
1536
+ }
1537
+ function filterParamsForPath(params, pathPattern) {
1538
+ const names = getPathParamNames(pathPattern);
1539
+ return Object.fromEntries(names.map((name) => [name, params[name]]).filter((entry) => entry[1] !== void 0));
1540
+ }
1541
+ function createRouteCacheKey(path, params, sortedSearch) {
1542
+ return JSON.stringify({
1543
+ path,
1544
+ params: sortRecord(params),
1545
+ search: sortedSearch
1546
+ });
1547
+ }
1548
+ function getActiveMatchPaths(route) {
1549
+ return [...getLayoutChain(route.options?.layout).map((layout) => layout.path), route.path];
1550
+ }
1551
+ function getPathParamNames(pathPattern) {
1552
+ const cached = pathParamNamesCache.get(pathPattern);
1553
+ if (cached) return cached;
1554
+ const names = Array.from(pathPattern.matchAll(/:([A-Za-z0-9_]+)/g), (match) => match[1]).filter((name) => Boolean(name));
1555
+ pathParamNamesCache.set(pathPattern, names);
1556
+ return names;
1557
+ }
1558
+ function sortRecord(record) {
1559
+ return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
1560
+ }
1561
+ function getCachedLoaderResult(key) {
1562
+ const cached = routeCache.get(key);
1563
+ if (!cached) return;
1564
+ routeCache.delete(key);
1565
+ routeCache.set(key, cached);
1566
+ return cached;
1567
+ }
1568
+ function setCachedLoaderResult(key, result) {
1569
+ if (routeCache.has(key)) routeCache.delete(key);
1570
+ routeCache.set(key, result);
1571
+ while (routeCache.size > ROUTE_CACHE_LIMIT) {
1572
+ const oldestKey = routeCache.keys().next().value;
1573
+ if (oldestKey === void 0) return;
1574
+ routeCache.delete(oldestKey);
1575
+ }
1576
+ }
1577
+ function getCachedRouteModule(id) {
1578
+ const cached = routeModuleCache.get(id);
1579
+ if (!cached) return null;
1580
+ routeModuleCache.delete(id);
1581
+ routeModuleCache.set(id, cached);
1582
+ return cached;
1583
+ }
1584
+ function setCachedRouteModule(id, route) {
1585
+ if (routeModuleCache.has(id)) routeModuleCache.delete(id);
1586
+ routeModuleCache.set(id, route);
1587
+ while (routeModuleCache.size > ROUTE_MODULE_CACHE_LIMIT) {
1588
+ const oldestKey = routeModuleCache.keys().next().value;
1589
+ if (oldestKey === void 0) return;
1590
+ routeModuleCache.delete(oldestKey);
1591
+ }
1592
+ }
1593
+ function prefetchRouteModuleForHref(href, target, download) {
1594
+ const currentUrl = new URL(window.location.href);
1595
+ const nextUrl = new URL(href, currentUrl);
1596
+ if (!shouldPrefetchLink({
1597
+ target,
1598
+ download,
1599
+ currentUrl,
1600
+ nextUrl
1601
+ })) return;
1602
+ const matched = findMatch(nextUrl.pathname);
1603
+ if (!matched || routeModuleCache.has(matched.entry.id)) return;
1604
+ if (routeModulePrefetchCache.get(matched.entry.id)) return;
1605
+ const prefetch = matched.entry.load().then((loaded) => {
1606
+ if (loaded.route) setCachedRouteModule(matched.entry.id, loaded.route);
1607
+ }).catch(() => {}).finally(() => {
1608
+ routeModulePrefetchCache.delete(matched.entry.id);
1609
+ });
1610
+ routeModulePrefetchCache.set(matched.entry.id, prefetch);
1611
+ }
1612
+ function withLoaderStaleState(result, stale) {
1613
+ if (result.stale === stale) return result;
1614
+ if (result.kind === "data") return {
1615
+ ...result,
1616
+ stale
1617
+ };
1618
+ return {
1619
+ ...result,
1620
+ stale,
1621
+ render: result.render
1622
+ };
1623
+ }
1624
+ //#endregion
1625
+ export { Link, mountApp, useLocation, useMatches, useNavigate, usePathname };