@pracht/core 0.0.0 → 0.1.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 +52 -0
- package/dist/{app-CAoDWWNO.mjs → app-Cep0el7c.mjs} +21 -7
- package/dist/index.d.mts +51 -7
- package/dist/index.mjs +375 -129
- package/package.json +13 -1
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @pracht/core
|
|
2
|
+
|
|
3
|
+
Core routing, rendering, server/client runtime, and type utilities for pracht.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pracht/core preact preact-render-to-string
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### Route Manifest
|
|
14
|
+
|
|
15
|
+
- `defineApp()` — define the application and its route tree
|
|
16
|
+
- `route()` — declare a route with path, component, loader, and rendering mode
|
|
17
|
+
- `group()` — group routes under a shared shell or middleware
|
|
18
|
+
|
|
19
|
+
### Server
|
|
20
|
+
|
|
21
|
+
- `handlePrachtRequest()` — server renderer that produces full HTML with hydration markers
|
|
22
|
+
- `matchAppRoute()` — segment-based route matching
|
|
23
|
+
|
|
24
|
+
`handlePrachtRequest()` sanitizes unexpected 5xx errors by default so raw server
|
|
25
|
+
messages do not leak into SSR HTML or route-state JSON. Explicit
|
|
26
|
+
`PrachtHttpError` 4xx messages are preserved. Pass `debugErrors: true` to expose
|
|
27
|
+
raw details intentionally during debugging; `@pracht/core` does not infer this
|
|
28
|
+
from environment variables. Debug responses also attach `error.diagnostics`
|
|
29
|
+
metadata for the failure phase and matched framework files when available.
|
|
30
|
+
|
|
31
|
+
### Client
|
|
32
|
+
|
|
33
|
+
- `startApp()` — client-side hydration and runtime
|
|
34
|
+
- `useRouteData()` — access loader data inside a route component
|
|
35
|
+
- `useRevalidateRoute()` — trigger a revalidation of the current route's data
|
|
36
|
+
- `useSubmitAction()` — submit a form action programmatically
|
|
37
|
+
- `<Form>` — progressive enhancement form component
|
|
38
|
+
|
|
39
|
+
### Types
|
|
40
|
+
|
|
41
|
+
- `LoaderData<T>` — infer the return type of a loader
|
|
42
|
+
- `RouteComponentProps<T>` — props type for route components
|
|
43
|
+
- `LoaderArgs` — argument type passed to loaders and actions
|
|
44
|
+
|
|
45
|
+
## Rendering Modes
|
|
46
|
+
|
|
47
|
+
Each route can specify its rendering mode:
|
|
48
|
+
|
|
49
|
+
- `ssr` — server-rendered on every request
|
|
50
|
+
- `ssg` — pre-rendered at build time
|
|
51
|
+
- `isg` — pre-rendered with time-based revalidation
|
|
52
|
+
- `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
|
}
|
|
@@ -169,6 +175,7 @@ interface RouteModule<TContext = any, TLoader extends LoaderLike = undefined> {
|
|
|
169
175
|
}
|
|
170
176
|
interface ShellModule<TContext = any> {
|
|
171
177
|
Shell: FunctionComponent<ShellProps>;
|
|
178
|
+
Loading?: FunctionComponent;
|
|
172
179
|
head?: (args: BaseRouteArgs<TContext>) => MaybePromise<HeadMetadata>;
|
|
173
180
|
}
|
|
174
181
|
type MiddlewareResult<TContext = any> = void | Response | {
|
|
@@ -198,7 +205,7 @@ declare class PrachtHttpError extends Error {
|
|
|
198
205
|
//#endregion
|
|
199
206
|
//#region src/app.d.ts
|
|
200
207
|
declare function timeRevalidate(seconds: number): TimeRevalidatePolicy;
|
|
201
|
-
declare function route(path: string, file:
|
|
208
|
+
declare function route(path: string, file: ModuleRef, meta?: RouteMeta): RouteDefinition;
|
|
202
209
|
declare function route(path: string, config: RouteConfig): RouteDefinition;
|
|
203
210
|
declare function group(meta: GroupMeta, routes: RouteTreeNode[]): GroupDefinition;
|
|
204
211
|
declare function defineApp(config: PrachtAppConfig): PrachtApp;
|
|
@@ -215,17 +222,42 @@ declare function buildPathFromSegments(segments: RouteSegment[], params: RoutePa
|
|
|
215
222
|
declare function resolveApiRoutes(files: string[], apiDir?: string): ResolvedApiRoute[];
|
|
216
223
|
declare function matchApiRoute(apiRoutes: ResolvedApiRoute[], pathname: string): ApiRouteMatch | undefined;
|
|
217
224
|
//#endregion
|
|
225
|
+
//#region src/forwardRef.d.ts
|
|
226
|
+
/**
|
|
227
|
+
* Pass ref down to a child. This is mainly used in libraries with HOCs that
|
|
228
|
+
* wrap components. Using `forwardRef` there is an easy way to get a reference
|
|
229
|
+
* of the wrapped component instead of one of the wrapper itself.
|
|
230
|
+
*/
|
|
231
|
+
declare function forwardRef<P = {}>(fn: ((props: P, ref: any) => any) & {
|
|
232
|
+
displayName?: string;
|
|
233
|
+
}): FunctionComponent<P & {
|
|
234
|
+
ref?: any;
|
|
235
|
+
}>;
|
|
236
|
+
//#endregion
|
|
218
237
|
//#region src/runtime.d.ts
|
|
238
|
+
type PrachtRuntimeDiagnosticPhase = "match" | "middleware" | "loader" | "action" | "render" | "api";
|
|
239
|
+
interface PrachtRuntimeDiagnostics {
|
|
240
|
+
phase: PrachtRuntimeDiagnosticPhase;
|
|
241
|
+
routeId?: string;
|
|
242
|
+
routePath?: string;
|
|
243
|
+
routeFile?: string;
|
|
244
|
+
loaderFile?: string;
|
|
245
|
+
shellFile?: string;
|
|
246
|
+
middlewareFiles?: string[];
|
|
247
|
+
status: number;
|
|
248
|
+
}
|
|
219
249
|
interface PrachtHydrationState<TData = unknown> {
|
|
220
250
|
url: string;
|
|
221
251
|
routeId: string;
|
|
222
252
|
data: TData;
|
|
223
253
|
error?: SerializedRouteError | null;
|
|
254
|
+
pending?: boolean;
|
|
224
255
|
}
|
|
225
256
|
interface SerializedRouteError {
|
|
226
257
|
message: string;
|
|
227
258
|
name: string;
|
|
228
259
|
status: number;
|
|
260
|
+
diagnostics?: PrachtRuntimeDiagnostics;
|
|
229
261
|
}
|
|
230
262
|
interface StartAppOptions<TData = unknown> {
|
|
231
263
|
initialData?: TData;
|
|
@@ -235,6 +267,8 @@ interface HandlePrachtRequestOptions<TContext = unknown> {
|
|
|
235
267
|
request: Request;
|
|
236
268
|
context?: TContext;
|
|
237
269
|
registry?: ModuleRegistry;
|
|
270
|
+
/** Expose raw server error details in rendered HTML and route-state JSON. */
|
|
271
|
+
debugErrors?: boolean;
|
|
238
272
|
clientEntryUrl?: string;
|
|
239
273
|
/** Per-source-file CSS map produced by the vite plugin (preferred over cssUrls). */
|
|
240
274
|
cssManifest?: Record<string, string[]>;
|
|
@@ -291,6 +325,16 @@ declare function useRevalidate(): () => Promise<unknown>;
|
|
|
291
325
|
/** @deprecated Use useRevalidate instead. */
|
|
292
326
|
declare const useRevalidateRoute: typeof useRevalidate;
|
|
293
327
|
declare function Form(props: FormProps): _$preact.VNode<_$preact.ClassAttributes<HTMLFormElement> & h.JSX.HTMLAttributes<HTMLFormElement>>;
|
|
328
|
+
type RouteStateResult = {
|
|
329
|
+
type: "data";
|
|
330
|
+
data: unknown;
|
|
331
|
+
} | {
|
|
332
|
+
type: "redirect";
|
|
333
|
+
location: string;
|
|
334
|
+
} | {
|
|
335
|
+
type: "error";
|
|
336
|
+
error: SerializedRouteError;
|
|
337
|
+
};
|
|
294
338
|
declare function handlePrachtRequest<TContext>(options: HandlePrachtRequestOptions<TContext>): Promise<Response>;
|
|
295
339
|
declare function applyDefaultSecurityHeaders(headers: Headers): Headers;
|
|
296
340
|
interface PrerenderResult {
|
|
@@ -340,4 +384,4 @@ interface InitClientRouterOptions {
|
|
|
340
384
|
}
|
|
341
385
|
declare function initClientRouter(options: InitClientRouterOptions): Promise<void>;
|
|
342
386
|
//#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 };
|
|
387
|
+
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, useLocation, useNavigate, useParams, useRevalidate, useRevalidateRoute, useRouteData };
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,37 @@
|
|
|
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-
|
|
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";
|
|
2
3
|
import { Suspense, lazy } from "preact-suspense";
|
|
3
|
-
import { createContext, h, hydrate, render } from "preact";
|
|
4
4
|
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
|
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
|
|
5
30
|
//#region src/runtime.ts
|
|
6
31
|
const SAFE_METHODS = new Set(["GET", "HEAD"]);
|
|
7
32
|
const HYDRATION_STATE_ELEMENT_ID = "pracht-state";
|
|
33
|
+
const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
|
|
34
|
+
const ROUTE_STATE_CACHE_CONTROL = "no-store";
|
|
8
35
|
const RouteDataContext = createContext(void 0);
|
|
9
36
|
function PrachtRuntimeProvider({ children, data, params = {}, routeId, url }) {
|
|
10
37
|
const [routeData, setRouteData] = useState(data);
|
|
@@ -109,7 +136,7 @@ function Form(props) {
|
|
|
109
136
|
async function fetchPrachtRouteState(url) {
|
|
110
137
|
const response = await fetch(url, {
|
|
111
138
|
headers: {
|
|
112
|
-
|
|
139
|
+
[ROUTE_STATE_REQUEST_HEADER]: "1",
|
|
113
140
|
"Cache-Control": "no-cache"
|
|
114
141
|
},
|
|
115
142
|
redirect: "manual"
|
|
@@ -134,6 +161,8 @@ async function fetchPrachtRouteState(url) {
|
|
|
134
161
|
async function handlePrachtRequest(options) {
|
|
135
162
|
const url = new URL(options.request.url);
|
|
136
163
|
const registry = options.registry ?? {};
|
|
164
|
+
const isRouteStateRequest = options.request.headers.get(ROUTE_STATE_REQUEST_HEADER) === "1";
|
|
165
|
+
const exposeDiagnostics = shouldExposeServerErrors(options);
|
|
137
166
|
if (options.apiRoutes?.length) {
|
|
138
167
|
const apiMatch = matchApiRoute(options.apiRoutes, url.pathname);
|
|
139
168
|
if (apiMatch) {
|
|
@@ -141,94 +170,142 @@ async function handlePrachtRequest(options) {
|
|
|
141
170
|
const middlewareFile = options.app.middleware[name];
|
|
142
171
|
return middlewareFile ? [middlewareFile] : [];
|
|
143
172
|
});
|
|
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
|
-
|
|
173
|
+
let currentPhase = "middleware";
|
|
174
|
+
try {
|
|
175
|
+
const middlewareResult = await runMiddlewareChain({
|
|
176
|
+
context: options.context ?? {},
|
|
177
|
+
middlewareFiles: apiMiddlewareFiles,
|
|
178
|
+
params: apiMatch.params,
|
|
179
|
+
registry,
|
|
180
|
+
request: options.request,
|
|
181
|
+
route: apiMatch.route,
|
|
182
|
+
url
|
|
183
|
+
});
|
|
184
|
+
if (middlewareResult.response) return middlewareResult.response;
|
|
185
|
+
currentPhase = "api";
|
|
186
|
+
const apiModule = await resolveRegistryModule(registry.apiModules, apiMatch.route.file);
|
|
187
|
+
if (!apiModule) throw new Error("API route module not found");
|
|
188
|
+
const handler = apiModule[options.request.method.toUpperCase()];
|
|
189
|
+
if (!handler) return withDefaultSecurityHeaders(new Response("Method not allowed", {
|
|
190
|
+
status: 405,
|
|
191
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
192
|
+
}));
|
|
193
|
+
return withDefaultSecurityHeaders(await handler({
|
|
194
|
+
request: options.request,
|
|
195
|
+
params: apiMatch.params,
|
|
196
|
+
context: middlewareResult.context,
|
|
197
|
+
signal: AbortSignal.timeout(3e4),
|
|
198
|
+
url,
|
|
199
|
+
route: apiMatch.route
|
|
200
|
+
}));
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return renderApiErrorResponse({
|
|
203
|
+
error,
|
|
204
|
+
middlewareFiles: apiMiddlewareFiles,
|
|
205
|
+
options,
|
|
206
|
+
phase: currentPhase,
|
|
207
|
+
route: apiMatch.route
|
|
208
|
+
});
|
|
209
|
+
}
|
|
172
210
|
}
|
|
173
211
|
}
|
|
174
212
|
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
|
-
|
|
213
|
+
if (!match) {
|
|
214
|
+
if (isRouteStateRequest) return jsonErrorResponse(createSerializedRouteError("Not found", 404, {
|
|
215
|
+
diagnostics: exposeDiagnostics ? buildRuntimeDiagnostics({
|
|
216
|
+
phase: "match",
|
|
217
|
+
status: 404
|
|
218
|
+
}) : void 0,
|
|
219
|
+
name: "Error"
|
|
220
|
+
}), { isRouteStateRequest: true });
|
|
221
|
+
return withDefaultSecurityHeaders(new Response("Not found", {
|
|
222
|
+
status: 404,
|
|
223
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
if (!SAFE_METHODS.has(options.request.method)) {
|
|
227
|
+
if (isRouteStateRequest) return jsonErrorResponse(createSerializedRouteError("Method not allowed", 405, {
|
|
228
|
+
diagnostics: exposeDiagnostics ? buildRuntimeDiagnostics({
|
|
229
|
+
middlewareFiles: match.route.middlewareFiles,
|
|
230
|
+
phase: "action",
|
|
231
|
+
route: match.route,
|
|
232
|
+
shellFile: match.route.shellFile,
|
|
233
|
+
status: 405
|
|
234
|
+
}) : void 0,
|
|
235
|
+
name: "Error"
|
|
236
|
+
}), { isRouteStateRequest: true });
|
|
237
|
+
return withRouteResponseHeaders(new Response("Method not allowed", {
|
|
238
|
+
status: 405,
|
|
239
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
240
|
+
}), { isRouteStateRequest });
|
|
241
|
+
}
|
|
242
|
+
let routeArgs = {
|
|
197
243
|
request: options.request,
|
|
198
244
|
params: match.params,
|
|
199
|
-
context,
|
|
245
|
+
context: options.context ?? {},
|
|
200
246
|
signal: AbortSignal.timeout(3e4),
|
|
201
247
|
url,
|
|
202
248
|
route: match.route
|
|
203
249
|
};
|
|
204
|
-
|
|
250
|
+
let routeModule;
|
|
205
251
|
let shellModule;
|
|
252
|
+
let loaderFile;
|
|
253
|
+
let currentPhase = "middleware";
|
|
206
254
|
try {
|
|
255
|
+
const middlewareResult = await runMiddlewareChain({
|
|
256
|
+
context: routeArgs.context,
|
|
257
|
+
middlewareFiles: match.route.middlewareFiles,
|
|
258
|
+
params: match.params,
|
|
259
|
+
registry,
|
|
260
|
+
request: options.request,
|
|
261
|
+
route: match.route,
|
|
262
|
+
url
|
|
263
|
+
});
|
|
264
|
+
if (middlewareResult.response) return withRouteResponseHeaders(middlewareResult.response, { isRouteStateRequest });
|
|
265
|
+
routeArgs = {
|
|
266
|
+
...routeArgs,
|
|
267
|
+
context: middlewareResult.context
|
|
268
|
+
};
|
|
269
|
+
currentPhase = "render";
|
|
270
|
+
routeModule = await resolveRegistryModule(registry.routeModules, match.route.file);
|
|
271
|
+
if (!routeModule) throw new Error("Route module not found");
|
|
272
|
+
currentPhase = "loader";
|
|
273
|
+
const { loader, loaderFile: resolvedLoaderFile } = await resolveDataFunctions(match.route, routeModule, registry);
|
|
274
|
+
loaderFile = resolvedLoaderFile;
|
|
207
275
|
const loaderResult = loader ? await loader(routeArgs) : void 0;
|
|
208
|
-
if (loaderResult instanceof Response) return
|
|
276
|
+
if (loaderResult instanceof Response) return withRouteResponseHeaders(loaderResult, { isRouteStateRequest });
|
|
209
277
|
const data = loaderResult;
|
|
210
|
-
if (isRouteStateRequest) return
|
|
278
|
+
if (isRouteStateRequest) return withRouteResponseHeaders(Response.json({ data }), { isRouteStateRequest: true });
|
|
279
|
+
currentPhase = "render";
|
|
211
280
|
shellModule = match.route.shellFile ? await resolveRegistryModule(registry.shellModules, match.route.shellFile) : void 0;
|
|
212
281
|
const head = await mergeHeadMetadata(shellModule, routeModule, routeArgs, data);
|
|
213
282
|
const cssUrls = resolvePageCssUrls(options, match.route.shellFile, match.route.file);
|
|
214
283
|
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
|
-
|
|
284
|
+
if (match.route.render === "spa") {
|
|
285
|
+
let body = "";
|
|
286
|
+
if (shellModule?.Shell || shellModule?.Loading) {
|
|
287
|
+
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
288
|
+
const Shell = shellModule?.Shell;
|
|
289
|
+
const Loading = shellModule?.Loading;
|
|
290
|
+
const loadingTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
|
|
291
|
+
if (loadingTree) body = await renderToStringAsync(loadingTree);
|
|
292
|
+
}
|
|
293
|
+
return htmlResponse(buildHtmlDocument({
|
|
294
|
+
head,
|
|
295
|
+
body,
|
|
296
|
+
hydrationState: {
|
|
297
|
+
url: url.pathname,
|
|
298
|
+
routeId: match.route.id ?? "",
|
|
299
|
+
data: null,
|
|
300
|
+
error: null,
|
|
301
|
+
pending: true
|
|
302
|
+
},
|
|
303
|
+
clientEntryUrl: options.clientEntryUrl,
|
|
304
|
+
cssUrls,
|
|
305
|
+
modulePreloadUrls
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
if (!routeModule.Component) throw new Error("Route has no Component export");
|
|
232
309
|
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
233
310
|
const Component = routeModule.Component;
|
|
234
311
|
const Shell = shellModule?.Shell;
|
|
@@ -259,7 +336,9 @@ async function handlePrachtRequest(options) {
|
|
|
259
336
|
return renderRouteErrorResponse({
|
|
260
337
|
error,
|
|
261
338
|
isRouteStateRequest,
|
|
339
|
+
loaderFile,
|
|
262
340
|
options,
|
|
341
|
+
phase: currentPhase,
|
|
263
342
|
routeArgs,
|
|
264
343
|
routeId: match.route.id ?? "",
|
|
265
344
|
routeModule,
|
|
@@ -314,15 +393,65 @@ async function navigateToClientLocation(location, options) {
|
|
|
314
393
|
function isPrachtHttpError(error) {
|
|
315
394
|
return error instanceof Error && error.name === "PrachtHttpError" && "status" in error;
|
|
316
395
|
}
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
396
|
+
function shouldExposeServerErrors(options) {
|
|
397
|
+
return options.debugErrors === true;
|
|
398
|
+
}
|
|
399
|
+
function createSerializedRouteError(message, status, options = {}) {
|
|
400
|
+
return {
|
|
401
|
+
message,
|
|
402
|
+
name: options.name ?? "Error",
|
|
403
|
+
status,
|
|
404
|
+
...options.diagnostics ? { diagnostics: options.diagnostics } : {}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function buildRuntimeDiagnostics(options) {
|
|
408
|
+
const route = options.route;
|
|
409
|
+
const routeId = route && "id" in route ? route.id : void 0;
|
|
410
|
+
return {
|
|
411
|
+
phase: options.phase,
|
|
412
|
+
routeId,
|
|
413
|
+
routePath: route?.path,
|
|
414
|
+
routeFile: route?.file,
|
|
415
|
+
loaderFile: options.loaderFile,
|
|
416
|
+
shellFile: options.shellFile,
|
|
417
|
+
middlewareFiles: options.middlewareFiles ? [...options.middlewareFiles] : [],
|
|
418
|
+
status: options.status
|
|
322
419
|
};
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
420
|
+
}
|
|
421
|
+
function normalizeRouteError(error, options) {
|
|
422
|
+
if (isPrachtHttpError(error)) {
|
|
423
|
+
const status = typeof error.status === "number" ? error.status : 500;
|
|
424
|
+
if (status >= 400 && status < 500) return {
|
|
425
|
+
message: error.message,
|
|
426
|
+
name: error.name,
|
|
427
|
+
status
|
|
428
|
+
};
|
|
429
|
+
if (options.exposeDetails) return {
|
|
430
|
+
message: error.message || "Internal Server Error",
|
|
431
|
+
name: error.name || "Error",
|
|
432
|
+
status
|
|
433
|
+
};
|
|
434
|
+
return {
|
|
435
|
+
message: "Internal Server Error",
|
|
436
|
+
name: "Error",
|
|
437
|
+
status
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (error instanceof Error) {
|
|
441
|
+
if (options.exposeDetails) return {
|
|
442
|
+
message: error.message || "Internal Server Error",
|
|
443
|
+
name: error.name || "Error",
|
|
444
|
+
status: 500
|
|
445
|
+
};
|
|
446
|
+
return {
|
|
447
|
+
message: "Internal Server Error",
|
|
448
|
+
name: "Error",
|
|
449
|
+
status: 500
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (options.exposeDetails) return {
|
|
453
|
+
message: typeof error === "string" && error ? error : "Internal Server Error",
|
|
454
|
+
name: "Error",
|
|
326
455
|
status: 500
|
|
327
456
|
};
|
|
328
457
|
return {
|
|
@@ -335,25 +464,58 @@ function deserializeRouteError$1(error) {
|
|
|
335
464
|
const result = new Error(error.message);
|
|
336
465
|
result.name = error.name;
|
|
337
466
|
result.status = error.status;
|
|
467
|
+
result.diagnostics = error.diagnostics;
|
|
338
468
|
return result;
|
|
339
469
|
}
|
|
470
|
+
function jsonErrorResponse(routeError, options) {
|
|
471
|
+
const response = new Response(JSON.stringify({ error: routeError }), {
|
|
472
|
+
status: routeError.status,
|
|
473
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
474
|
+
});
|
|
475
|
+
return options.isRouteStateRequest ? withRouteResponseHeaders(response, { isRouteStateRequest: true }) : withDefaultSecurityHeaders(response);
|
|
476
|
+
}
|
|
477
|
+
function renderApiErrorResponse(options) {
|
|
478
|
+
const exposeDetails = shouldExposeServerErrors(options.options);
|
|
479
|
+
const routeError = normalizeRouteError(options.error, { exposeDetails });
|
|
480
|
+
const routeErrorWithDiagnostics = exposeDetails ? {
|
|
481
|
+
...routeError,
|
|
482
|
+
diagnostics: buildRuntimeDiagnostics({
|
|
483
|
+
middlewareFiles: options.middlewareFiles,
|
|
484
|
+
phase: options.phase,
|
|
485
|
+
route: options.route,
|
|
486
|
+
status: routeError.status
|
|
487
|
+
})
|
|
488
|
+
} : routeError;
|
|
489
|
+
if (exposeDetails) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: false });
|
|
490
|
+
const message = routeErrorWithDiagnostics.status >= 500 ? "Internal Server Error" : routeErrorWithDiagnostics.message;
|
|
491
|
+
return withDefaultSecurityHeaders(new Response(message, {
|
|
492
|
+
status: routeErrorWithDiagnostics.status,
|
|
493
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
340
496
|
async function renderRouteErrorResponse(options) {
|
|
341
|
-
const
|
|
497
|
+
const exposeDetails = shouldExposeServerErrors(options.options);
|
|
498
|
+
const routeError = normalizeRouteError(options.error, { exposeDetails });
|
|
499
|
+
const routeErrorWithDiagnostics = exposeDetails ? {
|
|
500
|
+
...routeError,
|
|
501
|
+
diagnostics: buildRuntimeDiagnostics({
|
|
502
|
+
loaderFile: options.loaderFile,
|
|
503
|
+
middlewareFiles: options.routeArgs.route.middlewareFiles,
|
|
504
|
+
phase: options.phase,
|
|
505
|
+
route: options.routeArgs.route,
|
|
506
|
+
shellFile: options.shellFile,
|
|
507
|
+
status: routeError.status
|
|
508
|
+
})
|
|
509
|
+
} : routeError;
|
|
342
510
|
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;
|
|
511
|
+
if (options.isRouteStateRequest) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: true });
|
|
512
|
+
const message = routeErrorWithDiagnostics.status >= 500 ? "Internal Server Error" : routeErrorWithDiagnostics.message;
|
|
348
513
|
return withDefaultSecurityHeaders(new Response(message, {
|
|
349
|
-
status:
|
|
514
|
+
status: routeErrorWithDiagnostics.status,
|
|
350
515
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
351
516
|
}));
|
|
352
517
|
}
|
|
353
|
-
if (options.isRouteStateRequest) return
|
|
354
|
-
status: routeError.status,
|
|
355
|
-
headers: { "content-type": "application/json; charset=utf-8" }
|
|
356
|
-
}));
|
|
518
|
+
if (options.isRouteStateRequest) return jsonErrorResponse(routeErrorWithDiagnostics, { isRouteStateRequest: true });
|
|
357
519
|
const shellModule = options.shellModule ?? (options.shellFile ? await resolveRegistryModule(options.options.registry?.shellModules, options.shellFile) : void 0);
|
|
358
520
|
const head = shellModule?.head ? await shellModule.head(options.routeArgs) : {};
|
|
359
521
|
const cssUrls = resolvePageCssUrls(options.options, options.shellFile, options.routeArgs.route.file);
|
|
@@ -361,7 +523,7 @@ async function renderRouteErrorResponse(options) {
|
|
|
361
523
|
const { renderToStringAsync } = await import("preact-render-to-string");
|
|
362
524
|
const ErrorBoundary = options.routeModule.ErrorBoundary;
|
|
363
525
|
const Shell = shellModule?.Shell;
|
|
364
|
-
const errorValue = deserializeRouteError$1(
|
|
526
|
+
const errorValue = deserializeRouteError$1(routeErrorWithDiagnostics);
|
|
365
527
|
const componentTree = Shell ? h(Shell, null, h(ErrorBoundary, { error: errorValue })) : h(ErrorBoundary, { error: errorValue });
|
|
366
528
|
return htmlResponse(buildHtmlDocument({
|
|
367
529
|
head,
|
|
@@ -374,12 +536,12 @@ async function renderRouteErrorResponse(options) {
|
|
|
374
536
|
url: options.urlPathname,
|
|
375
537
|
routeId: options.routeId,
|
|
376
538
|
data: null,
|
|
377
|
-
error:
|
|
539
|
+
error: routeErrorWithDiagnostics
|
|
378
540
|
},
|
|
379
541
|
clientEntryUrl: options.options.clientEntryUrl,
|
|
380
542
|
cssUrls,
|
|
381
543
|
modulePreloadUrls
|
|
382
|
-
}),
|
|
544
|
+
}), routeErrorWithDiagnostics.status);
|
|
383
545
|
}
|
|
384
546
|
async function runMiddlewareChain(options) {
|
|
385
547
|
let context = options.context;
|
|
@@ -409,11 +571,18 @@ async function runMiddlewareChain(options) {
|
|
|
409
571
|
}
|
|
410
572
|
async function resolveDataFunctions(route, routeModule, registry) {
|
|
411
573
|
let loader = routeModule?.loader;
|
|
574
|
+
let loaderFile = routeModule?.loader ? route.file : void 0;
|
|
412
575
|
if (route.loaderFile) {
|
|
413
576
|
const dataModule = await resolveRegistryModule(registry.dataModules, route.loaderFile);
|
|
414
|
-
if (dataModule?.loader)
|
|
577
|
+
if (dataModule?.loader) {
|
|
578
|
+
loader = dataModule.loader;
|
|
579
|
+
loaderFile = route.loaderFile;
|
|
580
|
+
}
|
|
415
581
|
}
|
|
416
|
-
return {
|
|
582
|
+
return {
|
|
583
|
+
loader,
|
|
584
|
+
loaderFile
|
|
585
|
+
};
|
|
417
586
|
}
|
|
418
587
|
async function resolveRegistryModule(modules, file) {
|
|
419
588
|
if (!modules) return void 0;
|
|
@@ -461,10 +630,10 @@ function buildHtmlDocument(options) {
|
|
|
461
630
|
</html>`;
|
|
462
631
|
}
|
|
463
632
|
function htmlResponse(html, status = 200) {
|
|
464
|
-
return
|
|
633
|
+
return withRouteResponseHeaders(new Response(html, {
|
|
465
634
|
status,
|
|
466
635
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
467
|
-
}));
|
|
636
|
+
}), { isRouteStateRequest: false });
|
|
468
637
|
}
|
|
469
638
|
function applyDefaultSecurityHeaders(headers) {
|
|
470
639
|
if (!headers.has("permissions-policy")) headers.set("permissions-policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
|
|
@@ -481,6 +650,26 @@ function withDefaultSecurityHeaders(response) {
|
|
|
481
650
|
headers
|
|
482
651
|
});
|
|
483
652
|
}
|
|
653
|
+
function withRouteResponseHeaders(response, options) {
|
|
654
|
+
const headers = applyDefaultSecurityHeaders(new Headers(response.headers));
|
|
655
|
+
appendVaryHeader(headers, ROUTE_STATE_REQUEST_HEADER);
|
|
656
|
+
if (options.isRouteStateRequest && !headers.has("cache-control")) headers.set("cache-control", ROUTE_STATE_CACHE_CONTROL);
|
|
657
|
+
return new Response(response.body, {
|
|
658
|
+
status: response.status,
|
|
659
|
+
statusText: response.statusText,
|
|
660
|
+
headers
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function appendVaryHeader(headers, value) {
|
|
664
|
+
const current = headers.get("vary");
|
|
665
|
+
if (!current) {
|
|
666
|
+
headers.set("vary", value);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const values = current.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
670
|
+
if (values.includes("*") || values.includes(value.toLowerCase())) return;
|
|
671
|
+
headers.set("vary", `${current}, ${value}`);
|
|
672
|
+
}
|
|
484
673
|
function escapeHtml(str) {
|
|
485
674
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
486
675
|
}
|
|
@@ -488,7 +677,7 @@ function serializeJsonForHtml(value) {
|
|
|
488
677
|
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
489
678
|
}
|
|
490
679
|
async function prerenderApp(options) {
|
|
491
|
-
const { resolveApp } = await import("./app-
|
|
680
|
+
const { resolveApp } = await import("./app-Cep0el7c.mjs").then((n) => n.t);
|
|
492
681
|
const resolved = resolveApp(options.app);
|
|
493
682
|
const results = [];
|
|
494
683
|
const isgManifest = {};
|
|
@@ -530,7 +719,7 @@ async function collectSSGPaths(route, registry) {
|
|
|
530
719
|
console.warn(` Warning: SSG route "${route.path}" has dynamic segments but no getStaticPaths() export, skipping.`);
|
|
531
720
|
return [];
|
|
532
721
|
}
|
|
533
|
-
const { buildPathFromSegments } = await import("./app-
|
|
722
|
+
const { buildPathFromSegments } = await import("./app-Cep0el7c.mjs").then((n) => n.t);
|
|
534
723
|
return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
|
|
535
724
|
}
|
|
536
725
|
//#endregion
|
|
@@ -556,7 +745,7 @@ function prefetchRouteState(url) {
|
|
|
556
745
|
});
|
|
557
746
|
return promise;
|
|
558
747
|
}
|
|
559
|
-
function setupPrefetching(app) {
|
|
748
|
+
function setupPrefetching(app, warmModules) {
|
|
560
749
|
let hoverTimer = null;
|
|
561
750
|
function getInternalHref(anchor) {
|
|
562
751
|
const href = anchor.getAttribute("href");
|
|
@@ -587,6 +776,10 @@ function setupPrefetching(app) {
|
|
|
587
776
|
if (hoverTimer) clearTimeout(hoverTimer);
|
|
588
777
|
hoverTimer = setTimeout(() => {
|
|
589
778
|
prefetchRouteState(href);
|
|
779
|
+
if (warmModules) {
|
|
780
|
+
const m = matchAppRoute(app, href);
|
|
781
|
+
if (m) warmModules(m);
|
|
782
|
+
}
|
|
590
783
|
}, 50);
|
|
591
784
|
}, true);
|
|
592
785
|
document.addEventListener("mouseleave", (e) => {
|
|
@@ -604,6 +797,10 @@ function setupPrefetching(app) {
|
|
|
604
797
|
const strategy = getPrefetchStrategy(href);
|
|
605
798
|
if (strategy !== "hover" && strategy !== "intent") return;
|
|
606
799
|
prefetchRouteState(href);
|
|
800
|
+
if (warmModules) {
|
|
801
|
+
const m = matchAppRoute(app, href);
|
|
802
|
+
if (m) warmModules(m);
|
|
803
|
+
}
|
|
607
804
|
}, true);
|
|
608
805
|
if (typeof IntersectionObserver === "undefined") return;
|
|
609
806
|
const observer = new IntersectionObserver((entries) => {
|
|
@@ -613,6 +810,10 @@ function setupPrefetching(app) {
|
|
|
613
810
|
const href = getInternalHref(anchor);
|
|
614
811
|
if (!href) continue;
|
|
615
812
|
prefetchRouteState(href);
|
|
813
|
+
if (warmModules) {
|
|
814
|
+
const m = matchAppRoute(app, href);
|
|
815
|
+
if (m) warmModules(m);
|
|
816
|
+
}
|
|
616
817
|
observer.unobserve(anchor);
|
|
617
818
|
}
|
|
618
819
|
}, { rootMargin: "200px" });
|
|
@@ -641,15 +842,32 @@ function useNavigate() {
|
|
|
641
842
|
}
|
|
642
843
|
async function initClientRouter(options) {
|
|
643
844
|
const { app, routeModules, shellModules, root, findModuleKey } = options;
|
|
644
|
-
|
|
845
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
846
|
+
function loadModule(modules, key) {
|
|
847
|
+
let cached = moduleCache.get(key);
|
|
848
|
+
if (!cached) {
|
|
849
|
+
cached = modules[key]();
|
|
850
|
+
moduleCache.set(key, cached);
|
|
851
|
+
}
|
|
852
|
+
return cached;
|
|
853
|
+
}
|
|
854
|
+
function startRouteImport(match) {
|
|
645
855
|
const routeKey = findModuleKey(routeModules, match.route.file);
|
|
646
856
|
if (!routeKey) return null;
|
|
647
|
-
|
|
857
|
+
return loadModule(routeModules, routeKey);
|
|
858
|
+
}
|
|
859
|
+
function startShellImport(match) {
|
|
860
|
+
if (!match.route.shellFile) return null;
|
|
861
|
+
const shellKey = findModuleKey(shellModules, match.route.shellFile);
|
|
862
|
+
if (!shellKey) return null;
|
|
863
|
+
return loadModule(shellModules, shellKey);
|
|
864
|
+
}
|
|
865
|
+
async function buildRouteTree(match, state, routeModPromise, shellModPromise) {
|
|
866
|
+
const routeMod = await (routeModPromise ?? startRouteImport(match));
|
|
867
|
+
if (!routeMod) return null;
|
|
648
868
|
let Shell = null;
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (shellKey) Shell = (await shellModules[shellKey]()).Shell;
|
|
652
|
-
}
|
|
869
|
+
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
870
|
+
if (resolvedShell) Shell = resolvedShell.Shell;
|
|
653
871
|
const Component = state.error ? routeMod.ErrorBoundary : routeMod.Component;
|
|
654
872
|
if (!Component) return null;
|
|
655
873
|
const props = state.error ? { error: deserializeRouteError(state.error) } : {
|
|
@@ -664,18 +882,35 @@ async function initClientRouter(options) {
|
|
|
664
882
|
url: match.pathname
|
|
665
883
|
}, componentTree));
|
|
666
884
|
}
|
|
885
|
+
async function buildSpaPendingTree(match, shellModPromise) {
|
|
886
|
+
const resolvedShell = await (shellModPromise ?? startShellImport(match));
|
|
887
|
+
if (!resolvedShell) return null;
|
|
888
|
+
const Shell = resolvedShell.Shell;
|
|
889
|
+
const Loading = resolvedShell.Loading;
|
|
890
|
+
const componentTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
|
|
891
|
+
if (!componentTree) return null;
|
|
892
|
+
return h(NavigateContext.Provider, { value: navigate }, h(PrachtRuntimeProvider, {
|
|
893
|
+
data: void 0,
|
|
894
|
+
params: match.params,
|
|
895
|
+
routeId: match.route.id ?? "",
|
|
896
|
+
url: match.pathname
|
|
897
|
+
}, componentTree));
|
|
898
|
+
}
|
|
667
899
|
async function navigate(to, opts) {
|
|
668
900
|
const match = matchAppRoute(app, to);
|
|
669
901
|
if (!match) {
|
|
670
902
|
window.location.href = to;
|
|
671
903
|
return;
|
|
672
904
|
}
|
|
905
|
+
const statePromise = getCachedRouteState(to) ?? fetchPrachtRouteState(to);
|
|
906
|
+
const routeModPromise = startRouteImport(match);
|
|
907
|
+
const shellModPromise = startShellImport(match);
|
|
673
908
|
let state = {
|
|
674
909
|
data: void 0,
|
|
675
910
|
error: null
|
|
676
911
|
};
|
|
677
912
|
try {
|
|
678
|
-
const result = await
|
|
913
|
+
const result = await statePromise;
|
|
679
914
|
if (result.type === "redirect") {
|
|
680
915
|
if (result.location) {
|
|
681
916
|
await navigate(result.location, opts);
|
|
@@ -698,7 +933,7 @@ async function initClientRouter(options) {
|
|
|
698
933
|
}
|
|
699
934
|
if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "", to);
|
|
700
935
|
else history.pushState(null, "", to);
|
|
701
|
-
const tree = await buildRouteTree(match, state);
|
|
936
|
+
const tree = await buildRouteTree(match, state, routeModPromise, shellModPromise);
|
|
702
937
|
if (tree) {
|
|
703
938
|
render(tree, root);
|
|
704
939
|
window.scrollTo(0, 0);
|
|
@@ -715,29 +950,35 @@ async function initClientRouter(options) {
|
|
|
715
950
|
}
|
|
716
951
|
const initialMatch = matchAppRoute(app, options.initialState.url);
|
|
717
952
|
if (initialMatch) {
|
|
953
|
+
const initialShellPromise = initialMatch.route.render === "spa" && options.initialState.pending ? startShellImport(initialMatch) : null;
|
|
718
954
|
let state = {
|
|
719
955
|
data: options.initialState.data,
|
|
720
956
|
error: options.initialState.error ?? null
|
|
721
957
|
};
|
|
722
|
-
if (initialMatch.route.render === "spa" &&
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
958
|
+
if (initialMatch.route.render === "spa" && options.initialState.pending) {
|
|
959
|
+
const dataPromise = fetchPrachtRouteState(options.initialState.url);
|
|
960
|
+
const pendingTree = await buildSpaPendingTree(initialMatch, initialShellPromise);
|
|
961
|
+
if (pendingTree) hydrate(pendingTree, root);
|
|
962
|
+
try {
|
|
963
|
+
const result = await dataPromise;
|
|
964
|
+
if (result.type === "redirect") {
|
|
965
|
+
window.location.href = result.location;
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (result.type === "error") state = {
|
|
969
|
+
data: void 0,
|
|
970
|
+
error: result.error
|
|
971
|
+
};
|
|
972
|
+
else state = {
|
|
973
|
+
data: result.data,
|
|
974
|
+
error: null
|
|
975
|
+
};
|
|
976
|
+
} catch {
|
|
977
|
+
window.location.href = options.initialState.url;
|
|
726
978
|
return;
|
|
727
979
|
}
|
|
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
980
|
}
|
|
740
|
-
const tree = await buildRouteTree(initialMatch, state);
|
|
981
|
+
const tree = await buildRouteTree(initialMatch, state, void 0, initialShellPromise);
|
|
741
982
|
if (tree) if (initialMatch.route.render === "spa") render(tree, root);
|
|
742
983
|
else hydrate(tree, root);
|
|
743
984
|
}
|
|
@@ -767,7 +1008,11 @@ async function initClientRouter(options) {
|
|
|
767
1008
|
});
|
|
768
1009
|
window.__PRACHT_NAVIGATE__ = navigate;
|
|
769
1010
|
window.__PRACHT_ROUTER_READY__ = true;
|
|
770
|
-
|
|
1011
|
+
const warmModules = (match) => {
|
|
1012
|
+
startRouteImport(match);
|
|
1013
|
+
startShellImport(match);
|
|
1014
|
+
};
|
|
1015
|
+
setupPrefetching(app, warmModules);
|
|
771
1016
|
}
|
|
772
1017
|
const HYDRATION_BANNER_ID = "__pracht_hydration_warnings__";
|
|
773
1018
|
function appendHydrationWarning(message) {
|
|
@@ -821,6 +1066,7 @@ function deserializeRouteError(error) {
|
|
|
821
1066
|
const result = new Error(error.message);
|
|
822
1067
|
result.name = error.name;
|
|
823
1068
|
result.status = error.status;
|
|
1069
|
+
result.diagnostics = error.diagnostics;
|
|
824
1070
|
return result;
|
|
825
1071
|
}
|
|
826
1072
|
//#endregion
|
|
@@ -834,4 +1080,4 @@ var PrachtHttpError = class extends Error {
|
|
|
834
1080
|
}
|
|
835
1081
|
};
|
|
836
1082
|
//#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 };
|
|
1083
|
+
export { Form, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useLocation, useNavigate, useParams, useRevalidate, useRevalidateRoute, useRouteData };
|
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pracht/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/framework",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/JoviDeCroock/pracht/issues"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/JoviDeCroock/pracht",
|
|
11
|
+
"directory": "packages/framework"
|
|
12
|
+
},
|
|
4
13
|
"files": [
|
|
5
14
|
"dist"
|
|
6
15
|
],
|
|
@@ -15,6 +24,9 @@
|
|
|
15
24
|
"default": "./dist/error-overlay.mjs"
|
|
16
25
|
}
|
|
17
26
|
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"provenance": true
|
|
29
|
+
},
|
|
18
30
|
"dependencies": {
|
|
19
31
|
"preact-suspense": "^0.2.0"
|
|
20
32
|
},
|