@pracht/core 0.0.1 → 0.2.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/README.md +12 -1
- package/dist/{app-CAoDWWNO.mjs → app-Cep0el7c.mjs} +21 -7
- package/dist/index.d.mts +61 -8
- package/dist/index.mjs +429 -133
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -16,11 +16,22 @@ npm install @pracht/core preact preact-render-to-string
|
|
|
16
16
|
- `route()` — declare a route with path, component, loader, and rendering mode
|
|
17
17
|
- `group()` — group routes under a shared shell or middleware
|
|
18
18
|
|
|
19
|
+
Route modules may export the page as a function default export or as a named
|
|
20
|
+
`Component` export. Named exports such as `loader`, `head`, `ErrorBoundary`, and
|
|
21
|
+
`getStaticPaths` keep their special route-module behavior.
|
|
22
|
+
|
|
19
23
|
### Server
|
|
20
24
|
|
|
21
25
|
- `handlePrachtRequest()` — server renderer that produces full HTML with hydration markers
|
|
22
26
|
- `matchAppRoute()` — segment-based route matching
|
|
23
27
|
|
|
28
|
+
`handlePrachtRequest()` sanitizes unexpected 5xx errors by default so raw server
|
|
29
|
+
messages do not leak into SSR HTML or route-state JSON. Explicit
|
|
30
|
+
`PrachtHttpError` 4xx messages are preserved. Pass `debugErrors: true` to expose
|
|
31
|
+
raw details intentionally during debugging; `@pracht/core` does not infer this
|
|
32
|
+
from environment variables. Debug responses also attach `error.diagnostics`
|
|
33
|
+
metadata for the failure phase and matched framework files when available.
|
|
34
|
+
|
|
24
35
|
### Client
|
|
25
36
|
|
|
26
37
|
- `startApp()` — client-side hydration and runtime
|
|
@@ -42,4 +53,4 @@ Each route can specify its rendering mode:
|
|
|
42
53
|
- `ssr` — server-rendered on every request
|
|
43
54
|
- `ssg` — pre-rendered at build time
|
|
44
55
|
- `isg` — pre-rendered with time-based revalidation
|
|
45
|
-
- `spa` — client-only rendering
|
|
56
|
+
- `spa` — client-only route rendering with optional shell/loading HTML on first paint
|
|
@@ -30,21 +30,26 @@ function timeRevalidate(seconds) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
function route(path, fileOrConfig, meta = {}) {
|
|
33
|
-
if (typeof fileOrConfig === "string") return {
|
|
33
|
+
if (typeof fileOrConfig === "string" || typeof fileOrConfig === "function") return {
|
|
34
34
|
kind: "route",
|
|
35
35
|
path: normalizeRoutePath(path),
|
|
36
|
-
file: fileOrConfig,
|
|
36
|
+
file: resolveModuleRef(fileOrConfig),
|
|
37
37
|
...meta
|
|
38
38
|
};
|
|
39
39
|
const { component, loader, ...routeMeta } = fileOrConfig;
|
|
40
40
|
return {
|
|
41
41
|
kind: "route",
|
|
42
42
|
path: normalizeRoutePath(path),
|
|
43
|
-
file: component,
|
|
44
|
-
loaderFile: loader,
|
|
43
|
+
file: resolveModuleRef(component),
|
|
44
|
+
loaderFile: resolveModuleRef(loader),
|
|
45
45
|
...routeMeta
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
+
function resolveModuleRef(ref) {
|
|
49
|
+
if (ref === void 0) return void 0;
|
|
50
|
+
if (typeof ref === "string") return ref;
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
48
53
|
function group(meta, routes) {
|
|
49
54
|
return {
|
|
50
55
|
kind: "group",
|
|
@@ -54,12 +59,17 @@ function group(meta, routes) {
|
|
|
54
59
|
}
|
|
55
60
|
function defineApp(config) {
|
|
56
61
|
return {
|
|
57
|
-
shells: config.shells ?? {},
|
|
58
|
-
middleware: config.middleware ?? {},
|
|
62
|
+
shells: resolveModuleRefRecord(config.shells ?? {}),
|
|
63
|
+
middleware: resolveModuleRefRecord(config.middleware ?? {}),
|
|
59
64
|
api: config.api ?? {},
|
|
60
65
|
routes: config.routes
|
|
61
66
|
};
|
|
62
67
|
}
|
|
68
|
+
function resolveModuleRefRecord(record) {
|
|
69
|
+
const result = {};
|
|
70
|
+
for (const [key, value] of Object.entries(record)) result[key] = resolveModuleRef(value);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
63
73
|
function resolveApp(app) {
|
|
64
74
|
const routes = [];
|
|
65
75
|
const inherited = {
|
|
@@ -136,7 +146,11 @@ function matchRouteSegments(routeSegments, targetSegments) {
|
|
|
136
146
|
if (typeof targetSegment === "undefined") return null;
|
|
137
147
|
if (currentSegment.type === "static") {
|
|
138
148
|
if (currentSegment.value !== targetSegment) return null;
|
|
139
|
-
} else
|
|
149
|
+
} else try {
|
|
150
|
+
params[currentSegment.name] = decodeURIComponent(targetSegment);
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
140
154
|
routeIndex += 1;
|
|
141
155
|
targetIndex += 1;
|
|
142
156
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Suspense, lazy } from "preact-suspense";
|
|
2
1
|
import * as _$preact from "preact";
|
|
3
2
|
import { ComponentChildren, FunctionComponent, JSX, h } from "preact";
|
|
3
|
+
import { Suspense, lazy } from "preact-suspense";
|
|
4
4
|
|
|
5
5
|
//#region src/types.d.ts
|
|
6
6
|
/**
|
|
@@ -23,6 +23,12 @@ type RegisteredContext = Register extends {
|
|
|
23
23
|
} ? T : unknown;
|
|
24
24
|
type RenderMode = "spa" | "ssr" | "ssg" | "isg";
|
|
25
25
|
type RouteParams = Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* A reference to a module file — either a plain string path or a lazy import
|
|
28
|
+
* function. Using `() => import("./path")` enables IDE click-to-navigate.
|
|
29
|
+
* The vite plugin transforms import functions back to strings at build time.
|
|
30
|
+
*/
|
|
31
|
+
type ModuleRef = string | (() => Promise<any>);
|
|
26
32
|
interface TimeRevalidatePolicy {
|
|
27
33
|
kind: "time";
|
|
28
34
|
seconds: number;
|
|
@@ -68,8 +74,8 @@ interface ApiConfig {
|
|
|
68
74
|
middleware?: string[];
|
|
69
75
|
}
|
|
70
76
|
interface RouteConfig extends RouteMeta {
|
|
71
|
-
component:
|
|
72
|
-
loader?:
|
|
77
|
+
component: ModuleRef;
|
|
78
|
+
loader?: ModuleRef;
|
|
73
79
|
}
|
|
74
80
|
interface RouteDefinition extends RouteMeta {
|
|
75
81
|
kind: "route";
|
|
@@ -84,8 +90,8 @@ interface GroupDefinition {
|
|
|
84
90
|
}
|
|
85
91
|
type RouteTreeNode = RouteDefinition | GroupDefinition;
|
|
86
92
|
interface PrachtAppConfig {
|
|
87
|
-
shells?: Record<string,
|
|
88
|
-
middleware?: Record<string,
|
|
93
|
+
shells?: Record<string, ModuleRef>;
|
|
94
|
+
middleware?: Record<string, ModuleRef>;
|
|
89
95
|
api?: ApiConfig;
|
|
90
96
|
routes: RouteTreeNode[];
|
|
91
97
|
}
|
|
@@ -163,12 +169,14 @@ type LoaderFn<TContext = any, TData = unknown> = (args: LoaderArgs<TContext>) =>
|
|
|
163
169
|
interface RouteModule<TContext = any, TLoader extends LoaderLike = undefined> {
|
|
164
170
|
loader?: LoaderFn<TContext>;
|
|
165
171
|
head?: (args: HeadArgs<TLoader, TContext>) => MaybePromise<HeadMetadata>;
|
|
166
|
-
Component
|
|
172
|
+
Component?: FunctionComponent<RouteComponentProps<TLoader>>;
|
|
173
|
+
default?: FunctionComponent<RouteComponentProps<TLoader>>;
|
|
167
174
|
ErrorBoundary?: FunctionComponent<ErrorBoundaryProps>;
|
|
168
175
|
getStaticPaths?: () => MaybePromise<RouteParams[]>;
|
|
169
176
|
}
|
|
170
177
|
interface ShellModule<TContext = any> {
|
|
171
178
|
Shell: FunctionComponent<ShellProps>;
|
|
179
|
+
Loading?: FunctionComponent;
|
|
172
180
|
head?: (args: BaseRouteArgs<TContext>) => MaybePromise<HeadMetadata>;
|
|
173
181
|
}
|
|
174
182
|
type MiddlewareResult<TContext = any> = void | Response | {
|
|
@@ -198,7 +206,7 @@ declare class PrachtHttpError extends Error {
|
|
|
198
206
|
//#endregion
|
|
199
207
|
//#region src/app.d.ts
|
|
200
208
|
declare function timeRevalidate(seconds: number): TimeRevalidatePolicy;
|
|
201
|
-
declare function route(path: string, file:
|
|
209
|
+
declare function route(path: string, file: ModuleRef, meta?: RouteMeta): RouteDefinition;
|
|
202
210
|
declare function route(path: string, config: RouteConfig): RouteDefinition;
|
|
203
211
|
declare function group(meta: GroupMeta, routes: RouteTreeNode[]): GroupDefinition;
|
|
204
212
|
declare function defineApp(config: PrachtAppConfig): PrachtApp;
|
|
@@ -215,17 +223,50 @@ declare function buildPathFromSegments(segments: RouteSegment[], params: RoutePa
|
|
|
215
223
|
declare function resolveApiRoutes(files: string[], apiDir?: string): ResolvedApiRoute[];
|
|
216
224
|
declare function matchApiRoute(apiRoutes: ResolvedApiRoute[], pathname: string): ApiRouteMatch | undefined;
|
|
217
225
|
//#endregion
|
|
226
|
+
//#region src/forwardRef.d.ts
|
|
227
|
+
/**
|
|
228
|
+
* Pass ref down to a child. This is mainly used in libraries with HOCs that
|
|
229
|
+
* wrap components. Using `forwardRef` there is an easy way to get a reference
|
|
230
|
+
* of the wrapped component instead of one of the wrapper itself.
|
|
231
|
+
*/
|
|
232
|
+
declare function forwardRef<P = {}>(fn: ((props: P, ref: any) => any) & {
|
|
233
|
+
displayName?: string;
|
|
234
|
+
}): FunctionComponent<P & {
|
|
235
|
+
ref?: any;
|
|
236
|
+
}>;
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/hydration.d.ts
|
|
239
|
+
/**
|
|
240
|
+
* Returns `true` once the initial hydration (including all Suspense
|
|
241
|
+
* boundaries) has fully resolved. During SSR and hydration this returns
|
|
242
|
+
* `false`.
|
|
243
|
+
*/
|
|
244
|
+
declare function useIsHydrated(): boolean;
|
|
245
|
+
//#endregion
|
|
218
246
|
//#region src/runtime.d.ts
|
|
247
|
+
type PrachtRuntimeDiagnosticPhase = "match" | "middleware" | "loader" | "action" | "render" | "api";
|
|
248
|
+
interface PrachtRuntimeDiagnostics {
|
|
249
|
+
phase: PrachtRuntimeDiagnosticPhase;
|
|
250
|
+
routeId?: string;
|
|
251
|
+
routePath?: string;
|
|
252
|
+
routeFile?: string;
|
|
253
|
+
loaderFile?: string;
|
|
254
|
+
shellFile?: string;
|
|
255
|
+
middlewareFiles?: string[];
|
|
256
|
+
status: number;
|
|
257
|
+
}
|
|
219
258
|
interface PrachtHydrationState<TData = unknown> {
|
|
220
259
|
url: string;
|
|
221
260
|
routeId: string;
|
|
222
261
|
data: TData;
|
|
223
262
|
error?: SerializedRouteError | null;
|
|
263
|
+
pending?: boolean;
|
|
224
264
|
}
|
|
225
265
|
interface SerializedRouteError {
|
|
226
266
|
message: string;
|
|
227
267
|
name: string;
|
|
228
268
|
status: number;
|
|
269
|
+
diagnostics?: PrachtRuntimeDiagnostics;
|
|
229
270
|
}
|
|
230
271
|
interface StartAppOptions<TData = unknown> {
|
|
231
272
|
initialData?: TData;
|
|
@@ -235,6 +276,8 @@ interface HandlePrachtRequestOptions<TContext = unknown> {
|
|
|
235
276
|
request: Request;
|
|
236
277
|
context?: TContext;
|
|
237
278
|
registry?: ModuleRegistry;
|
|
279
|
+
/** Expose raw server error details in rendered HTML and route-state JSON. */
|
|
280
|
+
debugErrors?: boolean;
|
|
238
281
|
clientEntryUrl?: string;
|
|
239
282
|
/** Per-source-file CSS map produced by the vite plugin (preferred over cssUrls). */
|
|
240
283
|
cssManifest?: Record<string, string[]>;
|
|
@@ -291,6 +334,16 @@ declare function useRevalidate(): () => Promise<unknown>;
|
|
|
291
334
|
/** @deprecated Use useRevalidate instead. */
|
|
292
335
|
declare const useRevalidateRoute: typeof useRevalidate;
|
|
293
336
|
declare function Form(props: FormProps): _$preact.VNode<_$preact.ClassAttributes<HTMLFormElement> & h.JSX.HTMLAttributes<HTMLFormElement>>;
|
|
337
|
+
type RouteStateResult = {
|
|
338
|
+
type: "data";
|
|
339
|
+
data: unknown;
|
|
340
|
+
} | {
|
|
341
|
+
type: "redirect";
|
|
342
|
+
location: string;
|
|
343
|
+
} | {
|
|
344
|
+
type: "error";
|
|
345
|
+
error: SerializedRouteError;
|
|
346
|
+
};
|
|
294
347
|
declare function handlePrachtRequest<TContext>(options: HandlePrachtRequestOptions<TContext>): Promise<Response>;
|
|
295
348
|
declare function applyDefaultSecurityHeaders(headers: Headers): Headers;
|
|
296
349
|
interface PrerenderResult {
|
|
@@ -340,4 +393,4 @@ interface InitClientRouterOptions {
|
|
|
340
393
|
}
|
|
341
394
|
declare function initClientRouter(options: InitClientRouterOptions): Promise<void>;
|
|
342
395
|
//#endregion
|
|
343
|
-
export { type ApiConfig, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type BaseRouteArgs, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadMetadata, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, type LoaderArgs, type LoaderData, type LoaderFn, type Location, type MiddlewareArgs, type MiddlewareFn, type MiddlewareModule, type MiddlewareResult, type ModuleImporter, type ModuleRegistry, type NavigateFn, type PrachtApp, type PrachtAppConfig, PrachtHttpError, type PrachtHydrationState, PrachtRuntimeProvider, type PrefetchStrategy, type PrerenderAppOptions, type PrerenderAppResult, type PrerenderResult, type Register, type RenderMode, type ResolvedApiRoute, type ResolvedPrachtApp, type ResolvedRoute, type RouteComponentProps, type RouteConfig, type RouteDefinition, type RouteMatch, type RouteMeta, type RouteModule, type RouteParams, type RouteRevalidate, type RouteTreeNode, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useLocation, useNavigate, useParams, useRevalidate, useRevalidateRoute, useRouteData };
|
|
396
|
+
export { type ApiConfig, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type BaseRouteArgs, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadMetadata, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, type LoaderArgs, type LoaderData, type LoaderFn, type Location, type MiddlewareArgs, type MiddlewareFn, type MiddlewareModule, type MiddlewareResult, type ModuleImporter, type ModuleRef, type ModuleRegistry, type NavigateFn, type PrachtApp, type PrachtAppConfig, PrachtHttpError, type PrachtHydrationState, type PrachtRuntimeDiagnosticPhase, type PrachtRuntimeDiagnostics, PrachtRuntimeProvider, type PrefetchStrategy, type PrerenderAppOptions, type PrerenderAppResult, type PrerenderResult, type Register, type RenderMode, type ResolvedApiRoute, type ResolvedPrachtApp, type ResolvedRoute, type RouteComponentProps, type RouteConfig, type RouteDefinition, type RouteMatch, type RouteMeta, type RouteModule, type RouteParams, type RouteRevalidate, type RouteStateResult, type RouteTreeNode, type SerializedRouteError, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRevalidateRoute, useRouteData };
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,82 @@
|
|
|
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-
|
|
2
|
-
import {
|
|
3
|
-
import { createContext, h, hydrate, render } from "preact";
|
|
1
|
+
import { a as matchApiRoute, c as resolveApp, i as group, l as route, n as buildPathFromSegments, o as matchAppRoute, r as defineApp, s as resolveApiRoutes, u as timeRevalidate } from "./app-Cep0el7c.mjs";
|
|
2
|
+
import { createContext, h, hydrate, options, render } from "preact";
|
|
4
3
|
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
|
4
|
+
import { Suspense, lazy } from "preact-suspense";
|
|
5
|
+
//#region src/forwardRef.ts
|
|
6
|
+
let oldDiffHook = options.__b;
|
|
7
|
+
options.__b = (vnode) => {
|
|
8
|
+
if (vnode.type && vnode.type.__f && vnode.ref) {
|
|
9
|
+
vnode.props.ref = vnode.ref;
|
|
10
|
+
vnode.ref = null;
|
|
11
|
+
}
|
|
12
|
+
if (oldDiffHook) oldDiffHook(vnode);
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Pass ref down to a child. This is mainly used in libraries with HOCs that
|
|
16
|
+
* wrap components. Using `forwardRef` there is an easy way to get a reference
|
|
17
|
+
* of the wrapped component instead of one of the wrapper itself.
|
|
18
|
+
*/
|
|
19
|
+
function forwardRef(fn) {
|
|
20
|
+
function Forwarded(props) {
|
|
21
|
+
const clone = { ...props };
|
|
22
|
+
delete clone.ref;
|
|
23
|
+
return fn(clone, props.ref || null);
|
|
24
|
+
}
|
|
25
|
+
Forwarded.__f = true;
|
|
26
|
+
Forwarded.displayName = "ForwardRef(" + (fn.displayName || fn.name) + ")";
|
|
27
|
+
return Forwarded;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/hydration.ts
|
|
31
|
+
let _hydrating = false;
|
|
32
|
+
let _suspensionCount = 0;
|
|
33
|
+
let _hydrated = false;
|
|
34
|
+
const oldCatchError = options.__e;
|
|
35
|
+
options.__e = (err, newVNode, oldVNode, errorInfo) => {
|
|
36
|
+
if (_hydrating && !_hydrated && err && err.then) {
|
|
37
|
+
_suspensionCount++;
|
|
38
|
+
let settled = false;
|
|
39
|
+
const onSettled = () => {
|
|
40
|
+
if (settled) return;
|
|
41
|
+
settled = true;
|
|
42
|
+
_suspensionCount--;
|
|
43
|
+
};
|
|
44
|
+
err.then(onSettled, onSettled);
|
|
45
|
+
}
|
|
46
|
+
if (oldCatchError) oldCatchError(err, newVNode, oldVNode, errorInfo);
|
|
47
|
+
};
|
|
48
|
+
const oldDiffed = options.diffed;
|
|
49
|
+
options.diffed = (vnode) => {
|
|
50
|
+
if (_hydrating && !_hydrated && _suspensionCount <= 0) {
|
|
51
|
+
_hydrated = true;
|
|
52
|
+
_hydrating = false;
|
|
53
|
+
}
|
|
54
|
+
if (oldDiffed) oldDiffed(vnode);
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Mark the start of a hydration pass. Call this right before `hydrate()`.
|
|
58
|
+
*/
|
|
59
|
+
function markHydrating() {
|
|
60
|
+
if (!_hydrated) _hydrating = true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns `true` once the initial hydration (including all Suspense
|
|
64
|
+
* boundaries) has fully resolved. During SSR and hydration this returns
|
|
65
|
+
* `false`.
|
|
66
|
+
*/
|
|
67
|
+
function useIsHydrated() {
|
|
68
|
+
const [hydrated, setHydrated] = useState(_hydrated);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
setHydrated(true);
|
|
71
|
+
}, []);
|
|
72
|
+
return hydrated;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
5
75
|
//#region src/runtime.ts
|
|
6
76
|
const SAFE_METHODS = new Set(["GET", "HEAD"]);
|
|
7
77
|
const HYDRATION_STATE_ELEMENT_ID = "pracht-state";
|
|
78
|
+
const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
|
|
79
|
+
const ROUTE_STATE_CACHE_CONTROL = "no-store";
|
|
8
80
|
const RouteDataContext = createContext(void 0);
|
|
9
81
|
function PrachtRuntimeProvider({ children, data, params = {}, routeId, url }) {
|
|
10
82
|
const [routeData, setRouteData] = useState(data);
|
|
@@ -109,7 +181,7 @@ function Form(props) {
|
|
|
109
181
|
async function fetchPrachtRouteState(url) {
|
|
110
182
|
const response = await fetch(url, {
|
|
111
183
|
headers: {
|
|
112
|
-
|
|
184
|
+
[ROUTE_STATE_REQUEST_HEADER]: "1",
|
|
113
185
|
"Cache-Control": "no-cache"
|
|
114
186
|
},
|
|
115
187
|
redirect: "manual"
|
|
@@ -134,6 +206,8 @@ async function fetchPrachtRouteState(url) {
|
|
|
134
206
|
async function handlePrachtRequest(options) {
|
|
135
207
|
const url = new URL(options.request.url);
|
|
136
208
|
const registry = options.registry ?? {};
|
|
209
|
+
const isRouteStateRequest = options.request.headers.get(ROUTE_STATE_REQUEST_HEADER) === "1";
|
|
210
|
+
const exposeDiagnostics = shouldExposeServerErrors(options);
|
|
137
211
|
if (options.apiRoutes?.length) {
|
|
138
212
|
const apiMatch = matchApiRoute(options.apiRoutes, url.pathname);
|
|
139
213
|
if (apiMatch) {
|
|
@@ -141,96 +215,145 @@ async function handlePrachtRequest(options) {
|
|
|
141
215
|
const middlewareFile = options.app.middleware[name];
|
|
142
216
|
return middlewareFile ? [middlewareFile] : [];
|
|
143
217
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
218
|
+
let currentPhase = "middleware";
|
|
219
|
+
try {
|
|
220
|
+
const middlewareResult = await runMiddlewareChain({
|
|
221
|
+
context: options.context ?? {},
|
|
222
|
+
middlewareFiles: apiMiddlewareFiles,
|
|
223
|
+
params: apiMatch.params,
|
|
224
|
+
registry,
|
|
225
|
+
request: options.request,
|
|
226
|
+
route: apiMatch.route,
|
|
227
|
+
url
|
|
228
|
+
});
|
|
229
|
+
if (middlewareResult.response) return middlewareResult.response;
|
|
230
|
+
currentPhase = "api";
|
|
231
|
+
const apiModule = await resolveRegistryModule(registry.apiModules, apiMatch.route.file);
|
|
232
|
+
if (!apiModule) throw new Error("API route module not found");
|
|
233
|
+
const handler = apiModule[options.request.method.toUpperCase()];
|
|
234
|
+
if (!handler) return withDefaultSecurityHeaders(new Response("Method not allowed", {
|
|
235
|
+
status: 405,
|
|
236
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
237
|
+
}));
|
|
238
|
+
return withDefaultSecurityHeaders(await handler({
|
|
239
|
+
request: options.request,
|
|
240
|
+
params: apiMatch.params,
|
|
241
|
+
context: middlewareResult.context,
|
|
242
|
+
signal: AbortSignal.timeout(3e4),
|
|
243
|
+
url,
|
|
244
|
+
route: apiMatch.route
|
|
245
|
+
}));
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return renderApiErrorResponse({
|
|
248
|
+
error,
|
|
249
|
+
middlewareFiles: apiMiddlewareFiles,
|
|
250
|
+
options,
|
|
251
|
+
phase: currentPhase,
|
|
252
|
+
route: apiMatch.route
|
|
253
|
+
});
|
|
254
|
+
}
|
|
172
255
|
}
|
|
173
256
|
}
|
|
174
257
|
const match = matchAppRoute(options.app, url.pathname);
|
|
175
|
-
if (!match)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
258
|
+
if (!match) {
|
|
259
|
+
if (isRouteStateRequest) return jsonErrorResponse(createSerializedRouteError("Not found", 404, {
|
|
260
|
+
diagnostics: exposeDiagnostics ? buildRuntimeDiagnostics({
|
|
261
|
+
phase: "match",
|
|
262
|
+
status: 404
|
|
263
|
+
}) : void 0,
|
|
264
|
+
name: "Error"
|
|
265
|
+
}), { isRouteStateRequest: true });
|
|
266
|
+
return withDefaultSecurityHeaders(new Response("Not found", {
|
|
267
|
+
status: 404,
|
|
268
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
if (!SAFE_METHODS.has(options.request.method)) {
|
|
272
|
+
if (isRouteStateRequest) return jsonErrorResponse(createSerializedRouteError("Method not allowed", 405, {
|
|
273
|
+
diagnostics: exposeDiagnostics ? buildRuntimeDiagnostics({
|
|
274
|
+
middlewareFiles: match.route.middlewareFiles,
|
|
275
|
+
phase: "action",
|
|
276
|
+
route: match.route,
|
|
277
|
+
shellFile: match.route.shellFile,
|
|
278
|
+
status: 405
|
|
279
|
+
}) : void 0,
|
|
280
|
+
name: "Error"
|
|
281
|
+
}), { isRouteStateRequest: true });
|
|
282
|
+
return withRouteResponseHeaders(new Response("Method not allowed", {
|
|
283
|
+
status: 405,
|
|
284
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
285
|
+
}), { isRouteStateRequest });
|
|
286
|
+
}
|
|
287
|
+
let routeArgs = {
|
|
197
288
|
request: options.request,
|
|
198
289
|
params: match.params,
|
|
199
|
-
context,
|
|
290
|
+
context: options.context ?? {},
|
|
200
291
|
signal: AbortSignal.timeout(3e4),
|
|
201
292
|
url,
|
|
202
293
|
route: match.route
|
|
203
294
|
};
|
|
204
|
-
|
|
295
|
+
let routeModule;
|
|
205
296
|
let shellModule;
|
|
297
|
+
let loaderFile;
|
|
298
|
+
let currentPhase = "middleware";
|
|
206
299
|
try {
|
|
300
|
+
const middlewareResult = await runMiddlewareChain({
|
|
301
|
+
context: routeArgs.context,
|
|
302
|
+
middlewareFiles: match.route.middlewareFiles,
|
|
303
|
+
params: match.params,
|
|
304
|
+
registry,
|
|
305
|
+
request: options.request,
|
|
306
|
+
route: match.route,
|
|
307
|
+
url
|
|
308
|
+
});
|
|
309
|
+
if (middlewareResult.response) return withRouteResponseHeaders(middlewareResult.response, { isRouteStateRequest });
|
|
310
|
+
routeArgs = {
|
|
311
|
+
...routeArgs,
|
|
312
|
+
context: middlewareResult.context
|
|
313
|
+
};
|
|
314
|
+
currentPhase = "render";
|
|
315
|
+
routeModule = await resolveRegistryModule(registry.routeModules, match.route.file);
|
|
316
|
+
if (!routeModule) throw new Error("Route module not found");
|
|
317
|
+
currentPhase = "loader";
|
|
318
|
+
const { loader, loaderFile: resolvedLoaderFile } = await resolveDataFunctions(match.route, routeModule, registry);
|
|
319
|
+
loaderFile = resolvedLoaderFile;
|
|
207
320
|
const loaderResult = loader ? await loader(routeArgs) : void 0;
|
|
208
|
-
if (loaderResult instanceof Response) return
|
|
321
|
+
if (loaderResult instanceof Response) return withRouteResponseHeaders(loaderResult, { isRouteStateRequest });
|
|
209
322
|
const data = loaderResult;
|
|
210
|
-
if (isRouteStateRequest) return
|
|
323
|
+
if (isRouteStateRequest) return withRouteResponseHeaders(Response.json({ data }), { isRouteStateRequest: true });
|
|
324
|
+
currentPhase = "render";
|
|
211
325
|
shellModule = match.route.shellFile ? await resolveRegistryModule(registry.shellModules, match.route.shellFile) : void 0;
|
|
212
326
|
const head = await mergeHeadMetadata(shellModule, routeModule, routeArgs, data);
|
|
213
327
|
const cssUrls = resolvePageCssUrls(options, match.route.shellFile, match.route.file);
|
|
214
328
|
const modulePreloadUrls = resolvePageJsUrls(options, match.route.shellFile, match.route.file);
|
|
215
|
-
if (match.route.render === "spa")
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
329
|
+
if (match.route.render === "spa") {
|
|
330
|
+
let body = "";
|
|
331
|
+
if (shellModule?.Shell || shellModule?.Loading) {
|
|
332
|
+
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
333
|
+
const Shell = shellModule?.Shell;
|
|
334
|
+
const Loading = shellModule?.Loading;
|
|
335
|
+
const loadingTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
|
|
336
|
+
if (loadingTree) body = await renderToStringAsync(loadingTree);
|
|
337
|
+
}
|
|
338
|
+
return htmlResponse(buildHtmlDocument({
|
|
339
|
+
head,
|
|
340
|
+
body,
|
|
341
|
+
hydrationState: {
|
|
342
|
+
url: url.pathname,
|
|
343
|
+
routeId: match.route.id ?? "",
|
|
344
|
+
data: null,
|
|
345
|
+
error: null,
|
|
346
|
+
pending: true
|
|
347
|
+
},
|
|
348
|
+
clientEntryUrl: options.clientEntryUrl,
|
|
349
|
+
cssUrls,
|
|
350
|
+
modulePreloadUrls
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
const DefaultComponent = typeof routeModule.default === "function" ? routeModule.default : void 0;
|
|
354
|
+
const Component = routeModule.Component ?? DefaultComponent;
|
|
355
|
+
if (!Component) throw new Error("Route has no Component or default export");
|
|
232
356
|
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
233
|
-
const Component = routeModule.Component;
|
|
234
357
|
const Shell = shellModule?.Shell;
|
|
235
358
|
const componentProps = {
|
|
236
359
|
data,
|
|
@@ -259,7 +382,9 @@ async function handlePrachtRequest(options) {
|
|
|
259
382
|
return renderRouteErrorResponse({
|
|
260
383
|
error,
|
|
261
384
|
isRouteStateRequest,
|
|
385
|
+
loaderFile,
|
|
262
386
|
options,
|
|
387
|
+
phase: currentPhase,
|
|
263
388
|
routeArgs,
|
|
264
389
|
routeId: match.route.id ?? "",
|
|
265
390
|
routeModule,
|
|
@@ -314,15 +439,65 @@ async function navigateToClientLocation(location, options) {
|
|
|
314
439
|
function isPrachtHttpError(error) {
|
|
315
440
|
return error instanceof Error && error.name === "PrachtHttpError" && "status" in error;
|
|
316
441
|
}
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
442
|
+
function shouldExposeServerErrors(options) {
|
|
443
|
+
return options.debugErrors === true;
|
|
444
|
+
}
|
|
445
|
+
function createSerializedRouteError(message, status, options = {}) {
|
|
446
|
+
return {
|
|
447
|
+
message,
|
|
448
|
+
name: options.name ?? "Error",
|
|
449
|
+
status,
|
|
450
|
+
...options.diagnostics ? { diagnostics: options.diagnostics } : {}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function buildRuntimeDiagnostics(options) {
|
|
454
|
+
const route = options.route;
|
|
455
|
+
const routeId = route && "id" in route ? route.id : void 0;
|
|
456
|
+
return {
|
|
457
|
+
phase: options.phase,
|
|
458
|
+
routeId,
|
|
459
|
+
routePath: route?.path,
|
|
460
|
+
routeFile: route?.file,
|
|
461
|
+
loaderFile: options.loaderFile,
|
|
462
|
+
shellFile: options.shellFile,
|
|
463
|
+
middlewareFiles: options.middlewareFiles ? [...options.middlewareFiles] : [],
|
|
464
|
+
status: options.status
|
|
322
465
|
};
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
466
|
+
}
|
|
467
|
+
function normalizeRouteError(error, options) {
|
|
468
|
+
if (isPrachtHttpError(error)) {
|
|
469
|
+
const status = typeof error.status === "number" ? error.status : 500;
|
|
470
|
+
if (status >= 400 && status < 500) return {
|
|
471
|
+
message: error.message,
|
|
472
|
+
name: error.name,
|
|
473
|
+
status
|
|
474
|
+
};
|
|
475
|
+
if (options.exposeDetails) return {
|
|
476
|
+
message: error.message || "Internal Server Error",
|
|
477
|
+
name: error.name || "Error",
|
|
478
|
+
status
|
|
479
|
+
};
|
|
480
|
+
return {
|
|
481
|
+
message: "Internal Server Error",
|
|
482
|
+
name: "Error",
|
|
483
|
+
status
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (error instanceof Error) {
|
|
487
|
+
if (options.exposeDetails) return {
|
|
488
|
+
message: error.message || "Internal Server Error",
|
|
489
|
+
name: error.name || "Error",
|
|
490
|
+
status: 500
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
message: "Internal Server Error",
|
|
494
|
+
name: "Error",
|
|
495
|
+
status: 500
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (options.exposeDetails) return {
|
|
499
|
+
message: typeof error === "string" && error ? error : "Internal Server Error",
|
|
500
|
+
name: "Error",
|
|
326
501
|
status: 500
|
|
327
502
|
};
|
|
328
503
|
return {
|
|
@@ -335,25 +510,58 @@ function deserializeRouteError$1(error) {
|
|
|
335
510
|
const result = new Error(error.message);
|
|
336
511
|
result.name = error.name;
|
|
337
512
|
result.status = error.status;
|
|
513
|
+
result.diagnostics = error.diagnostics;
|
|
338
514
|
return result;
|
|
339
515
|
}
|
|
516
|
+
function jsonErrorResponse(routeError, options) {
|
|
517
|
+
const response = new Response(JSON.stringify({ error: routeError }), {
|
|
518
|
+
status: routeError.status,
|
|
519
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
520
|
+
});
|
|
521
|
+
return options.isRouteStateRequest ? withRouteResponseHeaders(response, { isRouteStateRequest: true }) : withDefaultSecurityHeaders(response);
|
|
522
|
+
}
|
|
523
|
+
function renderApiErrorResponse(options) {
|
|
524
|
+
const exposeDetails = shouldExposeServerErrors(options.options);
|
|
525
|
+
const routeError = normalizeRouteError(options.error, { exposeDetails });
|
|
526
|
+
const routeErrorWithDiagnostics = exposeDetails ? {
|
|
527
|
+
...routeError,
|
|
528
|
+
diagnostics: buildRuntimeDiagnostics({
|
|
529
|
+
middlewareFiles: options.middlewareFiles,
|
|
530
|
+
phase: options.phase,
|
|
531
|
+
route: options.route,
|
|
532
|
+
status: routeError.status
|
|
533
|
+
})
|
|
534
|
+
} : routeError;
|
|
535
|
+
if (exposeDetails) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: false });
|
|
536
|
+
const message = routeErrorWithDiagnostics.status >= 500 ? "Internal Server Error" : routeErrorWithDiagnostics.message;
|
|
537
|
+
return withDefaultSecurityHeaders(new Response(message, {
|
|
538
|
+
status: routeErrorWithDiagnostics.status,
|
|
539
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
340
542
|
async function renderRouteErrorResponse(options) {
|
|
341
|
-
const
|
|
543
|
+
const exposeDetails = shouldExposeServerErrors(options.options);
|
|
544
|
+
const routeError = normalizeRouteError(options.error, { exposeDetails });
|
|
545
|
+
const routeErrorWithDiagnostics = exposeDetails ? {
|
|
546
|
+
...routeError,
|
|
547
|
+
diagnostics: buildRuntimeDiagnostics({
|
|
548
|
+
loaderFile: options.loaderFile,
|
|
549
|
+
middlewareFiles: options.routeArgs.route.middlewareFiles,
|
|
550
|
+
phase: options.phase,
|
|
551
|
+
route: options.routeArgs.route,
|
|
552
|
+
shellFile: options.shellFile,
|
|
553
|
+
status: routeError.status
|
|
554
|
+
})
|
|
555
|
+
} : routeError;
|
|
342
556
|
if (!options.routeModule?.ErrorBoundary) {
|
|
343
|
-
if (options.isRouteStateRequest) return
|
|
344
|
-
|
|
345
|
-
headers: { "content-type": "application/json; charset=utf-8" }
|
|
346
|
-
}));
|
|
347
|
-
const message = routeError.status >= 500 ? "Internal Server Error" : routeError.message;
|
|
557
|
+
if (options.isRouteStateRequest) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: true });
|
|
558
|
+
const message = routeErrorWithDiagnostics.status >= 500 ? "Internal Server Error" : routeErrorWithDiagnostics.message;
|
|
348
559
|
return withDefaultSecurityHeaders(new Response(message, {
|
|
349
|
-
status:
|
|
560
|
+
status: routeErrorWithDiagnostics.status,
|
|
350
561
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
351
562
|
}));
|
|
352
563
|
}
|
|
353
|
-
if (options.isRouteStateRequest) return
|
|
354
|
-
status: routeError.status,
|
|
355
|
-
headers: { "content-type": "application/json; charset=utf-8" }
|
|
356
|
-
}));
|
|
564
|
+
if (options.isRouteStateRequest) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: true });
|
|
357
565
|
const shellModule = options.shellModule ?? (options.shellFile ? await resolveRegistryModule(options.options.registry?.shellModules, options.shellFile) : void 0);
|
|
358
566
|
const head = shellModule?.head ? await shellModule.head(options.routeArgs) : {};
|
|
359
567
|
const cssUrls = resolvePageCssUrls(options.options, options.shellFile, options.routeArgs.route.file);
|
|
@@ -361,7 +569,7 @@ async function renderRouteErrorResponse(options) {
|
|
|
361
569
|
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
362
570
|
const ErrorBoundary = options.routeModule.ErrorBoundary;
|
|
363
571
|
const Shell = shellModule?.Shell;
|
|
364
|
-
const errorValue = deserializeRouteError$1(
|
|
572
|
+
const errorValue = deserializeRouteError$1(routeErrorWithDiagnostics);
|
|
365
573
|
const componentTree = Shell ? h(Shell, null, h(ErrorBoundary, { error: errorValue })) : h(ErrorBoundary, { error: errorValue });
|
|
366
574
|
return htmlResponse(buildHtmlDocument({
|
|
367
575
|
head,
|
|
@@ -374,12 +582,12 @@ async function renderRouteErrorResponse(options) {
|
|
|
374
582
|
url: options.urlPathname,
|
|
375
583
|
routeId: options.routeId,
|
|
376
584
|
data: null,
|
|
377
|
-
error:
|
|
585
|
+
error: routeErrorWithDiagnostics
|
|
378
586
|
},
|
|
379
587
|
clientEntryUrl: options.options.clientEntryUrl,
|
|
380
588
|
cssUrls,
|
|
381
589
|
modulePreloadUrls
|
|
382
|
-
}),
|
|
590
|
+
}), routeErrorWithDiagnostics.status);
|
|
383
591
|
}
|
|
384
592
|
async function runMiddlewareChain(options) {
|
|
385
593
|
let context = options.context;
|
|
@@ -409,11 +617,18 @@ async function runMiddlewareChain(options) {
|
|
|
409
617
|
}
|
|
410
618
|
async function resolveDataFunctions(route, routeModule, registry) {
|
|
411
619
|
let loader = routeModule?.loader;
|
|
620
|
+
let loaderFile = routeModule?.loader ? route.file : void 0;
|
|
412
621
|
if (route.loaderFile) {
|
|
413
622
|
const dataModule = await resolveRegistryModule(registry.dataModules, route.loaderFile);
|
|
414
|
-
if (dataModule?.loader)
|
|
623
|
+
if (dataModule?.loader) {
|
|
624
|
+
loader = dataModule.loader;
|
|
625
|
+
loaderFile = route.loaderFile;
|
|
626
|
+
}
|
|
415
627
|
}
|
|
416
|
-
return {
|
|
628
|
+
return {
|
|
629
|
+
loader,
|
|
630
|
+
loaderFile
|
|
631
|
+
};
|
|
417
632
|
}
|
|
418
633
|
async function resolveRegistryModule(modules, file) {
|
|
419
634
|
if (!modules) return void 0;
|
|
@@ -461,10 +676,10 @@ function buildHtmlDocument(options) {
|
|
|
461
676
|
</html>`;
|
|
462
677
|
}
|
|
463
678
|
function htmlResponse(html, status = 200) {
|
|
464
|
-
return
|
|
679
|
+
return withRouteResponseHeaders(new Response(html, {
|
|
465
680
|
status,
|
|
466
681
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
467
|
-
}));
|
|
682
|
+
}), { isRouteStateRequest: false });
|
|
468
683
|
}
|
|
469
684
|
function applyDefaultSecurityHeaders(headers) {
|
|
470
685
|
if (!headers.has("permissions-policy")) headers.set("permissions-policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
|
|
@@ -481,6 +696,26 @@ function withDefaultSecurityHeaders(response) {
|
|
|
481
696
|
headers
|
|
482
697
|
});
|
|
483
698
|
}
|
|
699
|
+
function withRouteResponseHeaders(response, options) {
|
|
700
|
+
const headers = applyDefaultSecurityHeaders(new Headers(response.headers));
|
|
701
|
+
appendVaryHeader(headers, ROUTE_STATE_REQUEST_HEADER);
|
|
702
|
+
if (options.isRouteStateRequest && !headers.has("cache-control")) headers.set("cache-control", ROUTE_STATE_CACHE_CONTROL);
|
|
703
|
+
return new Response(response.body, {
|
|
704
|
+
status: response.status,
|
|
705
|
+
statusText: response.statusText,
|
|
706
|
+
headers
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
function appendVaryHeader(headers, value) {
|
|
710
|
+
const current = headers.get("vary");
|
|
711
|
+
if (!current) {
|
|
712
|
+
headers.set("vary", value);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const values = current.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
716
|
+
if (values.includes("*") || values.includes(value.toLowerCase())) return;
|
|
717
|
+
headers.set("vary", `${current}, ${value}`);
|
|
718
|
+
}
|
|
484
719
|
function escapeHtml(str) {
|
|
485
720
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
486
721
|
}
|
|
@@ -488,7 +723,7 @@ function serializeJsonForHtml(value) {
|
|
|
488
723
|
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
489
724
|
}
|
|
490
725
|
async function prerenderApp(options) {
|
|
491
|
-
const { resolveApp } = await import("./app-
|
|
726
|
+
const { resolveApp } = await import("./app-Cep0el7c.mjs").then((n) => n.t);
|
|
492
727
|
const resolved = resolveApp(options.app);
|
|
493
728
|
const results = [];
|
|
494
729
|
const isgManifest = {};
|
|
@@ -530,7 +765,7 @@ async function collectSSGPaths(route, registry) {
|
|
|
530
765
|
console.warn(` Warning: SSG route "${route.path}" has dynamic segments but no getStaticPaths() export, skipping.`);
|
|
531
766
|
return [];
|
|
532
767
|
}
|
|
533
|
-
const { buildPathFromSegments } = await import("./app-
|
|
768
|
+
const { buildPathFromSegments } = await import("./app-Cep0el7c.mjs").then((n) => n.t);
|
|
534
769
|
return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
|
|
535
770
|
}
|
|
536
771
|
//#endregion
|
|
@@ -556,7 +791,7 @@ function prefetchRouteState(url) {
|
|
|
556
791
|
});
|
|
557
792
|
return promise;
|
|
558
793
|
}
|
|
559
|
-
function setupPrefetching(app) {
|
|
794
|
+
function setupPrefetching(app, warmModules) {
|
|
560
795
|
let hoverTimer = null;
|
|
561
796
|
function getInternalHref(anchor) {
|
|
562
797
|
const href = anchor.getAttribute("href");
|
|
@@ -587,6 +822,10 @@ function setupPrefetching(app) {
|
|
|
587
822
|
if (hoverTimer) clearTimeout(hoverTimer);
|
|
588
823
|
hoverTimer = setTimeout(() => {
|
|
589
824
|
prefetchRouteState(href);
|
|
825
|
+
if (warmModules) {
|
|
826
|
+
const m = matchAppRoute(app, href);
|
|
827
|
+
if (m) warmModules(m);
|
|
828
|
+
}
|
|
590
829
|
}, 50);
|
|
591
830
|
}, true);
|
|
592
831
|
document.addEventListener("mouseleave", (e) => {
|
|
@@ -604,6 +843,10 @@ function setupPrefetching(app) {
|
|
|
604
843
|
const strategy = getPrefetchStrategy(href);
|
|
605
844
|
if (strategy !== "hover" && strategy !== "intent") return;
|
|
606
845
|
prefetchRouteState(href);
|
|
846
|
+
if (warmModules) {
|
|
847
|
+
const m = matchAppRoute(app, href);
|
|
848
|
+
if (m) warmModules(m);
|
|
849
|
+
}
|
|
607
850
|
}, true);
|
|
608
851
|
if (typeof IntersectionObserver === "undefined") return;
|
|
609
852
|
const observer = new IntersectionObserver((entries) => {
|
|
@@ -613,6 +856,10 @@ function setupPrefetching(app) {
|
|
|
613
856
|
const href = getInternalHref(anchor);
|
|
614
857
|
if (!href) continue;
|
|
615
858
|
prefetchRouteState(href);
|
|
859
|
+
if (warmModules) {
|
|
860
|
+
const m = matchAppRoute(app, href);
|
|
861
|
+
if (m) warmModules(m);
|
|
862
|
+
}
|
|
616
863
|
observer.unobserve(anchor);
|
|
617
864
|
}
|
|
618
865
|
}, { rootMargin: "200px" });
|
|
@@ -641,16 +888,34 @@ function useNavigate() {
|
|
|
641
888
|
}
|
|
642
889
|
async function initClientRouter(options) {
|
|
643
890
|
const { app, routeModules, shellModules, root, findModuleKey } = options;
|
|
644
|
-
|
|
891
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
892
|
+
function loadModule(modules, key) {
|
|
893
|
+
let cached = moduleCache.get(key);
|
|
894
|
+
if (!cached) {
|
|
895
|
+
cached = modules[key]();
|
|
896
|
+
moduleCache.set(key, cached);
|
|
897
|
+
}
|
|
898
|
+
return cached;
|
|
899
|
+
}
|
|
900
|
+
function startRouteImport(match) {
|
|
645
901
|
const routeKey = findModuleKey(routeModules, match.route.file);
|
|
646
902
|
if (!routeKey) return null;
|
|
647
|
-
|
|
903
|
+
return loadModule(routeModules, routeKey);
|
|
904
|
+
}
|
|
905
|
+
function startShellImport(match) {
|
|
906
|
+
if (!match.route.shellFile) return null;
|
|
907
|
+
const shellKey = findModuleKey(shellModules, match.route.shellFile);
|
|
908
|
+
if (!shellKey) return null;
|
|
909
|
+
return loadModule(shellModules, shellKey);
|
|
910
|
+
}
|
|
911
|
+
async function buildRouteTree(match, state, routeModPromise, shellModPromise) {
|
|
912
|
+
const routeMod = await (routeModPromise ?? startRouteImport(match));
|
|
913
|
+
if (!routeMod) return null;
|
|
648
914
|
let Shell = null;
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const Component = state.error ? routeMod.ErrorBoundary : routeMod.Component;
|
|
915
|
+
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
916
|
+
if (resolvedShell) Shell = resolvedShell.Shell;
|
|
917
|
+
const DefaultComponent = typeof routeMod.default === "function" ? routeMod.default : void 0;
|
|
918
|
+
const Component = state.error ? routeMod.ErrorBoundary : routeMod.Component ?? DefaultComponent;
|
|
654
919
|
if (!Component) return null;
|
|
655
920
|
const props = state.error ? { error: deserializeRouteError(state.error) } : {
|
|
656
921
|
data: state.data,
|
|
@@ -664,18 +929,35 @@ async function initClientRouter(options) {
|
|
|
664
929
|
url: match.pathname
|
|
665
930
|
}, componentTree));
|
|
666
931
|
}
|
|
932
|
+
async function buildSpaPendingTree(match, shellModPromise) {
|
|
933
|
+
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
934
|
+
if (!resolvedShell) return null;
|
|
935
|
+
const Shell = resolvedShell.Shell;
|
|
936
|
+
const Loading = resolvedShell.Loading;
|
|
937
|
+
const componentTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
|
|
938
|
+
if (!componentTree) return null;
|
|
939
|
+
return h(NavigateContext.Provider, { value: navigate }, h(PrachtRuntimeProvider, {
|
|
940
|
+
data: void 0,
|
|
941
|
+
params: match.params,
|
|
942
|
+
routeId: match.route.id ?? "",
|
|
943
|
+
url: match.pathname
|
|
944
|
+
}, componentTree));
|
|
945
|
+
}
|
|
667
946
|
async function navigate(to, opts) {
|
|
668
947
|
const match = matchAppRoute(app, to);
|
|
669
948
|
if (!match) {
|
|
670
949
|
window.location.href = to;
|
|
671
950
|
return;
|
|
672
951
|
}
|
|
952
|
+
const statePromise = getCachedRouteState(to) ?? fetchPrachtRouteState(to);
|
|
953
|
+
const routeModPromise = startRouteImport(match);
|
|
954
|
+
const shellModPromise = startShellImport(match);
|
|
673
955
|
let state = {
|
|
674
956
|
data: void 0,
|
|
675
957
|
error: null
|
|
676
958
|
};
|
|
677
959
|
try {
|
|
678
|
-
const result = await
|
|
960
|
+
const result = await statePromise;
|
|
679
961
|
if (result.type === "redirect") {
|
|
680
962
|
if (result.location) {
|
|
681
963
|
await navigate(result.location, opts);
|
|
@@ -698,7 +980,7 @@ async function initClientRouter(options) {
|
|
|
698
980
|
}
|
|
699
981
|
if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "", to);
|
|
700
982
|
else history.pushState(null, "", to);
|
|
701
|
-
const tree = await buildRouteTree(match, state);
|
|
983
|
+
const tree = await buildRouteTree(match, state, routeModPromise, shellModPromise);
|
|
702
984
|
if (tree) {
|
|
703
985
|
render(tree, root);
|
|
704
986
|
window.scrollTo(0, 0);
|
|
@@ -715,31 +997,40 @@ async function initClientRouter(options) {
|
|
|
715
997
|
}
|
|
716
998
|
const initialMatch = matchAppRoute(app, options.initialState.url);
|
|
717
999
|
if (initialMatch) {
|
|
1000
|
+
const initialShellPromise = initialMatch.route.render === "spa" && options.initialState.pending ? startShellImport(initialMatch) : null;
|
|
718
1001
|
let state = {
|
|
719
1002
|
data: options.initialState.data,
|
|
720
1003
|
error: options.initialState.error ?? null
|
|
721
1004
|
};
|
|
722
|
-
if (initialMatch.route.render === "spa" &&
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
1005
|
+
if (initialMatch.route.render === "spa" && options.initialState.pending) {
|
|
1006
|
+
const dataPromise = fetchPrachtRouteState(options.initialState.url);
|
|
1007
|
+
const pendingTree = await buildSpaPendingTree(initialMatch, initialShellPromise);
|
|
1008
|
+
if (pendingTree) hydrate(pendingTree, root);
|
|
1009
|
+
try {
|
|
1010
|
+
const result = await dataPromise;
|
|
1011
|
+
if (result.type === "redirect") {
|
|
1012
|
+
window.location.href = result.location;
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (result.type === "error") state = {
|
|
1016
|
+
data: void 0,
|
|
1017
|
+
error: result.error
|
|
1018
|
+
};
|
|
1019
|
+
else state = {
|
|
1020
|
+
data: result.data,
|
|
1021
|
+
error: null
|
|
1022
|
+
};
|
|
1023
|
+
} catch {
|
|
1024
|
+
window.location.href = options.initialState.url;
|
|
726
1025
|
return;
|
|
727
1026
|
}
|
|
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
1027
|
}
|
|
740
|
-
const tree = await buildRouteTree(initialMatch, state);
|
|
1028
|
+
const tree = await buildRouteTree(initialMatch, state, void 0, initialShellPromise);
|
|
741
1029
|
if (tree) if (initialMatch.route.render === "spa") render(tree, root);
|
|
742
|
-
else
|
|
1030
|
+
else {
|
|
1031
|
+
markHydrating();
|
|
1032
|
+
hydrate(tree, root);
|
|
1033
|
+
}
|
|
743
1034
|
}
|
|
744
1035
|
document.addEventListener("click", (e) => {
|
|
745
1036
|
const anchor = e.target.closest?.("a");
|
|
@@ -767,7 +1058,11 @@ async function initClientRouter(options) {
|
|
|
767
1058
|
});
|
|
768
1059
|
window.__PRACHT_NAVIGATE__ = navigate;
|
|
769
1060
|
window.__PRACHT_ROUTER_READY__ = true;
|
|
770
|
-
|
|
1061
|
+
const warmModules = (match) => {
|
|
1062
|
+
startRouteImport(match);
|
|
1063
|
+
startShellImport(match);
|
|
1064
|
+
};
|
|
1065
|
+
setupPrefetching(app, warmModules);
|
|
771
1066
|
}
|
|
772
1067
|
const HYDRATION_BANNER_ID = "__pracht_hydration_warnings__";
|
|
773
1068
|
function appendHydrationWarning(message) {
|
|
@@ -821,6 +1116,7 @@ function deserializeRouteError(error) {
|
|
|
821
1116
|
const result = new Error(error.message);
|
|
822
1117
|
result.name = error.name;
|
|
823
1118
|
result.status = error.status;
|
|
1119
|
+
result.diagnostics = error.diagnostics;
|
|
824
1120
|
return result;
|
|
825
1121
|
}
|
|
826
1122
|
//#endregion
|
|
@@ -834,4 +1130,4 @@ var PrachtHttpError = class extends Error {
|
|
|
834
1130
|
}
|
|
835
1131
|
};
|
|
836
1132
|
//#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 };
|
|
1133
|
+
export { Form, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRevalidateRoute, useRouteData };
|
package/package.json
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pracht/core",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/framework",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/JoviDeCroock/pracht/issues"
|
|
7
|
+
},
|
|
4
8
|
"repository": {
|
|
5
9
|
"type": "git",
|
|
6
10
|
"url": "https://github.com/JoviDeCroock/pracht",
|
|
7
11
|
"directory": "packages/framework"
|
|
8
12
|
},
|
|
9
|
-
"homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/framework",
|
|
10
|
-
"bugs": {
|
|
11
|
-
"url": "https://github.com/JoviDeCroock/pracht/issues"
|
|
12
|
-
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
|
-
"publishConfig": {
|
|
18
|
-
"provenance": true
|
|
19
|
-
},
|
|
20
17
|
"exports": {
|
|
21
18
|
".": {
|
|
22
19
|
"types": "./dist/index.d.mts",
|
|
@@ -27,6 +24,9 @@
|
|
|
27
24
|
"default": "./dist/error-overlay.mjs"
|
|
28
25
|
}
|
|
29
26
|
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"provenance": true
|
|
29
|
+
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"preact-suspense": "^0.2.0"
|
|
32
32
|
},
|