@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 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 params[currentSegment.name] = decodeURIComponent(targetSegment);
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: string;
72
- loader?: string;
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, string>;
88
- middleware?: Record<string, 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: FunctionComponent<RouteComponentProps<TLoader>>;
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: string, meta?: RouteMeta): RouteDefinition;
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-CAoDWWNO.mjs";
2
- import { Suspense, lazy } from "preact-suspense";
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
- "x-pracht-route-state-request": "1",
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
- const middlewareResult = await runMiddlewareChain({
145
- context: options.context ?? {},
146
- middlewareFiles: apiMiddlewareFiles,
147
- params: apiMatch.params,
148
- registry,
149
- request: options.request,
150
- route: apiMatch.route,
151
- url
152
- });
153
- if (middlewareResult.response) return middlewareResult.response;
154
- const apiModule = await resolveRegistryModule(registry.apiModules, apiMatch.route.file);
155
- if (!apiModule) return withDefaultSecurityHeaders(new Response("API route module not found", {
156
- status: 500,
157
- headers: { "content-type": "text/plain; charset=utf-8" }
158
- }));
159
- const handler = apiModule[options.request.method.toUpperCase()];
160
- if (!handler) return withDefaultSecurityHeaders(new Response("Method not allowed", {
161
- status: 405,
162
- headers: { "content-type": "text/plain; charset=utf-8" }
163
- }));
164
- return withDefaultSecurityHeaders(await handler({
165
- request: options.request,
166
- params: apiMatch.params,
167
- context: middlewareResult.context,
168
- signal: AbortSignal.timeout(3e4),
169
- url,
170
- route: apiMatch.route
171
- }));
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) return withDefaultSecurityHeaders(new Response("Not found", {
176
- status: 404,
177
- headers: { "content-type": "text/plain; charset=utf-8" }
178
- }));
179
- const isRouteStateRequest = options.request.headers.get("x-pracht-route-state-request") === "1";
180
- if (!SAFE_METHODS.has(options.request.method)) return withDefaultSecurityHeaders(new Response("Method not allowed", {
181
- status: 405,
182
- headers: { "content-type": "text/plain; charset=utf-8" }
183
- }));
184
- const middlewareResult = await runMiddlewareChain({
185
- context: options.context ?? {},
186
- middlewareFiles: match.route.middlewareFiles,
187
- params: match.params,
188
- registry,
189
- request: options.request,
190
- route: match.route,
191
- url
192
- });
193
- if (middlewareResult.response) return middlewareResult.response;
194
- const context = middlewareResult.context;
195
- const routeModule = await resolveRegistryModule(registry.routeModules, match.route.file);
196
- const routeArgs = {
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
- const { loader } = await resolveDataFunctions(match.route, routeModule, registry);
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 withDefaultSecurityHeaders(loaderResult);
321
+ if (loaderResult instanceof Response) return withRouteResponseHeaders(loaderResult, { isRouteStateRequest });
209
322
  const data = loaderResult;
210
- if (isRouteStateRequest) return withDefaultSecurityHeaders(Response.json({ data }));
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") return htmlResponse(buildHtmlDocument({
216
- head,
217
- body: "",
218
- hydrationState: {
219
- url: url.pathname,
220
- routeId: match.route.id ?? "",
221
- data: null,
222
- error: null
223
- },
224
- clientEntryUrl: options.clientEntryUrl,
225
- cssUrls,
226
- modulePreloadUrls
227
- }));
228
- if (!routeModule?.Component) return withDefaultSecurityHeaders(new Response("Route has no Component export", {
229
- status: 500,
230
- headers: { "content-type": "text/plain; charset=utf-8" }
231
- }));
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 normalizeRouteError(error) {
318
- if (isPrachtHttpError(error)) return {
319
- message: error.message,
320
- name: error.name,
321
- status: typeof error.status === "number" ? error.status : 500
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
- if (error instanceof Error) return {
324
- message: error.message || "Internal Server Error",
325
- name: error.name || "Error",
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 routeError = normalizeRouteError(options.error);
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 withDefaultSecurityHeaders(new Response(JSON.stringify({ error: routeError }), {
344
- status: routeError.status,
345
- headers: { "content-type": "application/json; charset=utf-8" }
346
- }));
347
- const message = routeError.status >= 500 ? "Internal Server Error" : routeError.message;
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: routeError.status,
560
+ status: routeErrorWithDiagnostics.status,
350
561
  headers: { "content-type": "text/plain; charset=utf-8" }
351
562
  }));
352
563
  }
353
- if (options.isRouteStateRequest) return withDefaultSecurityHeaders(new Response(JSON.stringify({ error: routeError }), {
354
- status: routeError.status,
355
- headers: { "content-type": "application/json; charset=utf-8" }
356
- }));
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(routeError);
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: routeError
585
+ error: routeErrorWithDiagnostics
378
586
  },
379
587
  clientEntryUrl: options.options.clientEntryUrl,
380
588
  cssUrls,
381
589
  modulePreloadUrls
382
- }), routeError.status);
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) loader = dataModule.loader;
623
+ if (dataModule?.loader) {
624
+ loader = dataModule.loader;
625
+ loaderFile = route.loaderFile;
626
+ }
415
627
  }
416
- return { loader };
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 withDefaultSecurityHeaders(new Response(html, {
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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-CAoDWWNO.mjs").then((n) => n.t);
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-CAoDWWNO.mjs").then((n) => n.t);
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
- async function buildRouteTree(match, state) {
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
- const routeMod = await routeModules[routeKey]();
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
- if (match.route.shellFile) {
650
- const shellKey = findModuleKey(shellModules, match.route.shellFile);
651
- if (shellKey) Shell = (await shellModules[shellKey]()).Shell;
652
- }
653
- const Component = state.error ? routeMod.ErrorBoundary : routeMod.Component;
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 (getCachedRouteState(to) ?? fetchPrachtRouteState(to));
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" && state.data == null && !state.error) try {
723
- const result = await fetchPrachtRouteState(options.initialState.url);
724
- if (result.type === "redirect") {
725
- window.location.href = result.location;
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 hydrate(tree, root);
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
- setupPrefetching(app);
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.1",
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
  },