@pracht/core 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-CAoDWWNO.mjs +227 -0
- package/dist/error-overlay.d.mts +16 -0
- package/dist/error-overlay.mjs +112 -0
- package/dist/index.d.mts +343 -0
- package/dist/index.mjs +837 -0
- package/package.json +28 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { a as matchApiRoute, c as resolveApp, i as group, l as route, n as buildPathFromSegments, o as matchAppRoute, r as defineApp, s as resolveApiRoutes, u as timeRevalidate } from "./app-CAoDWWNO.mjs";
|
|
2
|
+
import { Suspense, lazy } from "preact-suspense";
|
|
3
|
+
import { createContext, h, hydrate, render } from "preact";
|
|
4
|
+
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
|
5
|
+
//#region src/runtime.ts
|
|
6
|
+
const SAFE_METHODS = new Set(["GET", "HEAD"]);
|
|
7
|
+
const HYDRATION_STATE_ELEMENT_ID = "pracht-state";
|
|
8
|
+
const RouteDataContext = createContext(void 0);
|
|
9
|
+
function PrachtRuntimeProvider({ children, data, params = {}, routeId, url }) {
|
|
10
|
+
const [routeData, setRouteData] = useState(data);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setRouteData(data);
|
|
13
|
+
}, [
|
|
14
|
+
data,
|
|
15
|
+
routeId,
|
|
16
|
+
url
|
|
17
|
+
]);
|
|
18
|
+
const context = useMemo(() => ({
|
|
19
|
+
data: routeData,
|
|
20
|
+
params,
|
|
21
|
+
routeId,
|
|
22
|
+
setData: setRouteData,
|
|
23
|
+
url
|
|
24
|
+
}), [
|
|
25
|
+
routeData,
|
|
26
|
+
params,
|
|
27
|
+
routeId,
|
|
28
|
+
url
|
|
29
|
+
]);
|
|
30
|
+
return h(RouteDataContext.Provider, {
|
|
31
|
+
value: context,
|
|
32
|
+
children
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function startApp(options = {}) {
|
|
36
|
+
if (typeof window === "undefined") return options.initialData;
|
|
37
|
+
if (typeof options.initialData !== "undefined") return options.initialData;
|
|
38
|
+
return readHydrationState()?.data;
|
|
39
|
+
}
|
|
40
|
+
function readHydrationState() {
|
|
41
|
+
if (typeof window === "undefined") return;
|
|
42
|
+
if (window.__PRACHT_STATE__) return window.__PRACHT_STATE__;
|
|
43
|
+
const element = document.getElementById(HYDRATION_STATE_ELEMENT_ID);
|
|
44
|
+
if (!(element instanceof HTMLScriptElement)) return;
|
|
45
|
+
const raw = element.textContent;
|
|
46
|
+
if (!raw) return;
|
|
47
|
+
try {
|
|
48
|
+
const state = JSON.parse(raw);
|
|
49
|
+
window.__PRACHT_STATE__ = state;
|
|
50
|
+
return state;
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function useRouteData() {
|
|
56
|
+
return useContext(RouteDataContext)?.data;
|
|
57
|
+
}
|
|
58
|
+
function useLocation() {
|
|
59
|
+
return { pathname: useContext(RouteDataContext)?.url ?? (typeof window !== "undefined" ? window.location.pathname : "/") };
|
|
60
|
+
}
|
|
61
|
+
function useParams() {
|
|
62
|
+
return useContext(RouteDataContext)?.params ?? {};
|
|
63
|
+
}
|
|
64
|
+
function useRevalidate() {
|
|
65
|
+
const runtime = useContext(RouteDataContext);
|
|
66
|
+
return async () => {
|
|
67
|
+
if (typeof window === "undefined") return;
|
|
68
|
+
const result = await fetchPrachtRouteState(runtime?.url || window.location.pathname + window.location.search);
|
|
69
|
+
if (result.type === "redirect") {
|
|
70
|
+
await navigateToClientLocation(result.location);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (result.type === "error") throw deserializeRouteError$1(result.error);
|
|
74
|
+
runtime?.setData(result.data);
|
|
75
|
+
return result.data;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** @deprecated Use useRevalidate instead. */
|
|
79
|
+
const useRevalidateRoute = useRevalidate;
|
|
80
|
+
function Form(props) {
|
|
81
|
+
const { onSubmit, method, ...rest } = props;
|
|
82
|
+
return h("form", {
|
|
83
|
+
...rest,
|
|
84
|
+
method,
|
|
85
|
+
onSubmit: async (event) => {
|
|
86
|
+
onSubmit?.(event);
|
|
87
|
+
if (event.defaultPrevented) return;
|
|
88
|
+
const form = event.currentTarget;
|
|
89
|
+
if (!(form instanceof HTMLFormElement)) return;
|
|
90
|
+
const formMethod = (method ?? form.method ?? "post").toUpperCase();
|
|
91
|
+
if (SAFE_METHODS.has(formMethod)) return;
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
const response = await fetch(props.action ?? form.action, {
|
|
94
|
+
method: formMethod,
|
|
95
|
+
body: new FormData(form),
|
|
96
|
+
redirect: "manual"
|
|
97
|
+
});
|
|
98
|
+
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
|
|
99
|
+
const location = response.headers.get("location");
|
|
100
|
+
if (location) {
|
|
101
|
+
await navigateToClientLocation(location);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
window.location.href = props.action ?? form.action;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function fetchPrachtRouteState(url) {
|
|
110
|
+
const response = await fetch(url, {
|
|
111
|
+
headers: {
|
|
112
|
+
"x-pracht-route-state-request": "1",
|
|
113
|
+
"Cache-Control": "no-cache"
|
|
114
|
+
},
|
|
115
|
+
redirect: "manual"
|
|
116
|
+
});
|
|
117
|
+
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) return {
|
|
118
|
+
location: response.headers.get("location") ?? url,
|
|
119
|
+
type: "redirect"
|
|
120
|
+
};
|
|
121
|
+
const json = await response.json();
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
if (json.error) return {
|
|
124
|
+
error: json.error,
|
|
125
|
+
type: "error"
|
|
126
|
+
};
|
|
127
|
+
throw new Error(`Failed to fetch route state (${response.status})`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
data: json.data,
|
|
131
|
+
type: "data"
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function handlePrachtRequest(options) {
|
|
135
|
+
const url = new URL(options.request.url);
|
|
136
|
+
const registry = options.registry ?? {};
|
|
137
|
+
if (options.apiRoutes?.length) {
|
|
138
|
+
const apiMatch = matchApiRoute(options.apiRoutes, url.pathname);
|
|
139
|
+
if (apiMatch) {
|
|
140
|
+
const apiMiddlewareFiles = (options.app.api.middleware ?? []).flatMap((name) => {
|
|
141
|
+
const middlewareFile = options.app.middleware[name];
|
|
142
|
+
return middlewareFile ? [middlewareFile] : [];
|
|
143
|
+
});
|
|
144
|
+
const middlewareResult = await runMiddlewareChain({
|
|
145
|
+
context: options.context ?? {},
|
|
146
|
+
middlewareFiles: apiMiddlewareFiles,
|
|
147
|
+
params: apiMatch.params,
|
|
148
|
+
registry,
|
|
149
|
+
request: options.request,
|
|
150
|
+
route: apiMatch.route,
|
|
151
|
+
url
|
|
152
|
+
});
|
|
153
|
+
if (middlewareResult.response) return middlewareResult.response;
|
|
154
|
+
const apiModule = await resolveRegistryModule(registry.apiModules, apiMatch.route.file);
|
|
155
|
+
if (!apiModule) return withDefaultSecurityHeaders(new Response("API route module not found", {
|
|
156
|
+
status: 500,
|
|
157
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
158
|
+
}));
|
|
159
|
+
const handler = apiModule[options.request.method.toUpperCase()];
|
|
160
|
+
if (!handler) return withDefaultSecurityHeaders(new Response("Method not allowed", {
|
|
161
|
+
status: 405,
|
|
162
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
163
|
+
}));
|
|
164
|
+
return withDefaultSecurityHeaders(await handler({
|
|
165
|
+
request: options.request,
|
|
166
|
+
params: apiMatch.params,
|
|
167
|
+
context: middlewareResult.context,
|
|
168
|
+
signal: AbortSignal.timeout(3e4),
|
|
169
|
+
url,
|
|
170
|
+
route: apiMatch.route
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const match = matchAppRoute(options.app, url.pathname);
|
|
175
|
+
if (!match) return withDefaultSecurityHeaders(new Response("Not found", {
|
|
176
|
+
status: 404,
|
|
177
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
178
|
+
}));
|
|
179
|
+
const isRouteStateRequest = options.request.headers.get("x-pracht-route-state-request") === "1";
|
|
180
|
+
if (!SAFE_METHODS.has(options.request.method)) return withDefaultSecurityHeaders(new Response("Method not allowed", {
|
|
181
|
+
status: 405,
|
|
182
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
183
|
+
}));
|
|
184
|
+
const middlewareResult = await runMiddlewareChain({
|
|
185
|
+
context: options.context ?? {},
|
|
186
|
+
middlewareFiles: match.route.middlewareFiles,
|
|
187
|
+
params: match.params,
|
|
188
|
+
registry,
|
|
189
|
+
request: options.request,
|
|
190
|
+
route: match.route,
|
|
191
|
+
url
|
|
192
|
+
});
|
|
193
|
+
if (middlewareResult.response) return middlewareResult.response;
|
|
194
|
+
const context = middlewareResult.context;
|
|
195
|
+
const routeModule = await resolveRegistryModule(registry.routeModules, match.route.file);
|
|
196
|
+
const routeArgs = {
|
|
197
|
+
request: options.request,
|
|
198
|
+
params: match.params,
|
|
199
|
+
context,
|
|
200
|
+
signal: AbortSignal.timeout(3e4),
|
|
201
|
+
url,
|
|
202
|
+
route: match.route
|
|
203
|
+
};
|
|
204
|
+
const { loader } = await resolveDataFunctions(match.route, routeModule, registry);
|
|
205
|
+
let shellModule;
|
|
206
|
+
try {
|
|
207
|
+
const loaderResult = loader ? await loader(routeArgs) : void 0;
|
|
208
|
+
if (loaderResult instanceof Response) return withDefaultSecurityHeaders(loaderResult);
|
|
209
|
+
const data = loaderResult;
|
|
210
|
+
if (isRouteStateRequest) return withDefaultSecurityHeaders(Response.json({ data }));
|
|
211
|
+
shellModule = match.route.shellFile ? await resolveRegistryModule(registry.shellModules, match.route.shellFile) : void 0;
|
|
212
|
+
const head = await mergeHeadMetadata(shellModule, routeModule, routeArgs, data);
|
|
213
|
+
const cssUrls = resolvePageCssUrls(options, match.route.shellFile, match.route.file);
|
|
214
|
+
const modulePreloadUrls = resolvePageJsUrls(options, match.route.shellFile, match.route.file);
|
|
215
|
+
if (match.route.render === "spa") return htmlResponse(buildHtmlDocument({
|
|
216
|
+
head,
|
|
217
|
+
body: "",
|
|
218
|
+
hydrationState: {
|
|
219
|
+
url: url.pathname,
|
|
220
|
+
routeId: match.route.id ?? "",
|
|
221
|
+
data: null,
|
|
222
|
+
error: null
|
|
223
|
+
},
|
|
224
|
+
clientEntryUrl: options.clientEntryUrl,
|
|
225
|
+
cssUrls,
|
|
226
|
+
modulePreloadUrls
|
|
227
|
+
}));
|
|
228
|
+
if (!routeModule?.Component) return withDefaultSecurityHeaders(new Response("Route has no Component export", {
|
|
229
|
+
status: 500,
|
|
230
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
231
|
+
}));
|
|
232
|
+
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
233
|
+
const Component = routeModule.Component;
|
|
234
|
+
const Shell = shellModule?.Shell;
|
|
235
|
+
const componentProps = {
|
|
236
|
+
data,
|
|
237
|
+
params: match.params
|
|
238
|
+
};
|
|
239
|
+
const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
|
|
240
|
+
return htmlResponse(buildHtmlDocument({
|
|
241
|
+
head,
|
|
242
|
+
body: await renderToStringAsync(h(PrachtRuntimeProvider, {
|
|
243
|
+
data,
|
|
244
|
+
params: match.params,
|
|
245
|
+
routeId: match.route.id ?? "",
|
|
246
|
+
url: url.pathname
|
|
247
|
+
}, componentTree)),
|
|
248
|
+
hydrationState: {
|
|
249
|
+
url: url.pathname,
|
|
250
|
+
routeId: match.route.id ?? "",
|
|
251
|
+
data,
|
|
252
|
+
error: null
|
|
253
|
+
},
|
|
254
|
+
clientEntryUrl: options.clientEntryUrl,
|
|
255
|
+
cssUrls,
|
|
256
|
+
modulePreloadUrls
|
|
257
|
+
}));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return renderRouteErrorResponse({
|
|
260
|
+
error,
|
|
261
|
+
isRouteStateRequest,
|
|
262
|
+
options,
|
|
263
|
+
routeArgs,
|
|
264
|
+
routeId: match.route.id ?? "",
|
|
265
|
+
routeModule,
|
|
266
|
+
shellFile: match.route.shellFile,
|
|
267
|
+
shellModule,
|
|
268
|
+
urlPathname: url.pathname
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function resolvePageCssUrls(options, shellFile, routeFile) {
|
|
273
|
+
if (!options.cssManifest) return options.cssUrls ?? [];
|
|
274
|
+
const css = /* @__PURE__ */ new Set();
|
|
275
|
+
function addFromManifest(file) {
|
|
276
|
+
const suffix = file.replace(/^\.\//, "");
|
|
277
|
+
for (const [key, cssFiles] of Object.entries(options.cssManifest)) if (key === file || key.endsWith(`/${suffix}`) || key.endsWith(suffix)) {
|
|
278
|
+
for (const c of cssFiles) css.add(c);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (shellFile) addFromManifest(shellFile);
|
|
283
|
+
addFromManifest(routeFile);
|
|
284
|
+
return [...css];
|
|
285
|
+
}
|
|
286
|
+
function resolvePageJsUrls(options, shellFile, routeFile) {
|
|
287
|
+
if (!options.jsManifest) return [];
|
|
288
|
+
const js = /* @__PURE__ */ new Set();
|
|
289
|
+
function addFromManifest(file) {
|
|
290
|
+
const suffix = file.replace(/^\.\//, "");
|
|
291
|
+
for (const [key, jsFiles] of Object.entries(options.jsManifest)) if (key === file || key.endsWith(`/${suffix}`) || key.endsWith(suffix)) {
|
|
292
|
+
for (const j of jsFiles) js.add(j);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (shellFile) addFromManifest(shellFile);
|
|
297
|
+
addFromManifest(routeFile);
|
|
298
|
+
return [...js];
|
|
299
|
+
}
|
|
300
|
+
async function navigateToClientLocation(location, options) {
|
|
301
|
+
if (typeof window === "undefined") return;
|
|
302
|
+
const targetUrl = new URL(location, window.location.href);
|
|
303
|
+
const target = targetUrl.pathname + targetUrl.search + targetUrl.hash;
|
|
304
|
+
if (targetUrl.origin === window.location.origin && window.__PRACHT_NAVIGATE__) {
|
|
305
|
+
await window.__PRACHT_NAVIGATE__(target, options);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (options?.replace) {
|
|
309
|
+
window.location.replace(targetUrl.toString());
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
window.location.href = targetUrl.toString();
|
|
313
|
+
}
|
|
314
|
+
function isPrachtHttpError(error) {
|
|
315
|
+
return error instanceof Error && error.name === "PrachtHttpError" && "status" in error;
|
|
316
|
+
}
|
|
317
|
+
function normalizeRouteError(error) {
|
|
318
|
+
if (isPrachtHttpError(error)) return {
|
|
319
|
+
message: error.message,
|
|
320
|
+
name: error.name,
|
|
321
|
+
status: typeof error.status === "number" ? error.status : 500
|
|
322
|
+
};
|
|
323
|
+
if (error instanceof Error) return {
|
|
324
|
+
message: error.message || "Internal Server Error",
|
|
325
|
+
name: error.name || "Error",
|
|
326
|
+
status: 500
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
message: "Internal Server Error",
|
|
330
|
+
name: "Error",
|
|
331
|
+
status: 500
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function deserializeRouteError$1(error) {
|
|
335
|
+
const result = new Error(error.message);
|
|
336
|
+
result.name = error.name;
|
|
337
|
+
result.status = error.status;
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
async function renderRouteErrorResponse(options) {
|
|
341
|
+
const routeError = normalizeRouteError(options.error);
|
|
342
|
+
if (!options.routeModule?.ErrorBoundary) {
|
|
343
|
+
if (options.isRouteStateRequest) return withDefaultSecurityHeaders(new Response(JSON.stringify({ error: routeError }), {
|
|
344
|
+
status: routeError.status,
|
|
345
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
346
|
+
}));
|
|
347
|
+
const message = routeError.status >= 500 ? "Internal Server Error" : routeError.message;
|
|
348
|
+
return withDefaultSecurityHeaders(new Response(message, {
|
|
349
|
+
status: routeError.status,
|
|
350
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
if (options.isRouteStateRequest) return withDefaultSecurityHeaders(new Response(JSON.stringify({ error: routeError }), {
|
|
354
|
+
status: routeError.status,
|
|
355
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
356
|
+
}));
|
|
357
|
+
const shellModule = options.shellModule ?? (options.shellFile ? await resolveRegistryModule(options.options.registry?.shellModules, options.shellFile) : void 0);
|
|
358
|
+
const head = shellModule?.head ? await shellModule.head(options.routeArgs) : {};
|
|
359
|
+
const cssUrls = resolvePageCssUrls(options.options, options.shellFile, options.routeArgs.route.file);
|
|
360
|
+
const modulePreloadUrls = resolvePageJsUrls(options.options, options.shellFile, options.routeArgs.route.file);
|
|
361
|
+
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
362
|
+
const ErrorBoundary = options.routeModule.ErrorBoundary;
|
|
363
|
+
const Shell = shellModule?.Shell;
|
|
364
|
+
const errorValue = deserializeRouteError$1(routeError);
|
|
365
|
+
const componentTree = Shell ? h(Shell, null, h(ErrorBoundary, { error: errorValue })) : h(ErrorBoundary, { error: errorValue });
|
|
366
|
+
return htmlResponse(buildHtmlDocument({
|
|
367
|
+
head,
|
|
368
|
+
body: await renderToStringAsync(h(PrachtRuntimeProvider, {
|
|
369
|
+
data: null,
|
|
370
|
+
routeId: options.routeId,
|
|
371
|
+
url: options.urlPathname
|
|
372
|
+
}, componentTree)),
|
|
373
|
+
hydrationState: {
|
|
374
|
+
url: options.urlPathname,
|
|
375
|
+
routeId: options.routeId,
|
|
376
|
+
data: null,
|
|
377
|
+
error: routeError
|
|
378
|
+
},
|
|
379
|
+
clientEntryUrl: options.options.clientEntryUrl,
|
|
380
|
+
cssUrls,
|
|
381
|
+
modulePreloadUrls
|
|
382
|
+
}), routeError.status);
|
|
383
|
+
}
|
|
384
|
+
async function runMiddlewareChain(options) {
|
|
385
|
+
let context = options.context;
|
|
386
|
+
for (const mwFile of options.middlewareFiles) {
|
|
387
|
+
const mwModule = await resolveRegistryModule(options.registry.middlewareModules, mwFile);
|
|
388
|
+
if (!mwModule?.middleware) continue;
|
|
389
|
+
const result = await mwModule.middleware({
|
|
390
|
+
request: options.request,
|
|
391
|
+
params: options.params,
|
|
392
|
+
context,
|
|
393
|
+
signal: AbortSignal.timeout(3e4),
|
|
394
|
+
url: options.url,
|
|
395
|
+
route: options.route
|
|
396
|
+
});
|
|
397
|
+
if (!result) continue;
|
|
398
|
+
if (result instanceof Response) return { response: withDefaultSecurityHeaders(result) };
|
|
399
|
+
if ("redirect" in result) return { response: withDefaultSecurityHeaders(new Response(null, {
|
|
400
|
+
status: 302,
|
|
401
|
+
headers: { location: result.redirect }
|
|
402
|
+
})) };
|
|
403
|
+
if ("context" in result) context = {
|
|
404
|
+
...context,
|
|
405
|
+
...result.context
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return { context };
|
|
409
|
+
}
|
|
410
|
+
async function resolveDataFunctions(route, routeModule, registry) {
|
|
411
|
+
let loader = routeModule?.loader;
|
|
412
|
+
if (route.loaderFile) {
|
|
413
|
+
const dataModule = await resolveRegistryModule(registry.dataModules, route.loaderFile);
|
|
414
|
+
if (dataModule?.loader) loader = dataModule.loader;
|
|
415
|
+
}
|
|
416
|
+
return { loader };
|
|
417
|
+
}
|
|
418
|
+
async function resolveRegistryModule(modules, file) {
|
|
419
|
+
if (!modules) return void 0;
|
|
420
|
+
if (file in modules) return modules[file]();
|
|
421
|
+
const suffix = file.replace(/^\.\//, "");
|
|
422
|
+
for (const key of Object.keys(modules)) if (key.endsWith(`/${suffix}`) || key.endsWith(suffix)) return modules[key]();
|
|
423
|
+
}
|
|
424
|
+
async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
|
|
425
|
+
const shellHead = shellModule?.head ? await shellModule.head(routeArgs) : {};
|
|
426
|
+
const routeHead = routeModule?.head ? await routeModule.head({
|
|
427
|
+
...routeArgs,
|
|
428
|
+
data
|
|
429
|
+
}) : {};
|
|
430
|
+
return {
|
|
431
|
+
title: routeHead.title ?? shellHead.title,
|
|
432
|
+
lang: routeHead.lang ?? shellHead.lang,
|
|
433
|
+
meta: [...shellHead.meta ?? [], ...routeHead.meta ?? []],
|
|
434
|
+
link: [...shellHead.link ?? [], ...routeHead.link ?? []]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function buildHtmlDocument(options) {
|
|
438
|
+
const { head, body, hydrationState, clientEntryUrl, cssUrls = [], modulePreloadUrls = [] } = options;
|
|
439
|
+
const titleTag = head.title ? `<title>${escapeHtml(head.title)}</title>` : "";
|
|
440
|
+
const metaTags = (head.meta ?? []).map((m) => `<meta ${Object.entries(m).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ")}>`).join("\n ");
|
|
441
|
+
const linkTags = (head.link ?? []).map((l) => `<link ${Object.entries(l).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ")}>`).join("\n ");
|
|
442
|
+
const cssTags = cssUrls.map((url) => `<link rel="stylesheet" href="${escapeHtml(url)}">`).join("\n ");
|
|
443
|
+
const modulePreloadTags = modulePreloadUrls.map((url) => `<link rel="modulepreload" href="${escapeHtml(url)}">`).join("\n ");
|
|
444
|
+
const stateScript = `<script id="${HYDRATION_STATE_ELEMENT_ID}" type="application/json">${serializeJsonForHtml(hydrationState)}<\/script>`;
|
|
445
|
+
const entryScript = clientEntryUrl ? `<script type="module" src="${escapeHtml(clientEntryUrl)}"><\/script>` : "";
|
|
446
|
+
return `<!DOCTYPE html>
|
|
447
|
+
<html${head.lang ? ` lang="${escapeHtml(head.lang)}"` : ""}>
|
|
448
|
+
<head>
|
|
449
|
+
<meta charset="utf-8">
|
|
450
|
+
${titleTag}
|
|
451
|
+
${metaTags}
|
|
452
|
+
${linkTags}
|
|
453
|
+
${cssTags}
|
|
454
|
+
${modulePreloadTags}
|
|
455
|
+
</head>
|
|
456
|
+
<body>
|
|
457
|
+
<div id="pracht-root">${body}</div>
|
|
458
|
+
${stateScript}
|
|
459
|
+
${entryScript}
|
|
460
|
+
</body>
|
|
461
|
+
</html>`;
|
|
462
|
+
}
|
|
463
|
+
function htmlResponse(html, status = 200) {
|
|
464
|
+
return withDefaultSecurityHeaders(new Response(html, {
|
|
465
|
+
status,
|
|
466
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
function applyDefaultSecurityHeaders(headers) {
|
|
470
|
+
if (!headers.has("permissions-policy")) headers.set("permissions-policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
|
|
471
|
+
if (!headers.has("referrer-policy")) headers.set("referrer-policy", "strict-origin-when-cross-origin");
|
|
472
|
+
if (!headers.has("x-content-type-options")) headers.set("x-content-type-options", "nosniff");
|
|
473
|
+
if (!headers.has("x-frame-options")) headers.set("x-frame-options", "SAMEORIGIN");
|
|
474
|
+
return headers;
|
|
475
|
+
}
|
|
476
|
+
function withDefaultSecurityHeaders(response) {
|
|
477
|
+
const headers = applyDefaultSecurityHeaders(new Headers(response.headers));
|
|
478
|
+
return new Response(response.body, {
|
|
479
|
+
status: response.status,
|
|
480
|
+
statusText: response.statusText,
|
|
481
|
+
headers
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function escapeHtml(str) {
|
|
485
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
486
|
+
}
|
|
487
|
+
function serializeJsonForHtml(value) {
|
|
488
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
489
|
+
}
|
|
490
|
+
async function prerenderApp(options) {
|
|
491
|
+
const { resolveApp } = await import("./app-CAoDWWNO.mjs").then((n) => n.t);
|
|
492
|
+
const resolved = resolveApp(options.app);
|
|
493
|
+
const results = [];
|
|
494
|
+
const isgManifest = {};
|
|
495
|
+
for (const route of resolved.routes) {
|
|
496
|
+
if (route.render !== "ssg" && route.render !== "isg") continue;
|
|
497
|
+
const paths = await collectSSGPaths(route, options.registry);
|
|
498
|
+
for (const pathname of paths) {
|
|
499
|
+
const url = new URL(pathname, "http://localhost");
|
|
500
|
+
const request = new Request(url, { method: "GET" });
|
|
501
|
+
const response = await handlePrachtRequest({
|
|
502
|
+
app: options.app,
|
|
503
|
+
request,
|
|
504
|
+
registry: options.registry,
|
|
505
|
+
clientEntryUrl: options.clientEntryUrl,
|
|
506
|
+
cssManifest: options.cssManifest
|
|
507
|
+
});
|
|
508
|
+
if (response.status !== 200) {
|
|
509
|
+
console.warn(` Warning: ${route.render.toUpperCase()} route "${pathname}" returned status ${response.status}, skipping.`);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const html = await response.text();
|
|
513
|
+
results.push({
|
|
514
|
+
path: pathname,
|
|
515
|
+
html
|
|
516
|
+
});
|
|
517
|
+
if (route.render === "isg" && route.revalidate) isgManifest[pathname] = { revalidate: route.revalidate };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (options.withISGManifest) return {
|
|
521
|
+
pages: results,
|
|
522
|
+
isgManifest
|
|
523
|
+
};
|
|
524
|
+
return results;
|
|
525
|
+
}
|
|
526
|
+
async function collectSSGPaths(route, registry) {
|
|
527
|
+
if (!route.segments.some((s) => s.type === "param" || s.type === "catchall")) return [route.path];
|
|
528
|
+
const routeModule = await resolveRegistryModule(registry?.routeModules, route.file);
|
|
529
|
+
if (!routeModule?.getStaticPaths) {
|
|
530
|
+
console.warn(` Warning: SSG route "${route.path}" has dynamic segments but no getStaticPaths() export, skipping.`);
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
const { buildPathFromSegments } = await import("./app-CAoDWWNO.mjs").then((n) => n.t);
|
|
534
|
+
return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/prefetch.ts
|
|
538
|
+
const CACHE_TTL_MS = 3e4;
|
|
539
|
+
const prefetchCache = /* @__PURE__ */ new Map();
|
|
540
|
+
function getCachedRouteState(url) {
|
|
541
|
+
const entry = prefetchCache.get(url);
|
|
542
|
+
if (!entry) return null;
|
|
543
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
|
|
544
|
+
prefetchCache.delete(url);
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
return entry.promise;
|
|
548
|
+
}
|
|
549
|
+
function prefetchRouteState(url) {
|
|
550
|
+
const cached = getCachedRouteState(url);
|
|
551
|
+
if (cached) return cached;
|
|
552
|
+
const promise = fetchPrachtRouteState(url);
|
|
553
|
+
prefetchCache.set(url, {
|
|
554
|
+
promise,
|
|
555
|
+
timestamp: Date.now()
|
|
556
|
+
});
|
|
557
|
+
return promise;
|
|
558
|
+
}
|
|
559
|
+
function setupPrefetching(app) {
|
|
560
|
+
let hoverTimer = null;
|
|
561
|
+
function getInternalHref(anchor) {
|
|
562
|
+
const href = anchor.getAttribute("href");
|
|
563
|
+
if (!href || href.startsWith("#")) return null;
|
|
564
|
+
let url;
|
|
565
|
+
try {
|
|
566
|
+
url = new URL(href, window.location.origin);
|
|
567
|
+
} catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
if (url.origin !== window.location.origin) return null;
|
|
571
|
+
return url.pathname + url.search;
|
|
572
|
+
}
|
|
573
|
+
function getPrefetchStrategy(pathname) {
|
|
574
|
+
const match = matchAppRoute(app, pathname);
|
|
575
|
+
if (!match) return "none";
|
|
576
|
+
if (match.route.prefetch) return match.route.prefetch;
|
|
577
|
+
if (match.route.render === "spa") return "none";
|
|
578
|
+
return "intent";
|
|
579
|
+
}
|
|
580
|
+
document.addEventListener("mouseenter", (e) => {
|
|
581
|
+
const anchor = e.target.closest?.("a");
|
|
582
|
+
if (!anchor) return;
|
|
583
|
+
const href = getInternalHref(anchor);
|
|
584
|
+
if (!href) return;
|
|
585
|
+
const strategy = getPrefetchStrategy(href);
|
|
586
|
+
if (strategy !== "hover" && strategy !== "intent") return;
|
|
587
|
+
if (hoverTimer) clearTimeout(hoverTimer);
|
|
588
|
+
hoverTimer = setTimeout(() => {
|
|
589
|
+
prefetchRouteState(href);
|
|
590
|
+
}, 50);
|
|
591
|
+
}, true);
|
|
592
|
+
document.addEventListener("mouseleave", (e) => {
|
|
593
|
+
if (!e.target.closest?.("a")) return;
|
|
594
|
+
if (hoverTimer) {
|
|
595
|
+
clearTimeout(hoverTimer);
|
|
596
|
+
hoverTimer = null;
|
|
597
|
+
}
|
|
598
|
+
}, true);
|
|
599
|
+
document.addEventListener("focusin", (e) => {
|
|
600
|
+
const anchor = e.target.closest?.("a");
|
|
601
|
+
if (!anchor) return;
|
|
602
|
+
const href = getInternalHref(anchor);
|
|
603
|
+
if (!href) return;
|
|
604
|
+
const strategy = getPrefetchStrategy(href);
|
|
605
|
+
if (strategy !== "hover" && strategy !== "intent") return;
|
|
606
|
+
prefetchRouteState(href);
|
|
607
|
+
}, true);
|
|
608
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
609
|
+
const observer = new IntersectionObserver((entries) => {
|
|
610
|
+
for (const entry of entries) {
|
|
611
|
+
if (!entry.isIntersecting) continue;
|
|
612
|
+
const anchor = entry.target;
|
|
613
|
+
const href = getInternalHref(anchor);
|
|
614
|
+
if (!href) continue;
|
|
615
|
+
prefetchRouteState(href);
|
|
616
|
+
observer.unobserve(anchor);
|
|
617
|
+
}
|
|
618
|
+
}, { rootMargin: "200px" });
|
|
619
|
+
function observeViewportLinks() {
|
|
620
|
+
const anchors = document.querySelectorAll("a[href]");
|
|
621
|
+
for (const anchor of anchors) {
|
|
622
|
+
const href = getInternalHref(anchor);
|
|
623
|
+
if (!href) continue;
|
|
624
|
+
if (getPrefetchStrategy(href) !== "viewport") continue;
|
|
625
|
+
observer.observe(anchor);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
observeViewportLinks();
|
|
629
|
+
new MutationObserver(() => {
|
|
630
|
+
observeViewportLinks();
|
|
631
|
+
}).observe(document.body, {
|
|
632
|
+
childList: true,
|
|
633
|
+
subtree: true
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/router.ts
|
|
638
|
+
const NavigateContext = createContext(async () => {});
|
|
639
|
+
function useNavigate() {
|
|
640
|
+
return useContext(NavigateContext);
|
|
641
|
+
}
|
|
642
|
+
async function initClientRouter(options) {
|
|
643
|
+
const { app, routeModules, shellModules, root, findModuleKey } = options;
|
|
644
|
+
async function buildRouteTree(match, state) {
|
|
645
|
+
const routeKey = findModuleKey(routeModules, match.route.file);
|
|
646
|
+
if (!routeKey) return null;
|
|
647
|
+
const routeMod = await routeModules[routeKey]();
|
|
648
|
+
let Shell = null;
|
|
649
|
+
if (match.route.shellFile) {
|
|
650
|
+
const shellKey = findModuleKey(shellModules, match.route.shellFile);
|
|
651
|
+
if (shellKey) Shell = (await shellModules[shellKey]()).Shell;
|
|
652
|
+
}
|
|
653
|
+
const Component = state.error ? routeMod.ErrorBoundary : routeMod.Component;
|
|
654
|
+
if (!Component) return null;
|
|
655
|
+
const props = state.error ? { error: deserializeRouteError(state.error) } : {
|
|
656
|
+
data: state.data,
|
|
657
|
+
params: match.params
|
|
658
|
+
};
|
|
659
|
+
const componentTree = Shell ? h(Shell, null, h(Component, props)) : h(Component, props);
|
|
660
|
+
return h(NavigateContext.Provider, { value: navigate }, h(PrachtRuntimeProvider, {
|
|
661
|
+
data: state.data,
|
|
662
|
+
params: match.params,
|
|
663
|
+
routeId: match.route.id ?? "",
|
|
664
|
+
url: match.pathname
|
|
665
|
+
}, componentTree));
|
|
666
|
+
}
|
|
667
|
+
async function navigate(to, opts) {
|
|
668
|
+
const match = matchAppRoute(app, to);
|
|
669
|
+
if (!match) {
|
|
670
|
+
window.location.href = to;
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
let state = {
|
|
674
|
+
data: void 0,
|
|
675
|
+
error: null
|
|
676
|
+
};
|
|
677
|
+
try {
|
|
678
|
+
const result = await (getCachedRouteState(to) ?? fetchPrachtRouteState(to));
|
|
679
|
+
if (result.type === "redirect") {
|
|
680
|
+
if (result.location) {
|
|
681
|
+
await navigate(result.location, opts);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
window.location.href = to;
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (result.type === "error") state = {
|
|
688
|
+
data: void 0,
|
|
689
|
+
error: result.error
|
|
690
|
+
};
|
|
691
|
+
else state = {
|
|
692
|
+
data: result.data,
|
|
693
|
+
error: null
|
|
694
|
+
};
|
|
695
|
+
} catch {
|
|
696
|
+
window.location.href = to;
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "", to);
|
|
700
|
+
else history.pushState(null, "", to);
|
|
701
|
+
const tree = await buildRouteTree(match, state);
|
|
702
|
+
if (tree) {
|
|
703
|
+
render(tree, root);
|
|
704
|
+
window.scrollTo(0, 0);
|
|
705
|
+
} else window.location.href = to;
|
|
706
|
+
}
|
|
707
|
+
if (import.meta.env?.DEV) {
|
|
708
|
+
const prev = options.__m;
|
|
709
|
+
options.__m = (vnode, s) => {
|
|
710
|
+
const message = `Hydration mismatch in <${(typeof vnode.type === "function" ? vnode.type.displayName || vnode.type.name : vnode.type) || "Unknown"}>: ${s}`;
|
|
711
|
+
console.warn(`[pracht] ${message}`);
|
|
712
|
+
appendHydrationWarning(message);
|
|
713
|
+
if (prev) prev(vnode, s);
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const initialMatch = matchAppRoute(app, options.initialState.url);
|
|
717
|
+
if (initialMatch) {
|
|
718
|
+
let state = {
|
|
719
|
+
data: options.initialState.data,
|
|
720
|
+
error: options.initialState.error ?? null
|
|
721
|
+
};
|
|
722
|
+
if (initialMatch.route.render === "spa" && state.data == null && !state.error) try {
|
|
723
|
+
const result = await fetchPrachtRouteState(options.initialState.url);
|
|
724
|
+
if (result.type === "redirect") {
|
|
725
|
+
window.location.href = result.location;
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (result.type === "error") state = {
|
|
729
|
+
data: void 0,
|
|
730
|
+
error: result.error
|
|
731
|
+
};
|
|
732
|
+
else state = {
|
|
733
|
+
data: result.data,
|
|
734
|
+
error: null
|
|
735
|
+
};
|
|
736
|
+
} catch {
|
|
737
|
+
window.location.href = options.initialState.url;
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const tree = await buildRouteTree(initialMatch, state);
|
|
741
|
+
if (tree) if (initialMatch.route.render === "spa") render(tree, root);
|
|
742
|
+
else hydrate(tree, root);
|
|
743
|
+
}
|
|
744
|
+
document.addEventListener("click", (e) => {
|
|
745
|
+
const anchor = e.target.closest?.("a");
|
|
746
|
+
if (!anchor) return;
|
|
747
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
748
|
+
if (e.defaultPrevented) return;
|
|
749
|
+
if (e.button !== 0) return;
|
|
750
|
+
const target = anchor.getAttribute("target");
|
|
751
|
+
if (target && target !== "_self") return;
|
|
752
|
+
if (anchor.hasAttribute("download")) return;
|
|
753
|
+
const href = anchor.getAttribute("href");
|
|
754
|
+
if (!href || href.startsWith("#")) return;
|
|
755
|
+
let url;
|
|
756
|
+
try {
|
|
757
|
+
url = new URL(href, window.location.origin);
|
|
758
|
+
} catch {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (url.origin !== window.location.origin) return;
|
|
762
|
+
e.preventDefault();
|
|
763
|
+
navigate(url.pathname + url.search);
|
|
764
|
+
});
|
|
765
|
+
window.addEventListener("popstate", () => {
|
|
766
|
+
navigate(window.location.pathname + window.location.search, { _popstate: true });
|
|
767
|
+
});
|
|
768
|
+
window.__PRACHT_NAVIGATE__ = navigate;
|
|
769
|
+
window.__PRACHT_ROUTER_READY__ = true;
|
|
770
|
+
setupPrefetching(app);
|
|
771
|
+
}
|
|
772
|
+
const HYDRATION_BANNER_ID = "__pracht_hydration_warnings__";
|
|
773
|
+
function appendHydrationWarning(message) {
|
|
774
|
+
let container = document.getElementById(HYDRATION_BANNER_ID);
|
|
775
|
+
if (!container) {
|
|
776
|
+
container = document.createElement("div");
|
|
777
|
+
container.id = HYDRATION_BANNER_ID;
|
|
778
|
+
Object.assign(container.style, {
|
|
779
|
+
position: "fixed",
|
|
780
|
+
bottom: "0",
|
|
781
|
+
left: "0",
|
|
782
|
+
right: "0",
|
|
783
|
+
maxHeight: "30vh",
|
|
784
|
+
overflow: "auto",
|
|
785
|
+
background: "#2d1b00",
|
|
786
|
+
borderTop: "2px solid #f0ad4e",
|
|
787
|
+
color: "#ffc107",
|
|
788
|
+
fontFamily: "ui-monospace, Consolas, monospace",
|
|
789
|
+
fontSize: "13px",
|
|
790
|
+
padding: "12px 16px",
|
|
791
|
+
zIndex: "2147483647"
|
|
792
|
+
});
|
|
793
|
+
const header = document.createElement("div");
|
|
794
|
+
Object.assign(header.style, {
|
|
795
|
+
display: "flex",
|
|
796
|
+
justifyContent: "space-between",
|
|
797
|
+
alignItems: "center",
|
|
798
|
+
marginBottom: "8px"
|
|
799
|
+
});
|
|
800
|
+
header.innerHTML = "<strong style=\"color:#f0ad4e\">⚠ Hydration Mismatches</strong>";
|
|
801
|
+
const close = document.createElement("button");
|
|
802
|
+
close.textContent = "×";
|
|
803
|
+
Object.assign(close.style, {
|
|
804
|
+
background: "none",
|
|
805
|
+
border: "none",
|
|
806
|
+
color: "#f0ad4e",
|
|
807
|
+
fontSize: "18px",
|
|
808
|
+
cursor: "pointer"
|
|
809
|
+
});
|
|
810
|
+
close.onclick = () => container.remove();
|
|
811
|
+
header.appendChild(close);
|
|
812
|
+
container.appendChild(header);
|
|
813
|
+
document.body.appendChild(container);
|
|
814
|
+
}
|
|
815
|
+
const entry = document.createElement("div");
|
|
816
|
+
entry.textContent = message;
|
|
817
|
+
Object.assign(entry.style, { padding: "2px 0" });
|
|
818
|
+
container.appendChild(entry);
|
|
819
|
+
}
|
|
820
|
+
function deserializeRouteError(error) {
|
|
821
|
+
const result = new Error(error.message);
|
|
822
|
+
result.name = error.name;
|
|
823
|
+
result.status = error.status;
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region src/types.ts
|
|
828
|
+
var PrachtHttpError = class extends Error {
|
|
829
|
+
status;
|
|
830
|
+
constructor(status, message) {
|
|
831
|
+
super(message);
|
|
832
|
+
this.name = "PrachtHttpError";
|
|
833
|
+
this.status = status;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
//#endregion
|
|
837
|
+
export { Form, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useLocation, useNavigate, useParams, useRevalidate, useRevalidateRoute, useRouteData };
|