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