@pracht/core 0.8.0 → 0.9.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.
Files changed (38) hide show
  1. package/README.md +5 -1
  2. package/dist/app-BN3pjxtw.d.mts +24 -0
  3. package/dist/{app-BvC1uQG5.mjs → app-CGRKjZ6Z.mjs} +97 -8
  4. package/dist/app-graph-BlaCcGUZ.mjs +52 -0
  5. package/dist/app-graph-DBoDsHJ5.d.mts +38 -0
  6. package/dist/browser.d.mts +7 -6
  7. package/dist/browser.mjs +8 -7
  8. package/dist/client.d.mts +3 -3
  9. package/dist/client.mjs +3 -3
  10. package/dist/dev-404.d.mts +26 -0
  11. package/dist/dev-404.mjs +166 -0
  12. package/dist/devtools.d.mts +8 -0
  13. package/dist/devtools.mjs +156 -0
  14. package/dist/error-overlay.d.mts +38 -1
  15. package/dist/error-overlay.mjs +134 -5
  16. package/dist/index.d.mts +9 -7
  17. package/dist/index.mjs +9 -7
  18. package/dist/manifest.d.mts +2 -1
  19. package/dist/manifest.mjs +1 -1
  20. package/dist/{runtime-context-B5pREhcM.mjs → navigation-state-BjwXTeVz.mjs} +74 -2
  21. package/dist/{prefetch-B50kX9RL.mjs → prefetch-COYeuvQK.mjs} +44 -27
  22. package/dist/prefetch-api-Bf-P7XFo.d.mts +39 -0
  23. package/dist/prefetch-api-ChSCTbEB.mjs +69 -0
  24. package/dist/{prefetch-cache-DzP2Bj9H.mjs → prefetch-cache-BNLEp9H5.mjs} +12 -1
  25. package/dist/{prerender-B8_CqYwd.d.mts → prerender-BV42B9jK.d.mts} +31 -2
  26. package/dist/{prerender-D3E2H96p.mjs → prerender-C8BpANz3.mjs} +57 -15
  27. package/dist/{router-BcUmg1kB.d.mts → router-B_Aj5TtY.d.mts} +2 -2
  28. package/dist/router-E5Wh-dOR.mjs +694 -0
  29. package/dist/{runtime-context-ytVd7GmZ.d.mts → runtime-context-BYBwVA0M.d.mts} +1 -1
  30. package/dist/{runtime-middleware-aeBdgjYr.d.mts → runtime-middleware-pYubngnL.d.mts} +57 -4
  31. package/dist/server.d.mts +7 -5
  32. package/dist/server.mjs +6 -5
  33. package/dist/{app-BWriseLj.d.mts → types-B-sfVjaH.d.mts} +42 -22
  34. package/dist/{types-idmK5omD.mjs → types-DEPXdbyp.mjs} +42 -21
  35. package/package.json +9 -1
  36. package/dist/hydration-M1QzbQ1O.d.mts +0 -23
  37. package/dist/router-B7J4YYwg.mjs +0 -418
  38. /package/dist/{forwardRef-grZ6t4GS.mjs → forwardRef-B0cSkHwH.mjs} +0 -0
@@ -0,0 +1,156 @@
1
+ import { i as serializeAppRoutes, n as detectApiMethods, r as serializeApiRoutes, t as buildAppGraph } from "./app-graph-BlaCcGUZ.mjs";
2
+ //#region src/devtools.ts
3
+ const DEVTOOLS_PATH = "/_pracht";
4
+ const DEVTOOLS_JSON_PATH = "/_pracht.json";
5
+ function buildDevtoolsHtml(graph) {
6
+ const routeRows = graph.routes.map((route) => `<tr>
7
+ <td>${routeLinkHtml(route)}</td>
8
+ <td>${escapeHtml(route.render ?? "ssr")}</td>
9
+ <td>${escapeHtml(route.shell ?? "—")}</td>
10
+ <td>${escapeHtml(route.middleware.length > 0 ? route.middleware.join(" → ") : "—")}</td>
11
+ <td class="file">${escapeHtml(route.file)}</td>
12
+ </tr>`).join("\n");
13
+ const apiRows = graph.api.map((route) => `<tr>
14
+ <td>${apiLinkHtml(route)}</td>
15
+ <td>${escapeHtml(route.methods.length > 0 ? route.methods.join(", ") : "—")}</td>
16
+ <td class="file">${escapeHtml(route.file)}</td>
17
+ </tr>`).join("\n");
18
+ return `<!DOCTYPE html>
19
+ <html>
20
+ <head>
21
+ <meta charset="utf-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1">
23
+ <meta name="robots" content="noindex">
24
+ <title>pracht devtools</title>
25
+ <style>
26
+ * { margin: 0; padding: 0; box-sizing: border-box; }
27
+ body {
28
+ font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
29
+ background: #1a1a2e;
30
+ color: #e0e0e0;
31
+ padding: 32px;
32
+ line-height: 1.5;
33
+ }
34
+ .devtools {
35
+ max-width: 1100px;
36
+ margin: 0 auto;
37
+ }
38
+ .header {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 12px;
42
+ margin-bottom: 24px;
43
+ padding-bottom: 16px;
44
+ border-bottom: 1px solid #333;
45
+ }
46
+ .badge {
47
+ background: #4c6ef5;
48
+ color: #fff;
49
+ font-size: 11px;
50
+ font-weight: 700;
51
+ text-transform: uppercase;
52
+ letter-spacing: 0.05em;
53
+ padding: 4px 10px;
54
+ border-radius: 4px;
55
+ }
56
+ .title {
57
+ font-size: 14px;
58
+ color: #888;
59
+ }
60
+ .title a {
61
+ color: #a0c4ff;
62
+ }
63
+ h2 {
64
+ font-size: 14px;
65
+ font-weight: 600;
66
+ color: #a0c4ff;
67
+ margin: 24px 0 10px;
68
+ }
69
+ table {
70
+ width: 100%;
71
+ border-collapse: collapse;
72
+ font-size: 13px;
73
+ }
74
+ th {
75
+ text-align: left;
76
+ color: #888;
77
+ font-weight: 600;
78
+ padding: 6px 12px 6px 0;
79
+ border-bottom: 1px solid #333;
80
+ }
81
+ td {
82
+ padding: 6px 12px 6px 0;
83
+ border-bottom: 1px solid #26263e;
84
+ vertical-align: top;
85
+ word-break: break-word;
86
+ }
87
+ td a {
88
+ color: #74c0fc;
89
+ }
90
+ .file {
91
+ color: #888;
92
+ }
93
+ .empty {
94
+ font-size: 13px;
95
+ color: #888;
96
+ }
97
+ .hint {
98
+ margin-top: 24px;
99
+ font-size: 12px;
100
+ color: #666;
101
+ }
102
+ .hint a {
103
+ color: #a0c4ff;
104
+ }
105
+ </style>
106
+ </head>
107
+ <body>
108
+ <div class="devtools">
109
+ <div class="header">
110
+ <span class="badge">pracht</span>
111
+ <span class="title">devtools — resolved app graph (dev only)</span>
112
+ </div>
113
+ <h2>Page routes</h2>
114
+ <table>
115
+ <thead><tr><th>Route</th><th>Render</th><th>Shell</th><th>Middleware</th><th>Source</th></tr></thead>
116
+ <tbody>
117
+ ${routeRows}
118
+ </tbody>
119
+ </table>
120
+ ${graph.api.length > 0 ? `<h2>API routes</h2>
121
+ <table>
122
+ <thead><tr><th>Path</th><th>Methods</th><th>Source</th></tr></thead>
123
+ <tbody>
124
+ ${apiRows}
125
+ </tbody>
126
+ </table>` : `<h2>API routes</h2>
127
+ <p class="empty">No API routes found.</p>`}
128
+ <div class="hint">
129
+ Raw JSON at <a href="${DEVTOOLS_JSON_PATH}">${DEVTOOLS_JSON_PATH}</a> ·
130
+ same data as <code>pracht inspect --json</code> ·
131
+ per-request middleware/loader/render timings are on the <code>Server-Timing</code>
132
+ response header in the browser Network panel.
133
+ </div>
134
+ </div>
135
+ </body>
136
+ </html>`;
137
+ }
138
+ function routeLinkHtml(route) {
139
+ const label = escapeHtml(route.path);
140
+ if (!isLinkablePath(route.path)) return label;
141
+ return `<a href="${escapeHtml(route.path)}">${label}</a>`;
142
+ }
143
+ function apiLinkHtml(route) {
144
+ const label = escapeHtml(route.path);
145
+ if (!isLinkablePath(route.path) || !route.methods.includes("GET")) return label;
146
+ return `<a href="${escapeHtml(route.path)}">${label}</a>`;
147
+ }
148
+ /** Dynamic patterns (`:id`, `*`) are not navigable as-is — render them as text. */
149
+ function isLinkablePath(path) {
150
+ return !path.includes(":") && !path.includes("*");
151
+ }
152
+ function escapeHtml(str) {
153
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
154
+ }
155
+ //#endregion
156
+ export { DEVTOOLS_JSON_PATH, DEVTOOLS_PATH, buildAppGraph, buildDevtoolsHtml, detectApiMethods, serializeApiRoutes, serializeAppRoutes };
@@ -4,13 +4,50 @@
4
4
  *
5
5
  * Returns a standalone HTML document with inline styles and scripts.
6
6
  * Not a Preact component — must render even when Preact itself fails.
7
+ *
8
+ * Dev-only: the overlay is served exclusively by the vite-plugin dev SSR
9
+ * middleware, so it can rely on Vite's built-in `/__open-in-editor`
10
+ * endpoint (launch-editor middleware) to make stack frames clickable.
7
11
  */
8
12
  interface ErrorOverlayOptions {
9
13
  message: string;
10
14
  stack?: string;
11
15
  routeId?: string;
12
16
  file?: string;
17
+ /**
18
+ * Project root (Vite's `server.config.root`). Used to resolve
19
+ * dev-server URL paths such as `/src/routes/home.tsx` to filesystem
20
+ * paths for the open-in-editor links.
21
+ */
22
+ root?: string;
23
+ }
24
+ interface StackFrame {
25
+ /** The original stack line, unmodified. */
26
+ raw: string;
27
+ /** The exact `file:line:column` substring inside `raw`, when present. */
28
+ locationText?: string;
29
+ /** Normalized filesystem path suitable for `/__open-in-editor`. */
30
+ file?: string;
31
+ line?: number;
32
+ column?: number;
33
+ /** False for node_modules, `node:` internals, and Vite-internal frames. */
34
+ isApp: boolean;
13
35
  }
36
+ /**
37
+ * Parse a V8-style stack trace into frames. Non-frame lines (the message
38
+ * line, empty lines) are preserved as non-app frames without a location.
39
+ */
40
+ declare function parseStackFrames(stack: string, options?: {
41
+ root?: string;
42
+ }): StackFrame[];
43
+ /**
44
+ * Normalize a stack-frame path to a filesystem path that Vite's
45
+ * `/__open-in-editor` endpoint can open. Handles `file://` URLs,
46
+ * `http://` dev-server URLs, `/@fs/` prefixes, Vite query suffixes
47
+ * (`?t=123`, `?pracht-client`), and root-relative dev URLs like
48
+ * `/src/routes/home.tsx` (joined onto `root` when provided).
49
+ */
50
+ declare function normalizeStackFile(rawPath: string, root?: string): string | undefined;
14
51
  declare function buildErrorOverlayHtml(options: ErrorOverlayOptions): string;
15
52
  //#endregion
16
- export { ErrorOverlayOptions, buildErrorOverlayHtml };
53
+ export { ErrorOverlayOptions, StackFrame, buildErrorOverlayHtml, normalizeStackFile, parseStackFrames };
@@ -1,9 +1,75 @@
1
1
  //#region src/error-overlay.ts
2
+ const FRAME_PARENS = /^\s*at\s+(?:async\s+)?.*?\((.*)\)\s*$/;
3
+ const FRAME_BARE = /^\s*at\s+(?:async\s+)?(.*?)\s*$/;
4
+ const LOCATION = /^(.*?):(\d+):(\d+)$/;
5
+ const WINDOWS_DRIVE_PATH = /^\/?[A-Za-z]:[\\/]/;
6
+ /**
7
+ * Parse a V8-style stack trace into frames. Non-frame lines (the message
8
+ * line, empty lines) are preserved as non-app frames without a location.
9
+ */
10
+ function parseStackFrames(stack, options = {}) {
11
+ return stack.split("\n").map((line) => parseStackFrameLine(line, options.root));
12
+ }
13
+ function parseStackFrameLine(raw, root) {
14
+ const locationText = FRAME_PARENS.exec(raw)?.[1] ?? FRAME_BARE.exec(raw)?.[1];
15
+ if (!locationText) return {
16
+ raw,
17
+ isApp: false
18
+ };
19
+ const location = LOCATION.exec(locationText);
20
+ if (!location) return {
21
+ raw,
22
+ locationText,
23
+ isApp: !isInternalStackPath(locationText)
24
+ };
25
+ const [, rawPath, line, column] = location;
26
+ if (isInternalStackPath(rawPath)) return {
27
+ raw,
28
+ locationText,
29
+ isApp: false
30
+ };
31
+ return {
32
+ raw,
33
+ locationText,
34
+ file: normalizeStackFile(rawPath, root),
35
+ line: Number(line),
36
+ column: Number(column),
37
+ isApp: true
38
+ };
39
+ }
40
+ function isInternalStackPath(path) {
41
+ return path === "native" || path === "<anonymous>" || path.includes("(") || path.startsWith("node:") || path.startsWith("internal/") || path.startsWith("virtual:") || path.includes("\0") || path.includes("/node_modules/") || path.includes("\\node_modules\\") || path.includes("/@vite/");
42
+ }
43
+ /**
44
+ * Normalize a stack-frame path to a filesystem path that Vite's
45
+ * `/__open-in-editor` endpoint can open. Handles `file://` URLs,
46
+ * `http://` dev-server URLs, `/@fs/` prefixes, Vite query suffixes
47
+ * (`?t=123`, `?pracht-client`), and root-relative dev URLs like
48
+ * `/src/routes/home.tsx` (joined onto `root` when provided).
49
+ */
50
+ function normalizeStackFile(rawPath, root) {
51
+ let path = rawPath;
52
+ if (path.startsWith("http://") || path.startsWith("https://") || path.startsWith("file://")) try {
53
+ const url = new URL(path);
54
+ path = decodeURIComponent(url.pathname);
55
+ } catch {
56
+ return;
57
+ }
58
+ path = path.split("?")[0].split("#")[0];
59
+ if (path.startsWith("/@fs/")) path = path.slice(4);
60
+ if (WINDOWS_DRIVE_PATH.test(path) && path.startsWith("/")) path = path.slice(1);
61
+ if (!path) return void 0;
62
+ if (root && path.startsWith("/") && !WINDOWS_DRIVE_PATH.test(path)) {
63
+ const normalizedRoot = root.endsWith("/") ? root.slice(0, -1) : root;
64
+ if (path !== normalizedRoot && !path.startsWith(`${normalizedRoot}/`)) return `${normalizedRoot}${path}`;
65
+ }
66
+ return path;
67
+ }
2
68
  function buildErrorOverlayHtml(options) {
3
- const { message, stack, routeId, file } = options;
4
- const stackHtml = stack ? `<pre class="stack">${escapeHtml(stack)}</pre>` : "";
69
+ const { message, stack, routeId, file, root } = options;
70
+ const stackHtml = stack ? `<pre class="stack">${renderStackFrames(parseStackFrames(stack, { root }))}</pre>` : "";
5
71
  const routeHtml = routeId ? `<div class="meta"><span class="label">Route</span> <span class="value">${escapeHtml(routeId)}</span></div>` : "";
6
- const fileHtml = file ? `<div class="meta"><span class="label">File</span> <span class="value">${escapeHtml(file)}</span></div>` : "";
72
+ const fileHtml = file ? `<div class="meta"><span class="label">File</span> ${renderFileValue(file, root)}</div>` : "";
7
73
  return `<!DOCTYPE html>
8
74
  <html>
9
75
  <head>
@@ -75,6 +141,20 @@ function buildErrorOverlayHtml(options) {
75
141
  word-break: break-word;
76
142
  color: #ccc;
77
143
  }
144
+ .frame-internal {
145
+ opacity: 0.45;
146
+ }
147
+ .editor-link {
148
+ color: #a0c4ff;
149
+ text-decoration: underline;
150
+ text-decoration-style: dotted;
151
+ text-underline-offset: 3px;
152
+ cursor: pointer;
153
+ }
154
+ .editor-link:hover {
155
+ color: #d0e2ff;
156
+ text-decoration-style: solid;
157
+ }
78
158
  .hint {
79
159
  margin-top: 24px;
80
160
  font-size: 12px;
@@ -92,8 +172,19 @@ function buildErrorOverlayHtml(options) {
92
172
  ${routeHtml}
93
173
  ${fileHtml}
94
174
  ${stackHtml}
95
- <div class="hint">Fix the error and save — the page will reload automatically.</div>
175
+ <div class="hint">Click a stack frame to open it in your editor. Fix the error and save — the page will reload automatically.</div>
96
176
  </div>
177
+ <script>
178
+ // Open clicked stack frames in the editor via Vite's built-in
179
+ // /__open-in-editor endpoint (dev server only).
180
+ document.addEventListener("click", function (event) {
181
+ var target = event.target;
182
+ var link = target && target.closest ? target.closest("[data-editor-file]") : null;
183
+ if (!link) return;
184
+ event.preventDefault();
185
+ fetch("/__open-in-editor?file=" + encodeURIComponent(link.getAttribute("data-editor-file")));
186
+ });
187
+ <\/script>
97
188
  <script>
98
189
  // Auto-reload when Vite triggers a full reload (e.g. file saved after fix)
99
190
  if (import.meta.hot) {
@@ -105,8 +196,46 @@ function buildErrorOverlayHtml(options) {
105
196
  </body>
106
197
  </html>`;
107
198
  }
199
+ function renderStackFrames(frames) {
200
+ return frames.map(renderStackFrame).join("\n");
201
+ }
202
+ function renderStackFrame(frame) {
203
+ if (!frame.isApp) return frame.locationText ? `<span class="frame-internal">${escapeHtml(frame.raw)}</span>` : escapeHtml(frame.raw);
204
+ if (!frame.file || !frame.locationText) return escapeHtml(frame.raw);
205
+ const locationIndex = frame.raw.indexOf(frame.locationText);
206
+ if (locationIndex === -1) return escapeHtml(frame.raw);
207
+ const prefix = frame.raw.slice(0, locationIndex);
208
+ const suffix = frame.raw.slice(locationIndex + frame.locationText.length);
209
+ const link = renderEditorLink(frame.file, frame.line, frame.column, frame.locationText);
210
+ return `${escapeHtml(prefix)}${link}${escapeHtml(suffix)}`;
211
+ }
212
+ function renderEditorLink(file, line, column, label) {
213
+ let target = file;
214
+ if (line !== void 0) {
215
+ target += `:${line}`;
216
+ if (column !== void 0) target += `:${column}`;
217
+ }
218
+ return `<a class="editor-link" href="#" data-editor-file="${escapeHtml(target)}">${escapeHtml(label)}</a>`;
219
+ }
220
+ function renderFileValue(file, root) {
221
+ const resolved = resolveEditorFilePath(file, root);
222
+ if (!resolved) return `<span class="value">${escapeHtml(file)}</span>`;
223
+ return `<a class="value editor-link" href="#" data-editor-file="${escapeHtml(resolved)}">${escapeHtml(file)}</a>`;
224
+ }
225
+ /**
226
+ * Resolve the `file` metadata option (typically a manifest-relative path
227
+ * such as `./routes/home.tsx`) to a filesystem path for open-in-editor.
228
+ */
229
+ function resolveEditorFilePath(file, root) {
230
+ if (file.startsWith("./")) {
231
+ if (!root) return void 0;
232
+ return `${root.endsWith("/") ? root.slice(0, -1) : root}/src/${file.slice(2)}`;
233
+ }
234
+ if (file.startsWith("../")) return;
235
+ return normalizeStackFile(file, root);
236
+ }
108
237
  function escapeHtml(str) {
109
238
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
110
239
  }
111
240
  //#endregion
112
- export { buildErrorOverlayHtml };
241
+ export { buildErrorOverlayHtml, normalizeStackFile, parseStackFrames };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,10 @@
1
- import { $ as RouteComponentProps, A as HrefRouteDefinition, B as ModuleImporter, C as HeadAttributes, D as HrefArgs, E as HeadersArgs, G as PrachtAppConfig, H as ModuleRegistry, I as MiddlewareArgs, J as Register, K as PrachtHttpError, L as MiddlewareFn, M as LoaderArgs, N as LoaderData, O as HrefFn, P as LoaderFn, Q as ResolvedRoute, R as MiddlewareModule, S as HeadArgs, T as HeadScriptDescriptor, U as NavigateOptions, V as ModuleRef, W as PrachtApp, X as ResolvedApiRoute, Y as RenderMode, Z as ResolvedPrachtApp, _ as BuildHrefOptions, _t as ShellProps, a as matchApiRoute, at as RouteModule, b as GroupDefinition, c as resolveApp, ct as RouteParamsFor, d as ApiConfig, dt as RouteTarget, et as RouteConfig, f as ApiRouteArgs, ft as RouteTreeNode, g as BaseRouteArgs, gt as ShellModule, h as ApiRouteModule, ht as SearchParamsInput, i as group, it as RouteMeta, j as HttpMethod, k as HrefOptions, l as route, lt as RouteRevalidate, m as ApiRouteMatch, mt as SearchParamValue, n as buildPathFromSegments, nt as RouteId, o as matchAppRoute, ot as RouteParamInput, p as ApiRouteHandler, pt as SearchParamPrimitive, q as PrefetchStrategy, r as defineApp, rt as RouteMatch, s as resolveApiRoutes, st as RouteParams, t as buildHref, tt as RouteDefinition, u as timeRevalidate, ut as RouteSearchFor, v as DataModule, vt as TimeRevalidatePolicy, w as HeadMetadata, x as GroupMeta, y as ErrorBoundaryProps, z as MiddlewareNext } from "./app-BWriseLj.mjs";
2
- import { c as Link, d as useLocation, f as useParams, h as createHref, l as LinkProps, m as useRouteData, n as redirect, o as Form, p as useRevalidate, r as RouteStateResult, s as FormProps, t as RedirectOptions, u as Location } from "./runtime-middleware-aeBdgjYr.mjs";
3
- import { n as forwardRef, t as useIsHydrated } from "./hydration-M1QzbQ1O.mjs";
4
- import { a as startApp, c as SerializedRouteError, i as readHydrationState, n as PrachtRuntimeProvider, o as PrachtRuntimeDiagnosticPhase, r as StartAppOptions, s as PrachtRuntimeDiagnostics, t as PrachtHydrationState } from "./runtime-context-ytVd7GmZ.mjs";
5
- import { i as useNavigate, n as NavigateFn, r as initClientRouter, t as InitClientRouterOptions } from "./router-BcUmg1kB.mjs";
6
- import { a as prerenderApp, c as applyDefaultSecurityHeaders, i as PrerenderResult, n as PrerenderAppOptions, o as HandlePrachtRequestOptions, r as PrerenderAppResult, s as handlePrachtRequest, t as ISGManifestEntry } from "./prerender-B8_CqYwd.mjs";
1
+ import { $ as RouteParamInput, A as MiddlewareNext, B as RenderMode, C as LoaderArgs, D as MiddlewareArgs, F as PrachtApp, G as RouteConfig, H as ResolvedPrachtApp, I as PrachtAppConfig, J as RouteId, K as RouteDataFor, L as PrachtHttpError, M as ModuleRef, N as ModuleRegistry, O as MiddlewareFn, P as NavigateOptions, Q as RouteModule, R as PrefetchStrategy, S as LinkPrefetchStrategy, T as LoaderFn, U as ResolvedRoute, V as ResolvedApiRoute, W as RouteComponentProps, X as RouteMatch, Y as RouteLoaderData, Z as RouteMeta, _ as HrefArgs, a as ApiRouteModule, at as RouteTarget, b as HrefRouteDefinition, c as DataModule, ct as SearchParamValue, d as GroupMeta, dt as ShellProps, et as RouteParams, f as HeadArgs, ft as TimeRevalidatePolicy, g as HeadersArgs, h as HeadScriptDescriptor, i as ApiRouteMatch, j as ModuleImporter, k as MiddlewareModule, l as ErrorBoundaryProps, lt as SearchParamsInput, m as HeadMetadata, n as ApiRouteArgs, nt as RouteRevalidate, o as BaseRouteArgs, ot as RouteTreeNode, p as HeadAttributes, q as RouteDefinition, r as ApiRouteHandler, rt as RouteSearchFor, s as BuildHrefOptions, st as SearchParamPrimitive, t as ApiConfig, tt as RouteParamsFor, u as GroupDefinition, ut as ShellModule, v as HrefFn, w as LoaderData, x as HttpMethod, y as HrefOptions, z as Register } from "./types-B-sfVjaH.mjs";
2
+ 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, t as buildHref, u as timeRevalidate } from "./app-BN3pjxtw.mjs";
3
+ import { _ as NavigationLocation, c as Link, d as useLocation, f as useNavigation, g as Navigation, h as useRouteData, l as LinkProps, m as useRevalidate, n as redirect, o as Form, p as useParams, r as RouteStateResult, s as FormProps, t as RedirectOptions, u as Location, v as createHref } from "./runtime-middleware-pYubngnL.mjs";
4
+ import { i as forwardRef, n as prefetch, r as useIsHydrated, t as PrefetchFn } from "./prefetch-api-Bf-P7XFo.mjs";
5
+ import { a as startApp, c as SerializedRouteError, i as readHydrationState, n as PrachtRuntimeProvider, o as PrachtRuntimeDiagnosticPhase, r as StartAppOptions, s as PrachtRuntimeDiagnostics, t as PrachtHydrationState } from "./runtime-context-BYBwVA0M.mjs";
6
+ import { i as useNavigate, n as NavigateFn, r as initClientRouter, t as InitClientRouterOptions } from "./router-B_Aj5TtY.mjs";
7
+ import { a as buildAppGraph, c as serializeAppRoutes, i as AppGraphRoute, n as AppGraphApiRoute, o as detectApiMethods, r as AppGraphModuleAccess, s as serializeApiRoutes, t as AppGraph } from "./app-graph-DBoDsHJ5.mjs";
8
+ import { a as prerenderApp, c as applyDefaultSecurityHeaders, i as PrerenderResult, l as PrachtPhaseTimings, n as PrerenderAppOptions, o as HandlePrachtRequestOptions, r as PrerenderAppResult, s as handlePrachtRequest, t as ISGManifestEntry, u as formatServerTimingHeader } from "./prerender-BV42B9jK.mjs";
7
9
  import { Suspense, lazy } from "preact-suspense";
8
- export { type ApiConfig, type ApiRouteArgs, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type BaseRouteArgs, type BuildHrefOptions, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadAttributes, type HeadMetadata, type HeadScriptDescriptor, type HeadersArgs, type HrefArgs, type HrefFn, type HrefOptions, type HrefRouteDefinition, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, Link, type LinkProps, type LoaderArgs, type LoaderData, type LoaderFn, type Location, type MiddlewareArgs, type MiddlewareFn, type MiddlewareModule, type MiddlewareNext, type ModuleImporter, type ModuleRef, type ModuleRegistry, type NavigateFn, type NavigateOptions, type PrachtApp, type PrachtAppConfig, PrachtHttpError, type PrachtHydrationState, type PrachtRuntimeDiagnosticPhase, type PrachtRuntimeDiagnostics, PrachtRuntimeProvider, type PrefetchStrategy, type PrerenderAppOptions, type PrerenderAppResult, type PrerenderResult, type RedirectOptions, type Register, type RenderMode, type ResolvedApiRoute, type ResolvedPrachtApp, type ResolvedRoute, type RouteComponentProps, type RouteConfig, type RouteDefinition, type RouteId, type RouteMatch, type RouteMeta, type RouteModule, type RouteParamInput, type RouteParams, type RouteParamsFor, type RouteRevalidate, type RouteSearchFor, type RouteStateResult, type RouteTarget, type RouteTreeNode, type SearchParamPrimitive, type SearchParamValue, type SearchParamsInput, type SerializedRouteError, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildHref, buildPathFromSegments, createHref, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, redirect, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
10
+ export { type ApiConfig, type ApiRouteArgs, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type AppGraph, type AppGraphApiRoute, type AppGraphModuleAccess, type AppGraphRoute, type BaseRouteArgs, type BuildHrefOptions, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadAttributes, type HeadMetadata, type HeadScriptDescriptor, type HeadersArgs, type HrefArgs, type HrefFn, type HrefOptions, type HrefRouteDefinition, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, Link, type LinkPrefetchStrategy, type LinkProps, type LoaderArgs, type LoaderData, type LoaderFn, type Location, type MiddlewareArgs, type MiddlewareFn, type MiddlewareModule, type MiddlewareNext, type ModuleImporter, type ModuleRef, type ModuleRegistry, type NavigateFn, type NavigateOptions, type Navigation, type NavigationLocation, type PrachtApp, type PrachtAppConfig, PrachtHttpError, type PrachtHydrationState, type PrachtPhaseTimings, type PrachtRuntimeDiagnosticPhase, type PrachtRuntimeDiagnostics, PrachtRuntimeProvider, type PrefetchFn, type PrefetchStrategy, type PrerenderAppOptions, type PrerenderAppResult, type PrerenderResult, type RedirectOptions, type Register, type RenderMode, type ResolvedApiRoute, type ResolvedPrachtApp, type ResolvedRoute, type RouteComponentProps, type RouteConfig, type RouteDataFor, type RouteDefinition, type RouteId, type RouteLoaderData, type RouteMatch, type RouteMeta, type RouteModule, type RouteParamInput, type RouteParams, type RouteParamsFor, type RouteRevalidate, type RouteSearchFor, type RouteStateResult, type RouteTarget, type RouteTreeNode, type SearchParamPrimitive, type SearchParamValue, type SearchParamsInput, type SerializedRouteError, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildAppGraph, buildHref, buildPathFromSegments, createHref, defineApp, detectApiMethods, formatServerTimingHeader, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prefetch, prerenderApp, readHydrationState, redirect, resolveApiRoutes, resolveApp, route, serializeApiRoutes, serializeAppRoutes, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useNavigation, useParams, useRevalidate, useRouteData };
package/dist/index.mjs CHANGED
@@ -1,8 +1,10 @@
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, t as buildHref, u as timeRevalidate } from "./app-BvC1uQG5.mjs";
2
- import { S as createHref, _ as applyDefaultSecurityHeaders, a as useParams, i as useLocation, n as Form, o as useRevalidate, r as Link, s as useRouteData, t as PrachtHttpError, u as redirect } from "./types-idmK5omD.mjs";
3
- import { t as forwardRef } from "./forwardRef-grZ6t4GS.mjs";
4
- import { n as useNavigate, r as useIsHydrated, t as initClientRouter } from "./router-B7J4YYwg.mjs";
5
- import { i as startApp, r as readHydrationState, t as PrachtRuntimeProvider } from "./runtime-context-B5pREhcM.mjs";
6
- import { n as handlePrachtRequest, t as prerenderApp } from "./prerender-D3E2H96p.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, t as buildHref, u as timeRevalidate } from "./app-CGRKjZ6Z.mjs";
2
+ import { C as createHref, a as useNavigation, c as useRouteData, d as redirect, i as useLocation, n as Form, o as useParams, r as Link, s as useRevalidate, t as PrachtHttpError, v as applyDefaultSecurityHeaders } from "./types-DEPXdbyp.mjs";
3
+ import { i as serializeAppRoutes, n as detectApiMethods, r as serializeApiRoutes, t as buildAppGraph } from "./app-graph-BlaCcGUZ.mjs";
4
+ import { t as forwardRef } from "./forwardRef-B0cSkHwH.mjs";
5
+ import { n as useNavigate, r as useIsHydrated, t as initClientRouter } from "./router-E5Wh-dOR.mjs";
6
+ import { l as readHydrationState, s as PrachtRuntimeProvider, u as startApp } from "./navigation-state-BjwXTeVz.mjs";
7
+ import { n as handlePrachtRequest, r as formatServerTimingHeader, t as prerenderApp } from "./prerender-C8BpANz3.mjs";
8
+ import { t as prefetch } from "./prefetch-api-ChSCTbEB.mjs";
7
9
  import { Suspense, lazy } from "preact-suspense";
8
- export { Form, Link, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildHref, buildPathFromSegments, createHref, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, redirect, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
10
+ export { Form, Link, PrachtHttpError, PrachtRuntimeProvider, Suspense, applyDefaultSecurityHeaders, buildAppGraph, buildHref, buildPathFromSegments, createHref, defineApp, detectApiMethods, formatServerTimingHeader, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prefetch, prerenderApp, readHydrationState, redirect, resolveApiRoutes, resolveApp, route, serializeApiRoutes, serializeAppRoutes, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useNavigation, useParams, useRevalidate, useRouteData };
@@ -1,2 +1,3 @@
1
- import { G as PrachtAppConfig, V as ModuleRef, W as PrachtApp, Y as RenderMode, b as GroupDefinition, et as RouteConfig, ft as RouteTreeNode, i as group, it as RouteMeta, l as route, lt as RouteRevalidate, r as defineApp, tt as RouteDefinition, u as timeRevalidate, vt as TimeRevalidatePolicy, x as GroupMeta } from "./app-BWriseLj.mjs";
1
+ import { B as RenderMode, F as PrachtApp, G as RouteConfig, I as PrachtAppConfig, M as ModuleRef, Z as RouteMeta, d as GroupMeta, ft as TimeRevalidatePolicy, nt as RouteRevalidate, ot as RouteTreeNode, q as RouteDefinition, u as GroupDefinition } from "./types-B-sfVjaH.mjs";
2
+ import { i as group, l as route, r as defineApp, u as timeRevalidate } from "./app-BN3pjxtw.mjs";
2
3
  export { type GroupDefinition, type GroupMeta, type ModuleRef, type PrachtApp, type PrachtAppConfig, type RenderMode, type RouteConfig, type RouteDefinition, type RouteMeta, type RouteRevalidate, type RouteTreeNode, type TimeRevalidatePolicy, defineApp, group, route, timeRevalidate };
package/dist/manifest.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { i as group, l as route, r as defineApp, u as timeRevalidate } from "./app-BvC1uQG5.mjs";
1
+ import { i as group, l as route, r as defineApp, u as timeRevalidate } from "./app-CGRKjZ6Z.mjs";
2
2
  export { defineApp, group, route, timeRevalidate };
@@ -1,4 +1,4 @@
1
- import { d as EMPTY_ROUTE_PARAMS, f as HYDRATION_STATE_ELEMENT_ID } from "./prefetch-cache-DzP2Bj9H.mjs";
1
+ import { f as EMPTY_ROUTE_PARAMS, p as HYDRATION_STATE_ELEMENT_ID } from "./prefetch-cache-BNLEp9H5.mjs";
2
2
  import { createContext, h } from "preact";
3
3
  import { useEffect, useMemo, useState } from "preact/hooks";
4
4
  //#region src/runtime-errors.ts
@@ -161,4 +161,76 @@ function registerRuntimeRoutes(routes) {
161
161
  globalThis.__PRACHT_ROUTE_DEFINITIONS__ = routes;
162
162
  }
163
163
  //#endregion
164
- export { buildRuntimeDiagnostics as a, normalizeRouteError as c, startApp as i, shouldExposeServerErrors as l, RouteDataContext as n, createSerializedRouteError as o, readHydrationState as r, deserializeRouteError as s, PrachtRuntimeProvider as t };
164
+ //#region src/navigation-state.ts
165
+ const IDLE_NAVIGATION = { state: "idle" };
166
+ let currentNavigation = IDLE_NAVIGATION;
167
+ let navigationToken = 0;
168
+ const listeners = /* @__PURE__ */ new Set();
169
+ function getNavigation() {
170
+ return currentNavigation;
171
+ }
172
+ function subscribeToNavigation(listener) {
173
+ listeners.add(listener);
174
+ return () => {
175
+ listeners.delete(listener);
176
+ };
177
+ }
178
+ function emit() {
179
+ const snapshot = Array.from(listeners);
180
+ for (const listener of snapshot) listener(currentNavigation);
181
+ }
182
+ /**
183
+ * Mark a navigation as in-flight. Returns a token that must be passed to
184
+ * `settleNavigation()` — settling is a no-op when a newer navigation or
185
+ * submission has started in the meantime, so superseded navigations never
186
+ * stomp the state of the one that replaced them.
187
+ */
188
+ function beginLoadingNavigation(location) {
189
+ currentNavigation = {
190
+ state: "loading",
191
+ location
192
+ };
193
+ emit();
194
+ return ++navigationToken;
195
+ }
196
+ function beginSubmittingNavigation(location, formData) {
197
+ currentNavigation = {
198
+ state: "submitting",
199
+ location,
200
+ formData
201
+ };
202
+ emit();
203
+ return ++navigationToken;
204
+ }
205
+ function settleNavigation(token) {
206
+ if (token !== navigationToken) return;
207
+ if (currentNavigation.state === "idle") return;
208
+ currentNavigation = IDLE_NAVIGATION;
209
+ emit();
210
+ }
211
+ /**
212
+ * Parse a navigation target (relative or absolute) into the location shape
213
+ * exposed through `useNavigation()`.
214
+ */
215
+ function createNavigationLocation(url) {
216
+ const base = typeof window !== "undefined" ? window.location.href : "http://pracht.local";
217
+ let parsed;
218
+ try {
219
+ parsed = new URL(url, base);
220
+ } catch {
221
+ return {
222
+ hash: "",
223
+ href: url,
224
+ pathname: url,
225
+ search: ""
226
+ };
227
+ }
228
+ return {
229
+ hash: parsed.hash,
230
+ href: parsed.pathname + parsed.search + parsed.hash,
231
+ pathname: parsed.pathname,
232
+ search: parsed.search
233
+ };
234
+ }
235
+ //#endregion
236
+ export { settleNavigation as a, RouteDataContext as c, buildRuntimeDiagnostics as d, createSerializedRouteError as f, shouldExposeServerErrors as h, getNavigation as i, readHydrationState as l, normalizeRouteError as m, beginSubmittingNavigation as n, subscribeToNavigation as o, deserializeRouteError as p, createNavigationLocation as r, PrachtRuntimeProvider as s, beginLoadingNavigation as t, startApp as u };
@@ -1,18 +1,18 @@
1
- import { o as matchAppRoute } from "./app-BvC1uQG5.mjs";
2
- import { a as trimMapToSize, i as getCachedRouteState, n as cacheRouteState, s as fetchPrachtRouteState, t as EMPTY_ROUTE_STATE_PROMISE, u as routeNeedsServerFetch } from "./prefetch-cache-DzP2Bj9H.mjs";
1
+ import { o as matchAppRoute } from "./app-CGRKjZ6Z.mjs";
2
+ import { m as PREFETCH_ATTRIBUTE, o as trimMapToSize } from "./prefetch-cache-BNLEp9H5.mjs";
3
+ import { n as prefetchRouteState } from "./prefetch-api-ChSCTbEB.mjs";
3
4
  //#region src/prefetch.ts
4
5
  const MAX_MATCH_CACHE_ENTRIES = 250;
5
- function prefetchRouteState(url, route) {
6
- if (route && !routeNeedsServerFetch(route)) return EMPTY_ROUTE_STATE_PROMISE;
7
- const cached = getCachedRouteState(url);
8
- if (cached) return cached;
9
- const promise = fetchPrachtRouteState(url);
10
- cacheRouteState(url, promise);
11
- return promise;
12
- }
6
+ const LINK_PREFETCH_STRATEGIES = new Set([
7
+ "none",
8
+ "hover",
9
+ "intent",
10
+ "viewport",
11
+ "render"
12
+ ]);
13
13
  function setupPrefetching(app, warmModules) {
14
14
  let hoverTimer = null;
15
- const observedViewportAnchors = /* @__PURE__ */ new WeakSet();
15
+ const processedAnchors = /* @__PURE__ */ new WeakSet();
16
16
  const matchCache = /* @__PURE__ */ new Map();
17
17
  function getRoutePathname(url) {
18
18
  try {
@@ -50,17 +50,29 @@ function setupPrefetching(app, warmModules) {
50
50
  trimMapToSize(matchCache, MAX_MATCH_CACHE_ENTRIES);
51
51
  return entry;
52
52
  }
53
+ /**
54
+ * Per-anchor `data-pracht-prefetch` (rendered by `<Link prefetch>`) wins
55
+ * over the route-level strategy; unmatched hrefs are never prefetched.
56
+ */
57
+ function getAnchorStrategy(anchor, href) {
58
+ const entry = getMatchEntry(href);
59
+ if (!entry.match) return "none";
60
+ const override = anchor.getAttribute(PREFETCH_ATTRIBUTE);
61
+ if (override && LINK_PREFETCH_STRATEGIES.has(override)) return override;
62
+ return entry.strategy;
63
+ }
53
64
  function prefetchHref(href) {
54
65
  const match = getMatchEntry(href).match;
55
- prefetchRouteState(href, match?.route);
56
- if (warmModules && match) warmModules(match);
66
+ if (!match) return;
67
+ prefetchRouteState(href, match.route);
68
+ if (warmModules) warmModules(match);
57
69
  }
58
70
  document.addEventListener("mouseenter", (e) => {
59
71
  const anchor = e.target.closest?.("a");
60
72
  if (!anchor) return;
61
73
  const href = getInternalHref(anchor);
62
74
  if (!href) return;
63
- const strategy = getMatchEntry(href).strategy;
75
+ const strategy = getAnchorStrategy(anchor, href);
64
76
  if (strategy !== "hover" && strategy !== "intent") return;
65
77
  if (hoverTimer) clearTimeout(hoverTimer);
66
78
  hoverTimer = setTimeout(() => {
@@ -79,36 +91,41 @@ function setupPrefetching(app, warmModules) {
79
91
  if (!anchor) return;
80
92
  const href = getInternalHref(anchor);
81
93
  if (!href) return;
82
- const strategy = getMatchEntry(href).strategy;
94
+ const strategy = getAnchorStrategy(anchor, href);
83
95
  if (strategy !== "hover" && strategy !== "intent") return;
84
96
  prefetchHref(href);
85
97
  }, true);
86
- if (typeof IntersectionObserver === "undefined") return;
87
- const observer = new IntersectionObserver((entries) => {
98
+ const observer = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
88
99
  for (const entry of entries) {
89
100
  if (!entry.isIntersecting) continue;
90
101
  const anchor = entry.target;
91
102
  const href = getInternalHref(anchor);
92
103
  if (!href) continue;
93
104
  prefetchHref(href);
94
- observer.unobserve(anchor);
105
+ observer?.unobserve(anchor);
95
106
  }
96
107
  }, { rootMargin: "200px" });
97
- function observeAnchor(anchor) {
98
- if (observedViewportAnchors.has(anchor)) return;
108
+ function processAnchor(anchor) {
109
+ if (processedAnchors.has(anchor)) return;
99
110
  const href = getInternalHref(anchor);
100
111
  if (!href) return;
101
- if (getMatchEntry(href).strategy !== "viewport") return;
102
- observedViewportAnchors.add(anchor);
112
+ const strategy = getAnchorStrategy(anchor, href);
113
+ if (strategy === "render") {
114
+ processedAnchors.add(anchor);
115
+ prefetchHref(href);
116
+ return;
117
+ }
118
+ if (strategy !== "viewport" || !observer) return;
119
+ processedAnchors.add(anchor);
103
120
  observer.observe(anchor);
104
121
  }
105
- function observeViewportLinks(root) {
106
- if (root instanceof HTMLAnchorElement) observeAnchor(root);
107
- for (const anchor of root.querySelectorAll("a[href]")) observeAnchor(anchor);
122
+ function processAnchors(root) {
123
+ if (root instanceof HTMLAnchorElement) processAnchor(root);
124
+ for (const anchor of root.querySelectorAll("a[href]")) processAnchor(anchor);
108
125
  }
109
- observeViewportLinks(document.body);
126
+ processAnchors(document.body);
110
127
  new MutationObserver((records) => {
111
- for (const record of records) for (const node of record.addedNodes) if (node instanceof HTMLElement || node instanceof DocumentFragment) observeViewportLinks(node);
128
+ for (const record of records) for (const node of record.addedNodes) if (node instanceof HTMLElement || node instanceof DocumentFragment) processAnchors(node);
112
129
  }).observe(document.body, {
113
130
  childList: true,
114
131
  subtree: true