@pracht/core 0.2.5 → 0.2.7
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/README.md +3 -3
- package/dist/index.d.mts +10 -11
- package/dist/index.mjs +491 -173
- package/package.json +1 -1
- package/dist/app-Cep0el7c.mjs +0 -241
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,241 @@
|
|
|
1
|
-
import { a as matchApiRoute, c as resolveApp, i as group, l as route, n as buildPathFromSegments, o as matchAppRoute, r as defineApp, s as resolveApiRoutes, u as timeRevalidate } from "./app-Cep0el7c.mjs";
|
|
2
1
|
import { createContext, h, hydrate, options, render } from "preact";
|
|
3
2
|
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
|
4
3
|
import { Suspense, lazy } from "preact-suspense";
|
|
4
|
+
//#region src/app.ts
|
|
5
|
+
function timeRevalidate(seconds) {
|
|
6
|
+
if (!Number.isInteger(seconds) || seconds <= 0) throw new Error("timeRevalidate expects a positive integer number of seconds.");
|
|
7
|
+
return {
|
|
8
|
+
kind: "time",
|
|
9
|
+
seconds
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function route(path, fileOrConfig, meta = {}) {
|
|
13
|
+
if (typeof fileOrConfig === "string" || typeof fileOrConfig === "function") return {
|
|
14
|
+
kind: "route",
|
|
15
|
+
path: normalizeRoutePath(path),
|
|
16
|
+
file: resolveModuleRef(fileOrConfig),
|
|
17
|
+
...meta
|
|
18
|
+
};
|
|
19
|
+
const { component, loader, ...routeMeta } = fileOrConfig;
|
|
20
|
+
return {
|
|
21
|
+
kind: "route",
|
|
22
|
+
path: normalizeRoutePath(path),
|
|
23
|
+
file: resolveModuleRef(component),
|
|
24
|
+
loaderFile: resolveModuleRef(loader),
|
|
25
|
+
...routeMeta
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveModuleRef(ref) {
|
|
29
|
+
if (ref === void 0) return void 0;
|
|
30
|
+
if (typeof ref === "string") return ref;
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
function group(meta, routes) {
|
|
34
|
+
return {
|
|
35
|
+
kind: "group",
|
|
36
|
+
meta,
|
|
37
|
+
routes
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function defineApp(config) {
|
|
41
|
+
return {
|
|
42
|
+
shells: resolveModuleRefRecord(config.shells ?? {}),
|
|
43
|
+
middleware: resolveModuleRefRecord(config.middleware ?? {}),
|
|
44
|
+
api: config.api ?? {},
|
|
45
|
+
routes: config.routes
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function resolveModuleRefRecord(record) {
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const [key, value] of Object.entries(record)) result[key] = resolveModuleRef(value);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function resolveApp(app) {
|
|
54
|
+
const routes = [];
|
|
55
|
+
const inherited = {
|
|
56
|
+
pathPrefix: "/",
|
|
57
|
+
middleware: []
|
|
58
|
+
};
|
|
59
|
+
for (const node of app.routes) flattenRouteNode(app, node, inherited, routes);
|
|
60
|
+
return {
|
|
61
|
+
shells: app.shells,
|
|
62
|
+
middleware: app.middleware,
|
|
63
|
+
api: app.api,
|
|
64
|
+
routes,
|
|
65
|
+
apiRoutes: []
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function matchAppRoute(app, pathname) {
|
|
69
|
+
const resolved = isResolvedApp(app) ? app : resolveApp(app);
|
|
70
|
+
const normalizedPathname = normalizeRoutePath(pathname);
|
|
71
|
+
const targetSegments = splitPathSegments(normalizedPathname);
|
|
72
|
+
for (const currentRoute of resolved.routes) {
|
|
73
|
+
const params = matchRouteSegments(currentRoute.segments, targetSegments);
|
|
74
|
+
if (params) return {
|
|
75
|
+
route: currentRoute,
|
|
76
|
+
params,
|
|
77
|
+
pathname: normalizedPathname
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function flattenRouteNode(app, node, inherited, routes) {
|
|
82
|
+
if (node.kind === "group") {
|
|
83
|
+
const nextInherited = {
|
|
84
|
+
pathPrefix: mergeRoutePaths(inherited.pathPrefix, node.meta.pathPrefix),
|
|
85
|
+
shell: node.meta.shell ?? inherited.shell,
|
|
86
|
+
render: node.meta.render ?? inherited.render,
|
|
87
|
+
middleware: [...inherited.middleware, ...node.meta.middleware ?? []]
|
|
88
|
+
};
|
|
89
|
+
for (const child of node.routes) flattenRouteNode(app, child, nextInherited, routes);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const fullPath = mergeRoutePaths(inherited.pathPrefix, node.path);
|
|
93
|
+
const shell = node.shell ?? inherited.shell;
|
|
94
|
+
const middleware = [...inherited.middleware, ...node.middleware ?? []];
|
|
95
|
+
routes.push({
|
|
96
|
+
id: node.id ?? createRouteId(fullPath),
|
|
97
|
+
path: fullPath,
|
|
98
|
+
file: node.file,
|
|
99
|
+
loaderFile: node.loaderFile,
|
|
100
|
+
shell,
|
|
101
|
+
shellFile: shell ? app.shells[shell] : void 0,
|
|
102
|
+
render: node.render ?? inherited.render,
|
|
103
|
+
middleware,
|
|
104
|
+
middlewareFiles: middleware.flatMap((name) => {
|
|
105
|
+
const middlewareFile = app.middleware[name];
|
|
106
|
+
return middlewareFile ? [middlewareFile] : [];
|
|
107
|
+
}),
|
|
108
|
+
revalidate: node.revalidate,
|
|
109
|
+
segments: parseRouteSegments(fullPath)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function isResolvedApp(app) {
|
|
113
|
+
return app.routes.length === 0 || "segments" in app.routes[0];
|
|
114
|
+
}
|
|
115
|
+
function matchRouteSegments(routeSegments, targetSegments) {
|
|
116
|
+
const params = {};
|
|
117
|
+
let routeIndex = 0;
|
|
118
|
+
let targetIndex = 0;
|
|
119
|
+
while (routeIndex < routeSegments.length) {
|
|
120
|
+
const currentSegment = routeSegments[routeIndex];
|
|
121
|
+
if (currentSegment.type === "catchall") {
|
|
122
|
+
params[currentSegment.name] = targetSegments.slice(targetIndex).join("/");
|
|
123
|
+
return params;
|
|
124
|
+
}
|
|
125
|
+
const targetSegment = targetSegments[targetIndex];
|
|
126
|
+
if (typeof targetSegment === "undefined") return null;
|
|
127
|
+
if (currentSegment.type === "static") {
|
|
128
|
+
if (currentSegment.value !== targetSegment) return null;
|
|
129
|
+
} else try {
|
|
130
|
+
params[currentSegment.name] = decodeURIComponent(targetSegment);
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
routeIndex += 1;
|
|
135
|
+
targetIndex += 1;
|
|
136
|
+
}
|
|
137
|
+
return targetIndex === targetSegments.length ? params : null;
|
|
138
|
+
}
|
|
139
|
+
function parseRouteSegments(path) {
|
|
140
|
+
return splitPathSegments(path).map((segment) => {
|
|
141
|
+
if (segment === "*") return {
|
|
142
|
+
type: "catchall",
|
|
143
|
+
name: "*"
|
|
144
|
+
};
|
|
145
|
+
if (segment.startsWith(":")) return {
|
|
146
|
+
type: "param",
|
|
147
|
+
name: segment.slice(1)
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
type: "static",
|
|
151
|
+
value: segment
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function splitPathSegments(path) {
|
|
156
|
+
return normalizeRoutePath(path).split("/").filter(Boolean);
|
|
157
|
+
}
|
|
158
|
+
function mergeRoutePaths(prefix, path) {
|
|
159
|
+
if (!path) return normalizeRoutePath(prefix);
|
|
160
|
+
const normalizedPrefix = normalizeRoutePath(prefix);
|
|
161
|
+
const normalizedPath = normalizeRoutePath(path);
|
|
162
|
+
if (normalizedPrefix === "/") return normalizedPath;
|
|
163
|
+
if (normalizedPath === "/") return normalizedPrefix;
|
|
164
|
+
return normalizeRoutePath(`${normalizedPrefix}/${normalizedPath.slice(1)}`);
|
|
165
|
+
}
|
|
166
|
+
function normalizeRoutePath(path) {
|
|
167
|
+
if (!path || path === "/") return "/";
|
|
168
|
+
const collapsed = (path.startsWith("/") ? path : `/${path}`).replace(/\/{2,}/g, "/");
|
|
169
|
+
return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
|
|
170
|
+
}
|
|
171
|
+
function buildPathFromSegments(segments, params) {
|
|
172
|
+
return normalizeRoutePath("/" + segments.map((segment) => {
|
|
173
|
+
if (segment.type === "static") return segment.value;
|
|
174
|
+
if (segment.type === "param") return encodeURIComponent(params[segment.name] ?? "");
|
|
175
|
+
return params["*"] ?? "";
|
|
176
|
+
}).join("/"));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Convert a list of file paths from `import.meta.glob` into resolved API routes.
|
|
180
|
+
*
|
|
181
|
+
* Example: `"/src/api/health.ts"` → path `/api/health`
|
|
182
|
+
* `"/src/api/users/[id].ts"` → path `/api/users/:id`
|
|
183
|
+
* `"/src/api/index.ts"` → path `/api`
|
|
184
|
+
*/
|
|
185
|
+
function resolveApiRoutes(files, apiDir = "/src/api") {
|
|
186
|
+
const normalizedDir = apiDir.replace(/\/$/, "");
|
|
187
|
+
return files.map((file) => {
|
|
188
|
+
let relative = file;
|
|
189
|
+
if (relative.startsWith(normalizedDir)) relative = relative.slice(normalizedDir.length);
|
|
190
|
+
relative = relative.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
191
|
+
if (relative.endsWith("/index")) relative = relative.slice(0, -6) || "/";
|
|
192
|
+
relative = relative.replace(/\[([^\]]+)\]/g, ":$1");
|
|
193
|
+
const path = normalizeRoutePath(`/api${relative}`);
|
|
194
|
+
return {
|
|
195
|
+
path,
|
|
196
|
+
file,
|
|
197
|
+
segments: parseRouteSegments(path)
|
|
198
|
+
};
|
|
199
|
+
}).sort(compareResolvedApiRoutes);
|
|
200
|
+
}
|
|
201
|
+
function matchApiRoute(apiRoutes, pathname) {
|
|
202
|
+
const normalizedPathname = normalizeRoutePath(pathname);
|
|
203
|
+
const targetSegments = splitPathSegments(normalizedPathname);
|
|
204
|
+
for (const route of apiRoutes) {
|
|
205
|
+
const params = matchRouteSegments(route.segments, targetSegments);
|
|
206
|
+
if (params) return {
|
|
207
|
+
route,
|
|
208
|
+
params,
|
|
209
|
+
pathname: normalizedPathname
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function createRouteId(path) {
|
|
214
|
+
if (path === "/") return "index";
|
|
215
|
+
return path.slice(1).split("/").map((segment) => {
|
|
216
|
+
if (segment === "*") return "splat";
|
|
217
|
+
return segment.startsWith(":") ? segment.slice(1) : segment;
|
|
218
|
+
}).join("-").replace(/[^a-zA-Z0-9-]/g, "-");
|
|
219
|
+
}
|
|
220
|
+
function compareResolvedApiRoutes(left, right) {
|
|
221
|
+
const length = Math.max(left.segments.length, right.segments.length);
|
|
222
|
+
for (let index = 0; index < length; index += 1) {
|
|
223
|
+
const leftSegment = left.segments[index];
|
|
224
|
+
const rightSegment = right.segments[index];
|
|
225
|
+
if (!leftSegment) return 1;
|
|
226
|
+
if (!rightSegment) return -1;
|
|
227
|
+
const leftScore = getRouteSegmentSpecificity(leftSegment);
|
|
228
|
+
const rightScore = getRouteSegmentSpecificity(rightSegment);
|
|
229
|
+
if (leftScore !== rightScore) return rightScore - leftScore;
|
|
230
|
+
}
|
|
231
|
+
return left.path.localeCompare(right.path);
|
|
232
|
+
}
|
|
233
|
+
function getRouteSegmentSpecificity(segment) {
|
|
234
|
+
if (segment.type === "static") return 3;
|
|
235
|
+
if (segment.type === "param") return 2;
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
5
239
|
//#region src/forwardRef.ts
|
|
6
240
|
let oldDiffHook = options.__b;
|
|
7
241
|
options.__b = (vnode) => {
|
|
@@ -80,6 +314,7 @@ const SAFE_METHODS = new Set(["GET", "HEAD"]);
|
|
|
80
314
|
const HYDRATION_STATE_ELEMENT_ID = "pracht-state";
|
|
81
315
|
const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
|
|
82
316
|
const ROUTE_STATE_CACHE_CONTROL = "no-store";
|
|
317
|
+
const EMPTY_ROUTE_PARAMS = {};
|
|
83
318
|
let _renderToStringAsync;
|
|
84
319
|
async function getRenderToStringAsync() {
|
|
85
320
|
if (_renderToStringAsync) return _renderToStringAsync;
|
|
@@ -87,25 +322,37 @@ async function getRenderToStringAsync() {
|
|
|
87
322
|
return _renderToStringAsync;
|
|
88
323
|
}
|
|
89
324
|
const RouteDataContext = createContext(void 0);
|
|
90
|
-
function PrachtRuntimeProvider({ children, data, params =
|
|
91
|
-
const [
|
|
325
|
+
function PrachtRuntimeProvider({ children, data, params = EMPTY_ROUTE_PARAMS, routeId, stateVersion = 0, url }) {
|
|
326
|
+
const [routeDataState, setRouteDataState] = useState({
|
|
327
|
+
data,
|
|
328
|
+
stateVersion
|
|
329
|
+
});
|
|
330
|
+
const routeData = routeDataState.stateVersion === stateVersion ? routeDataState.data : data;
|
|
92
331
|
useEffect(() => {
|
|
93
|
-
|
|
332
|
+
setRouteDataState({
|
|
333
|
+
data,
|
|
334
|
+
stateVersion
|
|
335
|
+
});
|
|
94
336
|
}, [
|
|
95
337
|
data,
|
|
96
338
|
routeId,
|
|
339
|
+
stateVersion,
|
|
97
340
|
url
|
|
98
341
|
]);
|
|
99
342
|
const context = useMemo(() => ({
|
|
100
343
|
data: routeData,
|
|
101
344
|
params,
|
|
102
345
|
routeId,
|
|
103
|
-
setData:
|
|
346
|
+
setData: (nextData) => setRouteDataState({
|
|
347
|
+
data: nextData,
|
|
348
|
+
stateVersion
|
|
349
|
+
}),
|
|
104
350
|
url
|
|
105
351
|
}), [
|
|
106
352
|
routeData,
|
|
107
353
|
params,
|
|
108
354
|
routeId,
|
|
355
|
+
stateVersion,
|
|
109
356
|
url
|
|
110
357
|
]);
|
|
111
358
|
return h(RouteDataContext.Provider, {
|
|
@@ -125,19 +372,15 @@ function readHydrationState() {
|
|
|
125
372
|
if (!(element instanceof HTMLScriptElement)) return;
|
|
126
373
|
const raw = element.textContent;
|
|
127
374
|
if (!raw) return;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return state;
|
|
132
|
-
} catch {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
375
|
+
const state = JSON.parse(raw);
|
|
376
|
+
window.__PRACHT_STATE__ = state;
|
|
377
|
+
return state;
|
|
135
378
|
}
|
|
136
379
|
function useRouteData() {
|
|
137
380
|
return useContext(RouteDataContext)?.data;
|
|
138
381
|
}
|
|
139
382
|
function useLocation() {
|
|
140
|
-
return
|
|
383
|
+
return parseLocation(useContext(RouteDataContext)?.url ?? (typeof window !== "undefined" ? window.location.pathname + window.location.search : "/"));
|
|
141
384
|
}
|
|
142
385
|
function useParams() {
|
|
143
386
|
return useContext(RouteDataContext)?.params ?? {};
|
|
@@ -151,13 +394,11 @@ function useRevalidate() {
|
|
|
151
394
|
await navigateToClientLocation(result.location);
|
|
152
395
|
return;
|
|
153
396
|
}
|
|
154
|
-
if (result.type === "error") throw deserializeRouteError
|
|
397
|
+
if (result.type === "error") throw deserializeRouteError(result.error);
|
|
155
398
|
runtime?.setData(result.data);
|
|
156
399
|
return result.data;
|
|
157
400
|
};
|
|
158
401
|
}
|
|
159
|
-
/** @deprecated Use useRevalidate instead. */
|
|
160
|
-
const useRevalidateRoute = useRevalidate;
|
|
161
402
|
function Form(props) {
|
|
162
403
|
const { onSubmit, method, ...rest } = props;
|
|
163
404
|
return h("form", {
|
|
@@ -187,9 +428,10 @@ function Form(props) {
|
|
|
187
428
|
}
|
|
188
429
|
});
|
|
189
430
|
}
|
|
190
|
-
async function fetchPrachtRouteState(url) {
|
|
191
|
-
const
|
|
192
|
-
|
|
431
|
+
async function fetchPrachtRouteState(url, options) {
|
|
432
|
+
const fetchUrl = options?.useDataParam ? buildRouteStateUrl(url) : url;
|
|
433
|
+
const response = await fetch(fetchUrl, {
|
|
434
|
+
headers: options?.useDataParam ? {} : {
|
|
193
435
|
[ROUTE_STATE_REQUEST_HEADER]: "1",
|
|
194
436
|
"Cache-Control": "no-cache"
|
|
195
437
|
},
|
|
@@ -200,6 +442,10 @@ async function fetchPrachtRouteState(url) {
|
|
|
200
442
|
type: "redirect"
|
|
201
443
|
};
|
|
202
444
|
const json = await response.json();
|
|
445
|
+
if (json.redirect) return {
|
|
446
|
+
location: json.redirect,
|
|
447
|
+
type: "redirect"
|
|
448
|
+
};
|
|
203
449
|
if (!response.ok) {
|
|
204
450
|
if (json.error) return {
|
|
205
451
|
error: json.error,
|
|
@@ -212,10 +458,16 @@ async function fetchPrachtRouteState(url) {
|
|
|
212
458
|
type: "data"
|
|
213
459
|
};
|
|
214
460
|
}
|
|
461
|
+
function buildRouteStateUrl(url) {
|
|
462
|
+
return `${url}${url.includes("?") ? "&" : "?"}_data=1`;
|
|
463
|
+
}
|
|
215
464
|
async function handlePrachtRequest(options) {
|
|
216
465
|
const url = new URL(options.request.url);
|
|
466
|
+
const hasDataParam = url.searchParams.get("_data") === "1";
|
|
467
|
+
if (hasDataParam) url.searchParams.delete("_data");
|
|
468
|
+
const requestPath = getRequestPath(url);
|
|
217
469
|
const registry = options.registry ?? {};
|
|
218
|
-
const isRouteStateRequest = options.request.headers.get(ROUTE_STATE_REQUEST_HEADER) === "1";
|
|
470
|
+
const isRouteStateRequest = options.request.headers.get(ROUTE_STATE_REQUEST_HEADER) === "1" || hasDataParam;
|
|
219
471
|
const exposeDiagnostics = shouldExposeServerErrors(options);
|
|
220
472
|
if (options.apiRoutes?.length) {
|
|
221
473
|
const apiMatch = matchApiRoute(options.apiRoutes, url.pathname);
|
|
@@ -306,7 +558,7 @@ async function handlePrachtRequest(options) {
|
|
|
306
558
|
let loaderFile;
|
|
307
559
|
let currentPhase = "middleware";
|
|
308
560
|
try {
|
|
309
|
-
const
|
|
561
|
+
const middlewarePromise = runMiddlewareChain({
|
|
310
562
|
context: routeArgs.context,
|
|
311
563
|
middlewareFiles: match.route.middlewareFiles,
|
|
312
564
|
params: match.params,
|
|
@@ -315,25 +567,31 @@ async function handlePrachtRequest(options) {
|
|
|
315
567
|
route: match.route,
|
|
316
568
|
url
|
|
317
569
|
});
|
|
318
|
-
|
|
570
|
+
const routeModulePromise = resolveRegistryModule(registry.routeModules, match.route.file);
|
|
571
|
+
const shellModulePromise = match.route.shellFile ? resolveRegistryModule(registry.shellModules, match.route.shellFile) : Promise.resolve(void 0);
|
|
572
|
+
const dataFunctionsPromise = routeModulePromise.then((mod) => resolveDataFunctions(match.route, mod, registry));
|
|
573
|
+
routeModulePromise.catch(() => {});
|
|
574
|
+
shellModulePromise.catch(() => {});
|
|
575
|
+
dataFunctionsPromise.catch(() => {});
|
|
576
|
+
const middlewareResult = await middlewarePromise;
|
|
577
|
+
if (middlewareResult.response) return normalizePageResponse(middlewareResult.response, { isRouteStateRequest });
|
|
319
578
|
routeArgs = {
|
|
320
579
|
...routeArgs,
|
|
321
580
|
context: middlewareResult.context
|
|
322
581
|
};
|
|
323
582
|
currentPhase = "render";
|
|
324
|
-
routeModule = await
|
|
583
|
+
routeModule = await routeModulePromise;
|
|
325
584
|
if (!routeModule) throw new Error("Route module not found");
|
|
326
585
|
currentPhase = "loader";
|
|
327
|
-
const { loader, loaderFile: resolvedLoaderFile } = await
|
|
586
|
+
const { loader, loaderFile: resolvedLoaderFile } = await dataFunctionsPromise;
|
|
328
587
|
loaderFile = resolvedLoaderFile;
|
|
329
588
|
const loaderResult = loader ? await loader(routeArgs) : void 0;
|
|
330
|
-
if (loaderResult instanceof Response) return
|
|
589
|
+
if (loaderResult instanceof Response) return normalizePageResponse(loaderResult, { isRouteStateRequest });
|
|
331
590
|
const data = loaderResult;
|
|
332
591
|
if (isRouteStateRequest) return withRouteResponseHeaders(Response.json({ data }), { isRouteStateRequest: true });
|
|
333
592
|
currentPhase = "render";
|
|
334
|
-
shellModule =
|
|
335
|
-
const head = await mergeHeadMetadata(shellModule, routeModule, routeArgs, data);
|
|
336
|
-
const documentHeaders = await mergeDocumentHeaders(shellModule, routeModule, routeArgs, data);
|
|
593
|
+
shellModule = await shellModulePromise;
|
|
594
|
+
const [head, documentHeaders] = await Promise.all([mergeHeadMetadata(shellModule, routeModule, routeArgs, data), mergeDocumentHeaders(shellModule, routeModule, routeArgs, data)]);
|
|
337
595
|
const cssUrls = resolvePageCssUrls(options, match.route.shellFile, match.route.file);
|
|
338
596
|
const modulePreloadUrls = resolvePageJsUrls(options, match.route.shellFile, match.route.file);
|
|
339
597
|
if (match.route.render === "spa") {
|
|
@@ -348,7 +606,7 @@ async function handlePrachtRequest(options) {
|
|
|
348
606
|
head,
|
|
349
607
|
body,
|
|
350
608
|
hydrationState: {
|
|
351
|
-
url:
|
|
609
|
+
url: requestPath,
|
|
352
610
|
routeId: match.route.id ?? "",
|
|
353
611
|
data: null,
|
|
354
612
|
error: null,
|
|
@@ -356,29 +614,31 @@ async function handlePrachtRequest(options) {
|
|
|
356
614
|
},
|
|
357
615
|
clientEntryUrl: options.clientEntryUrl,
|
|
358
616
|
cssUrls,
|
|
359
|
-
modulePreloadUrls
|
|
617
|
+
modulePreloadUrls,
|
|
618
|
+
routeStatePreloadUrl: loader ? buildRouteStateUrl(requestPath) : void 0
|
|
360
619
|
}), 200, documentHeaders);
|
|
361
620
|
}
|
|
362
621
|
const DefaultComponent = typeof routeModule.default === "function" ? routeModule.default : void 0;
|
|
363
622
|
const Component = routeModule.Component ?? DefaultComponent;
|
|
364
623
|
if (!Component) throw new Error("Route has no Component or default export");
|
|
365
624
|
const Shell = shellModule?.Shell;
|
|
625
|
+
const Comp = Component;
|
|
366
626
|
const componentProps = {
|
|
367
627
|
data,
|
|
368
628
|
params: match.params
|
|
369
629
|
};
|
|
370
|
-
const componentTree = Shell ? h(Shell, null, h(
|
|
630
|
+
const componentTree = Shell ? h(Shell, null, h(Comp, componentProps)) : h(Comp, componentProps);
|
|
371
631
|
const tree = h(PrachtRuntimeProvider, {
|
|
372
632
|
data,
|
|
373
633
|
params: match.params,
|
|
374
634
|
routeId: match.route.id ?? "",
|
|
375
|
-
url:
|
|
635
|
+
url: requestPath
|
|
376
636
|
}, componentTree);
|
|
377
637
|
return htmlResponse(buildHtmlDocument({
|
|
378
638
|
head,
|
|
379
639
|
body: await (await getRenderToStringAsync())(tree),
|
|
380
640
|
hydrationState: {
|
|
381
|
-
url:
|
|
641
|
+
url: requestPath,
|
|
382
642
|
routeId: match.route.id ?? "",
|
|
383
643
|
data,
|
|
384
644
|
error: null
|
|
@@ -399,10 +659,20 @@ async function handlePrachtRequest(options) {
|
|
|
399
659
|
routeModule,
|
|
400
660
|
shellFile: match.route.shellFile,
|
|
401
661
|
shellModule,
|
|
402
|
-
|
|
662
|
+
requestPath
|
|
403
663
|
});
|
|
404
664
|
}
|
|
405
665
|
}
|
|
666
|
+
function parseLocation(value) {
|
|
667
|
+
const url = new URL(value, "http://pracht.local");
|
|
668
|
+
return {
|
|
669
|
+
pathname: url.pathname,
|
|
670
|
+
search: url.search
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function getRequestPath(url) {
|
|
674
|
+
return `${url.pathname}${url.search}`;
|
|
675
|
+
}
|
|
406
676
|
/** Strip leading `./` and `/` so all module paths share one canonical form. */
|
|
407
677
|
function normalizeModulePath(path) {
|
|
408
678
|
return path.replace(/^\.?\//, "");
|
|
@@ -433,27 +703,23 @@ function resolveManifestEntries(manifest, file) {
|
|
|
433
703
|
const resolved = getSuffixIndex(manifest).get(normalizeModulePath(file));
|
|
434
704
|
if (resolved) return manifest[resolved];
|
|
435
705
|
}
|
|
706
|
+
function resolvePageUrlsFromManifest(manifest, shellFile, routeFile) {
|
|
707
|
+
const urls = /* @__PURE__ */ new Set();
|
|
708
|
+
const add = (file) => {
|
|
709
|
+
const entries = resolveManifestEntries(manifest, file);
|
|
710
|
+
if (entries) for (const url of entries) urls.add(url);
|
|
711
|
+
};
|
|
712
|
+
if (shellFile) add(shellFile);
|
|
713
|
+
add(routeFile);
|
|
714
|
+
return [...urls];
|
|
715
|
+
}
|
|
436
716
|
function resolvePageCssUrls(options, shellFile, routeFile) {
|
|
437
|
-
if (!options.cssManifest) return
|
|
438
|
-
|
|
439
|
-
function addFromManifest(file) {
|
|
440
|
-
const entries = resolveManifestEntries(options.cssManifest, file);
|
|
441
|
-
if (entries) for (const c of entries) css.add(c);
|
|
442
|
-
}
|
|
443
|
-
if (shellFile) addFromManifest(shellFile);
|
|
444
|
-
addFromManifest(routeFile);
|
|
445
|
-
return [...css];
|
|
717
|
+
if (!options.cssManifest) return [];
|
|
718
|
+
return resolvePageUrlsFromManifest(options.cssManifest, shellFile, routeFile);
|
|
446
719
|
}
|
|
447
720
|
function resolvePageJsUrls(options, shellFile, routeFile) {
|
|
448
721
|
if (!options.jsManifest) return [];
|
|
449
|
-
|
|
450
|
-
function addFromManifest(file) {
|
|
451
|
-
const entries = resolveManifestEntries(options.jsManifest, file);
|
|
452
|
-
if (entries) for (const j of entries) js.add(j);
|
|
453
|
-
}
|
|
454
|
-
if (shellFile) addFromManifest(shellFile);
|
|
455
|
-
addFromManifest(routeFile);
|
|
456
|
-
return [...js];
|
|
722
|
+
return resolvePageUrlsFromManifest(options.jsManifest, shellFile, routeFile);
|
|
457
723
|
}
|
|
458
724
|
async function navigateToClientLocation(location, options) {
|
|
459
725
|
if (typeof window === "undefined") return;
|
|
@@ -539,7 +805,7 @@ function normalizeRouteError(error, options) {
|
|
|
539
805
|
status: 500
|
|
540
806
|
};
|
|
541
807
|
}
|
|
542
|
-
function deserializeRouteError
|
|
808
|
+
function deserializeRouteError(error) {
|
|
543
809
|
const result = new Error(error.message);
|
|
544
810
|
result.name = error.name;
|
|
545
811
|
result.status = error.status;
|
|
@@ -553,6 +819,24 @@ function jsonErrorResponse(routeError, options) {
|
|
|
553
819
|
headers
|
|
554
820
|
});
|
|
555
821
|
}
|
|
822
|
+
function jsonRedirectResponse(location, options) {
|
|
823
|
+
const headers = new Headers(options.headers);
|
|
824
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
825
|
+
return withRouteResponseHeaders(new Response(JSON.stringify({ redirect: location }), {
|
|
826
|
+
status: 200,
|
|
827
|
+
headers
|
|
828
|
+
}), { isRouteStateRequest: options.isRouteStateRequest });
|
|
829
|
+
}
|
|
830
|
+
function normalizePageResponse(response, options) {
|
|
831
|
+
if (options.isRouteStateRequest && response.status >= 300 && response.status < 400) {
|
|
832
|
+
const location = response.headers.get("location");
|
|
833
|
+
if (location) return jsonRedirectResponse(location, {
|
|
834
|
+
headers: response.headers,
|
|
835
|
+
isRouteStateRequest: true
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return withRouteResponseHeaders(response, options);
|
|
839
|
+
}
|
|
556
840
|
function renderApiErrorResponse(options) {
|
|
557
841
|
const exposeDetails = shouldExposeServerErrors(options.options);
|
|
558
842
|
const routeError = normalizeRouteError(options.error, { exposeDetails });
|
|
@@ -603,17 +887,17 @@ async function renderRouteErrorResponse(options) {
|
|
|
603
887
|
const renderToString = await getRenderToStringAsync();
|
|
604
888
|
const ErrorBoundary = options.routeModule.ErrorBoundary;
|
|
605
889
|
const Shell = shellModule?.Shell;
|
|
606
|
-
const errorValue = deserializeRouteError
|
|
890
|
+
const errorValue = deserializeRouteError(routeErrorWithDiagnostics);
|
|
607
891
|
const componentTree = Shell ? h(Shell, null, h(ErrorBoundary, { error: errorValue })) : h(ErrorBoundary, { error: errorValue });
|
|
608
892
|
return htmlResponse(buildHtmlDocument({
|
|
609
893
|
head,
|
|
610
894
|
body: await renderToString(h(PrachtRuntimeProvider, {
|
|
611
895
|
data: null,
|
|
612
896
|
routeId: options.routeId,
|
|
613
|
-
url: options.
|
|
897
|
+
url: options.requestPath
|
|
614
898
|
}, componentTree)),
|
|
615
899
|
hydrationState: {
|
|
616
|
-
url: options.
|
|
900
|
+
url: options.requestPath,
|
|
617
901
|
routeId: options.routeId,
|
|
618
902
|
data: null,
|
|
619
903
|
error: routeErrorWithDiagnostics
|
|
@@ -625,8 +909,10 @@ async function renderRouteErrorResponse(options) {
|
|
|
625
909
|
}
|
|
626
910
|
async function runMiddlewareChain(options) {
|
|
627
911
|
let context = options.context;
|
|
628
|
-
|
|
629
|
-
|
|
912
|
+
const modulePromises = options.middlewareFiles.map((mwFile) => resolveRegistryModule(options.registry.middlewareModules, mwFile));
|
|
913
|
+
for (const p of modulePromises) p.catch(() => {});
|
|
914
|
+
for (const modulePromise of modulePromises) {
|
|
915
|
+
const mwModule = await modulePromise;
|
|
630
916
|
if (!mwModule?.middleware) continue;
|
|
631
917
|
const result = await mwModule.middleware({
|
|
632
918
|
request: options.request,
|
|
@@ -671,11 +957,10 @@ async function resolveRegistryModule(modules, file) {
|
|
|
671
957
|
if (resolved) return modules[resolved]();
|
|
672
958
|
}
|
|
673
959
|
async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
|
|
674
|
-
const shellHead = shellModule?.head ?
|
|
675
|
-
const routeHead = routeModule?.head ? await routeModule.head({
|
|
960
|
+
const [shellHead, routeHead] = await Promise.all([shellModule?.head ? shellModule.head(routeArgs) : Promise.resolve({}), routeModule?.head ? routeModule.head({
|
|
676
961
|
...routeArgs,
|
|
677
962
|
data
|
|
678
|
-
}) : {};
|
|
963
|
+
}) : Promise.resolve({})]);
|
|
679
964
|
return {
|
|
680
965
|
title: routeHead.title ?? shellHead.title,
|
|
681
966
|
lang: routeHead.lang ?? shellHead.lang,
|
|
@@ -685,12 +970,11 @@ async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
|
|
|
685
970
|
}
|
|
686
971
|
async function mergeDocumentHeaders(shellModule, routeModule, routeArgs, data) {
|
|
687
972
|
const headers = new Headers();
|
|
688
|
-
const shellHeaders = shellModule?.headers ?
|
|
689
|
-
if (shellHeaders) applyHeaders(headers, shellHeaders);
|
|
690
|
-
const routeHeaders = routeModule?.headers ? await routeModule.headers({
|
|
973
|
+
const [shellHeaders, routeHeaders] = await Promise.all([shellModule?.headers ? shellModule.headers(routeArgs) : Promise.resolve(void 0), routeModule?.headers ? routeModule.headers({
|
|
691
974
|
...routeArgs,
|
|
692
975
|
data
|
|
693
|
-
}) : void 0;
|
|
976
|
+
}) : Promise.resolve(void 0)]);
|
|
977
|
+
if (shellHeaders) applyHeaders(headers, shellHeaders);
|
|
694
978
|
if (routeHeaders) applyHeaders(headers, routeHeaders);
|
|
695
979
|
return headers;
|
|
696
980
|
}
|
|
@@ -700,12 +984,13 @@ function applyHeaders(headers, init) {
|
|
|
700
984
|
});
|
|
701
985
|
}
|
|
702
986
|
function buildHtmlDocument(options) {
|
|
703
|
-
const { head, body, hydrationState, clientEntryUrl, cssUrls = [], modulePreloadUrls = [] } = options;
|
|
987
|
+
const { head, body, hydrationState, clientEntryUrl, cssUrls = [], modulePreloadUrls = [], routeStatePreloadUrl } = options;
|
|
704
988
|
const titleTag = head.title ? `<title>${escapeHtml(head.title)}</title>` : "";
|
|
705
989
|
const metaTags = (head.meta ?? []).map((m) => `<meta ${Object.entries(m).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ")}>`).join("\n ");
|
|
706
990
|
const linkTags = (head.link ?? []).map((l) => `<link ${Object.entries(l).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ")}>`).join("\n ");
|
|
707
991
|
const cssTags = cssUrls.map((url) => `<link rel="stylesheet" href="${escapeHtml(url)}">`).join("\n ");
|
|
708
992
|
const modulePreloadTags = modulePreloadUrls.map((url) => `<link rel="modulepreload" href="${escapeHtml(url)}">`).join("\n ");
|
|
993
|
+
const routeStatePreloadTag = routeStatePreloadUrl ? `<link rel="preload" as="fetch" href="${escapeHtml(routeStatePreloadUrl)}" crossorigin="anonymous">` : "";
|
|
709
994
|
const stateScript = `<script id="${HYDRATION_STATE_ELEMENT_ID}" type="application/json">${serializeJsonForHtml(hydrationState)}<\/script>`;
|
|
710
995
|
const entryScript = clientEntryUrl ? `<script type="module" src="${escapeHtml(clientEntryUrl)}"><\/script>` : "";
|
|
711
996
|
return `<!DOCTYPE html>
|
|
@@ -717,6 +1002,7 @@ function buildHtmlDocument(options) {
|
|
|
717
1002
|
${linkTags}
|
|
718
1003
|
${cssTags}
|
|
719
1004
|
${modulePreloadTags}
|
|
1005
|
+
${routeStatePreloadTag}
|
|
720
1006
|
</head>
|
|
721
1007
|
<body>
|
|
722
1008
|
<div id="pracht-root">${body}</div>
|
|
@@ -784,7 +1070,6 @@ function serializeJsonForHtml(value) {
|
|
|
784
1070
|
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
785
1071
|
}
|
|
786
1072
|
async function prerenderApp(options) {
|
|
787
|
-
const { resolveApp } = await import("./app-Cep0el7c.mjs").then((n) => n.t);
|
|
788
1073
|
const resolved = resolveApp(options.app);
|
|
789
1074
|
const results = [];
|
|
790
1075
|
const isgManifest = {};
|
|
@@ -846,7 +1131,6 @@ async function collectSSGPaths(route, registry) {
|
|
|
846
1131
|
console.warn(` Warning: SSG route "${route.path}" has dynamic segments but no getStaticPaths() export, skipping.`);
|
|
847
1132
|
return [];
|
|
848
1133
|
}
|
|
849
|
-
const { buildPathFromSegments } = await import("./app-Cep0el7c.mjs").then((n) => n.t);
|
|
850
1134
|
return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
|
|
851
1135
|
}
|
|
852
1136
|
//#endregion
|
|
@@ -874,6 +1158,13 @@ function prefetchRouteState(url) {
|
|
|
874
1158
|
}
|
|
875
1159
|
function setupPrefetching(app, warmModules) {
|
|
876
1160
|
let hoverTimer = null;
|
|
1161
|
+
function getRoutePathname(url) {
|
|
1162
|
+
try {
|
|
1163
|
+
return new URL(url, window.location.origin).pathname;
|
|
1164
|
+
} catch {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
877
1168
|
function getInternalHref(anchor) {
|
|
878
1169
|
const href = anchor.getAttribute("href");
|
|
879
1170
|
if (!href || href.startsWith("#")) return null;
|
|
@@ -887,10 +1178,11 @@ function setupPrefetching(app, warmModules) {
|
|
|
887
1178
|
return url.pathname + url.search;
|
|
888
1179
|
}
|
|
889
1180
|
function getPrefetchStrategy(pathname) {
|
|
890
|
-
const
|
|
1181
|
+
const routePathname = getRoutePathname(pathname);
|
|
1182
|
+
if (!routePathname) return "none";
|
|
1183
|
+
const match = matchAppRoute(app, routePathname);
|
|
891
1184
|
if (!match) return "none";
|
|
892
1185
|
if (match.route.prefetch) return match.route.prefetch;
|
|
893
|
-
if (match.route.render === "spa") return "none";
|
|
894
1186
|
return "intent";
|
|
895
1187
|
}
|
|
896
1188
|
document.addEventListener("mouseenter", (e) => {
|
|
@@ -904,7 +1196,8 @@ function setupPrefetching(app, warmModules) {
|
|
|
904
1196
|
hoverTimer = setTimeout(() => {
|
|
905
1197
|
prefetchRouteState(href);
|
|
906
1198
|
if (warmModules) {
|
|
907
|
-
const
|
|
1199
|
+
const pathname = getRoutePathname(href);
|
|
1200
|
+
const m = pathname ? matchAppRoute(app, pathname) : void 0;
|
|
908
1201
|
if (m) warmModules(m);
|
|
909
1202
|
}
|
|
910
1203
|
}, 50);
|
|
@@ -925,7 +1218,8 @@ function setupPrefetching(app, warmModules) {
|
|
|
925
1218
|
if (strategy !== "hover" && strategy !== "intent") return;
|
|
926
1219
|
prefetchRouteState(href);
|
|
927
1220
|
if (warmModules) {
|
|
928
|
-
const
|
|
1221
|
+
const pathname = getRoutePathname(href);
|
|
1222
|
+
const m = pathname ? matchAppRoute(app, pathname) : void 0;
|
|
929
1223
|
if (m) warmModules(m);
|
|
930
1224
|
}
|
|
931
1225
|
}, true);
|
|
@@ -938,7 +1232,8 @@ function setupPrefetching(app, warmModules) {
|
|
|
938
1232
|
if (!href) continue;
|
|
939
1233
|
prefetchRouteState(href);
|
|
940
1234
|
if (warmModules) {
|
|
941
|
-
const
|
|
1235
|
+
const pathname = getRoutePathname(href);
|
|
1236
|
+
const m = pathname ? matchAppRoute(app, pathname) : void 0;
|
|
942
1237
|
if (m) warmModules(m);
|
|
943
1238
|
}
|
|
944
1239
|
observer.unobserve(anchor);
|
|
@@ -989,7 +1284,30 @@ async function initClientRouter(options) {
|
|
|
989
1284
|
if (!shellKey) return null;
|
|
990
1285
|
return loadModule(shellModules, shellKey);
|
|
991
1286
|
}
|
|
992
|
-
|
|
1287
|
+
let updateRouteState = null;
|
|
1288
|
+
let routeStateVersion = 0;
|
|
1289
|
+
function RouterRoot({ initialState }) {
|
|
1290
|
+
const [routeState, setRouteState] = useState(initialState);
|
|
1291
|
+
updateRouteState = setRouteState;
|
|
1292
|
+
const navigateValue = useMemo(() => navigate, []);
|
|
1293
|
+
const { Shell, Component, componentProps, data, params, routeId, url, version } = routeState;
|
|
1294
|
+
const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
|
|
1295
|
+
return h(NavigateContext.Provider, { value: navigateValue }, h(PrachtRuntimeProvider, {
|
|
1296
|
+
data,
|
|
1297
|
+
params,
|
|
1298
|
+
routeId,
|
|
1299
|
+
stateVersion: version,
|
|
1300
|
+
url
|
|
1301
|
+
}, componentTree));
|
|
1302
|
+
}
|
|
1303
|
+
function applyRouteState(routeState) {
|
|
1304
|
+
if (updateRouteState) {
|
|
1305
|
+
updateRouteState(routeState);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
render(h(RouterRoot, { initialState: routeState }), root);
|
|
1309
|
+
}
|
|
1310
|
+
async function resolveRouteState(match, state, currentUrl, routeModPromise, shellModPromise) {
|
|
993
1311
|
const routeMod = await (routeModPromise ?? startRouteImport(match));
|
|
994
1312
|
if (!routeMod) return null;
|
|
995
1313
|
let Shell = null;
|
|
@@ -998,39 +1316,69 @@ async function initClientRouter(options) {
|
|
|
998
1316
|
const DefaultComponent = typeof routeMod.default === "function" ? routeMod.default : void 0;
|
|
999
1317
|
const Component = state.error ? routeMod.ErrorBoundary : routeMod.Component ?? DefaultComponent;
|
|
1000
1318
|
if (!Component) return null;
|
|
1001
|
-
const
|
|
1319
|
+
const componentProps = state.error ? { error: deserializeRouteError(state.error) } : {
|
|
1002
1320
|
data: state.data,
|
|
1003
1321
|
params: match.params
|
|
1004
1322
|
};
|
|
1005
|
-
|
|
1006
|
-
|
|
1323
|
+
return {
|
|
1324
|
+
Shell,
|
|
1325
|
+
Component,
|
|
1326
|
+
componentProps,
|
|
1007
1327
|
data: state.data,
|
|
1008
1328
|
params: match.params,
|
|
1009
1329
|
routeId: match.route.id ?? "",
|
|
1010
|
-
url:
|
|
1011
|
-
|
|
1330
|
+
url: currentUrl,
|
|
1331
|
+
version: ++routeStateVersion
|
|
1332
|
+
};
|
|
1012
1333
|
}
|
|
1013
|
-
async function
|
|
1334
|
+
async function resolveSpaPendingState(match, currentUrl, shellModPromise) {
|
|
1014
1335
|
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
1015
1336
|
if (!resolvedShell) return null;
|
|
1016
|
-
const Shell = resolvedShell.Shell;
|
|
1337
|
+
const Shell = resolvedShell.Shell ?? null;
|
|
1017
1338
|
const Loading = resolvedShell.Loading;
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1339
|
+
if (!Shell && !Loading) return null;
|
|
1340
|
+
return {
|
|
1341
|
+
Shell,
|
|
1342
|
+
Component: Loading ?? (() => null),
|
|
1343
|
+
componentProps: {},
|
|
1021
1344
|
data: void 0,
|
|
1022
1345
|
params: match.params,
|
|
1023
1346
|
routeId: match.route.id ?? "",
|
|
1024
|
-
url:
|
|
1025
|
-
|
|
1347
|
+
url: currentUrl,
|
|
1348
|
+
version: ++routeStateVersion
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function resolveRedirectTarget(location) {
|
|
1352
|
+
const targetUrl = new URL(location, window.location.href);
|
|
1353
|
+
const fullInternalTarget = targetUrl.pathname + targetUrl.search + targetUrl.hash;
|
|
1354
|
+
const internalPath = targetUrl.pathname + targetUrl.search;
|
|
1355
|
+
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
1356
|
+
const isCurrentLocation = targetUrl.origin === window.location.origin && fullInternalTarget === currentPath;
|
|
1357
|
+
if (targetUrl.origin !== window.location.origin) return {
|
|
1358
|
+
externalUrl: targetUrl.toString(),
|
|
1359
|
+
isCurrentLocation: false
|
|
1360
|
+
};
|
|
1361
|
+
if (targetUrl.hash) return {
|
|
1362
|
+
documentUrl: targetUrl.toString(),
|
|
1363
|
+
isCurrentLocation
|
|
1364
|
+
};
|
|
1365
|
+
return {
|
|
1366
|
+
internalPath,
|
|
1367
|
+
isCurrentLocation
|
|
1368
|
+
};
|
|
1026
1369
|
}
|
|
1027
1370
|
async function navigate(to, opts) {
|
|
1028
|
-
const
|
|
1029
|
-
if (!
|
|
1371
|
+
const target = resolveBrowserRouteTarget(to);
|
|
1372
|
+
if (!target) {
|
|
1030
1373
|
window.location.href = to;
|
|
1031
1374
|
return;
|
|
1032
1375
|
}
|
|
1033
|
-
const
|
|
1376
|
+
const match = matchAppRoute(app, target.pathname);
|
|
1377
|
+
if (!match) {
|
|
1378
|
+
window.location.href = target.browserUrl;
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
const statePromise = getCachedRouteState(target.requestUrl) ?? fetchPrachtRouteState(target.requestUrl);
|
|
1034
1382
|
const routeModPromise = startRouteImport(match);
|
|
1035
1383
|
const shellModPromise = startShellImport(match);
|
|
1036
1384
|
let state = {
|
|
@@ -1041,10 +1389,24 @@ async function initClientRouter(options) {
|
|
|
1041
1389
|
const result = await statePromise;
|
|
1042
1390
|
if (result.type === "redirect") {
|
|
1043
1391
|
if (result.location) {
|
|
1044
|
-
|
|
1392
|
+
const redirect = resolveRedirectTarget(result.location);
|
|
1393
|
+
if (redirect.externalUrl) {
|
|
1394
|
+
window.location.href = redirect.externalUrl;
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (redirect.isCurrentLocation) return;
|
|
1398
|
+
if (redirect.documentUrl) {
|
|
1399
|
+
window.location.href = redirect.documentUrl;
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (redirect.internalPath) {
|
|
1403
|
+
await navigate(redirect.internalPath, opts);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
window.location.href = result.location;
|
|
1045
1407
|
return;
|
|
1046
1408
|
}
|
|
1047
|
-
window.location.href =
|
|
1409
|
+
window.location.href = target.browserUrl;
|
|
1048
1410
|
return;
|
|
1049
1411
|
}
|
|
1050
1412
|
if (result.type === "error") state = {
|
|
@@ -1056,27 +1418,21 @@ async function initClientRouter(options) {
|
|
|
1056
1418
|
error: null
|
|
1057
1419
|
};
|
|
1058
1420
|
} catch {
|
|
1059
|
-
window.location.href =
|
|
1421
|
+
window.location.href = target.browserUrl;
|
|
1060
1422
|
return;
|
|
1061
1423
|
}
|
|
1062
|
-
if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "",
|
|
1063
|
-
else history.pushState(null, "",
|
|
1064
|
-
const
|
|
1065
|
-
if (
|
|
1066
|
-
|
|
1424
|
+
if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "", target.browserUrl);
|
|
1425
|
+
else history.pushState(null, "", target.browserUrl);
|
|
1426
|
+
const routeState = await resolveRouteState(match, state, target.requestUrl, routeModPromise, shellModPromise);
|
|
1427
|
+
if (routeState) {
|
|
1428
|
+
applyRouteState(routeState);
|
|
1067
1429
|
window.scrollTo(0, 0);
|
|
1068
|
-
} else window.location.href =
|
|
1069
|
-
}
|
|
1070
|
-
if (import.meta.env?.DEV) {
|
|
1071
|
-
const prev = options.__m;
|
|
1072
|
-
options.__m = (vnode, s) => {
|
|
1073
|
-
const message = `Hydration mismatch in <${(typeof vnode.type === "function" ? vnode.type.displayName || vnode.type.name : vnode.type) || "Unknown"}>: ${s}`;
|
|
1074
|
-
console.warn(`[pracht] ${message}`);
|
|
1075
|
-
appendHydrationWarning(message);
|
|
1076
|
-
if (prev) prev(vnode, s);
|
|
1077
|
-
};
|
|
1430
|
+
} else window.location.href = target.browserUrl;
|
|
1078
1431
|
}
|
|
1079
|
-
const
|
|
1432
|
+
const initialTarget = resolveBrowserRouteTarget(options.initialState.url);
|
|
1433
|
+
const initialRequestUrl = initialTarget?.requestUrl ?? options.initialState.url;
|
|
1434
|
+
const initialBrowserUrl = initialTarget?.browserUrl ?? options.initialState.url;
|
|
1435
|
+
const initialMatch = matchAppRoute(app, initialTarget?.pathname ?? options.initialState.url);
|
|
1080
1436
|
if (initialMatch) {
|
|
1081
1437
|
const initialShellPromise = initialMatch.route.render === "spa" && options.initialState.pending ? startShellImport(initialMatch) : null;
|
|
1082
1438
|
let state = {
|
|
@@ -1084,9 +1440,9 @@ async function initClientRouter(options) {
|
|
|
1084
1440
|
error: options.initialState.error ?? null
|
|
1085
1441
|
};
|
|
1086
1442
|
if (initialMatch.route.render === "spa" && options.initialState.pending) {
|
|
1087
|
-
const dataPromise = fetchPrachtRouteState(
|
|
1088
|
-
const
|
|
1089
|
-
if (
|
|
1443
|
+
const dataPromise = fetchPrachtRouteState(initialRequestUrl, { useDataParam: true });
|
|
1444
|
+
const pendingState = await resolveSpaPendingState(initialMatch, initialRequestUrl, initialShellPromise);
|
|
1445
|
+
if (pendingState) hydrate(h(RouterRoot, { initialState: pendingState }), root);
|
|
1090
1446
|
try {
|
|
1091
1447
|
const result = await dataPromise;
|
|
1092
1448
|
if (result.type === "redirect") {
|
|
@@ -1102,15 +1458,18 @@ async function initClientRouter(options) {
|
|
|
1102
1458
|
error: null
|
|
1103
1459
|
};
|
|
1104
1460
|
} catch {
|
|
1105
|
-
window.location.href =
|
|
1461
|
+
window.location.href = initialBrowserUrl;
|
|
1106
1462
|
return;
|
|
1107
1463
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1464
|
+
const resolvedState = await resolveRouteState(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
|
|
1465
|
+
if (resolvedState) applyRouteState(resolvedState);
|
|
1466
|
+
} else {
|
|
1467
|
+
const initialRouteState = await resolveRouteState(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
|
|
1468
|
+
if (initialRouteState) if (initialMatch.route.render === "spa") render(h(RouterRoot, { initialState: initialRouteState }), root);
|
|
1469
|
+
else {
|
|
1470
|
+
markHydrating();
|
|
1471
|
+
hydrate(h(RouterRoot, { initialState: initialRouteState }), root);
|
|
1472
|
+
}
|
|
1114
1473
|
}
|
|
1115
1474
|
}
|
|
1116
1475
|
document.addEventListener("click", (e) => {
|
|
@@ -1132,10 +1491,10 @@ async function initClientRouter(options) {
|
|
|
1132
1491
|
}
|
|
1133
1492
|
if (url.origin !== window.location.origin) return;
|
|
1134
1493
|
e.preventDefault();
|
|
1135
|
-
navigate(url.pathname + url.search);
|
|
1494
|
+
navigate(url.pathname + url.search + url.hash);
|
|
1136
1495
|
});
|
|
1137
1496
|
window.addEventListener("popstate", () => {
|
|
1138
|
-
navigate(window.location.pathname + window.location.search, { _popstate: true });
|
|
1497
|
+
navigate(window.location.pathname + window.location.search + window.location.hash, { _popstate: true });
|
|
1139
1498
|
});
|
|
1140
1499
|
window.__PRACHT_NAVIGATE__ = navigate;
|
|
1141
1500
|
window.__PRACHT_ROUTER_READY__ = true;
|
|
@@ -1145,60 +1504,19 @@ async function initClientRouter(options) {
|
|
|
1145
1504
|
};
|
|
1146
1505
|
setupPrefetching(app, warmModules);
|
|
1147
1506
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
overflow: "auto",
|
|
1161
|
-
background: "#2d1b00",
|
|
1162
|
-
borderTop: "2px solid #f0ad4e",
|
|
1163
|
-
color: "#ffc107",
|
|
1164
|
-
fontFamily: "ui-monospace, Consolas, monospace",
|
|
1165
|
-
fontSize: "13px",
|
|
1166
|
-
padding: "12px 16px",
|
|
1167
|
-
zIndex: "2147483647"
|
|
1168
|
-
});
|
|
1169
|
-
const header = document.createElement("div");
|
|
1170
|
-
Object.assign(header.style, {
|
|
1171
|
-
display: "flex",
|
|
1172
|
-
justifyContent: "space-between",
|
|
1173
|
-
alignItems: "center",
|
|
1174
|
-
marginBottom: "8px"
|
|
1175
|
-
});
|
|
1176
|
-
header.innerHTML = "<strong style=\"color:#f0ad4e\">⚠ Hydration Mismatches</strong>";
|
|
1177
|
-
const close = document.createElement("button");
|
|
1178
|
-
close.textContent = "×";
|
|
1179
|
-
Object.assign(close.style, {
|
|
1180
|
-
background: "none",
|
|
1181
|
-
border: "none",
|
|
1182
|
-
color: "#f0ad4e",
|
|
1183
|
-
fontSize: "18px",
|
|
1184
|
-
cursor: "pointer"
|
|
1185
|
-
});
|
|
1186
|
-
close.onclick = () => container.remove();
|
|
1187
|
-
header.appendChild(close);
|
|
1188
|
-
container.appendChild(header);
|
|
1189
|
-
document.body.appendChild(container);
|
|
1507
|
+
function resolveBrowserRouteTarget(to) {
|
|
1508
|
+
if (typeof window === "undefined") return null;
|
|
1509
|
+
try {
|
|
1510
|
+
const url = new URL(to, window.location.href);
|
|
1511
|
+
if (url.origin !== window.location.origin) return null;
|
|
1512
|
+
return {
|
|
1513
|
+
browserUrl: url.pathname + url.search + url.hash,
|
|
1514
|
+
pathname: url.pathname,
|
|
1515
|
+
requestUrl: url.pathname + url.search
|
|
1516
|
+
};
|
|
1517
|
+
} catch {
|
|
1518
|
+
return null;
|
|
1190
1519
|
}
|
|
1191
|
-
const entry = document.createElement("div");
|
|
1192
|
-
entry.textContent = message;
|
|
1193
|
-
Object.assign(entry.style, { padding: "2px 0" });
|
|
1194
|
-
container.appendChild(entry);
|
|
1195
|
-
}
|
|
1196
|
-
function deserializeRouteError(error) {
|
|
1197
|
-
const result = new Error(error.message);
|
|
1198
|
-
result.name = error.name;
|
|
1199
|
-
result.status = error.status;
|
|
1200
|
-
result.diagnostics = error.diagnostics;
|
|
1201
|
-
return result;
|
|
1202
1520
|
}
|
|
1203
1521
|
//#endregion
|
|
1204
1522
|
//#region src/types.ts
|
|
@@ -1211,4 +1529,4 @@ var PrachtHttpError = class extends Error {
|
|
|
1211
1529
|
}
|
|
1212
1530
|
};
|
|
1213
1531
|
//#endregion
|
|
1214
|
-
export { Form, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate,
|
|
1532
|
+
export { Form, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
|