@pracht/core 0.7.0 → 0.8.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/dist/app-BWriseLj.d.mts +298 -0
- package/dist/app-BvC1uQG5.mjs +308 -0
- package/dist/browser.d.mts +7 -0
- package/dist/browser.mjs +8 -0
- package/dist/client.d.mts +4 -0
- package/dist/client.mjs +4 -0
- package/dist/forwardRef-grZ6t4GS.mjs +27 -0
- package/dist/hydration-M1QzbQ1O.d.mts +23 -0
- package/dist/index.d.mts +7 -492
- package/dist/index.mjs +7 -2088
- package/dist/manifest.d.mts +2 -0
- package/dist/manifest.mjs +2 -0
- package/dist/prefetch-B50kX9RL.mjs +118 -0
- package/dist/prefetch-cache-DzP2Bj9H.mjs +129 -0
- package/dist/prerender-B8_CqYwd.d.mts +51 -0
- package/dist/prerender-D3E2H96p.mjs +648 -0
- package/dist/router-B7J4YYwg.mjs +418 -0
- package/dist/router-BcUmg1kB.d.mts +27 -0
- package/dist/runtime-context-B5pREhcM.mjs +164 -0
- package/dist/runtime-context-ytVd7GmZ.d.mts +72 -0
- package/dist/runtime-middleware-aeBdgjYr.d.mts +77 -0
- package/dist/server.d.mts +5 -0
- package/dist/server.mjs +5 -0
- package/dist/types-idmK5omD.mjs +355 -0
- package/package.json +19 -2
package/dist/index.mjs
CHANGED
|
@@ -1,2089 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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, t as buildHref, u as timeRevalidate } from "./app-BvC1uQG5.mjs";
|
|
2
|
+
import { S as createHref, _ as applyDefaultSecurityHeaders, a as useParams, i as useLocation, n as Form, o as useRevalidate, r as Link, s as useRouteData, t as PrachtHttpError, u as redirect } from "./types-idmK5omD.mjs";
|
|
3
|
+
import { t as forwardRef } from "./forwardRef-grZ6t4GS.mjs";
|
|
4
|
+
import { n as useNavigate, r as useIsHydrated, t as initClientRouter } from "./router-B7J4YYwg.mjs";
|
|
5
|
+
import { i as startApp, r as readHydrationState, t as PrachtRuntimeProvider } from "./runtime-context-B5pREhcM.mjs";
|
|
6
|
+
import { n as handlePrachtRequest, t as prerenderApp } from "./prerender-D3E2H96p.mjs";
|
|
3
7
|
import { Suspense, lazy } from "preact-suspense";
|
|
4
|
-
|
|
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
|
-
hasLoader: !!loader,
|
|
26
|
-
...routeMeta
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
function resolveModuleRef(ref) {
|
|
30
|
-
if (ref === void 0) return void 0;
|
|
31
|
-
if (typeof ref === "string") return ref;
|
|
32
|
-
return "";
|
|
33
|
-
}
|
|
34
|
-
function group(meta, routes) {
|
|
35
|
-
return {
|
|
36
|
-
kind: "group",
|
|
37
|
-
meta,
|
|
38
|
-
routes
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function defineApp(config) {
|
|
42
|
-
return {
|
|
43
|
-
shells: resolveModuleRefRecord(config.shells ?? {}),
|
|
44
|
-
middleware: resolveModuleRefRecord(config.middleware ?? {}),
|
|
45
|
-
api: config.api ?? {},
|
|
46
|
-
routes: config.routes
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function resolveModuleRefRecord(record) {
|
|
50
|
-
const result = {};
|
|
51
|
-
for (const [key, value] of Object.entries(record)) result[key] = resolveModuleRef(value);
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
function resolveApp(app) {
|
|
55
|
-
const routes = [];
|
|
56
|
-
const inherited = {
|
|
57
|
-
pathPrefix: "/",
|
|
58
|
-
middleware: []
|
|
59
|
-
};
|
|
60
|
-
for (const node of app.routes) flattenRouteNode(app, node, inherited, routes);
|
|
61
|
-
return {
|
|
62
|
-
shells: app.shells,
|
|
63
|
-
middleware: app.middleware,
|
|
64
|
-
api: app.api,
|
|
65
|
-
routes,
|
|
66
|
-
apiRoutes: []
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
function matchAppRoute(app, pathname) {
|
|
70
|
-
const resolved = isResolvedApp(app) ? app : resolveApp(app);
|
|
71
|
-
const normalizedPathname = normalizeRoutePath(pathname);
|
|
72
|
-
const targetSegments = splitPathSegments(normalizedPathname);
|
|
73
|
-
for (const currentRoute of resolved.routes) {
|
|
74
|
-
const params = matchRouteSegments(currentRoute.segments, targetSegments);
|
|
75
|
-
if (params) return {
|
|
76
|
-
route: currentRoute,
|
|
77
|
-
params,
|
|
78
|
-
pathname: normalizedPathname
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
function flattenRouteNode(app, node, inherited, routes) {
|
|
83
|
-
if (node.kind === "group") {
|
|
84
|
-
const nextInherited = {
|
|
85
|
-
pathPrefix: mergeRoutePaths(inherited.pathPrefix, node.meta.pathPrefix),
|
|
86
|
-
shell: node.meta.shell ?? inherited.shell,
|
|
87
|
-
render: node.meta.render ?? inherited.render,
|
|
88
|
-
middleware: [...inherited.middleware, ...node.meta.middleware ?? []]
|
|
89
|
-
};
|
|
90
|
-
for (const child of node.routes) flattenRouteNode(app, child, nextInherited, routes);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const fullPath = mergeRoutePaths(inherited.pathPrefix, node.path);
|
|
94
|
-
const shell = node.shell ?? inherited.shell;
|
|
95
|
-
const middleware = [...inherited.middleware, ...node.middleware ?? []];
|
|
96
|
-
routes.push({
|
|
97
|
-
id: node.id ?? createRouteId(fullPath),
|
|
98
|
-
path: fullPath,
|
|
99
|
-
file: node.file,
|
|
100
|
-
loaderFile: node.loaderFile,
|
|
101
|
-
hasLoader: node.loaderFile ? true : node.hasLoader,
|
|
102
|
-
shell,
|
|
103
|
-
shellFile: shell ? app.shells[shell] : void 0,
|
|
104
|
-
render: node.render ?? inherited.render,
|
|
105
|
-
middleware,
|
|
106
|
-
middlewareFiles: middleware.flatMap((name) => {
|
|
107
|
-
const middlewareFile = app.middleware[name];
|
|
108
|
-
return middlewareFile ? [middlewareFile] : [];
|
|
109
|
-
}),
|
|
110
|
-
revalidate: node.revalidate,
|
|
111
|
-
segments: parseRouteSegments(fullPath)
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
function isResolvedApp(app) {
|
|
115
|
-
return app.routes.length === 0 || "segments" in app.routes[0];
|
|
116
|
-
}
|
|
117
|
-
function matchRouteSegments(routeSegments, targetSegments) {
|
|
118
|
-
const params = {};
|
|
119
|
-
let routeIndex = 0;
|
|
120
|
-
let targetIndex = 0;
|
|
121
|
-
while (routeIndex < routeSegments.length) {
|
|
122
|
-
const currentSegment = routeSegments[routeIndex];
|
|
123
|
-
if (currentSegment.type === "catchall") {
|
|
124
|
-
try {
|
|
125
|
-
params[currentSegment.name] = targetSegments.slice(targetIndex).map(decodeURIComponent).join("/");
|
|
126
|
-
} catch {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
return params;
|
|
130
|
-
}
|
|
131
|
-
const targetSegment = targetSegments[targetIndex];
|
|
132
|
-
if (typeof targetSegment === "undefined") return null;
|
|
133
|
-
if (currentSegment.type === "static") {
|
|
134
|
-
if (currentSegment.value !== targetSegment) return null;
|
|
135
|
-
} else try {
|
|
136
|
-
params[currentSegment.name] = decodeURIComponent(targetSegment);
|
|
137
|
-
} catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
routeIndex += 1;
|
|
141
|
-
targetIndex += 1;
|
|
142
|
-
}
|
|
143
|
-
return targetIndex === targetSegments.length ? params : null;
|
|
144
|
-
}
|
|
145
|
-
function parseRouteSegments(path) {
|
|
146
|
-
return splitPathSegments(path).map((segment) => {
|
|
147
|
-
if (segment === "*") return {
|
|
148
|
-
type: "catchall",
|
|
149
|
-
name: "*"
|
|
150
|
-
};
|
|
151
|
-
if (segment.startsWith(":") && segment.endsWith("*")) return {
|
|
152
|
-
type: "catchall",
|
|
153
|
-
name: segment.slice(1, -1) || "*"
|
|
154
|
-
};
|
|
155
|
-
if (segment.startsWith(":")) return {
|
|
156
|
-
type: "param",
|
|
157
|
-
name: segment.slice(1)
|
|
158
|
-
};
|
|
159
|
-
assertSafeStaticRouteSegment(segment);
|
|
160
|
-
return {
|
|
161
|
-
type: "static",
|
|
162
|
-
value: segment
|
|
163
|
-
};
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
function splitPathSegments(path) {
|
|
167
|
-
return normalizeRoutePath(path).split("/").filter(Boolean);
|
|
168
|
-
}
|
|
169
|
-
function assertSafeStaticRouteSegment(segment) {
|
|
170
|
-
if (segment === "." || segment === "..") throw new Error(`Unsafe static route segment "${segment}" is not allowed.`);
|
|
171
|
-
if (segment.includes("\0") || /[\r\n\\]/.test(segment)) throw new Error(`Unsafe static route segment "${segment}" contains a forbidden character.`);
|
|
172
|
-
}
|
|
173
|
-
function mergeRoutePaths(prefix, path) {
|
|
174
|
-
if (!path) return normalizeRoutePath(prefix);
|
|
175
|
-
const normalizedPrefix = normalizeRoutePath(prefix);
|
|
176
|
-
const normalizedPath = normalizeRoutePath(path);
|
|
177
|
-
if (normalizedPrefix === "/") return normalizedPath;
|
|
178
|
-
if (normalizedPath === "/") return normalizedPrefix;
|
|
179
|
-
return normalizeRoutePath(`${normalizedPrefix}/${normalizedPath.slice(1)}`);
|
|
180
|
-
}
|
|
181
|
-
function normalizeRoutePath(path) {
|
|
182
|
-
if (!path || path === "/") return "/";
|
|
183
|
-
const collapsed = (path.startsWith("/") ? path : `/${path}`).replace(/\/{2,}/g, "/");
|
|
184
|
-
return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
|
|
185
|
-
}
|
|
186
|
-
function buildPathFromSegments(segments, params) {
|
|
187
|
-
return normalizeRoutePath("/" + segments.map((segment) => {
|
|
188
|
-
if (segment.type === "static") return segment.value;
|
|
189
|
-
if (segment.type === "param") return encodeDynamicPathSegment(params[segment.name] ?? "");
|
|
190
|
-
return (params[segment.name] ?? params["*"] ?? "").split("/").map((part) => encodeDynamicPathSegment(part)).join("/");
|
|
191
|
-
}).join("/"));
|
|
192
|
-
}
|
|
193
|
-
function buildHref(routes, routeId, ...args) {
|
|
194
|
-
return buildHrefUntyped(routes, String(routeId), args[0]);
|
|
195
|
-
}
|
|
196
|
-
function createHref(routes) {
|
|
197
|
-
return ((routeId, options) => buildHrefUntyped(routes, routeId, options));
|
|
198
|
-
}
|
|
199
|
-
function buildHrefUntyped(routes, routeId, options = {}) {
|
|
200
|
-
const route = routes.find((candidate) => candidate.id === routeId);
|
|
201
|
-
if (!route) throw new Error(`Unknown pracht route id: ${routeId}.`);
|
|
202
|
-
const segments = route.segments ?? parseRouteSegments(route.path);
|
|
203
|
-
return `${buildPathFromSegments(segments, normalizeHrefParams(segments, options.params ?? {}))}${serializeSearch(options.search)}${serializeHash(options.hash)}`;
|
|
204
|
-
}
|
|
205
|
-
function normalizeHrefParams(segments, params) {
|
|
206
|
-
const expected = new Set(segments.filter((segment) => segment.type === "param" || segment.type === "catchall").map((segment) => segment.name));
|
|
207
|
-
for (const name of expected) if (params[name] == null) throw new Error(`Missing route param: ${name}.`);
|
|
208
|
-
for (const name of Object.keys(params)) if (!expected.has(name)) throw new Error(`Unexpected route param: ${name}.`);
|
|
209
|
-
const normalized = {};
|
|
210
|
-
for (const name of expected) normalized[name] = String(params[name]);
|
|
211
|
-
return normalized;
|
|
212
|
-
}
|
|
213
|
-
function serializeSearch(search) {
|
|
214
|
-
if (search == null) return "";
|
|
215
|
-
if (typeof search === "string") {
|
|
216
|
-
if (!search) return "";
|
|
217
|
-
return search.startsWith("?") ? search : `?${search}`;
|
|
218
|
-
}
|
|
219
|
-
const serialized = (search instanceof URLSearchParams ? search : objectToSearchParams(search)).toString();
|
|
220
|
-
return serialized ? `?${serialized}` : "";
|
|
221
|
-
}
|
|
222
|
-
function objectToSearchParams(search) {
|
|
223
|
-
const params = new URLSearchParams();
|
|
224
|
-
for (const [key, value] of Object.entries(search)) {
|
|
225
|
-
if (Array.isArray(value)) {
|
|
226
|
-
for (const item of value) appendSearchValue(params, key, item);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
appendSearchValue(params, key, value);
|
|
230
|
-
}
|
|
231
|
-
return params;
|
|
232
|
-
}
|
|
233
|
-
function appendSearchValue(params, key, value) {
|
|
234
|
-
if (value == null) return;
|
|
235
|
-
params.append(key, String(value));
|
|
236
|
-
}
|
|
237
|
-
function serializeHash(hash) {
|
|
238
|
-
if (!hash) return "";
|
|
239
|
-
return hash.startsWith("#") ? hash : `#${hash}`;
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Encode one dynamic URL path segment for SSG/ISG output. `encodeURIComponent`
|
|
243
|
-
* leaves unreserved characters (including `.`) intact, and even percent-encoded
|
|
244
|
-
* dot segments are normalized by URL parsers. Reject exact `.` / `..` segments
|
|
245
|
-
* instead of allowing them to reach filesystem output path construction.
|
|
246
|
-
*/
|
|
247
|
-
function encodeDynamicPathSegment(part) {
|
|
248
|
-
if (part === "." || part === "..") throw new Error(`Unsafe dynamic route param segment "${part}" is not allowed.`);
|
|
249
|
-
return encodeURIComponent(part);
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Convert a list of file paths from `import.meta.glob` into resolved API routes.
|
|
253
|
-
*
|
|
254
|
-
* Example: `"/src/api/health.ts"` → path `/api/health`
|
|
255
|
-
* `"/src/api/users/[id].ts"` → path `/api/users/:id`
|
|
256
|
-
* `"/src/api/files/[...path].ts"` → path `/api/files/*`
|
|
257
|
-
* `"/src/api/index.ts"` → path `/api`
|
|
258
|
-
*/
|
|
259
|
-
function resolveApiRoutes(files, apiDir = "/src/api") {
|
|
260
|
-
const normalizedDir = apiDir.replace(/\/$/, "");
|
|
261
|
-
return files.map((file) => {
|
|
262
|
-
let relative = file;
|
|
263
|
-
if (relative.startsWith(normalizedDir)) relative = relative.slice(normalizedDir.length);
|
|
264
|
-
relative = relative.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
265
|
-
if (relative.endsWith("/index")) relative = relative.slice(0, -6) || "/";
|
|
266
|
-
relative = relative.replace(/\[\.\.\.[^\]]+\]/g, "*");
|
|
267
|
-
relative = relative.replace(/\[([^\]]+)\]/g, ":$1");
|
|
268
|
-
const path = normalizeRoutePath(`/api${relative}`);
|
|
269
|
-
return {
|
|
270
|
-
path,
|
|
271
|
-
file,
|
|
272
|
-
segments: parseRouteSegments(path)
|
|
273
|
-
};
|
|
274
|
-
}).sort(compareResolvedApiRoutes);
|
|
275
|
-
}
|
|
276
|
-
function matchApiRoute(apiRoutes, pathname) {
|
|
277
|
-
const normalizedPathname = normalizeRoutePath(pathname);
|
|
278
|
-
const targetSegments = splitPathSegments(normalizedPathname);
|
|
279
|
-
for (const route of apiRoutes) {
|
|
280
|
-
const params = matchRouteSegments(route.segments, targetSegments);
|
|
281
|
-
if (params) return {
|
|
282
|
-
route,
|
|
283
|
-
params,
|
|
284
|
-
pathname: normalizedPathname
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
function createRouteId(path) {
|
|
289
|
-
if (path === "/") return "index";
|
|
290
|
-
return path.slice(1).split("/").map((segment) => {
|
|
291
|
-
if (segment === "*") return "splat";
|
|
292
|
-
return segment.startsWith(":") ? segment.slice(1) : segment;
|
|
293
|
-
}).join("-").replace(/[^a-zA-Z0-9-]/g, "-");
|
|
294
|
-
}
|
|
295
|
-
function compareResolvedApiRoutes(left, right) {
|
|
296
|
-
const length = Math.max(left.segments.length, right.segments.length);
|
|
297
|
-
for (let index = 0; index < length; index += 1) {
|
|
298
|
-
const leftSegment = left.segments[index];
|
|
299
|
-
const rightSegment = right.segments[index];
|
|
300
|
-
if (!leftSegment) return 1;
|
|
301
|
-
if (!rightSegment) return -1;
|
|
302
|
-
const leftScore = getRouteSegmentSpecificity(leftSegment);
|
|
303
|
-
const rightScore = getRouteSegmentSpecificity(rightSegment);
|
|
304
|
-
if (leftScore !== rightScore) return rightScore - leftScore;
|
|
305
|
-
}
|
|
306
|
-
return left.path.localeCompare(right.path);
|
|
307
|
-
}
|
|
308
|
-
function getRouteSegmentSpecificity(segment) {
|
|
309
|
-
if (segment.type === "static") return 3;
|
|
310
|
-
if (segment.type === "param") return 2;
|
|
311
|
-
return 1;
|
|
312
|
-
}
|
|
313
|
-
//#endregion
|
|
314
|
-
//#region src/forwardRef.ts
|
|
315
|
-
let oldDiffHook = options.__b;
|
|
316
|
-
options.__b = (vnode) => {
|
|
317
|
-
if (vnode.type && vnode.type.__f && vnode.ref) {
|
|
318
|
-
vnode.props.ref = vnode.ref;
|
|
319
|
-
vnode.ref = null;
|
|
320
|
-
}
|
|
321
|
-
if (oldDiffHook) oldDiffHook(vnode);
|
|
322
|
-
};
|
|
323
|
-
/**
|
|
324
|
-
* Pass ref down to a child. This is mainly used in libraries with HOCs that
|
|
325
|
-
* wrap components. Using `forwardRef` there is an easy way to get a reference
|
|
326
|
-
* of the wrapped component instead of one of the wrapper itself.
|
|
327
|
-
*/
|
|
328
|
-
function forwardRef(fn) {
|
|
329
|
-
function Forwarded(props) {
|
|
330
|
-
const clone = { ...props };
|
|
331
|
-
delete clone.ref;
|
|
332
|
-
return fn(clone, props.ref || null);
|
|
333
|
-
}
|
|
334
|
-
Forwarded.__f = true;
|
|
335
|
-
Forwarded.displayName = "ForwardRef(" + (fn.displayName || fn.name) + ")";
|
|
336
|
-
return Forwarded;
|
|
337
|
-
}
|
|
338
|
-
//#endregion
|
|
339
|
-
//#region src/hydration.ts
|
|
340
|
-
const MODE_HYDRATE = 32;
|
|
341
|
-
let _hydrating = false;
|
|
342
|
-
let _suspensionCount = 0;
|
|
343
|
-
let _hydrated = false;
|
|
344
|
-
const oldCatchError = options.__e;
|
|
345
|
-
options.__e = (err, newVNode, oldVNode, errorInfo) => {
|
|
346
|
-
if (_hydrating && !_hydrated && err && err.then) {
|
|
347
|
-
if (!!(newVNode && newVNode.__u && newVNode.__u & MODE_HYDRATE) || !!(newVNode && newVNode.__h)) {
|
|
348
|
-
_suspensionCount++;
|
|
349
|
-
let settled = false;
|
|
350
|
-
const onSettled = () => {
|
|
351
|
-
if (settled) return;
|
|
352
|
-
settled = true;
|
|
353
|
-
_suspensionCount--;
|
|
354
|
-
};
|
|
355
|
-
err.then(onSettled, onSettled);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
if (oldCatchError) oldCatchError(err, newVNode, oldVNode, errorInfo);
|
|
359
|
-
};
|
|
360
|
-
const oldCommit = options.__c;
|
|
361
|
-
options.__c = (vnode, commitQueue) => {
|
|
362
|
-
if (_hydrating && !_hydrated && _suspensionCount <= 0) {
|
|
363
|
-
_hydrated = true;
|
|
364
|
-
_hydrating = false;
|
|
365
|
-
}
|
|
366
|
-
if (oldCommit) oldCommit(vnode, commitQueue);
|
|
367
|
-
};
|
|
368
|
-
/**
|
|
369
|
-
* Mark the start of a hydration pass. Call this right before `hydrate()`.
|
|
370
|
-
*/
|
|
371
|
-
function markHydrating() {
|
|
372
|
-
if (!_hydrated) _hydrating = true;
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Returns `true` once the initial hydration (including all Suspense
|
|
376
|
-
* boundaries) has fully resolved. During SSR and hydration this returns
|
|
377
|
-
* `false`.
|
|
378
|
-
*/
|
|
379
|
-
function useIsHydrated() {
|
|
380
|
-
const [hydrated, setHydrated] = useState(_hydrated);
|
|
381
|
-
useEffect(() => {
|
|
382
|
-
setHydrated(true);
|
|
383
|
-
}, []);
|
|
384
|
-
return hydrated;
|
|
385
|
-
}
|
|
386
|
-
//#endregion
|
|
387
|
-
//#region src/runtime-constants.ts
|
|
388
|
-
const SAFE_METHODS = new Set(["GET", "HEAD"]);
|
|
389
|
-
const HYDRATION_STATE_ELEMENT_ID = "pracht-state";
|
|
390
|
-
const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
|
|
391
|
-
const ROUTE_STATE_CACHE_CONTROL = "no-store";
|
|
392
|
-
const EMPTY_ROUTE_PARAMS = {};
|
|
393
|
-
//#endregion
|
|
394
|
-
//#region src/runtime-errors.ts
|
|
395
|
-
function isPrachtHttpError(error) {
|
|
396
|
-
return error instanceof Error && error.name === "PrachtHttpError" && "status" in error;
|
|
397
|
-
}
|
|
398
|
-
let warnedAboutProductionDebugErrors = false;
|
|
399
|
-
/**
|
|
400
|
-
* `debugErrors: true` opts into surfacing stack traces, module paths,
|
|
401
|
-
* and middleware names in error responses. That is great in dev and
|
|
402
|
-
* dangerous in production — a misconfigured deploy would leak internals
|
|
403
|
-
* to the public. When `NODE_ENV === "production"` we refuse to honor
|
|
404
|
-
* the flag and emit a single console warning so the misconfiguration
|
|
405
|
-
* is visible in logs.
|
|
406
|
-
*/
|
|
407
|
-
function shouldExposeServerErrors(options) {
|
|
408
|
-
if (options.debugErrors !== true) return false;
|
|
409
|
-
if ((typeof process !== "undefined" && process.env ? process.env.NODE_ENV : typeof globalThis !== "undefined" && globalThis.process ? globalThis.process?.env?.NODE_ENV : void 0) === "production") {
|
|
410
|
-
if (!warnedAboutProductionDebugErrors) {
|
|
411
|
-
warnedAboutProductionDebugErrors = true;
|
|
412
|
-
console.warn("[pracht] debugErrors is ignored in production builds. Remove it to silence this warning.");
|
|
413
|
-
}
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
return true;
|
|
417
|
-
}
|
|
418
|
-
function createSerializedRouteError(message, status, options = {}) {
|
|
419
|
-
return {
|
|
420
|
-
message,
|
|
421
|
-
name: options.name ?? "Error",
|
|
422
|
-
status,
|
|
423
|
-
...options.diagnostics ? { diagnostics: options.diagnostics } : {}
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
function buildRuntimeDiagnostics(options) {
|
|
427
|
-
const route = options.route;
|
|
428
|
-
const routeId = route && "id" in route ? route.id : void 0;
|
|
429
|
-
return {
|
|
430
|
-
phase: options.phase,
|
|
431
|
-
routeId,
|
|
432
|
-
routePath: route?.path,
|
|
433
|
-
routeFile: route?.file,
|
|
434
|
-
loaderFile: options.loaderFile,
|
|
435
|
-
shellFile: options.shellFile,
|
|
436
|
-
middlewareFiles: options.middlewareFiles ? [...options.middlewareFiles] : [],
|
|
437
|
-
status: options.status
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
function normalizeRouteError(error, options) {
|
|
441
|
-
if (isPrachtHttpError(error)) {
|
|
442
|
-
const status = typeof error.status === "number" ? error.status : 500;
|
|
443
|
-
if (status >= 400 && status < 500) return {
|
|
444
|
-
message: error.message,
|
|
445
|
-
name: error.name,
|
|
446
|
-
status
|
|
447
|
-
};
|
|
448
|
-
if (options.exposeDetails) return {
|
|
449
|
-
message: error.message || "Internal Server Error",
|
|
450
|
-
name: error.name || "Error",
|
|
451
|
-
status
|
|
452
|
-
};
|
|
453
|
-
return {
|
|
454
|
-
message: "Internal Server Error",
|
|
455
|
-
name: "Error",
|
|
456
|
-
status
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
if (error instanceof Error) {
|
|
460
|
-
if (options.exposeDetails) return {
|
|
461
|
-
message: error.message || "Internal Server Error",
|
|
462
|
-
name: error.name || "Error",
|
|
463
|
-
status: 500
|
|
464
|
-
};
|
|
465
|
-
return {
|
|
466
|
-
message: "Internal Server Error",
|
|
467
|
-
name: "Error",
|
|
468
|
-
status: 500
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
if (options.exposeDetails) return {
|
|
472
|
-
message: typeof error === "string" && error ? error : "Internal Server Error",
|
|
473
|
-
name: "Error",
|
|
474
|
-
status: 500
|
|
475
|
-
};
|
|
476
|
-
return {
|
|
477
|
-
message: "Internal Server Error",
|
|
478
|
-
name: "Error",
|
|
479
|
-
status: 500
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
function deserializeRouteError(error) {
|
|
483
|
-
const result = new Error(error.message);
|
|
484
|
-
result.name = error.name;
|
|
485
|
-
result.status = error.status;
|
|
486
|
-
result.diagnostics = error.diagnostics;
|
|
487
|
-
return result;
|
|
488
|
-
}
|
|
489
|
-
//#endregion
|
|
490
|
-
//#region src/runtime-headers.ts
|
|
491
|
-
const HEADER_CRLF_RE = /[\r\n]/;
|
|
492
|
-
/**
|
|
493
|
-
* Reject header values containing CR/LF. Some runtimes (Node `undici`
|
|
494
|
-
* Headers) throw on their own, but Web-runtime fetch implementations
|
|
495
|
-
* vary, and a user-supplied `headers()` value is never trusted input.
|
|
496
|
-
* Keeping the check here means response-splitting can't slip through on
|
|
497
|
-
* any adapter.
|
|
498
|
-
*/
|
|
499
|
-
function assertSafeHeaderValue(name, value) {
|
|
500
|
-
if (HEADER_CRLF_RE.test(value)) throw new Error(`Refused to set header "${name}": value contains CR or LF`);
|
|
501
|
-
}
|
|
502
|
-
function applyHeaders(headers, init) {
|
|
503
|
-
for (const [key, value] of iterateHeaderInit(init)) assertSafeHeaderValue(key, value);
|
|
504
|
-
new Headers(init).forEach((value, key) => {
|
|
505
|
-
headers.set(key, value);
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
function* iterateHeaderInit(init) {
|
|
509
|
-
if (init instanceof Headers) {
|
|
510
|
-
for (const entry of init.entries()) yield entry;
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
if (Array.isArray(init)) {
|
|
514
|
-
for (const entry of init) if (entry && entry.length >= 2) yield [entry[0], entry[1]];
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
for (const [key, value] of Object.entries(init)) yield [key, value];
|
|
518
|
-
}
|
|
519
|
-
function applyDefaultSecurityHeaders(headers) {
|
|
520
|
-
if (!headers.has("permissions-policy")) headers.set("permissions-policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
|
|
521
|
-
if (!headers.has("referrer-policy")) headers.set("referrer-policy", "strict-origin-when-cross-origin");
|
|
522
|
-
if (!headers.has("x-content-type-options")) headers.set("x-content-type-options", "nosniff");
|
|
523
|
-
if (!headers.has("x-frame-options")) headers.set("x-frame-options", "SAMEORIGIN");
|
|
524
|
-
return headers;
|
|
525
|
-
}
|
|
526
|
-
function applySecurityAndRouteHeaders(headers, options) {
|
|
527
|
-
applyDefaultSecurityHeaders(headers);
|
|
528
|
-
if (options) {
|
|
529
|
-
appendVaryHeader(headers, ROUTE_STATE_REQUEST_HEADER);
|
|
530
|
-
if (options.isRouteStateRequest && !headers.has("cache-control")) headers.set("cache-control", ROUTE_STATE_CACHE_CONTROL);
|
|
531
|
-
}
|
|
532
|
-
return headers;
|
|
533
|
-
}
|
|
534
|
-
function withDefaultSecurityHeaders(response) {
|
|
535
|
-
const headers = new Headers(response.headers);
|
|
536
|
-
applySecurityAndRouteHeaders(headers);
|
|
537
|
-
return new Response(response.body, {
|
|
538
|
-
status: response.status,
|
|
539
|
-
statusText: response.statusText,
|
|
540
|
-
headers
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
function withRouteResponseHeaders(response, options) {
|
|
544
|
-
const headers = new Headers(response.headers);
|
|
545
|
-
applySecurityAndRouteHeaders(headers, options);
|
|
546
|
-
return new Response(response.body, {
|
|
547
|
-
status: response.status,
|
|
548
|
-
statusText: response.statusText,
|
|
549
|
-
headers
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
function appendVaryHeader(headers, value) {
|
|
553
|
-
const current = headers.get("vary");
|
|
554
|
-
if (!current) {
|
|
555
|
-
headers.set("vary", value);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
const values = current.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
559
|
-
if (values.includes("*") || values.includes(value.toLowerCase())) return;
|
|
560
|
-
headers.set("vary", `${current}, ${value}`);
|
|
561
|
-
}
|
|
562
|
-
//#endregion
|
|
563
|
-
//#region src/runtime-client-fetch.ts
|
|
564
|
-
const SAFE_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
|
|
565
|
-
/**
|
|
566
|
-
* Parse a possibly-server-supplied redirect target against a base URL and
|
|
567
|
-
* return it only if it uses a safe navigation scheme (`http:` or `https:`).
|
|
568
|
-
*
|
|
569
|
-
* `javascript:`, `data:`, `vbscript:`, `blob:`, `file:` and similar schemes
|
|
570
|
-
* can execute script or bypass same-origin assumptions when assigned to
|
|
571
|
-
* `window.location.href` — a server-controlled redirect (from a loader,
|
|
572
|
-
* middleware, form action response, or API route) must never be able to
|
|
573
|
-
* trigger them. Returns `null` for unsafe or unparseable inputs.
|
|
574
|
-
*/
|
|
575
|
-
function parseSafeNavigationUrl(location, base) {
|
|
576
|
-
let targetUrl;
|
|
577
|
-
try {
|
|
578
|
-
targetUrl = new URL(location, base);
|
|
579
|
-
} catch {
|
|
580
|
-
return null;
|
|
581
|
-
}
|
|
582
|
-
if (!SAFE_NAVIGATION_PROTOCOLS.has(targetUrl.protocol)) return null;
|
|
583
|
-
return targetUrl;
|
|
584
|
-
}
|
|
585
|
-
function routeNeedsServerFetch(route) {
|
|
586
|
-
if (route.hasLoader === false && route.middlewareFiles.length === 0) return false;
|
|
587
|
-
return true;
|
|
588
|
-
}
|
|
589
|
-
function buildRouteStateUrl(url) {
|
|
590
|
-
return `${url}${url.includes("?") ? "&" : "?"}_data=1`;
|
|
591
|
-
}
|
|
592
|
-
async function fetchPrachtRouteState(url, options) {
|
|
593
|
-
const fetchUrl = options?.useDataParam ? buildRouteStateUrl(url) : url;
|
|
594
|
-
const response = await fetch(fetchUrl, {
|
|
595
|
-
headers: options?.useDataParam ? {} : {
|
|
596
|
-
[ROUTE_STATE_REQUEST_HEADER]: "1",
|
|
597
|
-
"Cache-Control": "no-cache"
|
|
598
|
-
},
|
|
599
|
-
redirect: "manual",
|
|
600
|
-
signal: options?.signal
|
|
601
|
-
});
|
|
602
|
-
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) return {
|
|
603
|
-
location: response.headers.get("location") ?? url,
|
|
604
|
-
type: "redirect"
|
|
605
|
-
};
|
|
606
|
-
const json = await response.json();
|
|
607
|
-
if (json.redirect) return {
|
|
608
|
-
location: json.redirect,
|
|
609
|
-
type: "redirect"
|
|
610
|
-
};
|
|
611
|
-
if (!response.ok) {
|
|
612
|
-
if (json.error) return {
|
|
613
|
-
error: json.error,
|
|
614
|
-
type: "error"
|
|
615
|
-
};
|
|
616
|
-
throw new Error(`Failed to fetch route state (${response.status})`);
|
|
617
|
-
}
|
|
618
|
-
return {
|
|
619
|
-
data: json.data,
|
|
620
|
-
type: "data"
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
async function navigateToClientLocation(location, options) {
|
|
624
|
-
if (typeof window === "undefined") return;
|
|
625
|
-
const targetUrl = parseSafeNavigationUrl(location, window.location.href);
|
|
626
|
-
if (!targetUrl) {
|
|
627
|
-
console.error(`[pracht] refused to navigate to unsafe URL: ${location}`);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
const target = targetUrl.pathname + targetUrl.search + targetUrl.hash;
|
|
631
|
-
if (targetUrl.origin === window.location.origin && window.__PRACHT_NAVIGATE__) {
|
|
632
|
-
await window.__PRACHT_NAVIGATE__(target, options);
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
if (options?.replace) {
|
|
636
|
-
window.location.replace(targetUrl.toString());
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
window.location.href = targetUrl.toString();
|
|
640
|
-
}
|
|
641
|
-
//#endregion
|
|
642
|
-
//#region src/prefetch.ts
|
|
643
|
-
const CACHE_TTL_MS = 3e4;
|
|
644
|
-
const MAX_PREFETCH_CACHE_ENTRIES = 100;
|
|
645
|
-
const MAX_MATCH_CACHE_ENTRIES = 250;
|
|
646
|
-
const EMPTY_ROUTE_STATE_PROMISE = Promise.resolve({
|
|
647
|
-
type: "data",
|
|
648
|
-
data: void 0
|
|
649
|
-
});
|
|
650
|
-
const prefetchCache = /* @__PURE__ */ new Map();
|
|
651
|
-
function clearPrefetchCache() {
|
|
652
|
-
prefetchCache.clear();
|
|
653
|
-
}
|
|
654
|
-
function getCachedRouteState(url) {
|
|
655
|
-
const entry = prefetchCache.get(url);
|
|
656
|
-
if (!entry) return null;
|
|
657
|
-
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
|
|
658
|
-
prefetchCache.delete(url);
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
prefetchCache.delete(url);
|
|
662
|
-
prefetchCache.set(url, entry);
|
|
663
|
-
return entry.promise;
|
|
664
|
-
}
|
|
665
|
-
function prefetchRouteState(url, route) {
|
|
666
|
-
if (route && !routeNeedsServerFetch(route)) return EMPTY_ROUTE_STATE_PROMISE;
|
|
667
|
-
const cached = getCachedRouteState(url);
|
|
668
|
-
if (cached) return cached;
|
|
669
|
-
sweepPrefetchCache();
|
|
670
|
-
const promise = fetchPrachtRouteState(url);
|
|
671
|
-
prefetchCache.set(url, {
|
|
672
|
-
promise,
|
|
673
|
-
timestamp: Date.now()
|
|
674
|
-
});
|
|
675
|
-
trimMapToSize(prefetchCache, MAX_PREFETCH_CACHE_ENTRIES);
|
|
676
|
-
return promise;
|
|
677
|
-
}
|
|
678
|
-
function setupPrefetching(app, warmModules) {
|
|
679
|
-
let hoverTimer = null;
|
|
680
|
-
const observedViewportAnchors = /* @__PURE__ */ new WeakSet();
|
|
681
|
-
const matchCache = /* @__PURE__ */ new Map();
|
|
682
|
-
function getRoutePathname(url) {
|
|
683
|
-
try {
|
|
684
|
-
return new URL(url, window.location.origin).pathname;
|
|
685
|
-
} catch {
|
|
686
|
-
return null;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
function getInternalHref(anchor) {
|
|
690
|
-
const href = anchor.getAttribute("href");
|
|
691
|
-
if (!href || href.startsWith("#")) return null;
|
|
692
|
-
let url;
|
|
693
|
-
try {
|
|
694
|
-
url = new URL(href, window.location.origin);
|
|
695
|
-
} catch {
|
|
696
|
-
return null;
|
|
697
|
-
}
|
|
698
|
-
if (url.origin !== window.location.origin) return null;
|
|
699
|
-
return url.pathname + url.search;
|
|
700
|
-
}
|
|
701
|
-
function getMatchEntry(href) {
|
|
702
|
-
const cached = matchCache.get(href);
|
|
703
|
-
if (cached) {
|
|
704
|
-
matchCache.delete(href);
|
|
705
|
-
matchCache.set(href, cached);
|
|
706
|
-
return cached;
|
|
707
|
-
}
|
|
708
|
-
const routePathname = getRoutePathname(href);
|
|
709
|
-
const match = routePathname ? matchAppRoute(app, routePathname) ?? null : null;
|
|
710
|
-
const entry = {
|
|
711
|
-
match,
|
|
712
|
-
strategy: match ? match.route.prefetch ?? "intent" : "none"
|
|
713
|
-
};
|
|
714
|
-
matchCache.set(href, entry);
|
|
715
|
-
trimMapToSize(matchCache, MAX_MATCH_CACHE_ENTRIES);
|
|
716
|
-
return entry;
|
|
717
|
-
}
|
|
718
|
-
function prefetchHref(href) {
|
|
719
|
-
const match = getMatchEntry(href).match;
|
|
720
|
-
prefetchRouteState(href, match?.route);
|
|
721
|
-
if (warmModules && match) warmModules(match);
|
|
722
|
-
}
|
|
723
|
-
document.addEventListener("mouseenter", (e) => {
|
|
724
|
-
const anchor = e.target.closest?.("a");
|
|
725
|
-
if (!anchor) return;
|
|
726
|
-
const href = getInternalHref(anchor);
|
|
727
|
-
if (!href) return;
|
|
728
|
-
const strategy = getMatchEntry(href).strategy;
|
|
729
|
-
if (strategy !== "hover" && strategy !== "intent") return;
|
|
730
|
-
if (hoverTimer) clearTimeout(hoverTimer);
|
|
731
|
-
hoverTimer = setTimeout(() => {
|
|
732
|
-
prefetchHref(href);
|
|
733
|
-
}, 50);
|
|
734
|
-
}, true);
|
|
735
|
-
document.addEventListener("mouseleave", (e) => {
|
|
736
|
-
if (!e.target.closest?.("a")) return;
|
|
737
|
-
if (hoverTimer) {
|
|
738
|
-
clearTimeout(hoverTimer);
|
|
739
|
-
hoverTimer = null;
|
|
740
|
-
}
|
|
741
|
-
}, true);
|
|
742
|
-
document.addEventListener("focusin", (e) => {
|
|
743
|
-
const anchor = e.target.closest?.("a");
|
|
744
|
-
if (!anchor) return;
|
|
745
|
-
const href = getInternalHref(anchor);
|
|
746
|
-
if (!href) return;
|
|
747
|
-
const strategy = getMatchEntry(href).strategy;
|
|
748
|
-
if (strategy !== "hover" && strategy !== "intent") return;
|
|
749
|
-
prefetchHref(href);
|
|
750
|
-
}, true);
|
|
751
|
-
if (typeof IntersectionObserver === "undefined") return;
|
|
752
|
-
const observer = new IntersectionObserver((entries) => {
|
|
753
|
-
for (const entry of entries) {
|
|
754
|
-
if (!entry.isIntersecting) continue;
|
|
755
|
-
const anchor = entry.target;
|
|
756
|
-
const href = getInternalHref(anchor);
|
|
757
|
-
if (!href) continue;
|
|
758
|
-
prefetchHref(href);
|
|
759
|
-
observer.unobserve(anchor);
|
|
760
|
-
}
|
|
761
|
-
}, { rootMargin: "200px" });
|
|
762
|
-
function observeAnchor(anchor) {
|
|
763
|
-
if (observedViewportAnchors.has(anchor)) return;
|
|
764
|
-
const href = getInternalHref(anchor);
|
|
765
|
-
if (!href) return;
|
|
766
|
-
if (getMatchEntry(href).strategy !== "viewport") return;
|
|
767
|
-
observedViewportAnchors.add(anchor);
|
|
768
|
-
observer.observe(anchor);
|
|
769
|
-
}
|
|
770
|
-
function observeViewportLinks(root) {
|
|
771
|
-
if (root instanceof HTMLAnchorElement) observeAnchor(root);
|
|
772
|
-
for (const anchor of root.querySelectorAll("a[href]")) observeAnchor(anchor);
|
|
773
|
-
}
|
|
774
|
-
observeViewportLinks(document.body);
|
|
775
|
-
new MutationObserver((records) => {
|
|
776
|
-
for (const record of records) for (const node of record.addedNodes) if (node instanceof HTMLElement || node instanceof DocumentFragment) observeViewportLinks(node);
|
|
777
|
-
}).observe(document.body, {
|
|
778
|
-
childList: true,
|
|
779
|
-
subtree: true
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
function sweepPrefetchCache(now = Date.now()) {
|
|
783
|
-
for (const [url, entry] of prefetchCache) if (now - entry.timestamp > CACHE_TTL_MS) prefetchCache.delete(url);
|
|
784
|
-
}
|
|
785
|
-
function trimMapToSize(map, maxEntries) {
|
|
786
|
-
while (map.size > maxEntries) {
|
|
787
|
-
const first = map.keys().next();
|
|
788
|
-
if (first.done) return;
|
|
789
|
-
map.delete(first.value);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
//#endregion
|
|
793
|
-
//#region src/runtime-hooks.ts
|
|
794
|
-
const RouteDataContext = createContext(void 0);
|
|
795
|
-
function PrachtRuntimeProvider({ children, data, params = EMPTY_ROUTE_PARAMS, routeId, routes, stateVersion = 0, url }) {
|
|
796
|
-
registerRuntimeRoutes(routes);
|
|
797
|
-
const [routeDataState, setRouteDataState] = useState({
|
|
798
|
-
data,
|
|
799
|
-
stateVersion
|
|
800
|
-
});
|
|
801
|
-
const routeData = routeDataState.stateVersion === stateVersion ? routeDataState.data : data;
|
|
802
|
-
useEffect(() => {
|
|
803
|
-
setRouteDataState({
|
|
804
|
-
data,
|
|
805
|
-
stateVersion
|
|
806
|
-
});
|
|
807
|
-
}, [
|
|
808
|
-
data,
|
|
809
|
-
routeId,
|
|
810
|
-
stateVersion,
|
|
811
|
-
url
|
|
812
|
-
]);
|
|
813
|
-
const context = useMemo(() => ({
|
|
814
|
-
data: routeData,
|
|
815
|
-
params,
|
|
816
|
-
routeId,
|
|
817
|
-
routes,
|
|
818
|
-
setData: (nextData) => setRouteDataState({
|
|
819
|
-
data: nextData,
|
|
820
|
-
stateVersion
|
|
821
|
-
}),
|
|
822
|
-
url
|
|
823
|
-
}), [
|
|
824
|
-
routeData,
|
|
825
|
-
params,
|
|
826
|
-
routeId,
|
|
827
|
-
routes,
|
|
828
|
-
stateVersion,
|
|
829
|
-
url
|
|
830
|
-
]);
|
|
831
|
-
return h(RouteDataContext.Provider, {
|
|
832
|
-
value: context,
|
|
833
|
-
children
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
function startApp(options = {}) {
|
|
837
|
-
if (typeof window === "undefined") return options.initialData;
|
|
838
|
-
if (typeof options.initialData !== "undefined") return options.initialData;
|
|
839
|
-
return readHydrationState()?.data;
|
|
840
|
-
}
|
|
841
|
-
function readHydrationState() {
|
|
842
|
-
if (typeof window === "undefined") return;
|
|
843
|
-
if (window.__PRACHT_STATE__) return window.__PRACHT_STATE__;
|
|
844
|
-
const element = document.getElementById(HYDRATION_STATE_ELEMENT_ID);
|
|
845
|
-
if (!(element instanceof HTMLScriptElement)) return;
|
|
846
|
-
const raw = element.textContent;
|
|
847
|
-
if (!raw) return;
|
|
848
|
-
const state = JSON.parse(raw);
|
|
849
|
-
window.__PRACHT_STATE__ = state;
|
|
850
|
-
return state;
|
|
851
|
-
}
|
|
852
|
-
function useRouteData() {
|
|
853
|
-
return useContext(RouteDataContext)?.data;
|
|
854
|
-
}
|
|
855
|
-
function useLocation() {
|
|
856
|
-
return parseLocation(useContext(RouteDataContext)?.url ?? (typeof window !== "undefined" ? window.location.pathname + window.location.search : "/"));
|
|
857
|
-
}
|
|
858
|
-
function useParams() {
|
|
859
|
-
return useContext(RouteDataContext)?.params ?? {};
|
|
860
|
-
}
|
|
861
|
-
function useRevalidate() {
|
|
862
|
-
const runtime = useContext(RouteDataContext);
|
|
863
|
-
return async () => {
|
|
864
|
-
if (typeof window === "undefined") return;
|
|
865
|
-
const result = await fetchPrachtRouteState(runtime?.url || window.location.pathname + window.location.search);
|
|
866
|
-
if (result.type === "redirect") {
|
|
867
|
-
await navigateToClientLocation(result.location);
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
if (result.type === "error") throw deserializeRouteError(result.error);
|
|
871
|
-
runtime?.setData(result.data);
|
|
872
|
-
return result.data;
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
function Link(props) {
|
|
876
|
-
const routes = useContext(RouteDataContext)?.routes ?? globalThis.__PRACHT_ROUTE_DEFINITIONS__;
|
|
877
|
-
if (!routes) throw new Error("<Link route=...> must render inside a pracht route tree.");
|
|
878
|
-
const { route, params, search, hash, ...anchorProps } = props;
|
|
879
|
-
return h("a", {
|
|
880
|
-
...anchorProps,
|
|
881
|
-
href: buildHref(routes, route, {
|
|
882
|
-
params,
|
|
883
|
-
search,
|
|
884
|
-
hash
|
|
885
|
-
})
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
function registerRuntimeRoutes(routes) {
|
|
889
|
-
if (!routes) return;
|
|
890
|
-
globalThis.__PRACHT_ROUTE_DEFINITIONS__ = routes;
|
|
891
|
-
}
|
|
892
|
-
function Form(props) {
|
|
893
|
-
const { onSubmit, method, ...rest } = props;
|
|
894
|
-
return h("form", {
|
|
895
|
-
...rest,
|
|
896
|
-
method,
|
|
897
|
-
onSubmit: async (event) => {
|
|
898
|
-
onSubmit?.(event);
|
|
899
|
-
if (event.defaultPrevented) return;
|
|
900
|
-
const form = event.currentTarget;
|
|
901
|
-
if (!(form instanceof HTMLFormElement)) return;
|
|
902
|
-
const formMethod = (method ?? form.method ?? "post").toUpperCase();
|
|
903
|
-
if (SAFE_METHODS.has(formMethod)) return;
|
|
904
|
-
event.preventDefault();
|
|
905
|
-
clearPrefetchCache();
|
|
906
|
-
const response = await fetch(props.action ?? form.action, {
|
|
907
|
-
method: formMethod,
|
|
908
|
-
body: new FormData(form),
|
|
909
|
-
redirect: "manual"
|
|
910
|
-
});
|
|
911
|
-
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
|
|
912
|
-
const location = response.headers.get("location");
|
|
913
|
-
if (location) {
|
|
914
|
-
await navigateToClientLocation(location);
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
window.location.href = props.action ?? form.action;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
function parseLocation(value) {
|
|
923
|
-
const url = new URL(value, "http://pracht.local");
|
|
924
|
-
return {
|
|
925
|
-
pathname: url.pathname,
|
|
926
|
-
search: url.search
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
//#endregion
|
|
930
|
-
//#region src/runtime-html.ts
|
|
931
|
-
function escapeHtml(str) {
|
|
932
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
933
|
-
}
|
|
934
|
-
function serializeJsonForHtml(value) {
|
|
935
|
-
return escapeScriptText(JSON.stringify(value) ?? "null");
|
|
936
|
-
}
|
|
937
|
-
function escapeScriptText(value) {
|
|
938
|
-
return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
939
|
-
}
|
|
940
|
-
const SAFE_ATTRIBUTE_NAME_RE = /^[A-Za-z_:][A-Za-z0-9:._-]*$/;
|
|
941
|
-
const GLOBAL_HEAD_ATTRIBUTE_PREFIXES = ["data-", "aria-"];
|
|
942
|
-
const META_ATTRIBUTES = new Set([
|
|
943
|
-
"charset",
|
|
944
|
-
"content",
|
|
945
|
-
"http-equiv",
|
|
946
|
-
"itemprop",
|
|
947
|
-
"media",
|
|
948
|
-
"name",
|
|
949
|
-
"property"
|
|
950
|
-
]);
|
|
951
|
-
const LINK_ATTRIBUTES = new Set([
|
|
952
|
-
"as",
|
|
953
|
-
"blocking",
|
|
954
|
-
"color",
|
|
955
|
-
"crossorigin",
|
|
956
|
-
"disabled",
|
|
957
|
-
"fetchpriority",
|
|
958
|
-
"href",
|
|
959
|
-
"hreflang",
|
|
960
|
-
"imagesizes",
|
|
961
|
-
"imagesrcset",
|
|
962
|
-
"integrity",
|
|
963
|
-
"media",
|
|
964
|
-
"referrerpolicy",
|
|
965
|
-
"rel",
|
|
966
|
-
"sizes",
|
|
967
|
-
"title",
|
|
968
|
-
"type"
|
|
969
|
-
]);
|
|
970
|
-
const SCRIPT_ATTRIBUTES = new Set([
|
|
971
|
-
"async",
|
|
972
|
-
"blocking",
|
|
973
|
-
"class",
|
|
974
|
-
"crossorigin",
|
|
975
|
-
"defer",
|
|
976
|
-
"fetchpriority",
|
|
977
|
-
"id",
|
|
978
|
-
"integrity",
|
|
979
|
-
"nomodule",
|
|
980
|
-
"nonce",
|
|
981
|
-
"referrerpolicy",
|
|
982
|
-
"src",
|
|
983
|
-
"type"
|
|
984
|
-
]);
|
|
985
|
-
function renderAttributes(attributes, allowedAttributes) {
|
|
986
|
-
return Object.entries(attributes).filter(([key, value]) => isAllowedHeadAttribute(key, value, allowedAttributes)).map(([key, value]) => `${key}="${escapeHtml(value ?? "")}"`).join(" ");
|
|
987
|
-
}
|
|
988
|
-
function isAllowedHeadAttribute(key, value, allowedAttributes) {
|
|
989
|
-
if (key === "children" || typeof value === "undefined" || !SAFE_ATTRIBUTE_NAME_RE.test(key)) return false;
|
|
990
|
-
const normalized = key.toLowerCase();
|
|
991
|
-
if (normalized.startsWith("on")) return false;
|
|
992
|
-
return allowedAttributes.has(normalized) || GLOBAL_HEAD_ATTRIBUTE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
993
|
-
}
|
|
994
|
-
function buildHtmlDocument(options) {
|
|
995
|
-
const { head, body, hydrationState, clientEntryUrl, cssUrls = [], modulePreloadUrls = [], routeStatePreloadUrl } = options;
|
|
996
|
-
const titleTag = head.title ? `<title>${escapeHtml(head.title)}</title>` : "";
|
|
997
|
-
const metaTags = (head.meta ?? []).map((m) => renderAttributes(m, META_ATTRIBUTES)).filter(Boolean).map((attrs) => `<meta ${attrs}>`).join("\n ");
|
|
998
|
-
const linkTags = (head.link ?? []).map((l) => renderAttributes(l, LINK_ATTRIBUTES)).filter(Boolean).map((attrs) => `<link ${attrs}>`).join("\n ");
|
|
999
|
-
const scriptTags = (head.script ?? []).map((script) => {
|
|
1000
|
-
const attrs = renderAttributes(script, SCRIPT_ATTRIBUTES);
|
|
1001
|
-
const children = script.children ? escapeScriptText(script.children) : "";
|
|
1002
|
-
return attrs ? `<script ${attrs}>${children}<\/script>` : `<script>${children}<\/script>`;
|
|
1003
|
-
}).join("\n ");
|
|
1004
|
-
const cssTags = cssUrls.map((url) => `<link rel="stylesheet" href="${escapeHtml(url)}">`).join("\n ");
|
|
1005
|
-
const modulePreloadTags = modulePreloadUrls.map((url) => `<link rel="modulepreload" href="${escapeHtml(url)}">`).join("\n ");
|
|
1006
|
-
const routeStatePreloadTag = routeStatePreloadUrl ? `<link rel="preload" as="fetch" href="${escapeHtml(routeStatePreloadUrl)}" crossorigin="anonymous">` : "";
|
|
1007
|
-
const stateScript = `<script id="${HYDRATION_STATE_ELEMENT_ID}" type="application/json">${serializeJsonForHtml(hydrationState)}<\/script>`;
|
|
1008
|
-
const entryScript = clientEntryUrl ? `<script type="module" src="${escapeHtml(clientEntryUrl)}"><\/script>` : "";
|
|
1009
|
-
return `<!DOCTYPE html>
|
|
1010
|
-
<html${head.lang ? ` lang="${escapeHtml(head.lang)}"` : ""}>
|
|
1011
|
-
<head>
|
|
1012
|
-
<meta charset="utf-8">
|
|
1013
|
-
${titleTag}
|
|
1014
|
-
${metaTags}
|
|
1015
|
-
${linkTags}
|
|
1016
|
-
${scriptTags}
|
|
1017
|
-
${cssTags}
|
|
1018
|
-
${modulePreloadTags}
|
|
1019
|
-
${routeStatePreloadTag}
|
|
1020
|
-
</head>
|
|
1021
|
-
<body>
|
|
1022
|
-
<div id="pracht-root">${body}</div>
|
|
1023
|
-
${stateScript}
|
|
1024
|
-
${entryScript}
|
|
1025
|
-
</body>
|
|
1026
|
-
</html>`;
|
|
1027
|
-
}
|
|
1028
|
-
function htmlResponse(html, status = 200, initHeaders) {
|
|
1029
|
-
const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
|
|
1030
|
-
if (initHeaders) applyHeaders(headers, initHeaders);
|
|
1031
|
-
applySecurityAndRouteHeaders(headers, { isRouteStateRequest: false });
|
|
1032
|
-
return new Response(html, {
|
|
1033
|
-
status,
|
|
1034
|
-
headers
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
//#endregion
|
|
1038
|
-
//#region src/runtime-manifest.ts
|
|
1039
|
-
/** Strip leading `./` and `/` so all module paths share one canonical form. */
|
|
1040
|
-
function normalizeModulePath(path) {
|
|
1041
|
-
return path.replace(/^\.?\//, "");
|
|
1042
|
-
}
|
|
1043
|
-
function buildSuffixIndex(manifest) {
|
|
1044
|
-
const index = /* @__PURE__ */ new Map();
|
|
1045
|
-
for (const key of Object.keys(manifest)) {
|
|
1046
|
-
const normalized = normalizeModulePath(key);
|
|
1047
|
-
if (!normalized) continue;
|
|
1048
|
-
if (!index.has(normalized)) index.set(normalized, key);
|
|
1049
|
-
for (let i = normalized.indexOf("/"); i !== -1; i = normalized.indexOf("/", i + 1)) {
|
|
1050
|
-
const suffix = normalized.slice(i + 1);
|
|
1051
|
-
if (suffix && !index.has(suffix)) index.set(suffix, key);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
return index;
|
|
1055
|
-
}
|
|
1056
|
-
const suffixIndexCache = /* @__PURE__ */ new WeakMap();
|
|
1057
|
-
function getSuffixIndex(manifest) {
|
|
1058
|
-
let index = suffixIndexCache.get(manifest);
|
|
1059
|
-
if (index) return index;
|
|
1060
|
-
index = buildSuffixIndex(manifest);
|
|
1061
|
-
suffixIndexCache.set(manifest, index);
|
|
1062
|
-
return index;
|
|
1063
|
-
}
|
|
1064
|
-
function resolveManifestEntries(manifest, file) {
|
|
1065
|
-
if (file in manifest) return manifest[file];
|
|
1066
|
-
const resolved = getSuffixIndex(manifest).get(normalizeModulePath(file));
|
|
1067
|
-
if (resolved) return manifest[resolved];
|
|
1068
|
-
}
|
|
1069
|
-
function resolvePageUrlsFromManifest(manifest, shellFile, routeFile) {
|
|
1070
|
-
const urls = /* @__PURE__ */ new Set();
|
|
1071
|
-
const add = (file) => {
|
|
1072
|
-
const entries = resolveManifestEntries(manifest, file);
|
|
1073
|
-
if (entries) for (const url of entries) urls.add(url);
|
|
1074
|
-
};
|
|
1075
|
-
if (shellFile) add(shellFile);
|
|
1076
|
-
add(routeFile);
|
|
1077
|
-
return [...urls];
|
|
1078
|
-
}
|
|
1079
|
-
function resolvePageCssUrls(cssManifest, shellFile, routeFile) {
|
|
1080
|
-
if (!cssManifest) return [];
|
|
1081
|
-
return resolvePageUrlsFromManifest(cssManifest, shellFile, routeFile);
|
|
1082
|
-
}
|
|
1083
|
-
function resolvePageJsUrls(jsManifest, shellFile, routeFile) {
|
|
1084
|
-
if (!jsManifest) return [];
|
|
1085
|
-
return resolvePageUrlsFromManifest(jsManifest, shellFile, routeFile);
|
|
1086
|
-
}
|
|
1087
|
-
async function resolveRegistryModule(modules, file) {
|
|
1088
|
-
if (!modules) return void 0;
|
|
1089
|
-
if (file in modules) return modules[file]();
|
|
1090
|
-
const resolved = getSuffixIndex(modules).get(normalizeModulePath(file));
|
|
1091
|
-
if (resolved) return modules[resolved]();
|
|
1092
|
-
}
|
|
1093
|
-
async function resolveDataFunctions(route, routeModule, registry) {
|
|
1094
|
-
let loader = routeModule?.loader;
|
|
1095
|
-
let loaderFile = routeModule?.loader ? route.file : void 0;
|
|
1096
|
-
if (route.loaderFile) {
|
|
1097
|
-
const dataModule = await resolveRegistryModule(registry.dataModules, route.loaderFile);
|
|
1098
|
-
if (dataModule?.loader) {
|
|
1099
|
-
loader = dataModule.loader;
|
|
1100
|
-
loaderFile = route.loaderFile;
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
return {
|
|
1104
|
-
loader,
|
|
1105
|
-
loaderFile
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
//#endregion
|
|
1109
|
-
//#region src/runtime-middleware.ts
|
|
1110
|
-
const DEFAULT_REDIRECT_STATUS_SAFE = 302;
|
|
1111
|
-
const DEFAULT_REDIRECT_STATUS_UNSAFE = 303;
|
|
1112
|
-
/**
|
|
1113
|
-
* Build a safe redirect response from middleware/loader output. Rejects
|
|
1114
|
-
* non-http(s) schemes (no `javascript:`/`data:`/etc.) and CR/LF injection
|
|
1115
|
-
* against the `Location` header. When status is omitted, non-GET/HEAD
|
|
1116
|
-
* requests default to 303 so the browser does not resend the body to the
|
|
1117
|
-
* redirect target; safe methods default to 302.
|
|
1118
|
-
*
|
|
1119
|
-
* The original `target` string is preserved on success (relative paths
|
|
1120
|
-
* stay relative) — we only parse it to validate scheme, not to rewrite
|
|
1121
|
-
* it. Both the original input and its resolved URL must be CR/LF-free.
|
|
1122
|
-
*/
|
|
1123
|
-
function buildRedirectResponse(target, options) {
|
|
1124
|
-
if (/[\r\n]/.test(target)) throw new Error("Refused redirect target containing CR/LF");
|
|
1125
|
-
if (!parseSafeNavigationUrl(target, options.baseUrl)) throw new Error("Refused unsafe redirect target");
|
|
1126
|
-
const method = (options.method ?? "GET").toUpperCase();
|
|
1127
|
-
const defaultStatus = SAFE_METHODS.has(method) ? DEFAULT_REDIRECT_STATUS_SAFE : DEFAULT_REDIRECT_STATUS_UNSAFE;
|
|
1128
|
-
const status = options.status ?? defaultStatus;
|
|
1129
|
-
return new Response(null, {
|
|
1130
|
-
status,
|
|
1131
|
-
headers: { location: target }
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
async function runMiddlewareChain(options) {
|
|
1135
|
-
let context = options.context;
|
|
1136
|
-
const modulePromises = options.middlewareFiles.map((mwFile) => resolveRegistryModule(options.registry.middlewareModules, mwFile));
|
|
1137
|
-
for (const p of modulePromises) p.catch(() => {});
|
|
1138
|
-
for (const modulePromise of modulePromises) {
|
|
1139
|
-
const mwModule = await modulePromise;
|
|
1140
|
-
if (!mwModule?.middleware) continue;
|
|
1141
|
-
const result = await mwModule.middleware({
|
|
1142
|
-
request: options.request,
|
|
1143
|
-
params: options.params,
|
|
1144
|
-
context,
|
|
1145
|
-
signal: AbortSignal.timeout(3e4),
|
|
1146
|
-
url: options.url,
|
|
1147
|
-
route: options.route
|
|
1148
|
-
});
|
|
1149
|
-
if (!result) continue;
|
|
1150
|
-
if (result instanceof Response) return { response: withDefaultSecurityHeaders(result) };
|
|
1151
|
-
if ("redirect" in result) {
|
|
1152
|
-
const status = "status" in result ? result.status : void 0;
|
|
1153
|
-
return { response: withDefaultSecurityHeaders(buildRedirectResponse(result.redirect, {
|
|
1154
|
-
baseUrl: options.request.url,
|
|
1155
|
-
method: options.request.method,
|
|
1156
|
-
status
|
|
1157
|
-
})) };
|
|
1158
|
-
}
|
|
1159
|
-
if ("context" in result) context = {
|
|
1160
|
-
...context,
|
|
1161
|
-
...result.context
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
return { context };
|
|
1165
|
-
}
|
|
1166
|
-
async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
|
|
1167
|
-
const [shellHead, routeHead] = await Promise.all([shellModule?.head ? shellModule.head(routeArgs) : Promise.resolve({}), routeModule?.head ? routeModule.head({
|
|
1168
|
-
...routeArgs,
|
|
1169
|
-
data
|
|
1170
|
-
}) : Promise.resolve({})]);
|
|
1171
|
-
return {
|
|
1172
|
-
title: routeHead.title ?? shellHead.title,
|
|
1173
|
-
lang: routeHead.lang ?? shellHead.lang,
|
|
1174
|
-
meta: [...shellHead.meta ?? [], ...routeHead.meta ?? []],
|
|
1175
|
-
link: [...shellHead.link ?? [], ...routeHead.link ?? []],
|
|
1176
|
-
script: [...shellHead.script ?? [], ...routeHead.script ?? []]
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
async function mergeDocumentHeaders(shellModule, routeModule, routeArgs, data) {
|
|
1180
|
-
const headers = new Headers();
|
|
1181
|
-
const [shellHeaders, routeHeaders] = await Promise.all([shellModule?.headers ? shellModule.headers(routeArgs) : Promise.resolve(void 0), routeModule?.headers ? routeModule.headers({
|
|
1182
|
-
...routeArgs,
|
|
1183
|
-
data
|
|
1184
|
-
}) : Promise.resolve(void 0)]);
|
|
1185
|
-
if (shellHeaders) applyHeaders(headers, shellHeaders);
|
|
1186
|
-
if (routeHeaders) applyHeaders(headers, routeHeaders);
|
|
1187
|
-
return headers;
|
|
1188
|
-
}
|
|
1189
|
-
//#endregion
|
|
1190
|
-
//#region src/runtime-response.ts
|
|
1191
|
-
let _renderToStringAsync;
|
|
1192
|
-
async function getRenderToStringAsync() {
|
|
1193
|
-
if (_renderToStringAsync) return _renderToStringAsync;
|
|
1194
|
-
_renderToStringAsync = (await import("preact-render-to-string")).renderToStringAsync;
|
|
1195
|
-
return _renderToStringAsync;
|
|
1196
|
-
}
|
|
1197
|
-
function jsonErrorResponse(routeError, options) {
|
|
1198
|
-
const headers = applySecurityAndRouteHeaders(new Headers({ "content-type": "application/json; charset=utf-8" }), options.isRouteStateRequest ? { isRouteStateRequest: true } : void 0);
|
|
1199
|
-
return new Response(JSON.stringify({ error: routeError }), {
|
|
1200
|
-
status: routeError.status,
|
|
1201
|
-
headers
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
function jsonRedirectResponse(location, options) {
|
|
1205
|
-
const headers = new Headers(options.headers);
|
|
1206
|
-
headers.set("content-type", "application/json; charset=utf-8");
|
|
1207
|
-
return withRouteResponseHeaders(new Response(JSON.stringify({ redirect: location }), {
|
|
1208
|
-
status: 200,
|
|
1209
|
-
headers
|
|
1210
|
-
}), { isRouteStateRequest: options.isRouteStateRequest });
|
|
1211
|
-
}
|
|
1212
|
-
function normalizePageResponse(response, options) {
|
|
1213
|
-
if (options.isRouteStateRequest && response.status >= 300 && response.status < 400) {
|
|
1214
|
-
const location = response.headers.get("location");
|
|
1215
|
-
if (location) return jsonRedirectResponse(location, {
|
|
1216
|
-
headers: response.headers,
|
|
1217
|
-
isRouteStateRequest: true
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
return withRouteResponseHeaders(response, options);
|
|
1221
|
-
}
|
|
1222
|
-
function renderApiErrorResponse(options) {
|
|
1223
|
-
const exposeDetails = shouldExposeServerErrors(options.options);
|
|
1224
|
-
const routeError = normalizeRouteError(options.error, { exposeDetails });
|
|
1225
|
-
const routeErrorWithDiagnostics = exposeDetails ? {
|
|
1226
|
-
...routeError,
|
|
1227
|
-
diagnostics: buildRuntimeDiagnostics({
|
|
1228
|
-
middlewareFiles: options.middlewareFiles,
|
|
1229
|
-
phase: options.phase,
|
|
1230
|
-
route: options.route,
|
|
1231
|
-
status: routeError.status
|
|
1232
|
-
})
|
|
1233
|
-
} : routeError;
|
|
1234
|
-
if (exposeDetails) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: false });
|
|
1235
|
-
const message = routeErrorWithDiagnostics.status >= 500 ? "Internal Server Error" : routeErrorWithDiagnostics.message;
|
|
1236
|
-
return withDefaultSecurityHeaders(new Response(message, {
|
|
1237
|
-
status: routeErrorWithDiagnostics.status,
|
|
1238
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
1239
|
-
}));
|
|
1240
|
-
}
|
|
1241
|
-
async function renderRouteErrorResponse(options) {
|
|
1242
|
-
const exposeDetails = shouldExposeServerErrors(options.options);
|
|
1243
|
-
const routeError = normalizeRouteError(options.error, { exposeDetails });
|
|
1244
|
-
const routeErrorWithDiagnostics = exposeDetails ? {
|
|
1245
|
-
...routeError,
|
|
1246
|
-
diagnostics: buildRuntimeDiagnostics({
|
|
1247
|
-
loaderFile: options.loaderFile,
|
|
1248
|
-
middlewareFiles: options.routeArgs.route.middlewareFiles,
|
|
1249
|
-
phase: options.phase,
|
|
1250
|
-
route: options.routeArgs.route,
|
|
1251
|
-
shellFile: options.shellFile,
|
|
1252
|
-
status: routeError.status
|
|
1253
|
-
})
|
|
1254
|
-
} : routeError;
|
|
1255
|
-
if (options.isRouteStateRequest) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: true });
|
|
1256
|
-
const shellModule = options.shellModule ?? (options.shellFile ? await resolveRegistryModule(options.options.registry?.shellModules, options.shellFile) : void 0);
|
|
1257
|
-
const ErrorBoundary = options.routeModule?.ErrorBoundary ?? shellModule?.ErrorBoundary;
|
|
1258
|
-
if (!ErrorBoundary) {
|
|
1259
|
-
const message = routeErrorWithDiagnostics.status >= 500 && !exposeDetails ? "Internal Server Error" : routeErrorWithDiagnostics.message;
|
|
1260
|
-
const diagnostics = exposeDetails && routeErrorWithDiagnostics.diagnostics ? `\n\n${JSON.stringify(routeErrorWithDiagnostics.diagnostics, null, 2)}` : "";
|
|
1261
|
-
return withDefaultSecurityHeaders(new Response(`${message}${diagnostics}`, {
|
|
1262
|
-
status: routeErrorWithDiagnostics.status,
|
|
1263
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
1264
|
-
}));
|
|
1265
|
-
}
|
|
1266
|
-
const head = shellModule?.head ? await shellModule.head(options.routeArgs) : {};
|
|
1267
|
-
const documentHeaders = await mergeDocumentHeaders(shellModule, void 0, options.routeArgs, void 0);
|
|
1268
|
-
const cssUrls = resolvePageCssUrls(options.options.cssManifest, options.shellFile, options.routeArgs.route.file);
|
|
1269
|
-
const modulePreloadUrls = resolvePageJsUrls(options.options.jsManifest, options.shellFile, options.routeArgs.route.file);
|
|
1270
|
-
const renderToString = await getRenderToStringAsync();
|
|
1271
|
-
const Boundary = ErrorBoundary;
|
|
1272
|
-
const Shell = shellModule?.Shell;
|
|
1273
|
-
const errorValue = deserializeRouteError(routeErrorWithDiagnostics);
|
|
1274
|
-
const componentTree = Shell ? h(Shell, null, h(Boundary, { error: errorValue })) : h(Boundary, { error: errorValue });
|
|
1275
|
-
return htmlResponse(buildHtmlDocument({
|
|
1276
|
-
head,
|
|
1277
|
-
body: await renderToString(h(PrachtRuntimeProvider, {
|
|
1278
|
-
data: null,
|
|
1279
|
-
routeId: options.routeId,
|
|
1280
|
-
routes: options.routes,
|
|
1281
|
-
url: options.requestPath
|
|
1282
|
-
}, componentTree)),
|
|
1283
|
-
hydrationState: {
|
|
1284
|
-
url: options.requestPath,
|
|
1285
|
-
routeId: options.routeId,
|
|
1286
|
-
data: null,
|
|
1287
|
-
error: routeErrorWithDiagnostics
|
|
1288
|
-
},
|
|
1289
|
-
clientEntryUrl: options.options.clientEntryUrl,
|
|
1290
|
-
cssUrls,
|
|
1291
|
-
modulePreloadUrls
|
|
1292
|
-
}), routeErrorWithDiagnostics.status, documentHeaders);
|
|
1293
|
-
}
|
|
1294
|
-
//#endregion
|
|
1295
|
-
//#region src/runtime-negotiation.ts
|
|
1296
|
-
const MARKDOWN_MEDIA_TYPE = "text/markdown";
|
|
1297
|
-
function parseAccept(header) {
|
|
1298
|
-
if (!header) return [];
|
|
1299
|
-
const entries = [];
|
|
1300
|
-
for (const raw of header.split(",")) {
|
|
1301
|
-
const parts = raw.trim().split(";");
|
|
1302
|
-
const type = parts.shift()?.trim().toLowerCase();
|
|
1303
|
-
if (!type) continue;
|
|
1304
|
-
let quality = 1;
|
|
1305
|
-
for (const param of parts) {
|
|
1306
|
-
const [key, value] = param.split("=").map((p) => p.trim());
|
|
1307
|
-
if (key === "q" && value != null) {
|
|
1308
|
-
const parsed = Number.parseFloat(value);
|
|
1309
|
-
if (!Number.isNaN(parsed)) quality = parsed;
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
entries.push({
|
|
1313
|
-
type,
|
|
1314
|
-
quality
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
return entries;
|
|
1318
|
-
}
|
|
1319
|
-
function prefersMarkdown(accept) {
|
|
1320
|
-
const entries = parseAccept(accept);
|
|
1321
|
-
if (!entries.length) return false;
|
|
1322
|
-
const md = entries.find((e) => e.type === MARKDOWN_MEDIA_TYPE);
|
|
1323
|
-
if (!md || md.quality === 0) return false;
|
|
1324
|
-
const html = entries.find((e) => e.type === "text/html");
|
|
1325
|
-
if (!html) return true;
|
|
1326
|
-
return md.quality >= html.quality;
|
|
1327
|
-
}
|
|
1328
|
-
function markdownResponse(source) {
|
|
1329
|
-
const headers = new Headers({
|
|
1330
|
-
"content-type": "text/markdown; charset=utf-8",
|
|
1331
|
-
"cache-control": "public, max-age=0, must-revalidate"
|
|
1332
|
-
});
|
|
1333
|
-
appendVaryHeader(headers, "Accept");
|
|
1334
|
-
applyDefaultSecurityHeaders(headers);
|
|
1335
|
-
return new Response(source, {
|
|
1336
|
-
status: 200,
|
|
1337
|
-
headers
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
//#endregion
|
|
1341
|
-
//#region src/runtime.ts
|
|
1342
|
-
const SAME_ORIGIN_FETCH_SITE = "same-origin";
|
|
1343
|
-
/**
|
|
1344
|
-
* Stricter variant of first-party detection used for CSRF protection on
|
|
1345
|
-
* state-changing API requests. It rejects any browser signal that points
|
|
1346
|
-
* outside this exact origin — a cross-origin form POST will send `Origin`
|
|
1347
|
-
* from the attacker, and `Sec-Fetch-Site: same-site` is not enough because
|
|
1348
|
-
* sibling subdomains can be attacker-controlled. Requests with no browser
|
|
1349
|
-
* provenance headers are treated as non-browser callers.
|
|
1350
|
-
*/
|
|
1351
|
-
function isSameOriginMutation(request, url) {
|
|
1352
|
-
const site = request.headers.get("sec-fetch-site");
|
|
1353
|
-
if (site && site !== SAME_ORIGIN_FETCH_SITE) return false;
|
|
1354
|
-
const origin = request.headers.get("origin");
|
|
1355
|
-
if (origin) try {
|
|
1356
|
-
return new URL(origin).origin === url.origin;
|
|
1357
|
-
} catch {
|
|
1358
|
-
return false;
|
|
1359
|
-
}
|
|
1360
|
-
if (site === SAME_ORIGIN_FETCH_SITE) return true;
|
|
1361
|
-
const referer = request.headers.get("referer");
|
|
1362
|
-
if (referer) try {
|
|
1363
|
-
return new URL(referer).origin === url.origin;
|
|
1364
|
-
} catch {
|
|
1365
|
-
return false;
|
|
1366
|
-
}
|
|
1367
|
-
return true;
|
|
1368
|
-
}
|
|
1369
|
-
/**
|
|
1370
|
-
* Heuristic "this request came from our own page" check. Used to gate
|
|
1371
|
-
* the `_data=1` query-param form of the route-state endpoint, which is
|
|
1372
|
-
* otherwise reachable via any cross-origin `<a href>` / redirect.
|
|
1373
|
-
*
|
|
1374
|
-
* Accepts a request as first-party when:
|
|
1375
|
-
* - Sec-Fetch-Site is `same-origin` (modern browsers),
|
|
1376
|
-
* - OR Sec-Fetch-Site is absent AND the Origin header matches the
|
|
1377
|
-
* request URL's origin (older clients that still send Origin),
|
|
1378
|
-
* - OR Sec-Fetch-Site/Origin are absent AND Referer matches the request
|
|
1379
|
-
* URL's origin,
|
|
1380
|
-
* - OR no Origin/Sec-Fetch-Site/Referer is present (non-browser clients like
|
|
1381
|
-
* curl — CSRF is not the threat model there; blocking would break
|
|
1382
|
-
* tests and CLIs).
|
|
1383
|
-
*/
|
|
1384
|
-
function isFirstPartyFetch(request) {
|
|
1385
|
-
const site = request.headers.get("sec-fetch-site");
|
|
1386
|
-
if (site && site !== SAME_ORIGIN_FETCH_SITE) return false;
|
|
1387
|
-
const origin = request.headers.get("origin");
|
|
1388
|
-
if (origin) try {
|
|
1389
|
-
return new URL(origin).origin === new URL(request.url).origin;
|
|
1390
|
-
} catch {
|
|
1391
|
-
return false;
|
|
1392
|
-
}
|
|
1393
|
-
if (site === SAME_ORIGIN_FETCH_SITE) return true;
|
|
1394
|
-
const referer = request.headers.get("referer");
|
|
1395
|
-
if (referer) try {
|
|
1396
|
-
return new URL(referer).origin === new URL(request.url).origin;
|
|
1397
|
-
} catch {
|
|
1398
|
-
return false;
|
|
1399
|
-
}
|
|
1400
|
-
return true;
|
|
1401
|
-
}
|
|
1402
|
-
async function handlePrachtRequest(options) {
|
|
1403
|
-
const url = new URL(options.request.url);
|
|
1404
|
-
const hasDataParam = url.searchParams.get("_data") === "1";
|
|
1405
|
-
if (hasDataParam) url.searchParams.delete("_data");
|
|
1406
|
-
const requestPath = getRequestPath(url);
|
|
1407
|
-
const registry = options.registry ?? {};
|
|
1408
|
-
const resolvedApp = getResolvedApp(options.app);
|
|
1409
|
-
const headerSignalsRouteState = options.request.headers.get(ROUTE_STATE_REQUEST_HEADER) === "1";
|
|
1410
|
-
const dataParamIsFirstParty = hasDataParam && isFirstPartyFetch(options.request);
|
|
1411
|
-
const isRouteStateRequest = headerSignalsRouteState || dataParamIsFirstParty;
|
|
1412
|
-
const exposeDiagnostics = shouldExposeServerErrors(options);
|
|
1413
|
-
if (options.apiRoutes?.length) {
|
|
1414
|
-
const apiMatch = matchApiRoute(options.apiRoutes, url.pathname);
|
|
1415
|
-
if (apiMatch) {
|
|
1416
|
-
const apiMiddlewareFiles = (options.app.api.middleware ?? []).flatMap((name) => {
|
|
1417
|
-
const middlewareFile = options.app.middleware[name];
|
|
1418
|
-
return middlewareFile ? [middlewareFile] : [];
|
|
1419
|
-
});
|
|
1420
|
-
let currentPhase = "middleware";
|
|
1421
|
-
if ((options.app.api.requireSameOrigin ?? true) && !SAFE_METHODS.has(options.request.method) && !isSameOriginMutation(options.request, url)) return withDefaultSecurityHeaders(new Response("Cross-origin request blocked", {
|
|
1422
|
-
status: 403,
|
|
1423
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
1424
|
-
}));
|
|
1425
|
-
try {
|
|
1426
|
-
const middlewareResult = await runMiddlewareChain({
|
|
1427
|
-
context: options.context ?? {},
|
|
1428
|
-
middlewareFiles: apiMiddlewareFiles,
|
|
1429
|
-
params: apiMatch.params,
|
|
1430
|
-
registry,
|
|
1431
|
-
request: options.request,
|
|
1432
|
-
route: apiMatch.route,
|
|
1433
|
-
url
|
|
1434
|
-
});
|
|
1435
|
-
if (middlewareResult.response) return middlewareResult.response;
|
|
1436
|
-
currentPhase = "api";
|
|
1437
|
-
const apiModule = await resolveRegistryModule(registry.apiModules, apiMatch.route.file);
|
|
1438
|
-
if (!apiModule) throw new Error("API route module not found");
|
|
1439
|
-
const handler = apiModule[options.request.method.toUpperCase()] ?? apiModule.default;
|
|
1440
|
-
if (!handler) return withDefaultSecurityHeaders(new Response("Method not allowed", {
|
|
1441
|
-
status: 405,
|
|
1442
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
1443
|
-
}));
|
|
1444
|
-
return withDefaultSecurityHeaders(await handler({
|
|
1445
|
-
request: options.request,
|
|
1446
|
-
params: apiMatch.params,
|
|
1447
|
-
context: middlewareResult.context,
|
|
1448
|
-
signal: AbortSignal.timeout(3e4),
|
|
1449
|
-
url,
|
|
1450
|
-
route: apiMatch.route
|
|
1451
|
-
}));
|
|
1452
|
-
} catch (error) {
|
|
1453
|
-
return renderApiErrorResponse({
|
|
1454
|
-
error,
|
|
1455
|
-
middlewareFiles: apiMiddlewareFiles,
|
|
1456
|
-
options,
|
|
1457
|
-
phase: currentPhase,
|
|
1458
|
-
route: apiMatch.route
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
const match = matchAppRoute(resolvedApp, url.pathname);
|
|
1464
|
-
if (!match) {
|
|
1465
|
-
if (isRouteStateRequest) return jsonErrorResponse(createSerializedRouteError("Not found", 404, {
|
|
1466
|
-
diagnostics: exposeDiagnostics ? buildRuntimeDiagnostics({
|
|
1467
|
-
phase: "match",
|
|
1468
|
-
status: 404
|
|
1469
|
-
}) : void 0,
|
|
1470
|
-
name: "Error"
|
|
1471
|
-
}), { isRouteStateRequest: true });
|
|
1472
|
-
return withDefaultSecurityHeaders(new Response("Not found", {
|
|
1473
|
-
status: 404,
|
|
1474
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
1475
|
-
}));
|
|
1476
|
-
}
|
|
1477
|
-
if (!SAFE_METHODS.has(options.request.method)) {
|
|
1478
|
-
if (isRouteStateRequest) return jsonErrorResponse(createSerializedRouteError("Method not allowed", 405, {
|
|
1479
|
-
diagnostics: exposeDiagnostics ? buildRuntimeDiagnostics({
|
|
1480
|
-
middlewareFiles: match.route.middlewareFiles,
|
|
1481
|
-
phase: "action",
|
|
1482
|
-
route: match.route,
|
|
1483
|
-
shellFile: match.route.shellFile,
|
|
1484
|
-
status: 405
|
|
1485
|
-
}) : void 0,
|
|
1486
|
-
name: "Error"
|
|
1487
|
-
}), { isRouteStateRequest: true });
|
|
1488
|
-
return withRouteResponseHeaders(new Response("Method not allowed", {
|
|
1489
|
-
status: 405,
|
|
1490
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
1491
|
-
}), { isRouteStateRequest });
|
|
1492
|
-
}
|
|
1493
|
-
let routeArgs = {
|
|
1494
|
-
request: options.request,
|
|
1495
|
-
params: match.params,
|
|
1496
|
-
context: options.context ?? {},
|
|
1497
|
-
signal: AbortSignal.timeout(3e4),
|
|
1498
|
-
url,
|
|
1499
|
-
route: match.route
|
|
1500
|
-
};
|
|
1501
|
-
let routeModule;
|
|
1502
|
-
let shellModule;
|
|
1503
|
-
let loaderFile;
|
|
1504
|
-
let currentPhase = "middleware";
|
|
1505
|
-
try {
|
|
1506
|
-
const middlewarePromise = runMiddlewareChain({
|
|
1507
|
-
context: routeArgs.context,
|
|
1508
|
-
middlewareFiles: match.route.middlewareFiles,
|
|
1509
|
-
params: match.params,
|
|
1510
|
-
registry,
|
|
1511
|
-
request: options.request,
|
|
1512
|
-
route: match.route,
|
|
1513
|
-
url
|
|
1514
|
-
});
|
|
1515
|
-
const routeModulePromise = resolveRegistryModule(registry.routeModules, match.route.file);
|
|
1516
|
-
const shellModulePromise = match.route.shellFile ? resolveRegistryModule(registry.shellModules, match.route.shellFile) : Promise.resolve(void 0);
|
|
1517
|
-
const dataFunctionsPromise = routeModulePromise.then((mod) => resolveDataFunctions(match.route, mod, registry));
|
|
1518
|
-
routeModulePromise.catch(() => {});
|
|
1519
|
-
shellModulePromise.catch(() => {});
|
|
1520
|
-
dataFunctionsPromise.catch(() => {});
|
|
1521
|
-
const middlewareResult = await middlewarePromise;
|
|
1522
|
-
if (middlewareResult.response) return normalizePageResponse(middlewareResult.response, { isRouteStateRequest });
|
|
1523
|
-
routeArgs = {
|
|
1524
|
-
...routeArgs,
|
|
1525
|
-
context: middlewareResult.context
|
|
1526
|
-
};
|
|
1527
|
-
currentPhase = "render";
|
|
1528
|
-
routeModule = await routeModulePromise;
|
|
1529
|
-
if (!routeModule) throw new Error("Route module not found");
|
|
1530
|
-
if (!isRouteStateRequest && typeof routeModule.markdown === "string" && prefersMarkdown(options.request.headers.get("accept"))) return markdownResponse(routeModule.markdown);
|
|
1531
|
-
currentPhase = "loader";
|
|
1532
|
-
const { loader, loaderFile: resolvedLoaderFile } = await dataFunctionsPromise;
|
|
1533
|
-
loaderFile = resolvedLoaderFile;
|
|
1534
|
-
const loaderResult = loader ? await loader(routeArgs) : void 0;
|
|
1535
|
-
if (loaderResult instanceof Response) return normalizePageResponse(loaderResult, { isRouteStateRequest });
|
|
1536
|
-
const data = loaderResult;
|
|
1537
|
-
if (isRouteStateRequest) return withRouteResponseHeaders(Response.json({ data }), { isRouteStateRequest: true });
|
|
1538
|
-
currentPhase = "render";
|
|
1539
|
-
shellModule = await shellModulePromise;
|
|
1540
|
-
const [head, documentHeaders] = await Promise.all([mergeHeadMetadata(shellModule, routeModule, routeArgs, data), mergeDocumentHeaders(shellModule, routeModule, routeArgs, data)]);
|
|
1541
|
-
const cssUrls = resolvePageCssUrls(options.cssManifest, match.route.shellFile, match.route.file);
|
|
1542
|
-
const modulePreloadUrls = resolvePageJsUrls(options.jsManifest, match.route.shellFile, match.route.file);
|
|
1543
|
-
if (match.route.render === "spa") {
|
|
1544
|
-
let body = "";
|
|
1545
|
-
const Shell = shellModule?.Shell;
|
|
1546
|
-
const Loading = shellModule?.Loading;
|
|
1547
|
-
const loadingTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
|
|
1548
|
-
if (loadingTree) {
|
|
1549
|
-
const tree = h(PrachtRuntimeProvider, {
|
|
1550
|
-
data: null,
|
|
1551
|
-
params: match.params,
|
|
1552
|
-
routeId: match.route.id ?? "",
|
|
1553
|
-
routes: resolvedApp.routes,
|
|
1554
|
-
url: requestPath
|
|
1555
|
-
}, loadingTree);
|
|
1556
|
-
body = await (await getRenderToStringAsync())(tree);
|
|
1557
|
-
}
|
|
1558
|
-
return htmlResponse(buildHtmlDocument({
|
|
1559
|
-
head,
|
|
1560
|
-
body,
|
|
1561
|
-
hydrationState: {
|
|
1562
|
-
url: requestPath,
|
|
1563
|
-
routeId: match.route.id ?? "",
|
|
1564
|
-
data: null,
|
|
1565
|
-
error: null,
|
|
1566
|
-
pending: true
|
|
1567
|
-
},
|
|
1568
|
-
clientEntryUrl: options.clientEntryUrl,
|
|
1569
|
-
cssUrls,
|
|
1570
|
-
modulePreloadUrls,
|
|
1571
|
-
routeStatePreloadUrl: loader ? buildRouteStateUrl(requestPath) : void 0
|
|
1572
|
-
}), 200, documentHeaders);
|
|
1573
|
-
}
|
|
1574
|
-
const DefaultComponent = typeof routeModule.default === "function" ? routeModule.default : void 0;
|
|
1575
|
-
const Component = routeModule.Component ?? DefaultComponent;
|
|
1576
|
-
if (!Component) throw new Error("Route has no Component or default export");
|
|
1577
|
-
const Shell = shellModule?.Shell;
|
|
1578
|
-
const Comp = Component;
|
|
1579
|
-
const componentProps = {
|
|
1580
|
-
data,
|
|
1581
|
-
params: match.params
|
|
1582
|
-
};
|
|
1583
|
-
const componentTree = Shell ? h(Shell, null, h(Comp, componentProps)) : h(Comp, componentProps);
|
|
1584
|
-
const tree = h(PrachtRuntimeProvider, {
|
|
1585
|
-
data,
|
|
1586
|
-
params: match.params,
|
|
1587
|
-
routeId: match.route.id ?? "",
|
|
1588
|
-
routes: resolvedApp.routes,
|
|
1589
|
-
url: requestPath
|
|
1590
|
-
}, componentTree);
|
|
1591
|
-
return htmlResponse(buildHtmlDocument({
|
|
1592
|
-
head,
|
|
1593
|
-
body: await (await getRenderToStringAsync())(tree),
|
|
1594
|
-
hydrationState: {
|
|
1595
|
-
url: requestPath,
|
|
1596
|
-
routeId: match.route.id ?? "",
|
|
1597
|
-
data,
|
|
1598
|
-
error: null
|
|
1599
|
-
},
|
|
1600
|
-
clientEntryUrl: options.clientEntryUrl,
|
|
1601
|
-
cssUrls,
|
|
1602
|
-
modulePreloadUrls
|
|
1603
|
-
}), 200, documentHeaders);
|
|
1604
|
-
} catch (error) {
|
|
1605
|
-
return renderRouteErrorResponse({
|
|
1606
|
-
error,
|
|
1607
|
-
isRouteStateRequest,
|
|
1608
|
-
loaderFile,
|
|
1609
|
-
options,
|
|
1610
|
-
phase: currentPhase,
|
|
1611
|
-
routeArgs,
|
|
1612
|
-
routeId: match.route.id ?? "",
|
|
1613
|
-
routeModule,
|
|
1614
|
-
routes: resolvedApp.routes,
|
|
1615
|
-
shellFile: match.route.shellFile,
|
|
1616
|
-
shellModule,
|
|
1617
|
-
requestPath
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
function getRequestPath(url) {
|
|
1622
|
-
return `${url.pathname}${url.search}`;
|
|
1623
|
-
}
|
|
1624
|
-
function getResolvedApp(app) {
|
|
1625
|
-
const routes = app.routes;
|
|
1626
|
-
if (routes.length === 0 || isHrefRouteDefinition(routes[0])) return app;
|
|
1627
|
-
return resolveApp(app);
|
|
1628
|
-
}
|
|
1629
|
-
function isHrefRouteDefinition(value) {
|
|
1630
|
-
return Boolean(value && typeof value === "object" && "path" in value && "segments" in value && Array.isArray(value.segments));
|
|
1631
|
-
}
|
|
1632
|
-
//#endregion
|
|
1633
|
-
//#region src/prerender.ts
|
|
1634
|
-
const DANGEROUS_PRERENDER_HEADER_NAMES = new Set([
|
|
1635
|
-
"authorization",
|
|
1636
|
-
"proxy-authenticate",
|
|
1637
|
-
"proxy-authorization",
|
|
1638
|
-
"set-cookie",
|
|
1639
|
-
"www-authenticate"
|
|
1640
|
-
]);
|
|
1641
|
-
const SECRET_SHAPED_PRERENDER_HEADER_RE = /^x-.*(?:api[-_]?key|client[-_]?secret|credential|jwt[-_]?secret|password|private[-_]?key|refresh[-_]?token|secret|session[-_]?secret|token|webhook[-_]?secret)(?:$|[-_])/i;
|
|
1642
|
-
async function prerenderApp(options) {
|
|
1643
|
-
const resolved = resolveApp(options.app);
|
|
1644
|
-
const results = [];
|
|
1645
|
-
const isgManifest = {};
|
|
1646
|
-
const work = [];
|
|
1647
|
-
for (const route of resolved.routes) {
|
|
1648
|
-
if (route.render !== "ssg" && route.render !== "isg") continue;
|
|
1649
|
-
const paths = await collectSSGPaths(route, options.registry);
|
|
1650
|
-
for (const pathname of paths) work.push({
|
|
1651
|
-
pathname,
|
|
1652
|
-
render: route.render,
|
|
1653
|
-
revalidate: route.revalidate
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
const concurrency = options.concurrency ?? 10;
|
|
1657
|
-
if (!Number.isInteger(concurrency) || concurrency <= 0) throw new Error("prerenderApp({ concurrency }) expects a positive integer.");
|
|
1658
|
-
for (let i = 0; i < work.length; i += concurrency) {
|
|
1659
|
-
const batch = work.slice(i, i + concurrency);
|
|
1660
|
-
const batchResults = await Promise.all(batch.map(async (item) => {
|
|
1661
|
-
const url = new URL(item.pathname, "http://localhost");
|
|
1662
|
-
const request = new Request(url, { method: "GET" });
|
|
1663
|
-
const response = await handlePrachtRequest({
|
|
1664
|
-
app: options.app,
|
|
1665
|
-
request,
|
|
1666
|
-
registry: options.registry,
|
|
1667
|
-
clientEntryUrl: options.clientEntryUrl,
|
|
1668
|
-
cssManifest: options.cssManifest,
|
|
1669
|
-
jsManifest: options.jsManifest
|
|
1670
|
-
});
|
|
1671
|
-
if (response.status !== 200) {
|
|
1672
|
-
console.warn(` Warning: ${item.render.toUpperCase()} route "${item.pathname}" returned status ${response.status}, skipping.`);
|
|
1673
|
-
return null;
|
|
1674
|
-
}
|
|
1675
|
-
assertSafePrerenderHeaders(response.headers, item);
|
|
1676
|
-
const html = await response.text();
|
|
1677
|
-
return {
|
|
1678
|
-
headers: Object.fromEntries(response.headers),
|
|
1679
|
-
html,
|
|
1680
|
-
item
|
|
1681
|
-
};
|
|
1682
|
-
}));
|
|
1683
|
-
for (const result of batchResults) {
|
|
1684
|
-
if (!result) continue;
|
|
1685
|
-
results.push({
|
|
1686
|
-
path: result.item.pathname,
|
|
1687
|
-
html: result.html,
|
|
1688
|
-
headers: result.headers
|
|
1689
|
-
});
|
|
1690
|
-
if (result.item.render === "isg" && result.item.revalidate) isgManifest[result.item.pathname] = { revalidate: result.item.revalidate };
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
if (options.withISGManifest) return {
|
|
1694
|
-
pages: results,
|
|
1695
|
-
isgManifest
|
|
1696
|
-
};
|
|
1697
|
-
return results;
|
|
1698
|
-
}
|
|
1699
|
-
function assertSafePrerenderHeaders(headers, item) {
|
|
1700
|
-
const dangerous = [...headers.keys()].filter(isDangerousPrerenderHeader);
|
|
1701
|
-
if (dangerous.length === 0) return;
|
|
1702
|
-
const names = dangerous.map((name) => `"${name}"`).join(", ");
|
|
1703
|
-
throw new Error(`Refusing to prerender ${item.render.toUpperCase()} route "${item.pathname}" because its document headers include ${names}. SSG/ISG document headers are serialized into public static output and replayed for every visitor. Move cookies/authentication headers to API routes, loaders, middleware responses, or SSR-only routes.`);
|
|
1704
|
-
}
|
|
1705
|
-
function isDangerousPrerenderHeader(name) {
|
|
1706
|
-
const normalized = name.toLowerCase();
|
|
1707
|
-
return DANGEROUS_PRERENDER_HEADER_NAMES.has(normalized) || SECRET_SHAPED_PRERENDER_HEADER_RE.test(normalized);
|
|
1708
|
-
}
|
|
1709
|
-
async function collectSSGPaths(route, registry) {
|
|
1710
|
-
if (!route.segments.some((s) => s.type === "param" || s.type === "catchall")) return [route.path];
|
|
1711
|
-
const routeModule = await resolveRegistryModule(registry?.routeModules, route.file);
|
|
1712
|
-
if (!routeModule?.getStaticPaths) {
|
|
1713
|
-
console.warn(` Warning: SSG route "${route.path}" has dynamic segments but no getStaticPaths() export, skipping.`);
|
|
1714
|
-
return [];
|
|
1715
|
-
}
|
|
1716
|
-
return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
|
|
1717
|
-
}
|
|
1718
|
-
//#endregion
|
|
1719
|
-
//#region src/hydration-mismatch.ts
|
|
1720
|
-
const HYDRATION_BANNER_ID = "__pracht_hydration_mismatch__";
|
|
1721
|
-
let installed = false;
|
|
1722
|
-
function installHydrationMismatchWarning() {
|
|
1723
|
-
if (installed) return;
|
|
1724
|
-
installed = true;
|
|
1725
|
-
const prev = options.__m;
|
|
1726
|
-
options.__m = function(vnode) {
|
|
1727
|
-
appendHydrationWarning(vnode);
|
|
1728
|
-
if (prev) prev(vnode);
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
|
-
function appendHydrationWarning(vnode) {
|
|
1732
|
-
if (typeof document === "undefined") return;
|
|
1733
|
-
const componentName = getVNodeName(vnode);
|
|
1734
|
-
let banner = document.getElementById(HYDRATION_BANNER_ID);
|
|
1735
|
-
const message = `Hydration mismatch detected on <${componentName}>. The server-rendered HTML did not match the client.`;
|
|
1736
|
-
if (banner) {
|
|
1737
|
-
const list = banner.querySelector(`[data-pracht-mismatch-list]`);
|
|
1738
|
-
if (list) {
|
|
1739
|
-
const item = document.createElement("li");
|
|
1740
|
-
item.textContent = message;
|
|
1741
|
-
list.appendChild(item);
|
|
1742
|
-
}
|
|
1743
|
-
return;
|
|
1744
|
-
}
|
|
1745
|
-
banner = document.createElement("div");
|
|
1746
|
-
banner.id = HYDRATION_BANNER_ID;
|
|
1747
|
-
banner.setAttribute("role", "alert");
|
|
1748
|
-
banner.style.cssText = [
|
|
1749
|
-
"position:fixed",
|
|
1750
|
-
"top:0",
|
|
1751
|
-
"left:0",
|
|
1752
|
-
"right:0",
|
|
1753
|
-
"z-index:2147483647",
|
|
1754
|
-
"background:#1a1a2e",
|
|
1755
|
-
"color:#ff6b6b",
|
|
1756
|
-
"padding:12px 16px",
|
|
1757
|
-
"font:12px/1.5 ui-monospace,Menlo,Consolas,monospace",
|
|
1758
|
-
"border-bottom:2px solid #e74c3c",
|
|
1759
|
-
"box-shadow:0 2px 8px rgba(0,0,0,0.3)"
|
|
1760
|
-
].join(";");
|
|
1761
|
-
const title = document.createElement("strong");
|
|
1762
|
-
title.textContent = "pracht: hydration mismatch";
|
|
1763
|
-
title.style.cssText = "display:block;margin-bottom:4px;color:#fff";
|
|
1764
|
-
banner.appendChild(title);
|
|
1765
|
-
const list = document.createElement("ul");
|
|
1766
|
-
list.setAttribute("data-pracht-mismatch-list", "");
|
|
1767
|
-
list.style.cssText = "margin:0;padding-left:18px";
|
|
1768
|
-
const item = document.createElement("li");
|
|
1769
|
-
item.textContent = message;
|
|
1770
|
-
list.appendChild(item);
|
|
1771
|
-
banner.appendChild(list);
|
|
1772
|
-
document.body.appendChild(banner);
|
|
1773
|
-
}
|
|
1774
|
-
function getVNodeName(vnode) {
|
|
1775
|
-
if (!vnode) return "Unknown";
|
|
1776
|
-
const type = vnode.type;
|
|
1777
|
-
if (typeof type === "string") return type;
|
|
1778
|
-
if (typeof type === "function") {
|
|
1779
|
-
const fn = type;
|
|
1780
|
-
return fn.displayName || fn.name || "Component";
|
|
1781
|
-
}
|
|
1782
|
-
return "Unknown";
|
|
1783
|
-
}
|
|
1784
|
-
//#endregion
|
|
1785
|
-
//#region src/router.ts
|
|
1786
|
-
const NavigateContext = createContext(async () => {});
|
|
1787
|
-
function useNavigate() {
|
|
1788
|
-
return useContext(NavigateContext);
|
|
1789
|
-
}
|
|
1790
|
-
async function initClientRouter(options) {
|
|
1791
|
-
const { app, routeModules, shellModules, root, findModuleKey } = options;
|
|
1792
|
-
if (import.meta.env?.DEV) installHydrationMismatchWarning();
|
|
1793
|
-
const moduleCache = /* @__PURE__ */ new Map();
|
|
1794
|
-
function loadModule(modules, key) {
|
|
1795
|
-
let cached = moduleCache.get(key);
|
|
1796
|
-
if (!cached) {
|
|
1797
|
-
cached = modules[key]();
|
|
1798
|
-
moduleCache.set(key, cached);
|
|
1799
|
-
}
|
|
1800
|
-
return cached;
|
|
1801
|
-
}
|
|
1802
|
-
function startRouteImport(match) {
|
|
1803
|
-
const routeKey = findModuleKey(routeModules, match.route.file);
|
|
1804
|
-
if (!routeKey) return null;
|
|
1805
|
-
return loadModule(routeModules, routeKey);
|
|
1806
|
-
}
|
|
1807
|
-
function startShellImport(match) {
|
|
1808
|
-
if (!match.route.shellFile) return null;
|
|
1809
|
-
const shellKey = findModuleKey(shellModules, match.route.shellFile);
|
|
1810
|
-
if (!shellKey) return null;
|
|
1811
|
-
return loadModule(shellModules, shellKey);
|
|
1812
|
-
}
|
|
1813
|
-
let updateRouteState = null;
|
|
1814
|
-
let routeStateVersion = 0;
|
|
1815
|
-
let latestNavigationId = 0;
|
|
1816
|
-
let activeNavigationAbort = null;
|
|
1817
|
-
function RouterRoot({ initialState }) {
|
|
1818
|
-
const [routeState, setRouteState] = useState(initialState);
|
|
1819
|
-
updateRouteState = setRouteState;
|
|
1820
|
-
const navigateValue = useMemo(() => navigate, []);
|
|
1821
|
-
const { Shell, Component, componentProps, data, params, routeId, url, version } = routeState;
|
|
1822
|
-
const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
|
|
1823
|
-
return h(NavigateContext.Provider, { value: navigateValue }, h(PrachtRuntimeProvider, {
|
|
1824
|
-
data,
|
|
1825
|
-
params,
|
|
1826
|
-
routeId,
|
|
1827
|
-
routes: app.routes,
|
|
1828
|
-
stateVersion: version,
|
|
1829
|
-
url
|
|
1830
|
-
}, componentTree));
|
|
1831
|
-
}
|
|
1832
|
-
function applyRouteState(routeState) {
|
|
1833
|
-
if (updateRouteState) {
|
|
1834
|
-
updateRouteState(routeState);
|
|
1835
|
-
return;
|
|
1836
|
-
}
|
|
1837
|
-
render(h(RouterRoot, { initialState: routeState }), root);
|
|
1838
|
-
}
|
|
1839
|
-
async function resolveRouteState(match, state, currentUrl, routeModPromise, shellModPromise) {
|
|
1840
|
-
const routeMod = await (routeModPromise ?? startRouteImport(match));
|
|
1841
|
-
if (!routeMod) return null;
|
|
1842
|
-
let Shell = null;
|
|
1843
|
-
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
1844
|
-
if (resolvedShell) Shell = resolvedShell.Shell;
|
|
1845
|
-
const DefaultComponent = typeof routeMod.default === "function" ? routeMod.default : void 0;
|
|
1846
|
-
const ErrorBoundary = routeMod.ErrorBoundary ?? resolvedShell?.ErrorBoundary;
|
|
1847
|
-
const Component = state.error ? ErrorBoundary : routeMod.Component ?? DefaultComponent;
|
|
1848
|
-
if (!Component) return null;
|
|
1849
|
-
const componentProps = state.error ? { error: deserializeRouteError(state.error) } : {
|
|
1850
|
-
data: state.data,
|
|
1851
|
-
params: match.params
|
|
1852
|
-
};
|
|
1853
|
-
return {
|
|
1854
|
-
Shell,
|
|
1855
|
-
Component,
|
|
1856
|
-
componentProps,
|
|
1857
|
-
data: state.data,
|
|
1858
|
-
params: match.params,
|
|
1859
|
-
routeId: match.route.id ?? "",
|
|
1860
|
-
url: currentUrl,
|
|
1861
|
-
version: ++routeStateVersion
|
|
1862
|
-
};
|
|
1863
|
-
}
|
|
1864
|
-
async function resolveSpaPendingState(match, currentUrl, shellModPromise) {
|
|
1865
|
-
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
1866
|
-
if (!resolvedShell) return null;
|
|
1867
|
-
const Shell = resolvedShell.Shell ?? null;
|
|
1868
|
-
const Loading = resolvedShell.Loading;
|
|
1869
|
-
if (!Shell && !Loading) return null;
|
|
1870
|
-
return {
|
|
1871
|
-
Shell,
|
|
1872
|
-
Component: Loading ?? (() => null),
|
|
1873
|
-
componentProps: {},
|
|
1874
|
-
data: void 0,
|
|
1875
|
-
params: match.params,
|
|
1876
|
-
routeId: match.route.id ?? "",
|
|
1877
|
-
url: currentUrl,
|
|
1878
|
-
version: ++routeStateVersion
|
|
1879
|
-
};
|
|
1880
|
-
}
|
|
1881
|
-
function resolveRedirectTarget(location) {
|
|
1882
|
-
const targetUrl = parseSafeNavigationUrl(location, window.location.href);
|
|
1883
|
-
if (!targetUrl) return {
|
|
1884
|
-
isCurrentLocation: false,
|
|
1885
|
-
unsafe: true
|
|
1886
|
-
};
|
|
1887
|
-
const fullInternalTarget = targetUrl.pathname + targetUrl.search + targetUrl.hash;
|
|
1888
|
-
const internalPath = targetUrl.pathname + targetUrl.search;
|
|
1889
|
-
const currentPath = window.location.pathname + window.location.search + window.location.hash;
|
|
1890
|
-
const isCurrentLocation = targetUrl.origin === window.location.origin && fullInternalTarget === currentPath;
|
|
1891
|
-
if (targetUrl.origin !== window.location.origin) return {
|
|
1892
|
-
externalUrl: targetUrl.toString(),
|
|
1893
|
-
isCurrentLocation: false
|
|
1894
|
-
};
|
|
1895
|
-
if (targetUrl.hash) return {
|
|
1896
|
-
documentUrl: targetUrl.toString(),
|
|
1897
|
-
isCurrentLocation
|
|
1898
|
-
};
|
|
1899
|
-
return {
|
|
1900
|
-
internalPath,
|
|
1901
|
-
isCurrentLocation
|
|
1902
|
-
};
|
|
1903
|
-
}
|
|
1904
|
-
async function navigate(to, opts) {
|
|
1905
|
-
const navigationId = ++latestNavigationId;
|
|
1906
|
-
activeNavigationAbort?.abort();
|
|
1907
|
-
const abortController = new AbortController();
|
|
1908
|
-
activeNavigationAbort = abortController;
|
|
1909
|
-
const navigationTarget = typeof to === "string" ? to : buildHref(app.routes, to.route, to);
|
|
1910
|
-
const target = resolveBrowserRouteTarget(navigationTarget);
|
|
1911
|
-
if (!target) {
|
|
1912
|
-
window.location.href = navigationTarget;
|
|
1913
|
-
return;
|
|
1914
|
-
}
|
|
1915
|
-
const match = matchAppRoute(app, target.pathname);
|
|
1916
|
-
if (!match) {
|
|
1917
|
-
window.location.href = target.browserUrl;
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
let statePromise;
|
|
1921
|
-
if (routeNeedsServerFetch(match.route)) statePromise = getCachedRouteState(target.requestUrl) ?? fetchPrachtRouteState(target.requestUrl, { signal: abortController.signal });
|
|
1922
|
-
else statePromise = Promise.resolve({
|
|
1923
|
-
type: "data",
|
|
1924
|
-
data: void 0
|
|
1925
|
-
});
|
|
1926
|
-
const routeModPromise = startRouteImport(match);
|
|
1927
|
-
const shellModPromise = startShellImport(match);
|
|
1928
|
-
let state = {
|
|
1929
|
-
data: void 0,
|
|
1930
|
-
error: null
|
|
1931
|
-
};
|
|
1932
|
-
try {
|
|
1933
|
-
const result = await statePromise;
|
|
1934
|
-
if (navigationId !== latestNavigationId) return;
|
|
1935
|
-
if (result.type === "redirect") {
|
|
1936
|
-
if (result.location) {
|
|
1937
|
-
const redirect = resolveRedirectTarget(result.location);
|
|
1938
|
-
if (redirect.unsafe) {
|
|
1939
|
-
console.error(`[pracht] refused to navigate to unsafe URL: ${result.location}`);
|
|
1940
|
-
return;
|
|
1941
|
-
}
|
|
1942
|
-
if (redirect.externalUrl) {
|
|
1943
|
-
window.location.href = redirect.externalUrl;
|
|
1944
|
-
return;
|
|
1945
|
-
}
|
|
1946
|
-
if (redirect.isCurrentLocation) return;
|
|
1947
|
-
if (redirect.documentUrl) {
|
|
1948
|
-
window.location.href = redirect.documentUrl;
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1951
|
-
if (redirect.internalPath) {
|
|
1952
|
-
await navigate(redirect.internalPath, opts);
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
window.location.href = target.browserUrl;
|
|
1956
|
-
return;
|
|
1957
|
-
}
|
|
1958
|
-
window.location.href = target.browserUrl;
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
if (result.type === "error") state = {
|
|
1962
|
-
data: void 0,
|
|
1963
|
-
error: result.error
|
|
1964
|
-
};
|
|
1965
|
-
else state = {
|
|
1966
|
-
data: result.data,
|
|
1967
|
-
error: null
|
|
1968
|
-
};
|
|
1969
|
-
} catch {
|
|
1970
|
-
if (abortController.signal.aborted || navigationId !== latestNavigationId) return;
|
|
1971
|
-
window.location.href = target.browserUrl;
|
|
1972
|
-
return;
|
|
1973
|
-
}
|
|
1974
|
-
if (navigationId !== latestNavigationId) return;
|
|
1975
|
-
if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "", target.browserUrl);
|
|
1976
|
-
else history.pushState(null, "", target.browserUrl);
|
|
1977
|
-
const routeState = await resolveRouteState(match, state, target.requestUrl, routeModPromise, shellModPromise);
|
|
1978
|
-
if (navigationId !== latestNavigationId) return;
|
|
1979
|
-
if (routeState) {
|
|
1980
|
-
applyRouteState(routeState);
|
|
1981
|
-
window.scrollTo(0, 0);
|
|
1982
|
-
} else window.location.href = target.browserUrl;
|
|
1983
|
-
}
|
|
1984
|
-
const initialTarget = resolveBrowserRouteTarget(options.initialState.url);
|
|
1985
|
-
const initialRequestUrl = initialTarget?.requestUrl ?? options.initialState.url;
|
|
1986
|
-
const initialBrowserUrl = initialTarget?.browserUrl ?? options.initialState.url;
|
|
1987
|
-
const initialMatch = matchAppRoute(app, initialTarget?.pathname ?? options.initialState.url);
|
|
1988
|
-
if (initialMatch) {
|
|
1989
|
-
const initialShellPromise = initialMatch.route.render === "spa" && options.initialState.pending ? startShellImport(initialMatch) : null;
|
|
1990
|
-
let state = {
|
|
1991
|
-
data: options.initialState.data,
|
|
1992
|
-
error: options.initialState.error ?? null
|
|
1993
|
-
};
|
|
1994
|
-
if (initialMatch.route.render === "spa" && options.initialState.pending) {
|
|
1995
|
-
const dataPromise = fetchPrachtRouteState(initialRequestUrl, { useDataParam: true });
|
|
1996
|
-
const pendingState = await resolveSpaPendingState(initialMatch, initialRequestUrl, initialShellPromise);
|
|
1997
|
-
if (pendingState) hydrate(h(RouterRoot, { initialState: pendingState }), root);
|
|
1998
|
-
try {
|
|
1999
|
-
const result = await dataPromise;
|
|
2000
|
-
if (result.type === "redirect") {
|
|
2001
|
-
const safeRedirect = parseSafeNavigationUrl(result.location, window.location.href);
|
|
2002
|
-
if (!safeRedirect) {
|
|
2003
|
-
console.error(`[pracht] refused to navigate to unsafe URL: ${result.location}`);
|
|
2004
|
-
return;
|
|
2005
|
-
}
|
|
2006
|
-
window.location.href = safeRedirect.toString();
|
|
2007
|
-
return;
|
|
2008
|
-
}
|
|
2009
|
-
if (result.type === "error") state = {
|
|
2010
|
-
data: void 0,
|
|
2011
|
-
error: result.error
|
|
2012
|
-
};
|
|
2013
|
-
else state = {
|
|
2014
|
-
data: result.data,
|
|
2015
|
-
error: null
|
|
2016
|
-
};
|
|
2017
|
-
} catch {
|
|
2018
|
-
window.location.href = initialBrowserUrl;
|
|
2019
|
-
return;
|
|
2020
|
-
}
|
|
2021
|
-
const resolvedState = await resolveRouteState(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
|
|
2022
|
-
if (resolvedState) applyRouteState(resolvedState);
|
|
2023
|
-
} else {
|
|
2024
|
-
const initialRouteState = await resolveRouteState(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
|
|
2025
|
-
if (initialRouteState) if (initialMatch.route.render === "spa") render(h(RouterRoot, { initialState: initialRouteState }), root);
|
|
2026
|
-
else {
|
|
2027
|
-
markHydrating();
|
|
2028
|
-
hydrate(h(RouterRoot, { initialState: initialRouteState }), root);
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
document.addEventListener("click", (e) => {
|
|
2033
|
-
const anchor = e.target.closest?.("a");
|
|
2034
|
-
if (!anchor) return;
|
|
2035
|
-
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
2036
|
-
if (e.defaultPrevented) return;
|
|
2037
|
-
if (e.button !== 0) return;
|
|
2038
|
-
const target = anchor.getAttribute("target");
|
|
2039
|
-
if (target && target !== "_self") return;
|
|
2040
|
-
if (anchor.hasAttribute("download")) return;
|
|
2041
|
-
const href = anchor.getAttribute("href");
|
|
2042
|
-
if (!href || href.startsWith("#")) return;
|
|
2043
|
-
let url;
|
|
2044
|
-
try {
|
|
2045
|
-
url = new URL(href, window.location.origin);
|
|
2046
|
-
} catch {
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
if (url.origin !== window.location.origin) return;
|
|
2050
|
-
e.preventDefault();
|
|
2051
|
-
navigate(url.pathname + url.search + url.hash);
|
|
2052
|
-
});
|
|
2053
|
-
window.addEventListener("popstate", () => {
|
|
2054
|
-
navigate(window.location.pathname + window.location.search + window.location.hash, { _popstate: true });
|
|
2055
|
-
});
|
|
2056
|
-
window.__PRACHT_NAVIGATE__ = navigate;
|
|
2057
|
-
window.__PRACHT_ROUTER_READY__ = true;
|
|
2058
|
-
const warmModules = (match) => {
|
|
2059
|
-
startRouteImport(match);
|
|
2060
|
-
startShellImport(match);
|
|
2061
|
-
};
|
|
2062
|
-
setupPrefetching(app, warmModules);
|
|
2063
|
-
}
|
|
2064
|
-
function resolveBrowserRouteTarget(to) {
|
|
2065
|
-
if (typeof window === "undefined") return null;
|
|
2066
|
-
try {
|
|
2067
|
-
const url = new URL(to, window.location.href);
|
|
2068
|
-
if (url.origin !== window.location.origin) return null;
|
|
2069
|
-
return {
|
|
2070
|
-
browserUrl: url.pathname + url.search + url.hash,
|
|
2071
|
-
pathname: url.pathname,
|
|
2072
|
-
requestUrl: url.pathname + url.search
|
|
2073
|
-
};
|
|
2074
|
-
} catch {
|
|
2075
|
-
return null;
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
//#endregion
|
|
2079
|
-
//#region src/types.ts
|
|
2080
|
-
var PrachtHttpError = class extends Error {
|
|
2081
|
-
status;
|
|
2082
|
-
constructor(status, message) {
|
|
2083
|
-
super(message);
|
|
2084
|
-
this.name = "PrachtHttpError";
|
|
2085
|
-
this.status = status;
|
|
2086
|
-
}
|
|
2087
|
-
};
|
|
2088
|
-
//#endregion
|
|
2089
|
-
export { Form, Link, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildHref, buildPathFromSegments, createHref, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
|
|
8
|
+
export { Form, Link, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildHref, buildPathFromSegments, createHref, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, redirect, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
|