@pracht/core 0.0.1 → 0.1.0

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