@pracht/core 0.2.6 → 0.2.7

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
@@ -37,7 +37,7 @@ metadata for the failure phase and matched framework files when available.
37
37
  - `startApp()` — client-side hydration and runtime
38
38
  - `useLocation()` — access the current pathname and search string separately
39
39
  - `useRouteData()` — access loader data inside a route component
40
- - `useRevalidateRoute()` — trigger a revalidation of the current route's data
40
+ - `useRevalidate()` — trigger a revalidation of the current route's data
41
41
  - `<Form>` — progressive enhancement form component
42
42
 
43
43
  ### Types
package/dist/index.d.mts CHANGED
@@ -35,7 +35,9 @@ interface TimeRevalidatePolicy {
35
35
  }
36
36
  type RouteRevalidate = TimeRevalidatePolicy;
37
37
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
38
- type ApiRouteArgs<TContext = RegisteredContext> = BaseRouteArgs<TContext>;
38
+ type ApiRouteArgs<TContext = RegisteredContext> = Omit<BaseRouteArgs<TContext>, "route"> & {
39
+ route: ResolvedApiRoute;
40
+ };
39
41
  type ApiRouteHandler<TContext = RegisteredContext> = (args: ApiRouteArgs<TContext>) => MaybePromise<Response>;
40
42
  interface ApiRouteModule<TContext = any> {
41
43
  default?: ApiRouteHandler<TContext>;
@@ -286,10 +288,8 @@ interface HandlePrachtRequestOptions<TContext = unknown> {
286
288
  /** Expose raw server error details in rendered HTML and route-state JSON. */
287
289
  debugErrors?: boolean;
288
290
  clientEntryUrl?: string;
289
- /** Per-source-file CSS map produced by the vite plugin (preferred over cssUrls). */
291
+ /** Per-source-file CSS map produced by the vite plugin. */
290
292
  cssManifest?: Record<string, string[]>;
291
- /** @deprecated Pass cssManifest instead for per-page CSS resolution. */
292
- cssUrls?: string[];
293
293
  /** Per-source-file JS chunk map produced by the vite plugin for modulepreload hints. */
294
294
  jsManifest?: Record<string, string[]>;
295
295
  apiRoutes?: ResolvedApiRoute[];
@@ -318,12 +318,14 @@ declare function PrachtRuntimeProvider<TData>({
318
318
  data,
319
319
  params,
320
320
  routeId,
321
+ stateVersion,
321
322
  url
322
323
  }: {
323
324
  children: ComponentChildren;
324
325
  data: TData;
325
326
  params?: RouteParams;
326
327
  routeId: string;
328
+ stateVersion?: number;
327
329
  url: string;
328
330
  }): _$preact.VNode<_$preact.Attributes & {
329
331
  value: PrachtRuntimeValue | undefined;
@@ -339,8 +341,6 @@ interface Location {
339
341
  declare function useLocation(): Location;
340
342
  declare function useParams(): RouteParams;
341
343
  declare function useRevalidate(): () => Promise<unknown>;
342
- /** @deprecated Use useRevalidate instead. */
343
- declare const useRevalidateRoute: typeof useRevalidate;
344
344
  declare function Form(props: FormProps): _$preact.VNode<_$preact.ClassAttributes<HTMLFormElement> & h.JSX.HTMLAttributes<HTMLFormElement>>;
345
345
  type RouteStateResult = {
346
346
  type: "data";
@@ -370,12 +370,10 @@ interface PrerenderAppOptions {
370
370
  app: PrachtApp;
371
371
  registry?: ModuleRegistry;
372
372
  clientEntryUrl?: string;
373
- /** Per-source-file CSS map produced by the vite plugin (preferred over cssUrls). */
373
+ /** Per-source-file CSS map produced by the vite plugin. */
374
374
  cssManifest?: Record<string, string[]>;
375
375
  /** Per-source-file JS map produced by the vite plugin for modulepreload hints. */
376
376
  jsManifest?: Record<string, string[]>;
377
- /** @deprecated Pass cssManifest instead for per-page CSS resolution. */
378
- cssUrls?: string[];
379
377
  }
380
378
  declare function prerenderApp(options: PrerenderAppOptions): Promise<PrerenderResult[]>;
381
379
  declare function prerenderApp(options: PrerenderAppOptions & {
@@ -389,7 +387,7 @@ declare global {
389
387
  __PRACHT_ROUTER_READY__?: boolean;
390
388
  }
391
389
  }
392
- type ModuleMap = Record<string, () => Promise<any>>;
390
+ type ModuleMap = Record<string, () => Promise<unknown>>;
393
391
  type NavigateFn = (to: string, options?: {
394
392
  replace?: boolean;
395
393
  }) => Promise<void>;
@@ -404,4 +402,4 @@ interface InitClientRouterOptions {
404
402
  }
405
403
  declare function initClientRouter(options: InitClientRouterOptions): Promise<void>;
406
404
  //#endregion
407
- export { type ApiConfig, type ApiRouteArgs, 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 HeadersArgs, 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 };
405
+ export { type ApiConfig, type ApiRouteArgs, 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 HeadersArgs, 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, useRouteData };
package/dist/index.mjs CHANGED
@@ -1,7 +1,241 @@
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-CUL3q2ZA.mjs";
2
1
  import { createContext, h, hydrate, options, render } from "preact";
3
2
  import { useContext, useEffect, useMemo, useState } from "preact/hooks";
4
3
  import { Suspense, lazy } from "preact-suspense";
4
+ //#region src/app.ts
5
+ function timeRevalidate(seconds) {
6
+ if (!Number.isInteger(seconds) || seconds <= 0) throw new Error("timeRevalidate expects a positive integer number of seconds.");
7
+ return {
8
+ kind: "time",
9
+ seconds
10
+ };
11
+ }
12
+ function route(path, fileOrConfig, meta = {}) {
13
+ if (typeof fileOrConfig === "string" || typeof fileOrConfig === "function") return {
14
+ kind: "route",
15
+ path: normalizeRoutePath(path),
16
+ file: resolveModuleRef(fileOrConfig),
17
+ ...meta
18
+ };
19
+ const { component, loader, ...routeMeta } = fileOrConfig;
20
+ return {
21
+ kind: "route",
22
+ path: normalizeRoutePath(path),
23
+ file: resolveModuleRef(component),
24
+ loaderFile: resolveModuleRef(loader),
25
+ ...routeMeta
26
+ };
27
+ }
28
+ function resolveModuleRef(ref) {
29
+ if (ref === void 0) return void 0;
30
+ if (typeof ref === "string") return ref;
31
+ return "";
32
+ }
33
+ function group(meta, routes) {
34
+ return {
35
+ kind: "group",
36
+ meta,
37
+ routes
38
+ };
39
+ }
40
+ function defineApp(config) {
41
+ return {
42
+ shells: resolveModuleRefRecord(config.shells ?? {}),
43
+ middleware: resolveModuleRefRecord(config.middleware ?? {}),
44
+ api: config.api ?? {},
45
+ routes: config.routes
46
+ };
47
+ }
48
+ function resolveModuleRefRecord(record) {
49
+ const result = {};
50
+ for (const [key, value] of Object.entries(record)) result[key] = resolveModuleRef(value);
51
+ return result;
52
+ }
53
+ function resolveApp(app) {
54
+ const routes = [];
55
+ const inherited = {
56
+ pathPrefix: "/",
57
+ middleware: []
58
+ };
59
+ for (const node of app.routes) flattenRouteNode(app, node, inherited, routes);
60
+ return {
61
+ shells: app.shells,
62
+ middleware: app.middleware,
63
+ api: app.api,
64
+ routes,
65
+ apiRoutes: []
66
+ };
67
+ }
68
+ function matchAppRoute(app, pathname) {
69
+ const resolved = isResolvedApp(app) ? app : resolveApp(app);
70
+ const normalizedPathname = normalizeRoutePath(pathname);
71
+ const targetSegments = splitPathSegments(normalizedPathname);
72
+ for (const currentRoute of resolved.routes) {
73
+ const params = matchRouteSegments(currentRoute.segments, targetSegments);
74
+ if (params) return {
75
+ route: currentRoute,
76
+ params,
77
+ pathname: normalizedPathname
78
+ };
79
+ }
80
+ }
81
+ function flattenRouteNode(app, node, inherited, routes) {
82
+ if (node.kind === "group") {
83
+ const nextInherited = {
84
+ pathPrefix: mergeRoutePaths(inherited.pathPrefix, node.meta.pathPrefix),
85
+ shell: node.meta.shell ?? inherited.shell,
86
+ render: node.meta.render ?? inherited.render,
87
+ middleware: [...inherited.middleware, ...node.meta.middleware ?? []]
88
+ };
89
+ for (const child of node.routes) flattenRouteNode(app, child, nextInherited, routes);
90
+ return;
91
+ }
92
+ const fullPath = mergeRoutePaths(inherited.pathPrefix, node.path);
93
+ const shell = node.shell ?? inherited.shell;
94
+ const middleware = [...inherited.middleware, ...node.middleware ?? []];
95
+ routes.push({
96
+ id: node.id ?? createRouteId(fullPath),
97
+ path: fullPath,
98
+ file: node.file,
99
+ loaderFile: node.loaderFile,
100
+ shell,
101
+ shellFile: shell ? app.shells[shell] : void 0,
102
+ render: node.render ?? inherited.render,
103
+ middleware,
104
+ middlewareFiles: middleware.flatMap((name) => {
105
+ const middlewareFile = app.middleware[name];
106
+ return middlewareFile ? [middlewareFile] : [];
107
+ }),
108
+ revalidate: node.revalidate,
109
+ segments: parseRouteSegments(fullPath)
110
+ });
111
+ }
112
+ function isResolvedApp(app) {
113
+ return app.routes.length === 0 || "segments" in app.routes[0];
114
+ }
115
+ function matchRouteSegments(routeSegments, targetSegments) {
116
+ const params = {};
117
+ let routeIndex = 0;
118
+ let targetIndex = 0;
119
+ while (routeIndex < routeSegments.length) {
120
+ const currentSegment = routeSegments[routeIndex];
121
+ if (currentSegment.type === "catchall") {
122
+ params[currentSegment.name] = targetSegments.slice(targetIndex).join("/");
123
+ return params;
124
+ }
125
+ const targetSegment = targetSegments[targetIndex];
126
+ if (typeof targetSegment === "undefined") return null;
127
+ if (currentSegment.type === "static") {
128
+ if (currentSegment.value !== targetSegment) return null;
129
+ } else try {
130
+ params[currentSegment.name] = decodeURIComponent(targetSegment);
131
+ } catch {
132
+ return null;
133
+ }
134
+ routeIndex += 1;
135
+ targetIndex += 1;
136
+ }
137
+ return targetIndex === targetSegments.length ? params : null;
138
+ }
139
+ function parseRouteSegments(path) {
140
+ return splitPathSegments(path).map((segment) => {
141
+ if (segment === "*") return {
142
+ type: "catchall",
143
+ name: "*"
144
+ };
145
+ if (segment.startsWith(":")) return {
146
+ type: "param",
147
+ name: segment.slice(1)
148
+ };
149
+ return {
150
+ type: "static",
151
+ value: segment
152
+ };
153
+ });
154
+ }
155
+ function splitPathSegments(path) {
156
+ return normalizeRoutePath(path).split("/").filter(Boolean);
157
+ }
158
+ function mergeRoutePaths(prefix, path) {
159
+ if (!path) return normalizeRoutePath(prefix);
160
+ const normalizedPrefix = normalizeRoutePath(prefix);
161
+ const normalizedPath = normalizeRoutePath(path);
162
+ if (normalizedPrefix === "/") return normalizedPath;
163
+ if (normalizedPath === "/") return normalizedPrefix;
164
+ return normalizeRoutePath(`${normalizedPrefix}/${normalizedPath.slice(1)}`);
165
+ }
166
+ function normalizeRoutePath(path) {
167
+ if (!path || path === "/") return "/";
168
+ const collapsed = (path.startsWith("/") ? path : `/${path}`).replace(/\/{2,}/g, "/");
169
+ return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
170
+ }
171
+ function buildPathFromSegments(segments, params) {
172
+ return normalizeRoutePath("/" + segments.map((segment) => {
173
+ if (segment.type === "static") return segment.value;
174
+ if (segment.type === "param") return encodeURIComponent(params[segment.name] ?? "");
175
+ return params["*"] ?? "";
176
+ }).join("/"));
177
+ }
178
+ /**
179
+ * Convert a list of file paths from `import.meta.glob` into resolved API routes.
180
+ *
181
+ * Example: `"/src/api/health.ts"` → path `/api/health`
182
+ * `"/src/api/users/[id].ts"` → path `/api/users/:id`
183
+ * `"/src/api/index.ts"` → path `/api`
184
+ */
185
+ function resolveApiRoutes(files, apiDir = "/src/api") {
186
+ const normalizedDir = apiDir.replace(/\/$/, "");
187
+ return files.map((file) => {
188
+ let relative = file;
189
+ if (relative.startsWith(normalizedDir)) relative = relative.slice(normalizedDir.length);
190
+ relative = relative.replace(/\.(ts|tsx|js|jsx)$/, "");
191
+ if (relative.endsWith("/index")) relative = relative.slice(0, -6) || "/";
192
+ relative = relative.replace(/\[([^\]]+)\]/g, ":$1");
193
+ const path = normalizeRoutePath(`/api${relative}`);
194
+ return {
195
+ path,
196
+ file,
197
+ segments: parseRouteSegments(path)
198
+ };
199
+ }).sort(compareResolvedApiRoutes);
200
+ }
201
+ function matchApiRoute(apiRoutes, pathname) {
202
+ const normalizedPathname = normalizeRoutePath(pathname);
203
+ const targetSegments = splitPathSegments(normalizedPathname);
204
+ for (const route of apiRoutes) {
205
+ const params = matchRouteSegments(route.segments, targetSegments);
206
+ if (params) return {
207
+ route,
208
+ params,
209
+ pathname: normalizedPathname
210
+ };
211
+ }
212
+ }
213
+ function createRouteId(path) {
214
+ if (path === "/") return "index";
215
+ return path.slice(1).split("/").map((segment) => {
216
+ if (segment === "*") return "splat";
217
+ return segment.startsWith(":") ? segment.slice(1) : segment;
218
+ }).join("-").replace(/[^a-zA-Z0-9-]/g, "-");
219
+ }
220
+ function compareResolvedApiRoutes(left, right) {
221
+ const length = Math.max(left.segments.length, right.segments.length);
222
+ for (let index = 0; index < length; index += 1) {
223
+ const leftSegment = left.segments[index];
224
+ const rightSegment = right.segments[index];
225
+ if (!leftSegment) return 1;
226
+ if (!rightSegment) return -1;
227
+ const leftScore = getRouteSegmentSpecificity(leftSegment);
228
+ const rightScore = getRouteSegmentSpecificity(rightSegment);
229
+ if (leftScore !== rightScore) return rightScore - leftScore;
230
+ }
231
+ return left.path.localeCompare(right.path);
232
+ }
233
+ function getRouteSegmentSpecificity(segment) {
234
+ if (segment.type === "static") return 3;
235
+ if (segment.type === "param") return 2;
236
+ return 1;
237
+ }
238
+ //#endregion
5
239
  //#region src/forwardRef.ts
6
240
  let oldDiffHook = options.__b;
7
241
  options.__b = (vnode) => {
@@ -80,6 +314,7 @@ const SAFE_METHODS = new Set(["GET", "HEAD"]);
80
314
  const HYDRATION_STATE_ELEMENT_ID = "pracht-state";
81
315
  const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
82
316
  const ROUTE_STATE_CACHE_CONTROL = "no-store";
317
+ const EMPTY_ROUTE_PARAMS = {};
83
318
  let _renderToStringAsync;
84
319
  async function getRenderToStringAsync() {
85
320
  if (_renderToStringAsync) return _renderToStringAsync;
@@ -87,25 +322,37 @@ async function getRenderToStringAsync() {
87
322
  return _renderToStringAsync;
88
323
  }
89
324
  const RouteDataContext = createContext(void 0);
90
- function PrachtRuntimeProvider({ children, data, params = {}, routeId, url }) {
91
- const [routeData, setRouteData] = useState(data);
325
+ function PrachtRuntimeProvider({ children, data, params = EMPTY_ROUTE_PARAMS, routeId, stateVersion = 0, url }) {
326
+ const [routeDataState, setRouteDataState] = useState({
327
+ data,
328
+ stateVersion
329
+ });
330
+ const routeData = routeDataState.stateVersion === stateVersion ? routeDataState.data : data;
92
331
  useEffect(() => {
93
- setRouteData(data);
332
+ setRouteDataState({
333
+ data,
334
+ stateVersion
335
+ });
94
336
  }, [
95
337
  data,
96
338
  routeId,
339
+ stateVersion,
97
340
  url
98
341
  ]);
99
342
  const context = useMemo(() => ({
100
343
  data: routeData,
101
344
  params,
102
345
  routeId,
103
- setData: setRouteData,
346
+ setData: (nextData) => setRouteDataState({
347
+ data: nextData,
348
+ stateVersion
349
+ }),
104
350
  url
105
351
  }), [
106
352
  routeData,
107
353
  params,
108
354
  routeId,
355
+ stateVersion,
109
356
  url
110
357
  ]);
111
358
  return h(RouteDataContext.Provider, {
@@ -125,13 +372,9 @@ function readHydrationState() {
125
372
  if (!(element instanceof HTMLScriptElement)) return;
126
373
  const raw = element.textContent;
127
374
  if (!raw) return;
128
- try {
129
- const state = JSON.parse(raw);
130
- window.__PRACHT_STATE__ = state;
131
- return state;
132
- } catch {
133
- return;
134
- }
375
+ const state = JSON.parse(raw);
376
+ window.__PRACHT_STATE__ = state;
377
+ return state;
135
378
  }
136
379
  function useRouteData() {
137
380
  return useContext(RouteDataContext)?.data;
@@ -151,13 +394,11 @@ function useRevalidate() {
151
394
  await navigateToClientLocation(result.location);
152
395
  return;
153
396
  }
154
- if (result.type === "error") throw deserializeRouteError$1(result.error);
397
+ if (result.type === "error") throw deserializeRouteError(result.error);
155
398
  runtime?.setData(result.data);
156
399
  return result.data;
157
400
  };
158
401
  }
159
- /** @deprecated Use useRevalidate instead. */
160
- const useRevalidateRoute = useRevalidate;
161
402
  function Form(props) {
162
403
  const { onSubmit, method, ...rest } = props;
163
404
  return h("form", {
@@ -187,9 +428,10 @@ function Form(props) {
187
428
  }
188
429
  });
189
430
  }
190
- async function fetchPrachtRouteState(url) {
191
- const response = await fetch(url, {
192
- headers: {
431
+ async function fetchPrachtRouteState(url, options) {
432
+ const fetchUrl = options?.useDataParam ? buildRouteStateUrl(url) : url;
433
+ const response = await fetch(fetchUrl, {
434
+ headers: options?.useDataParam ? {} : {
193
435
  [ROUTE_STATE_REQUEST_HEADER]: "1",
194
436
  "Cache-Control": "no-cache"
195
437
  },
@@ -216,11 +458,16 @@ async function fetchPrachtRouteState(url) {
216
458
  type: "data"
217
459
  };
218
460
  }
461
+ function buildRouteStateUrl(url) {
462
+ return `${url}${url.includes("?") ? "&" : "?"}_data=1`;
463
+ }
219
464
  async function handlePrachtRequest(options) {
220
465
  const url = new URL(options.request.url);
466
+ const hasDataParam = url.searchParams.get("_data") === "1";
467
+ if (hasDataParam) url.searchParams.delete("_data");
221
468
  const requestPath = getRequestPath(url);
222
469
  const registry = options.registry ?? {};
223
- const isRouteStateRequest = options.request.headers.get(ROUTE_STATE_REQUEST_HEADER) === "1";
470
+ const isRouteStateRequest = options.request.headers.get(ROUTE_STATE_REQUEST_HEADER) === "1" || hasDataParam;
224
471
  const exposeDiagnostics = shouldExposeServerErrors(options);
225
472
  if (options.apiRoutes?.length) {
226
473
  const apiMatch = matchApiRoute(options.apiRoutes, url.pathname);
@@ -311,7 +558,7 @@ async function handlePrachtRequest(options) {
311
558
  let loaderFile;
312
559
  let currentPhase = "middleware";
313
560
  try {
314
- const middlewareResult = await runMiddlewareChain({
561
+ const middlewarePromise = runMiddlewareChain({
315
562
  context: routeArgs.context,
316
563
  middlewareFiles: match.route.middlewareFiles,
317
564
  params: match.params,
@@ -320,25 +567,31 @@ async function handlePrachtRequest(options) {
320
567
  route: match.route,
321
568
  url
322
569
  });
570
+ const routeModulePromise = resolveRegistryModule(registry.routeModules, match.route.file);
571
+ const shellModulePromise = match.route.shellFile ? resolveRegistryModule(registry.shellModules, match.route.shellFile) : Promise.resolve(void 0);
572
+ const dataFunctionsPromise = routeModulePromise.then((mod) => resolveDataFunctions(match.route, mod, registry));
573
+ routeModulePromise.catch(() => {});
574
+ shellModulePromise.catch(() => {});
575
+ dataFunctionsPromise.catch(() => {});
576
+ const middlewareResult = await middlewarePromise;
323
577
  if (middlewareResult.response) return normalizePageResponse(middlewareResult.response, { isRouteStateRequest });
324
578
  routeArgs = {
325
579
  ...routeArgs,
326
580
  context: middlewareResult.context
327
581
  };
328
582
  currentPhase = "render";
329
- routeModule = await resolveRegistryModule(registry.routeModules, match.route.file);
583
+ routeModule = await routeModulePromise;
330
584
  if (!routeModule) throw new Error("Route module not found");
331
585
  currentPhase = "loader";
332
- const { loader, loaderFile: resolvedLoaderFile } = await resolveDataFunctions(match.route, routeModule, registry);
586
+ const { loader, loaderFile: resolvedLoaderFile } = await dataFunctionsPromise;
333
587
  loaderFile = resolvedLoaderFile;
334
588
  const loaderResult = loader ? await loader(routeArgs) : void 0;
335
589
  if (loaderResult instanceof Response) return normalizePageResponse(loaderResult, { isRouteStateRequest });
336
590
  const data = loaderResult;
337
591
  if (isRouteStateRequest) return withRouteResponseHeaders(Response.json({ data }), { isRouteStateRequest: true });
338
592
  currentPhase = "render";
339
- shellModule = match.route.shellFile ? await resolveRegistryModule(registry.shellModules, match.route.shellFile) : void 0;
340
- const head = await mergeHeadMetadata(shellModule, routeModule, routeArgs, data);
341
- const documentHeaders = await mergeDocumentHeaders(shellModule, routeModule, routeArgs, data);
593
+ shellModule = await shellModulePromise;
594
+ const [head, documentHeaders] = await Promise.all([mergeHeadMetadata(shellModule, routeModule, routeArgs, data), mergeDocumentHeaders(shellModule, routeModule, routeArgs, data)]);
342
595
  const cssUrls = resolvePageCssUrls(options, match.route.shellFile, match.route.file);
343
596
  const modulePreloadUrls = resolvePageJsUrls(options, match.route.shellFile, match.route.file);
344
597
  if (match.route.render === "spa") {
@@ -361,18 +614,20 @@ async function handlePrachtRequest(options) {
361
614
  },
362
615
  clientEntryUrl: options.clientEntryUrl,
363
616
  cssUrls,
364
- modulePreloadUrls
617
+ modulePreloadUrls,
618
+ routeStatePreloadUrl: loader ? buildRouteStateUrl(requestPath) : void 0
365
619
  }), 200, documentHeaders);
366
620
  }
367
621
  const DefaultComponent = typeof routeModule.default === "function" ? routeModule.default : void 0;
368
622
  const Component = routeModule.Component ?? DefaultComponent;
369
623
  if (!Component) throw new Error("Route has no Component or default export");
370
624
  const Shell = shellModule?.Shell;
625
+ const Comp = Component;
371
626
  const componentProps = {
372
627
  data,
373
628
  params: match.params
374
629
  };
375
- const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
630
+ const componentTree = Shell ? h(Shell, null, h(Comp, componentProps)) : h(Comp, componentProps);
376
631
  const tree = h(PrachtRuntimeProvider, {
377
632
  data,
378
633
  params: match.params,
@@ -409,18 +664,11 @@ async function handlePrachtRequest(options) {
409
664
  }
410
665
  }
411
666
  function parseLocation(value) {
412
- try {
413
- const url = new URL(value, "http://pracht.local");
414
- return {
415
- pathname: url.pathname,
416
- search: url.search
417
- };
418
- } catch {
419
- return {
420
- pathname: value || "/",
421
- search: ""
422
- };
423
- }
667
+ const url = new URL(value, "http://pracht.local");
668
+ return {
669
+ pathname: url.pathname,
670
+ search: url.search
671
+ };
424
672
  }
425
673
  function getRequestPath(url) {
426
674
  return `${url.pathname}${url.search}`;
@@ -455,27 +703,23 @@ function resolveManifestEntries(manifest, file) {
455
703
  const resolved = getSuffixIndex(manifest).get(normalizeModulePath(file));
456
704
  if (resolved) return manifest[resolved];
457
705
  }
706
+ function resolvePageUrlsFromManifest(manifest, shellFile, routeFile) {
707
+ const urls = /* @__PURE__ */ new Set();
708
+ const add = (file) => {
709
+ const entries = resolveManifestEntries(manifest, file);
710
+ if (entries) for (const url of entries) urls.add(url);
711
+ };
712
+ if (shellFile) add(shellFile);
713
+ add(routeFile);
714
+ return [...urls];
715
+ }
458
716
  function resolvePageCssUrls(options, shellFile, routeFile) {
459
- if (!options.cssManifest) return options.cssUrls ?? [];
460
- const css = /* @__PURE__ */ new Set();
461
- function addFromManifest(file) {
462
- const entries = resolveManifestEntries(options.cssManifest, file);
463
- if (entries) for (const c of entries) css.add(c);
464
- }
465
- if (shellFile) addFromManifest(shellFile);
466
- addFromManifest(routeFile);
467
- return [...css];
717
+ if (!options.cssManifest) return [];
718
+ return resolvePageUrlsFromManifest(options.cssManifest, shellFile, routeFile);
468
719
  }
469
720
  function resolvePageJsUrls(options, shellFile, routeFile) {
470
721
  if (!options.jsManifest) return [];
471
- const js = /* @__PURE__ */ new Set();
472
- function addFromManifest(file) {
473
- const entries = resolveManifestEntries(options.jsManifest, file);
474
- if (entries) for (const j of entries) js.add(j);
475
- }
476
- if (shellFile) addFromManifest(shellFile);
477
- addFromManifest(routeFile);
478
- return [...js];
722
+ return resolvePageUrlsFromManifest(options.jsManifest, shellFile, routeFile);
479
723
  }
480
724
  async function navigateToClientLocation(location, options) {
481
725
  if (typeof window === "undefined") return;
@@ -561,7 +805,7 @@ function normalizeRouteError(error, options) {
561
805
  status: 500
562
806
  };
563
807
  }
564
- function deserializeRouteError$1(error) {
808
+ function deserializeRouteError(error) {
565
809
  const result = new Error(error.message);
566
810
  result.name = error.name;
567
811
  result.status = error.status;
@@ -643,7 +887,7 @@ async function renderRouteErrorResponse(options) {
643
887
  const renderToString = await getRenderToStringAsync();
644
888
  const ErrorBoundary = options.routeModule.ErrorBoundary;
645
889
  const Shell = shellModule?.Shell;
646
- const errorValue = deserializeRouteError$1(routeErrorWithDiagnostics);
890
+ const errorValue = deserializeRouteError(routeErrorWithDiagnostics);
647
891
  const componentTree = Shell ? h(Shell, null, h(ErrorBoundary, { error: errorValue })) : h(ErrorBoundary, { error: errorValue });
648
892
  return htmlResponse(buildHtmlDocument({
649
893
  head,
@@ -665,8 +909,10 @@ async function renderRouteErrorResponse(options) {
665
909
  }
666
910
  async function runMiddlewareChain(options) {
667
911
  let context = options.context;
668
- for (const mwFile of options.middlewareFiles) {
669
- const mwModule = await resolveRegistryModule(options.registry.middlewareModules, mwFile);
912
+ const modulePromises = options.middlewareFiles.map((mwFile) => resolveRegistryModule(options.registry.middlewareModules, mwFile));
913
+ for (const p of modulePromises) p.catch(() => {});
914
+ for (const modulePromise of modulePromises) {
915
+ const mwModule = await modulePromise;
670
916
  if (!mwModule?.middleware) continue;
671
917
  const result = await mwModule.middleware({
672
918
  request: options.request,
@@ -711,11 +957,10 @@ async function resolveRegistryModule(modules, file) {
711
957
  if (resolved) return modules[resolved]();
712
958
  }
713
959
  async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
714
- const shellHead = shellModule?.head ? await shellModule.head(routeArgs) : {};
715
- const routeHead = routeModule?.head ? await routeModule.head({
960
+ const [shellHead, routeHead] = await Promise.all([shellModule?.head ? shellModule.head(routeArgs) : Promise.resolve({}), routeModule?.head ? routeModule.head({
716
961
  ...routeArgs,
717
962
  data
718
- }) : {};
963
+ }) : Promise.resolve({})]);
719
964
  return {
720
965
  title: routeHead.title ?? shellHead.title,
721
966
  lang: routeHead.lang ?? shellHead.lang,
@@ -725,12 +970,11 @@ async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
725
970
  }
726
971
  async function mergeDocumentHeaders(shellModule, routeModule, routeArgs, data) {
727
972
  const headers = new Headers();
728
- const shellHeaders = shellModule?.headers ? await shellModule.headers(routeArgs) : void 0;
729
- if (shellHeaders) applyHeaders(headers, shellHeaders);
730
- const routeHeaders = routeModule?.headers ? await routeModule.headers({
973
+ const [shellHeaders, routeHeaders] = await Promise.all([shellModule?.headers ? shellModule.headers(routeArgs) : Promise.resolve(void 0), routeModule?.headers ? routeModule.headers({
731
974
  ...routeArgs,
732
975
  data
733
- }) : void 0;
976
+ }) : Promise.resolve(void 0)]);
977
+ if (shellHeaders) applyHeaders(headers, shellHeaders);
734
978
  if (routeHeaders) applyHeaders(headers, routeHeaders);
735
979
  return headers;
736
980
  }
@@ -740,12 +984,13 @@ function applyHeaders(headers, init) {
740
984
  });
741
985
  }
742
986
  function buildHtmlDocument(options) {
743
- const { head, body, hydrationState, clientEntryUrl, cssUrls = [], modulePreloadUrls = [] } = options;
987
+ const { head, body, hydrationState, clientEntryUrl, cssUrls = [], modulePreloadUrls = [], routeStatePreloadUrl } = options;
744
988
  const titleTag = head.title ? `<title>${escapeHtml(head.title)}</title>` : "";
745
989
  const metaTags = (head.meta ?? []).map((m) => `<meta ${Object.entries(m).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ")}>`).join("\n ");
746
990
  const linkTags = (head.link ?? []).map((l) => `<link ${Object.entries(l).map(([k, v]) => `${k}="${escapeHtml(v)}"`).join(" ")}>`).join("\n ");
747
991
  const cssTags = cssUrls.map((url) => `<link rel="stylesheet" href="${escapeHtml(url)}">`).join("\n ");
748
992
  const modulePreloadTags = modulePreloadUrls.map((url) => `<link rel="modulepreload" href="${escapeHtml(url)}">`).join("\n ");
993
+ const routeStatePreloadTag = routeStatePreloadUrl ? `<link rel="preload" as="fetch" href="${escapeHtml(routeStatePreloadUrl)}" crossorigin="anonymous">` : "";
749
994
  const stateScript = `<script id="${HYDRATION_STATE_ELEMENT_ID}" type="application/json">${serializeJsonForHtml(hydrationState)}<\/script>`;
750
995
  const entryScript = clientEntryUrl ? `<script type="module" src="${escapeHtml(clientEntryUrl)}"><\/script>` : "";
751
996
  return `<!DOCTYPE html>
@@ -757,6 +1002,7 @@ function buildHtmlDocument(options) {
757
1002
  ${linkTags}
758
1003
  ${cssTags}
759
1004
  ${modulePreloadTags}
1005
+ ${routeStatePreloadTag}
760
1006
  </head>
761
1007
  <body>
762
1008
  <div id="pracht-root">${body}</div>
@@ -824,7 +1070,6 @@ function serializeJsonForHtml(value) {
824
1070
  return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
825
1071
  }
826
1072
  async function prerenderApp(options) {
827
- const { resolveApp } = await import("./app-CUL3q2ZA.mjs").then((n) => n.t);
828
1073
  const resolved = resolveApp(options.app);
829
1074
  const results = [];
830
1075
  const isgManifest = {};
@@ -886,7 +1131,6 @@ async function collectSSGPaths(route, registry) {
886
1131
  console.warn(` Warning: SSG route "${route.path}" has dynamic segments but no getStaticPaths() export, skipping.`);
887
1132
  return [];
888
1133
  }
889
- const { buildPathFromSegments } = await import("./app-CUL3q2ZA.mjs").then((n) => n.t);
890
1134
  return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
891
1135
  }
892
1136
  //#endregion
@@ -939,7 +1183,6 @@ function setupPrefetching(app, warmModules) {
939
1183
  const match = matchAppRoute(app, routePathname);
940
1184
  if (!match) return "none";
941
1185
  if (match.route.prefetch) return match.route.prefetch;
942
- if (match.route.render === "spa") return "none";
943
1186
  return "intent";
944
1187
  }
945
1188
  document.addEventListener("mouseenter", (e) => {
@@ -1041,7 +1284,30 @@ async function initClientRouter(options) {
1041
1284
  if (!shellKey) return null;
1042
1285
  return loadModule(shellModules, shellKey);
1043
1286
  }
1044
- async function buildRouteTree(match, state, currentUrl, routeModPromise, shellModPromise) {
1287
+ let updateRouteState = null;
1288
+ let routeStateVersion = 0;
1289
+ function RouterRoot({ initialState }) {
1290
+ const [routeState, setRouteState] = useState(initialState);
1291
+ updateRouteState = setRouteState;
1292
+ const navigateValue = useMemo(() => navigate, []);
1293
+ const { Shell, Component, componentProps, data, params, routeId, url, version } = routeState;
1294
+ const componentTree = Shell ? h(Shell, null, h(Component, componentProps)) : h(Component, componentProps);
1295
+ return h(NavigateContext.Provider, { value: navigateValue }, h(PrachtRuntimeProvider, {
1296
+ data,
1297
+ params,
1298
+ routeId,
1299
+ stateVersion: version,
1300
+ url
1301
+ }, componentTree));
1302
+ }
1303
+ function applyRouteState(routeState) {
1304
+ if (updateRouteState) {
1305
+ updateRouteState(routeState);
1306
+ return;
1307
+ }
1308
+ render(h(RouterRoot, { initialState: routeState }), root);
1309
+ }
1310
+ async function resolveRouteState(match, state, currentUrl, routeModPromise, shellModPromise) {
1045
1311
  const routeMod = await (routeModPromise ?? startRouteImport(match));
1046
1312
  if (!routeMod) return null;
1047
1313
  let Shell = null;
@@ -1050,31 +1316,37 @@ async function initClientRouter(options) {
1050
1316
  const DefaultComponent = typeof routeMod.default === "function" ? routeMod.default : void 0;
1051
1317
  const Component = state.error ? routeMod.ErrorBoundary : routeMod.Component ?? DefaultComponent;
1052
1318
  if (!Component) return null;
1053
- const props = state.error ? { error: deserializeRouteError(state.error) } : {
1319
+ const componentProps = state.error ? { error: deserializeRouteError(state.error) } : {
1054
1320
  data: state.data,
1055
1321
  params: match.params
1056
1322
  };
1057
- const componentTree = Shell ? h(Shell, null, h(Component, props)) : h(Component, props);
1058
- return h(NavigateContext.Provider, { value: navigate }, h(PrachtRuntimeProvider, {
1323
+ return {
1324
+ Shell,
1325
+ Component,
1326
+ componentProps,
1059
1327
  data: state.data,
1060
1328
  params: match.params,
1061
1329
  routeId: match.route.id ?? "",
1062
- url: currentUrl
1063
- }, componentTree));
1330
+ url: currentUrl,
1331
+ version: ++routeStateVersion
1332
+ };
1064
1333
  }
1065
- async function buildSpaPendingTree(match, currentUrl, shellModPromise) {
1334
+ async function resolveSpaPendingState(match, currentUrl, shellModPromise) {
1066
1335
  const resolvedShell = await (shellModPromise ?? startShellImport(match));
1067
1336
  if (!resolvedShell) return null;
1068
- const Shell = resolvedShell.Shell;
1337
+ const Shell = resolvedShell.Shell ?? null;
1069
1338
  const Loading = resolvedShell.Loading;
1070
- const componentTree = Shell != null ? h(Shell, null, Loading ? h(Loading, null) : null) : Loading ? h(Loading, null) : null;
1071
- if (!componentTree) return null;
1072
- return h(NavigateContext.Provider, { value: navigate }, h(PrachtRuntimeProvider, {
1339
+ if (!Shell && !Loading) return null;
1340
+ return {
1341
+ Shell,
1342
+ Component: Loading ?? (() => null),
1343
+ componentProps: {},
1073
1344
  data: void 0,
1074
1345
  params: match.params,
1075
1346
  routeId: match.route.id ?? "",
1076
- url: currentUrl
1077
- }, componentTree));
1347
+ url: currentUrl,
1348
+ version: ++routeStateVersion
1349
+ };
1078
1350
  }
1079
1351
  function resolveRedirectTarget(location) {
1080
1352
  const targetUrl = new URL(location, window.location.href);
@@ -1151,21 +1423,12 @@ async function initClientRouter(options) {
1151
1423
  }
1152
1424
  if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "", target.browserUrl);
1153
1425
  else history.pushState(null, "", target.browserUrl);
1154
- const tree = await buildRouteTree(match, state, target.requestUrl, routeModPromise, shellModPromise);
1155
- if (tree) {
1156
- render(tree, root);
1426
+ const routeState = await resolveRouteState(match, state, target.requestUrl, routeModPromise, shellModPromise);
1427
+ if (routeState) {
1428
+ applyRouteState(routeState);
1157
1429
  window.scrollTo(0, 0);
1158
1430
  } else window.location.href = target.browserUrl;
1159
1431
  }
1160
- if (import.meta.env?.DEV) {
1161
- const prev = options.__m;
1162
- options.__m = (vnode, s) => {
1163
- const message = `Hydration mismatch in <${(typeof vnode.type === "function" ? vnode.type.displayName || vnode.type.name : vnode.type) || "Unknown"}>: ${s}`;
1164
- console.warn(`[pracht] ${message}`);
1165
- appendHydrationWarning(message);
1166
- if (prev) prev(vnode, s);
1167
- };
1168
- }
1169
1432
  const initialTarget = resolveBrowserRouteTarget(options.initialState.url);
1170
1433
  const initialRequestUrl = initialTarget?.requestUrl ?? options.initialState.url;
1171
1434
  const initialBrowserUrl = initialTarget?.browserUrl ?? options.initialState.url;
@@ -1177,9 +1440,9 @@ async function initClientRouter(options) {
1177
1440
  error: options.initialState.error ?? null
1178
1441
  };
1179
1442
  if (initialMatch.route.render === "spa" && options.initialState.pending) {
1180
- const dataPromise = fetchPrachtRouteState(initialRequestUrl);
1181
- const pendingTree = await buildSpaPendingTree(initialMatch, initialRequestUrl, initialShellPromise);
1182
- if (pendingTree) hydrate(pendingTree, root);
1443
+ const dataPromise = fetchPrachtRouteState(initialRequestUrl, { useDataParam: true });
1444
+ const pendingState = await resolveSpaPendingState(initialMatch, initialRequestUrl, initialShellPromise);
1445
+ if (pendingState) hydrate(h(RouterRoot, { initialState: pendingState }), root);
1183
1446
  try {
1184
1447
  const result = await dataPromise;
1185
1448
  if (result.type === "redirect") {
@@ -1198,12 +1461,15 @@ async function initClientRouter(options) {
1198
1461
  window.location.href = initialBrowserUrl;
1199
1462
  return;
1200
1463
  }
1201
- }
1202
- const tree = await buildRouteTree(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
1203
- if (tree) if (initialMatch.route.render === "spa") render(tree, root);
1204
- else {
1205
- markHydrating();
1206
- hydrate(tree, root);
1464
+ const resolvedState = await resolveRouteState(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
1465
+ if (resolvedState) applyRouteState(resolvedState);
1466
+ } else {
1467
+ const initialRouteState = await resolveRouteState(initialMatch, state, initialRequestUrl, void 0, initialShellPromise);
1468
+ if (initialRouteState) if (initialMatch.route.render === "spa") render(h(RouterRoot, { initialState: initialRouteState }), root);
1469
+ else {
1470
+ markHydrating();
1471
+ hydrate(h(RouterRoot, { initialState: initialRouteState }), root);
1472
+ }
1207
1473
  }
1208
1474
  }
1209
1475
  document.addEventListener("click", (e) => {
@@ -1238,61 +1504,6 @@ async function initClientRouter(options) {
1238
1504
  };
1239
1505
  setupPrefetching(app, warmModules);
1240
1506
  }
1241
- const HYDRATION_BANNER_ID = "__pracht_hydration_warnings__";
1242
- function appendHydrationWarning(message) {
1243
- let container = document.getElementById(HYDRATION_BANNER_ID);
1244
- if (!container) {
1245
- container = document.createElement("div");
1246
- container.id = HYDRATION_BANNER_ID;
1247
- Object.assign(container.style, {
1248
- position: "fixed",
1249
- bottom: "0",
1250
- left: "0",
1251
- right: "0",
1252
- maxHeight: "30vh",
1253
- overflow: "auto",
1254
- background: "#2d1b00",
1255
- borderTop: "2px solid #f0ad4e",
1256
- color: "#ffc107",
1257
- fontFamily: "ui-monospace, Consolas, monospace",
1258
- fontSize: "13px",
1259
- padding: "12px 16px",
1260
- zIndex: "2147483647"
1261
- });
1262
- const header = document.createElement("div");
1263
- Object.assign(header.style, {
1264
- display: "flex",
1265
- justifyContent: "space-between",
1266
- alignItems: "center",
1267
- marginBottom: "8px"
1268
- });
1269
- header.innerHTML = "<strong style=\"color:#f0ad4e\">⚠ Hydration Mismatches</strong>";
1270
- const close = document.createElement("button");
1271
- close.textContent = "×";
1272
- Object.assign(close.style, {
1273
- background: "none",
1274
- border: "none",
1275
- color: "#f0ad4e",
1276
- fontSize: "18px",
1277
- cursor: "pointer"
1278
- });
1279
- close.onclick = () => container.remove();
1280
- header.appendChild(close);
1281
- container.appendChild(header);
1282
- document.body.appendChild(container);
1283
- }
1284
- const entry = document.createElement("div");
1285
- entry.textContent = message;
1286
- Object.assign(entry.style, { padding: "2px 0" });
1287
- container.appendChild(entry);
1288
- }
1289
- function deserializeRouteError(error) {
1290
- const result = new Error(error.message);
1291
- result.name = error.name;
1292
- result.status = error.status;
1293
- result.diagnostics = error.diagnostics;
1294
- return result;
1295
- }
1296
1507
  function resolveBrowserRouteTarget(to) {
1297
1508
  if (typeof window === "undefined") return null;
1298
1509
  try {
@@ -1318,4 +1529,4 @@ var PrachtHttpError = class extends Error {
1318
1529
  }
1319
1530
  };
1320
1531
  //#endregion
1321
- 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 };
1532
+ 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, useRouteData };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/core",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "license": "MIT",
5
5
  "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/framework",
6
6
  "bugs": {
@@ -1,259 +0,0 @@
1
- //#region \0rolldown/runtime.js
2
- var __defProp = Object.defineProperty;
3
- var __exportAll = (all, no_symbols) => {
4
- let target = {};
5
- for (var name in all) __defProp(target, name, {
6
- get: all[name],
7
- enumerable: true
8
- });
9
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
- return target;
11
- };
12
- //#endregion
13
- //#region src/app.ts
14
- var app_exports = /* @__PURE__ */ __exportAll({
15
- buildPathFromSegments: () => buildPathFromSegments,
16
- defineApp: () => defineApp,
17
- group: () => group,
18
- matchApiRoute: () => matchApiRoute,
19
- matchAppRoute: () => matchAppRoute,
20
- resolveApiRoutes: () => resolveApiRoutes,
21
- resolveApp: () => resolveApp,
22
- route: () => route,
23
- timeRevalidate: () => timeRevalidate
24
- });
25
- function timeRevalidate(seconds) {
26
- if (!Number.isInteger(seconds) || seconds <= 0) throw new Error("timeRevalidate expects a positive integer number of seconds.");
27
- return {
28
- kind: "time",
29
- seconds
30
- };
31
- }
32
- function route(path, fileOrConfig, meta = {}) {
33
- if (typeof fileOrConfig === "string" || typeof fileOrConfig === "function") return {
34
- kind: "route",
35
- path: normalizeRoutePath(path),
36
- file: resolveModuleRef(fileOrConfig),
37
- ...meta
38
- };
39
- const { component, loader, ...routeMeta } = fileOrConfig;
40
- return {
41
- kind: "route",
42
- path: normalizeRoutePath(path),
43
- file: resolveModuleRef(component),
44
- loaderFile: resolveModuleRef(loader),
45
- ...routeMeta
46
- };
47
- }
48
- function resolveModuleRef(ref) {
49
- if (ref === void 0) return void 0;
50
- if (typeof ref === "string") return ref;
51
- return "";
52
- }
53
- function group(meta, routes) {
54
- return {
55
- kind: "group",
56
- meta,
57
- routes
58
- };
59
- }
60
- function defineApp(config) {
61
- return {
62
- shells: resolveModuleRefRecord(config.shells ?? {}),
63
- middleware: resolveModuleRefRecord(config.middleware ?? {}),
64
- api: config.api ?? {},
65
- routes: config.routes
66
- };
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
- }
73
- function resolveApp(app) {
74
- const routes = [];
75
- const inherited = {
76
- pathPrefix: "/",
77
- middleware: []
78
- };
79
- for (const node of app.routes) flattenRouteNode(app, node, inherited, routes);
80
- return {
81
- shells: app.shells,
82
- middleware: app.middleware,
83
- api: app.api,
84
- routes,
85
- apiRoutes: []
86
- };
87
- }
88
- function matchAppRoute(app, pathname) {
89
- const resolved = isResolvedApp(app) ? app : resolveApp(app);
90
- const normalizedPathname = normalizeRoutePath(pathname);
91
- const targetSegments = splitPathSegments(normalizedPathname);
92
- for (const currentRoute of resolved.routes) {
93
- const params = matchRouteSegments(currentRoute.segments, targetSegments);
94
- if (params) return {
95
- route: currentRoute,
96
- params,
97
- pathname: normalizedPathname
98
- };
99
- }
100
- }
101
- function flattenRouteNode(app, node, inherited, routes) {
102
- if (node.kind === "group") {
103
- const nextInherited = {
104
- pathPrefix: mergeRoutePaths(inherited.pathPrefix, node.meta.pathPrefix),
105
- shell: node.meta.shell ?? inherited.shell,
106
- render: node.meta.render ?? inherited.render,
107
- middleware: [...inherited.middleware, ...node.meta.middleware ?? []]
108
- };
109
- for (const child of node.routes) flattenRouteNode(app, child, nextInherited, routes);
110
- return;
111
- }
112
- const fullPath = mergeRoutePaths(inherited.pathPrefix, node.path);
113
- const shell = node.shell ?? inherited.shell;
114
- const middleware = [...inherited.middleware, ...node.middleware ?? []];
115
- routes.push({
116
- id: node.id ?? createRouteId(fullPath),
117
- path: fullPath,
118
- file: node.file,
119
- loaderFile: node.loaderFile,
120
- shell,
121
- shellFile: shell ? app.shells[shell] : void 0,
122
- render: node.render ?? inherited.render,
123
- middleware,
124
- middlewareFiles: middleware.flatMap((name) => {
125
- const middlewareFile = app.middleware[name];
126
- return middlewareFile ? [middlewareFile] : [];
127
- }),
128
- revalidate: node.revalidate,
129
- segments: parseRouteSegments(fullPath)
130
- });
131
- }
132
- function isResolvedApp(app) {
133
- return app.routes.length === 0 || "segments" in app.routes[0];
134
- }
135
- function matchRouteSegments(routeSegments, targetSegments) {
136
- const params = {};
137
- let routeIndex = 0;
138
- let targetIndex = 0;
139
- while (routeIndex < routeSegments.length) {
140
- const currentSegment = routeSegments[routeIndex];
141
- if (currentSegment.type === "catchall") {
142
- params[currentSegment.name] = targetSegments.slice(targetIndex).join("/");
143
- return params;
144
- }
145
- const targetSegment = targetSegments[targetIndex];
146
- if (typeof targetSegment === "undefined") return null;
147
- if (currentSegment.type === "static") {
148
- if (currentSegment.value !== targetSegment) return null;
149
- } else try {
150
- params[currentSegment.name] = decodeURIComponent(targetSegment);
151
- } catch {
152
- return null;
153
- }
154
- routeIndex += 1;
155
- targetIndex += 1;
156
- }
157
- return targetIndex === targetSegments.length ? params : null;
158
- }
159
- function parseRouteSegments(path) {
160
- return splitPathSegments(path).map((segment) => {
161
- if (segment === "*") return {
162
- type: "catchall",
163
- name: "*"
164
- };
165
- if (segment.startsWith(":")) return {
166
- type: "param",
167
- name: segment.slice(1)
168
- };
169
- return {
170
- type: "static",
171
- value: segment
172
- };
173
- });
174
- }
175
- function splitPathSegments(path) {
176
- return normalizeRoutePath(path).split("/").filter(Boolean);
177
- }
178
- function mergeRoutePaths(prefix, path) {
179
- if (!path) return normalizeRoutePath(prefix);
180
- const normalizedPrefix = normalizeRoutePath(prefix);
181
- const normalizedPath = normalizeRoutePath(path);
182
- if (normalizedPrefix === "/") return normalizedPath;
183
- if (normalizedPath === "/") return normalizedPrefix;
184
- return normalizeRoutePath(`${normalizedPrefix}/${normalizedPath.slice(1)}`);
185
- }
186
- function normalizeRoutePath(path) {
187
- if (!path || path === "/") return "/";
188
- const collapsed = (path.startsWith("/") ? path : `/${path}`).replace(/\/{2,}/g, "/");
189
- return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
190
- }
191
- function buildPathFromSegments(segments, params) {
192
- return normalizeRoutePath("/" + segments.map((segment) => {
193
- if (segment.type === "static") return segment.value;
194
- if (segment.type === "param") return encodeURIComponent(params[segment.name] ?? "");
195
- return params["*"] ?? "";
196
- }).join("/"));
197
- }
198
- /**
199
- * Convert a list of file paths from `import.meta.glob` into resolved API routes.
200
- *
201
- * Example: `"/src/api/health.ts"` → path `/api/health`
202
- * `"/src/api/users/[id].ts"` → path `/api/users/:id`
203
- * `"/src/api/index.ts"` → path `/api`
204
- */
205
- function resolveApiRoutes(files, apiDir = "/src/api") {
206
- const normalizedDir = apiDir.replace(/\/$/, "");
207
- return files.map((file) => {
208
- let relative = file;
209
- if (relative.startsWith(normalizedDir)) relative = relative.slice(normalizedDir.length);
210
- relative = relative.replace(/\.(ts|tsx|js|jsx)$/, "");
211
- if (relative.endsWith("/index")) relative = relative.slice(0, -6) || "/";
212
- relative = relative.replace(/\[([^\]]+)\]/g, ":$1");
213
- const path = normalizeRoutePath(`/api${relative}`);
214
- return {
215
- path,
216
- file,
217
- segments: parseRouteSegments(path)
218
- };
219
- }).sort(compareResolvedApiRoutes);
220
- }
221
- function matchApiRoute(apiRoutes, pathname) {
222
- const normalizedPathname = normalizeRoutePath(pathname);
223
- const targetSegments = splitPathSegments(normalizedPathname);
224
- for (const route of apiRoutes) {
225
- const params = matchRouteSegments(route.segments, targetSegments);
226
- if (params) return {
227
- route,
228
- params,
229
- pathname: normalizedPathname
230
- };
231
- }
232
- }
233
- function createRouteId(path) {
234
- if (path === "/") return "index";
235
- return path.slice(1).split("/").map((segment) => {
236
- if (segment === "*") return "splat";
237
- return segment.startsWith(":") ? segment.slice(1) : segment;
238
- }).join("-").replace(/[^a-zA-Z0-9-]/g, "-");
239
- }
240
- function compareResolvedApiRoutes(left, right) {
241
- const length = Math.max(left.segments.length, right.segments.length);
242
- for (let index = 0; index < length; index += 1) {
243
- const leftSegment = left.segments[index];
244
- const rightSegment = right.segments[index];
245
- if (!leftSegment) return 1;
246
- if (!rightSegment) return -1;
247
- const leftScore = getRouteSegmentSpecificity(leftSegment);
248
- const rightScore = getRouteSegmentSpecificity(rightSegment);
249
- if (leftScore !== rightScore) return rightScore - leftScore;
250
- }
251
- return left.path.localeCompare(right.path);
252
- }
253
- function getRouteSegmentSpecificity(segment) {
254
- if (segment.type === "static") return 3;
255
- if (segment.type === "param") return 2;
256
- return 1;
257
- }
258
- //#endregion
259
- export { matchApiRoute as a, resolveApp as c, group as i, route as l, buildPathFromSegments as n, matchAppRoute as o, defineApp as r, resolveApiRoutes as s, app_exports as t, timeRevalidate as u };