@pracht/vite-plugin 0.9.0 → 0.11.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/dist/index.d.mts CHANGED
@@ -135,8 +135,53 @@ interface PrachtLlmsTxtOptions {
135
135
  * ```
136
136
  */
137
137
  exclude?: string[];
138
+ /**
139
+ * Ceiling on how many prerendered instances a single dynamic route
140
+ * contributes to the Pages section. Defaults to 50; `0` lists every
141
+ * instance.
142
+ *
143
+ * The instances kept are the first ones `getStaticPaths()` returns, after
144
+ * `exclude` is applied — the author's order, which for a blog is usually
145
+ * newest-first.
146
+ *
147
+ * llms.txt is an index, not a sitemap. A 5,000-post blog expanded through
148
+ * `getStaticPaths()` produces a 5,000-line, 180 KB file — larger than most
149
+ * agent context budgets. Truncation is never silent: a line above the Pages
150
+ * section names the route and the ratio it lists.
151
+ */
152
+ maxPagesPerRoute?: number;
153
+ }
154
+ /**
155
+ * Optional client-router features, compiled out of the client bundle when
156
+ * disabled. Every one defaults to `true`. Turn a feature off only when the app
157
+ * really does not use it: the router silently stops honouring the
158
+ * corresponding route options and `<Link>` props.
159
+ */
160
+ interface PrachtClientOptions {
161
+ /**
162
+ * JS prefetching of route-state JSON and route/shell chunks, driven by
163
+ * `route({ prefetch })` and `<Link prefetch>`. Off also drops the separate
164
+ * prefetch chunk the router loads on every page, and makes the imperative
165
+ * `prefetch()` export a no-op.
166
+ */
167
+ prefetch?: boolean;
138
168
  }
139
169
  interface PrachtPluginOptions {
170
+ /**
171
+ * Switch off client-router features the app does not use, so they are
172
+ * compiled out of the client bundle. See {@link PrachtClientOptions}.
173
+ */
174
+ client?: PrachtClientOptions;
175
+ /**
176
+ * Group the Preact runtime into a shared `vendor` chunk. Defaults to `true`.
177
+ *
178
+ * The group is appended to whatever `build.rollupOptions.output` chunking
179
+ * the app configures, so app-level grouping and the framework chunk compose
180
+ * (see `frameworkChunkGroups()`). Set `false` to contribute nothing at all —
181
+ * for an app that places `frameworkChunkGroups()` itself, or one that wants
182
+ * Preact merged into its own chunks.
183
+ */
184
+ vendorChunk?: boolean;
140
185
  appFile?: string;
141
186
  routesDir?: string;
142
187
  shellsDir?: string;
@@ -198,6 +243,50 @@ interface PrachtPluginOptions {
198
243
  llmsTxt?: false | PrachtLlmsTxtOptions;
199
244
  }
200
245
  //#endregion
246
+ //#region src/chunk-groups.d.ts
247
+ /**
248
+ * Pracht's client chunking policy, expressed as something an app can build on.
249
+ *
250
+ * The framework has exactly one opinion here: Preact belongs in its own chunk,
251
+ * shared by every route and cached across deploys that only change app code.
252
+ * Everything else about chunking is the app's call — merging the long tail of
253
+ * small initial chunks, splitting a heavy dependency out of a route, grouping
254
+ * by feature.
255
+ *
256
+ * Those two have to coexist, and under Rolldown that is not automatic:
257
+ * `output.codeSplitting` makes `manualChunks` and `advancedChunks` ignored
258
+ * outright, so a plugin that hard-codes one form silently deletes whichever
259
+ * form the app used. Pracht therefore looks at what the app configured and
260
+ * contributes its group in the same form, as one entry appended to the app's
261
+ * list rather than as a replacement for it.
262
+ *
263
+ * Precedence follows Rolldown's own rule — higher `priority` first, then
264
+ * declaration order. The app's groups are declared first, so an app group that
265
+ * would also capture Preact wins at equal priority, and pracht's group only
266
+ * takes what nothing else claimed. To keep the framework chunk intact while
267
+ * merging everything around it, give the app group a `test` that excludes
268
+ * Preact, or raise pracht's group by placing {@link frameworkChunkGroups}
269
+ * explicitly and setting `vendorChunk: false`.
270
+ */
271
+ /** Name of the chunk pracht groups the Preact runtime into. */
272
+ declare const FRAMEWORK_VENDOR_CHUNK = "vendor";
273
+ interface ChunkGroup {
274
+ name: string;
275
+ test?: RegExp | string;
276
+ priority?: number;
277
+ minSize?: number;
278
+ [option: string]: unknown;
279
+ }
280
+ /**
281
+ * Pracht's chunk groups, as a fresh array an app can place in its own
282
+ * `output.codeSplitting.groups`.
283
+ *
284
+ * Use this together with `pracht({ vendorChunk: false })` when the framework
285
+ * group has to sit somewhere other than last — pracht then contributes no
286
+ * chunking config of its own and the app's list is the whole policy.
287
+ */
288
+ declare function frameworkChunkGroups(): ChunkGroup[];
289
+ //#endregion
201
290
  //#region src/plugin-codegen.d.ts
202
291
  declare function createPrachtClientModuleSource(options?: PrachtPluginOptions, buildOptions?: {
203
292
  root?: string;
@@ -224,10 +313,12 @@ interface ExtractedCapability {
224
313
  name: string;
225
314
  /** Manifest-relative module path, e.g. "./capabilities/notes-search.ts". */
226
315
  file: string;
316
+ title: string;
227
317
  description: string;
228
318
  effect: string | null;
229
319
  httpPath: string | null;
230
320
  webmcp: boolean;
321
+ webmcpUntrustedContent: boolean;
231
322
  inputSchema: Record<string, unknown> | null;
232
323
  }
233
324
  /**
@@ -256,9 +347,17 @@ declare function createPrachtCapabilitiesClientModuleSource(options?: PrachtPlug
256
347
  * validation/middleware/policy stays server-side. Each dispatch carries the
257
348
  * transport marker header so audit events can attribute it to WebMCP.
258
349
  *
259
- * Targets the Chrome origin-trial API: `document.modelContext.registerTool()`
260
- * (Chrome 150+; `navigator.modelContext` is the deprecated pre-150 location
261
- * and is kept as a fallback). No-ops silently when the API is absent.
350
+ * Targets the WebMCP CG draft API: `document.modelContext.registerTool()`
351
+ * (ChatGPT desktop's built-in browser; Chromium 150+ within the 149–156
352
+ * origin trial the `document` getter landed in 150 and the deprecated
353
+ * `navigator.modelContext` alias was removed in 152, so trial builds before
354
+ * 150 are not targeted and no fallback is kept; current polyfills install the
355
+ * `document` shape). No-ops silently when the API is absent.
356
+ *
357
+ * `execute()` returns the capability envelope (`{ ok, data }` /
358
+ * `{ ok: false, error }`) as a plain object: per the spec the host serializes
359
+ * the returned value itself, so wrapping it in MCP-style content blocks would
360
+ * reach the agent double-encoded.
262
361
  */
263
362
  declare function createPrachtWebmcpModuleSource(options?: PrachtPluginOptions, buildOptions?: {
264
363
  root?: string;
@@ -267,4 +366,4 @@ declare function createPrachtWebmcpModuleSource(options?: PrachtPluginOptions, b
267
366
  //#region src/index.d.ts
268
367
  declare function pracht(options?: PrachtPluginOptions): Plugin[];
269
368
  //#endregion
270
- export { type EnvLeakReference, type EnvSafetyOptions, type LlmsTxtSection, PRACHT_CAPABILITIES_MODULE_ID, PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PRACHT_WEBMCP_MODULE_ID, PUBLIC_ENV_PREFIX, type PrachtAdapter, type PrachtLlmsTxtOptions, type PrachtPluginOptions, type RenderMode, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtCapabilitiesClientModuleSource, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, createPrachtWebmcpModuleSource, extractCapabilities, formatEnvLeakError, pracht, scanCodeForEnvLeaks };
369
+ export { type ChunkGroup, type EnvLeakReference, type EnvSafetyOptions, FRAMEWORK_VENDOR_CHUNK, type LlmsTxtSection, PRACHT_CAPABILITIES_MODULE_ID, PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PRACHT_WEBMCP_MODULE_ID, PUBLIC_ENV_PREFIX, type PrachtAdapter, type PrachtClientOptions, type PrachtLlmsTxtOptions, type PrachtPluginOptions, type RenderMode, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtCapabilitiesClientModuleSource, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, createPrachtWebmcpModuleSource, extractCapabilities, formatEnvLeakError, frameworkChunkGroups, pracht, scanCodeForEnvLeaks };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as createRouteStaticPathsHints, d as extensionGlob, f as normalizeAdditionalExtensions, i as scanPagesDirectory, l as DEFAULT_ROUTE_EXTENSIONS, n as generatePagesManifestSource, o as createRouteHeadHints, p as withAdditionalExtensions, s as createRouteLoaderHints, u as LEGACY_BARE_ROUTE_EXTENSIONS } from "./pages-router-KcCRkhnf.mjs";
1
+ import { c as createRouteLoaderHints, d as LEGACY_BARE_ROUTE_EXTENSIONS, f as extensionGlob, i as scanPagesDirectory, l as createRouteStaticPathsHints, m as withAdditionalExtensions, n as generatePagesManifestSource, o as createRouteHeadHints, p as normalizeAdditionalExtensions, s as createRouteHeadersHints, u as DEFAULT_ROUTE_EXTENSIONS } from "./pages-router-MA9rOl88.mjs";
2
2
  import { createRequire, isBuiltin } from "node:module";
3
3
  import { preactSsrPrecompile } from "@pracht/preact-ssr-precompile";
4
4
  import preact from "@preact/preset-vite";
@@ -6,6 +6,7 @@ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "n
6
6
  import { dirname, extname, join, resolve } from "node:path";
7
7
  import { loadEnv, parseAst } from "vite";
8
8
  import { PRACHT_GRAPH_ONLY_ENV } from "@pracht/core/server";
9
+ import { DEV_ROUTE_DATA_STALE_EVENT } from "@pracht/core/client";
9
10
  import { CAPABILITY_SETTLED_EVENT, CAPABILITY_TRANSPORT_HEADER, CONFIRMATION_HEADER } from "@pracht/capabilities";
10
11
  import { extractCapabilityProjection, extractCapabilityRegistrations, extractDefineAppObjectBody, scanTopLevelProperties } from "@pracht/capabilities/static";
11
12
  import { createNodeServerEntryModule } from "@pracht/adapter-node";
@@ -26,6 +27,47 @@ function stripPrachtClientModuleQuery(id) {
26
27
  const query = id.slice(queryStart + 1).split("&").filter((part) => part !== CLIENT_MODULE_QUERY);
27
28
  return query.length > 0 ? `${path}?${query.join("&")}` : path;
28
29
  }
30
+ /** Extensions `@prefresh/vite` accepts: `/\.(c|m)?(t|j)sx?$/`, anchored at end. */
31
+ const PREFRESH_EXTENSION_RE = /\.((?:c|m)?[tj]sx?)$/i;
32
+ function isPrefreshCompatibleId(id) {
33
+ return PREFRESH_EXTENSION_RE.test(id);
34
+ }
35
+ /**
36
+ * The id to hand `@prefresh/vite` for a pracht client module.
37
+ *
38
+ * Prefresh uses the id for exactly three things: its `/\.(c|m)?(t|j)sx?$/`
39
+ * filter, a `/\.tsx?$/` check that picks the TypeScript parser plugin, and the
40
+ * key it embeds in the `$RefreshReg$` it injects. A query-carrying id fails the
41
+ * first two, which is why route and shell modules got no Fast Refresh at all —
42
+ * but simply stripping the query fails the third: one file under `src/routes`
43
+ * can reach the browser as *two* module instances, once through the route glob
44
+ * as `…/x.tsx?pracht-client` and once as a plain import from a sibling route.
45
+ * Both would then register under the same key, and `@prefresh/core` treats a
46
+ * second `register()` for a known key with a different function object as a
47
+ * pending component replacement — which the next unrelated Fast Refresh
48
+ * flushes, tearing down and re-running the untouched copy's effects.
49
+ *
50
+ * A reserved, length-prefixed namespace keeps the real extension last, so the
51
+ * filter and parser check still pass, while giving each complete module id its
52
+ * own registration key. Keeping the authored id verbatim makes the mapping
53
+ * injective; keeping it behind a non-file prefix prevents a real sibling such
54
+ * as `x.pracht-client.tsx` from colliding with the synthetic key. The id is
55
+ * never resolved against the filesystem; the JSX dev transform has already
56
+ * stamped `_jsxFileName` from the real id by the time prefresh runs, so dev
57
+ * source locations and open-in-editor are unaffected.
58
+ *
59
+ * Compiled formats whose real extension prefresh rejects (`.md`, `.mdx`, and
60
+ * configured additional formats) instead keep that extension in the basename
61
+ * and receive a synthetic `.jsx`. Their companion Vite plugin has already
62
+ * turned the authored format into JavaScript by the time this id is used.
63
+ */
64
+ function toPrachtClientPrefreshId(id) {
65
+ const stripped = stripPrachtClientModuleQuery(id);
66
+ const queryStart = stripped.indexOf("?");
67
+ const path = queryStart === -1 ? stripped : stripped.slice(0, queryStart);
68
+ const parserExtension = PREFRESH_EXTENSION_RE.exec(path)?.[1] ?? "jsx";
69
+ return `pracht-client:${id.length}:${id}.${parserExtension}`;
70
+ }
29
71
  function getRolldownLang(id) {
30
72
  const path = stripPrachtClientModuleQuery(id).split("?")[0];
31
73
  if (/\.(c|m)?tsx$/i.test(path)) return "tsx";
@@ -892,6 +934,87 @@ function enqueueDependencies(target, dependencies) {
892
934
  for (const name of dependencies) target.add(name);
893
935
  }
894
936
  //#endregion
937
+ //#region src/chunk-groups.ts
938
+ /**
939
+ * Pracht's client chunking policy, expressed as something an app can build on.
940
+ *
941
+ * The framework has exactly one opinion here: Preact belongs in its own chunk,
942
+ * shared by every route and cached across deploys that only change app code.
943
+ * Everything else about chunking is the app's call — merging the long tail of
944
+ * small initial chunks, splitting a heavy dependency out of a route, grouping
945
+ * by feature.
946
+ *
947
+ * Those two have to coexist, and under Rolldown that is not automatic:
948
+ * `output.codeSplitting` makes `manualChunks` and `advancedChunks` ignored
949
+ * outright, so a plugin that hard-codes one form silently deletes whichever
950
+ * form the app used. Pracht therefore looks at what the app configured and
951
+ * contributes its group in the same form, as one entry appended to the app's
952
+ * list rather than as a replacement for it.
953
+ *
954
+ * Precedence follows Rolldown's own rule — higher `priority` first, then
955
+ * declaration order. The app's groups are declared first, so an app group that
956
+ * would also capture Preact wins at equal priority, and pracht's group only
957
+ * takes what nothing else claimed. To keep the framework chunk intact while
958
+ * merging everything around it, give the app group a `test` that excludes
959
+ * Preact, or raise pracht's group by placing {@link frameworkChunkGroups}
960
+ * explicitly and setting `vendorChunk: false`.
961
+ */
962
+ /**
963
+ * Modules that make up the framework runtime's vendor chunk.
964
+ *
965
+ * `[\\/]` rather than `/` so the group matches on Windows, and no trailing
966
+ * boundary so the Preact family — `preact/hooks`, `preact-suspense`,
967
+ * `preact-render-to-string` — lands in one chunk with Preact itself.
968
+ */
969
+ const FRAMEWORK_VENDOR_TEST = /node_modules[\\/]preact/;
970
+ /** Name of the chunk pracht groups the Preact runtime into. */
971
+ const FRAMEWORK_VENDOR_CHUNK = "vendor";
972
+ /**
973
+ * Pracht's chunk groups, as a fresh array an app can place in its own
974
+ * `output.codeSplitting.groups`.
975
+ *
976
+ * Use this together with `pracht({ vendorChunk: false })` when the framework
977
+ * group has to sit somewhere other than last — pracht then contributes no
978
+ * chunking config of its own and the app's list is the whole policy.
979
+ */
980
+ function frameworkChunkGroups() {
981
+ return [{
982
+ name: FRAMEWORK_VENDOR_CHUNK,
983
+ test: FRAMEWORK_VENDOR_TEST
984
+ }];
985
+ }
986
+ /** Whether a module id belongs in the framework vendor chunk. */
987
+ function isFrameworkVendorModule(id) {
988
+ return FRAMEWORK_VENDOR_TEST.test(id);
989
+ }
990
+ /**
991
+ * Build the chunking config pracht contributes, given what the app configured.
992
+ *
993
+ * Returns a partial `output` because Vite merges a plugin's `config()` result
994
+ * over the user config and concatenates arrays: returning only pracht's group
995
+ * is what appends it to the app's list instead of replacing it.
996
+ */
997
+ function frameworkChunkConfig(output) {
998
+ if (Array.isArray(output)) return { warning: "build.rollupOptions.output is an array, so pracht did not add its Preact vendor chunk group. Add frameworkChunkGroups() from @pracht/vite-plugin to each output's codeSplitting.groups to keep the framework chunk." };
999
+ const options = output ?? {};
1000
+ if (options.codeSplitting === false) return {};
1001
+ const groups = frameworkChunkGroups();
1002
+ if (options.codeSplitting === void 0) {
1003
+ if (isRecord(options.advancedChunks)) return { output: { advancedChunks: { groups } } };
1004
+ if (typeof options.manualChunks === "function") {
1005
+ const appManualChunks = options.manualChunks;
1006
+ return { output: { manualChunks(id, meta) {
1007
+ if (isFrameworkVendorModule(id)) return FRAMEWORK_VENDOR_CHUNK;
1008
+ return appManualChunks(id, meta);
1009
+ } } };
1010
+ }
1011
+ }
1012
+ return { output: { codeSplitting: { groups } } };
1013
+ }
1014
+ function isRecord(value) {
1015
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1016
+ }
1017
+ //#endregion
895
1018
  //#region src/env-safety.ts
896
1019
  /**
897
1020
  * Env vars Vite defines on `import.meta.env` in every bundle, plus NODE_ENV
@@ -1220,6 +1343,116 @@ function createEnvSafetyPlugin(envSafety) {
1220
1343
  };
1221
1344
  }
1222
1345
  //#endregion
1346
+ //#region src/client-module-prefresh.ts
1347
+ /**
1348
+ * Give route and shell modules Preact Fast Refresh.
1349
+ *
1350
+ * `@prefresh/vite` gates its transform on `/\.(c|m)?(t|j)sx?$/`, a pattern
1351
+ * anchored at the end of the id — so an id carrying a query never matches.
1352
+ * Pracht loads route and shell modules in the browser through
1353
+ * `import.meta.glob(..., { query: "?pracht-client" })` so its post transform
1354
+ * can strip server-only exports, which means the module the browser actually
1355
+ * runs is `/src/routes/home.tsx?pracht-client`. Prefresh skipped it, no
1356
+ * `import.meta.hot.accept` was injected, and with no self-accepting boundary
1357
+ * the update propagated to the non-accepting virtual client entry: every edit
1358
+ * to a route or a shell became a full page reload with client state loss.
1359
+ * Components outside those directories were unaffected, which is why this hid
1360
+ * for so long — Fast Refresh worked everywhere except the files a route-based
1361
+ * framework is mostly made of.
1362
+ *
1363
+ * Running after `pracht:client-module-transform` (both are `post`; array order
1364
+ * decides) is deliberate: prefresh sees the stripped module, whose exports are
1365
+ * only components, rather than the authored one where a co-located `loader`
1366
+ * would stop it self-accepting anyway.
1367
+ *
1368
+ * The id prefresh is handed is synthetic — see `toPrachtClientPrefreshId`. It
1369
+ * must satisfy prefresh's extension filter *and* stay distinct from the id of
1370
+ * the authored file, because the same file can be in the client graph twice and
1371
+ * the id doubles as prefresh's component registration key.
1372
+ */
1373
+ function createClientModulePrefreshPlugin(preactPlugins, config = {}) {
1374
+ const transform = resolvePrefreshTransform(preactPlugins);
1375
+ if (!transform) return null;
1376
+ return {
1377
+ name: "pracht:client-module-prefresh",
1378
+ enforce: "post",
1379
+ apply: "serve",
1380
+ async transform(code, id, transformOptions) {
1381
+ if (transformOptions?.ssr) return null;
1382
+ const carriesClientQuery = isPrachtClientModuleId(id);
1383
+ const isBareCompiledFormat = !carriesClientQuery && !isPrefreshCompatibleId(id) && config.isRouteOrShellModule?.(id) === true;
1384
+ if (!carriesClientQuery && !isBareCompiledFormat) return null;
1385
+ return await transform.call(this, code, toPrachtClientPrefreshId(id), transformOptions);
1386
+ }
1387
+ };
1388
+ }
1389
+ /**
1390
+ * `@preact/preset-vite` returns a plugin array whose shape is its own business;
1391
+ * find prefresh by name rather than by position, and treat its absence as "no
1392
+ * Fast Refresh configured" rather than an error.
1393
+ */
1394
+ function resolvePrefreshTransform(preactPlugins) {
1395
+ for (const plugin of flattenPlugins(preactPlugins)) {
1396
+ if (plugin.name !== "prefresh") continue;
1397
+ const transform = plugin.transform;
1398
+ if (typeof transform === "function") return transform;
1399
+ if (transform && typeof transform === "object" && "handler" in transform) return transform.handler;
1400
+ }
1401
+ return null;
1402
+ }
1403
+ function flattenPlugins(plugins) {
1404
+ const flat = [];
1405
+ const visit = (option) => {
1406
+ if (!option || typeof option.then === "function") return;
1407
+ if (Array.isArray(option)) {
1408
+ for (const nested of option) visit(nested);
1409
+ return;
1410
+ }
1411
+ if (typeof option === "object" && "name" in option) flat.push(option);
1412
+ };
1413
+ for (const plugin of plugins) visit(plugin);
1414
+ return flat;
1415
+ }
1416
+ //#endregion
1417
+ //#region src/head-hint-reload.ts
1418
+ /**
1419
+ * Which changed files reach routes with generated client hints.
1420
+ *
1421
+ * Head and response-header hints decide whether a dependency edit must reload
1422
+ * the document; loader hints decide whether it must re-fetch active route data.
1423
+ * The importer walk is shared because all three are keyed by route source.
1424
+ */
1425
+ function toPosixPath$2(path) {
1426
+ return path.replace(/\\/g, "/");
1427
+ }
1428
+ function reachesRouteHintedModule(modules, serverRoot, routeHints, options = {}) {
1429
+ const pending = options.startAtImporters ? modules.flatMap((module) => [...module.importers ?? []]) : [...modules];
1430
+ const seen = /* @__PURE__ */ new Set();
1431
+ while (pending.length > 0) {
1432
+ const module = pending.pop();
1433
+ if (!module || seen.has(module)) continue;
1434
+ seen.add(module);
1435
+ const modulePath = module.file ?? module.id?.split("?", 1)[0];
1436
+ if (modulePath) {
1437
+ const normalizedPath = toPosixPath$2(modulePath);
1438
+ if (routeHints[normalizedPath.startsWith(serverRoot) ? normalizedPath.slice(serverRoot.length) : normalizedPath] === true) return true;
1439
+ }
1440
+ if (module.importers) pending.push(...module.importers);
1441
+ }
1442
+ return false;
1443
+ }
1444
+ //#endregion
1445
+ //#region src/route-data-stale.ts
1446
+ function sendRouteDataStale(server) {
1447
+ const hot = server.environments?.client?.hot;
1448
+ if (!hot) return false;
1449
+ hot.send({
1450
+ type: "custom",
1451
+ event: DEV_ROUTE_DATA_STALE_EVENT
1452
+ });
1453
+ return true;
1454
+ }
1455
+ //#endregion
1223
1456
  //#region src/hot-update-reload.ts
1224
1457
  /**
1225
1458
  * True when `file` participates in server rendering but has no runtime
@@ -1351,7 +1584,10 @@ function createDefaultNodeAdapter() {
1351
1584
  }
1352
1585
  //#endregion
1353
1586
  //#region src/plugin-options.ts
1587
+ const CLIENT_FEATURE_DEFAULTS = { prefetch: true };
1354
1588
  const DEFAULTS = {
1589
+ client: CLIENT_FEATURE_DEFAULTS,
1590
+ vendorChunk: true,
1355
1591
  appFile: "/src/routes.ts",
1356
1592
  middlewareDir: "/src/middleware",
1357
1593
  routesDir: "/src/routes",
@@ -1377,6 +1613,8 @@ function resolveOptions(options) {
1377
1613
  ...options
1378
1614
  };
1379
1615
  if (resolved.llmsTxt === void 0) resolved.llmsTxt = false;
1616
+ resolved.client = resolveClientOptions(options.client);
1617
+ if (typeof resolved.vendorChunk !== "boolean") throw new Error(`pracht({ vendorChunk }) expects a boolean, got ${JSON.stringify(resolved.vendorChunk)}.`);
1380
1618
  resolved.additionalExtensions = normalizeAdditionalExtensions(resolved.additionalExtensions);
1381
1619
  if (!new Set([
1382
1620
  "spa",
@@ -1390,6 +1628,20 @@ function resolveOptions(options) {
1390
1628
  validateLlmsTxt(resolved.llmsTxt);
1391
1629
  return resolved;
1392
1630
  }
1631
+ function resolveClientOptions(client) {
1632
+ if (client === void 0) return CLIENT_FEATURE_DEFAULTS;
1633
+ if (typeof client !== "object" || client === null) throw new Error("pracht({ client }) expects an options object.");
1634
+ const resolved = { ...CLIENT_FEATURE_DEFAULTS };
1635
+ for (const key of Object.keys(CLIENT_FEATURE_DEFAULTS)) {
1636
+ const value = client[key];
1637
+ if (value === void 0) continue;
1638
+ if (typeof value !== "boolean") throw new Error(`pracht({ client: { ${key} } }) expects a boolean, got ${JSON.stringify(value)}.`);
1639
+ resolved[key] = value;
1640
+ }
1641
+ const unknown = Object.keys(client).filter((key) => !(key in CLIENT_FEATURE_DEFAULTS));
1642
+ if (unknown.length > 0) throw new Error(`pracht({ client }) does not accept ${unknown.map((key) => JSON.stringify(key)).join(", ")}. Known features: ${Object.keys(CLIENT_FEATURE_DEFAULTS).join(", ")}.`);
1643
+ return resolved;
1644
+ }
1393
1645
  const LLMS_TXT_SECTIONS = new Set([
1394
1646
  "pages",
1395
1647
  "api",
@@ -1401,6 +1653,10 @@ function validateLlmsTxt(llmsTxt) {
1401
1653
  if (llmsTxt.include !== void 0) {
1402
1654
  if (!(Array.isArray(llmsTxt.include) && llmsTxt.include.every((section) => LLMS_TXT_SECTIONS.has(section)))) throw new Error(`pracht({ llmsTxt: { include } }) expects an array of "pages", "api", and/or "capabilities", got ${JSON.stringify(llmsTxt.include)}.`);
1403
1655
  }
1656
+ if (llmsTxt.maxPagesPerRoute !== void 0) {
1657
+ const value = llmsTxt.maxPagesPerRoute;
1658
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`pracht({ llmsTxt: { maxPagesPerRoute } }) expects a non-negative integer (0 lists every page), got ${JSON.stringify(value)}.`);
1659
+ }
1404
1660
  }
1405
1661
  function validateBudgets(budgets) {
1406
1662
  for (const [key, value] of Object.entries(budgets)) {
@@ -1592,7 +1848,7 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1592
1848
  "// Generated by @pracht/vite-plugin from the app manifest capability registrations.",
1593
1849
  "// Contains only http-exposed capability names, endpoints, and effects —",
1594
1850
  "// capability modules themselves are server-only and never reach the client graph.",
1595
- "import { createUseCapability, withBase } from \"@pracht/core\";",
1851
+ "import { createUseCapability, ensureCapabilityRevalidation, withBase } from \"@pracht/core\";",
1596
1852
  "",
1597
1853
  `const endpoints = Object.assign(Object.create(null), JSON.parse(${JSON.stringify(JSON.stringify(endpoints))}));`,
1598
1854
  "",
@@ -1653,6 +1909,7 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1653
1909
  "}",
1654
1910
  "",
1655
1911
  "export async function callCapability(name, input, opts) {",
1912
+ " ensureCapabilityRevalidation();",
1656
1913
  " const endpoint = endpoints[name];",
1657
1914
  " if (!endpoint) {",
1658
1915
  " return {",
@@ -1722,16 +1979,30 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1722
1979
  * validation/middleware/policy stays server-side. Each dispatch carries the
1723
1980
  * transport marker header so audit events can attribute it to WebMCP.
1724
1981
  *
1725
- * Targets the Chrome origin-trial API: `document.modelContext.registerTool()`
1726
- * (Chrome 150+; `navigator.modelContext` is the deprecated pre-150 location
1727
- * and is kept as a fallback). No-ops silently when the API is absent.
1982
+ * Targets the WebMCP CG draft API: `document.modelContext.registerTool()`
1983
+ * (ChatGPT desktop's built-in browser; Chromium 150+ within the 149–156
1984
+ * origin trial the `document` getter landed in 150 and the deprecated
1985
+ * `navigator.modelContext` alias was removed in 152, so trial builds before
1986
+ * 150 are not targeted and no fallback is kept; current polyfills install the
1987
+ * `document` shape). No-ops silently when the API is absent.
1988
+ *
1989
+ * `execute()` returns the capability envelope (`{ ok, data }` /
1990
+ * `{ ok: false, error }`) as a plain object: per the spec the host serializes
1991
+ * the returned value itself, so wrapping it in MCP-style content blocks would
1992
+ * reach the agent double-encoded.
1728
1993
  */
1729
1994
  function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
1730
1995
  const tools = extractCapabilities(options, buildOptions.root).filter((capability) => capability.webmcp).map((capability) => ({
1731
1996
  name: capability.name,
1997
+ ...capability.title ? { title: capability.title } : {},
1732
1998
  description: capability.description,
1733
- effect: capability.effect,
1734
- inputSchema: capability.inputSchema
1999
+ inputSchema: capability.inputSchema,
2000
+ annotations: {
2001
+ readOnlyHint: capability.effect === "read",
2002
+ ...capability.effect === "read" ? { destructiveHint: false } : {},
2003
+ idempotentHint: capability.effect === "read",
2004
+ ...capability.webmcpUntrustedContent ? { untrustedContentHint: true } : {}
2005
+ }
1735
2006
  }));
1736
2007
  return [
1737
2008
  "// Generated by @pracht/vite-plugin — WebMCP page-tool registration shim.",
@@ -1742,30 +2013,27 @@ function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
1742
2013
  "",
1743
2014
  "export function registerPrachtWebmcpTools() {",
1744
2015
  " const modelContext =",
1745
- " (typeof document !== \"undefined\" && document.modelContext) ||",
1746
- " (typeof navigator !== \"undefined\" && navigator.modelContext) ||",
1747
- " null;",
2016
+ " (typeof document !== \"undefined\" && document.modelContext) || null;",
1748
2017
  " if (!modelContext || typeof modelContext.registerTool !== \"function\") {",
1749
2018
  " return false;",
1750
2019
  " }",
1751
2020
  " for (const tool of tools) {",
1752
2021
  " try {",
1753
2022
  " const registration = modelContext.registerTool({",
1754
- " name: tool.name,",
1755
- " description: tool.description,",
1756
- " inputSchema: tool.inputSchema,",
1757
- " annotations: { readOnlyHint: tool.effect === \"read\" },",
1758
- " async execute(input) {",
1759
- " const result = await callCapability(tool.name, input, { headers: transportHeaders });",
1760
- " return { content: [{ type: \"text\", text: JSON.stringify(result) }] };",
2023
+ " ...tool,",
2024
+ " async execute(input, { signal } = {}) {",
2025
+ " return callCapability(tool.name, input, {",
2026
+ " headers: transportHeaders,",
2027
+ " signal,",
2028
+ " });",
1761
2029
  " },",
1762
2030
  " });",
1763
2031
  " if (registration && typeof registration.catch === \"function\") {",
1764
2032
  " registration.catch(() => {});",
1765
2033
  " }",
1766
2034
  " } catch {",
1767
- " // Origin-trial API surface may shift; a failed registration must",
1768
- " // never break the page.",
2035
+ " // The API is still an origin-trial surface; a failed registration",
2036
+ " // must never break the page.",
1769
2037
  " }",
1770
2038
  " }",
1771
2039
  " return true;",
@@ -1783,10 +2051,7 @@ function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
1783
2051
  function createWebmcpBootstrapSource() {
1784
2052
  return [
1785
2053
  "// WebMCP page tools — loaded only when the browser exposes the API.",
1786
- "if (",
1787
- " typeof document !== \"undefined\" &&",
1788
- " (document.modelContext || (typeof navigator !== \"undefined\" && navigator.modelContext))",
1789
- ") {",
2054
+ "if (typeof document !== \"undefined\" && document.modelContext) {",
1790
2055
  " import(\"virtual:pracht/webmcp\").catch(() => {});",
1791
2056
  "}",
1792
2057
  ""
@@ -1907,7 +2172,7 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1907
2172
  const appFilePosix = resolved.appFile.replace(/\\/g, "/").replace(/^\.\//, "");
1908
2173
  const appDir = (appFilePosix.startsWith("/") ? appFilePosix : `/${appFilePosix}`).replace(/\/[^/]*$/, "") || "/";
1909
2174
  return [
1910
- "import { resolveApp, initClientRouter, readHydrationState } from \"@pracht/core/client\";",
2175
+ "import { resolveApp, initClientRouter, readHydrationState, DEV_ROUTE_DATA_STALE_EVENT, refreshDevRouteData } from \"@pracht/core/client\";",
1911
2176
  appImport,
1912
2177
  "",
1913
2178
  `const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
@@ -1992,6 +2257,18 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
1992
2257
  " });",
1993
2258
  "}",
1994
2259
  "",
2260
+ "// A route module's loader, head, and getStaticPaths are stripped",
2261
+ "// out of the browser copy, so Fast Refresh patching the component in place",
2262
+ "// leaves the page holding data the server would no longer send. The dev",
2263
+ "// server says when that happened; re-fetching route state is what the full",
2264
+ "// page reload used to deliver as a side effect. This entry is the only",
2265
+ "// module in the graph with an import.meta.hot of its own — an installed",
2266
+ "// @pracht/core is a pre-bundled dependency and has none. Production builds",
2267
+ "// replace import.meta.hot with undefined and drop the whole branch.",
2268
+ "if (import.meta.hot) {",
2269
+ " import.meta.hot.on(DEV_ROUTE_DATA_STALE_EVENT, refreshDevRouteData);",
2270
+ "}",
2271
+ "",
1995
2272
  ...hasWebmcpCapabilities(resolved, buildOptions.root) ? createWebmcpBootstrapSource() : []
1996
2273
  ].join("\n");
1997
2274
  }
@@ -2130,6 +2407,7 @@ function resolveLlmsTxtConfig(resolved, root = process.cwd()) {
2130
2407
  if (resolved.llmsTxt.origin) config.origin = resolved.llmsTxt.origin;
2131
2408
  if (resolved.llmsTxt.include) config.include = resolved.llmsTxt.include;
2132
2409
  if (resolved.llmsTxt.exclude?.length) config.exclude = resolved.llmsTxt.exclude;
2410
+ if (resolved.llmsTxt.maxPagesPerRoute !== void 0) config.maxPagesPerRoute = resolved.llmsTxt.maxPagesPerRoute;
2133
2411
  return config;
2134
2412
  }
2135
2413
  function createApplyRouteLoaderHintsSource() {
@@ -2171,6 +2449,19 @@ function createRouteHeadHintsForVirtualModules(options, root = process.cwd()) {
2171
2449
  rootRelativePrefix: prefix
2172
2450
  })));
2173
2451
  }
2452
+ function createRouteHeadersHintsForVirtualModules(options, root = process.cwd()) {
2453
+ if (options.pagesDir) return createRouteHeadersHints(resolve(root, options.pagesDir.slice(1)), {
2454
+ additionalExtensions: options.additionalExtensions,
2455
+ rootRelativePrefix: options.pagesDir
2456
+ });
2457
+ const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2458
+ const directories = [[options.routesDir, resolve(root, options.routesDir.slice(1))], [options.shellsDir, resolve(root, options.shellsDir.slice(1))]];
2459
+ return Object.assign({}, ...directories.map(([prefix, directory]) => createRouteHeadersHints(directory, {
2460
+ additionalExtensions: options.additionalExtensions,
2461
+ appFileDir,
2462
+ rootRelativePrefix: prefix
2463
+ })));
2464
+ }
2174
2465
  /**
2175
2466
  * `getStaticPaths()` presence per route file. Only routes matter — a shell
2176
2467
  * cannot enumerate paths — so unlike the head hints this skips the shells
@@ -2202,6 +2493,19 @@ function createRouteLoaderHintsForVirtualModules(options, root = process.cwd())
2202
2493
  rootRelativePrefix: options.routesDir
2203
2494
  });
2204
2495
  }
2496
+ /**
2497
+ * Server data modules that can own a separately wired route loader.
2498
+ *
2499
+ * These hints stay on the dev-server side: unlike the per-route table above,
2500
+ * the generated browser entry has no use for data-module filenames. The HMR
2501
+ * importer walk does, though — a shared module can be client-reachable through
2502
+ * a component and server-reachable through `route(..., { loader })`, so it
2503
+ * cannot rely on the server-only full-reload fallback.
2504
+ */
2505
+ function createServerLoaderHintsForHotUpdates(options, root = process.cwd()) {
2506
+ const hints = createRouteLoaderHints(resolve(root, options.serverDir.slice(1)), { rootRelativePrefix: options.serverDir });
2507
+ return Object.fromEntries(Object.entries(hints).filter((entry) => entry[1] === true));
2508
+ }
2205
2509
  function createPrachtRegistryModuleSource(options = {}) {
2206
2510
  const resolved = resolveOptions(options);
2207
2511
  const apiGlobs = [`${resolved.apiDir}/**/*.{ts,js,tsx,jsx}`, `!${resolved.apiDir}/**/*.d.ts`];
@@ -2258,6 +2562,38 @@ function generatePagesAppInlineSource(options, root = process.cwd()) {
2258
2562
  pagesAppSourceCache.set(cacheKey, source);
2259
2563
  return source;
2260
2564
  }
2565
+ function createAgentTrafficBuffer(limit = 200) {
2566
+ const capacity = Math.max(1, Math.floor(limit));
2567
+ const events = [];
2568
+ let recorded = 0;
2569
+ return {
2570
+ record(event) {
2571
+ recorded += 1;
2572
+ events.push({
2573
+ at: Date.now(),
2574
+ capability: event.capability,
2575
+ effect: event.effect,
2576
+ transport: event.transport,
2577
+ via: event.via,
2578
+ outcome: event.outcome,
2579
+ status: event.status,
2580
+ durationMs: event.durationMs,
2581
+ agent: event.agent ? {
2582
+ agentDomain: event.agent.agentDomain,
2583
+ keyId: event.agent.keyId
2584
+ } : null
2585
+ });
2586
+ while (events.length > capacity) events.shift();
2587
+ },
2588
+ snapshot() {
2589
+ return {
2590
+ limit: capacity,
2591
+ recorded,
2592
+ events: [...events].reverse()
2593
+ };
2594
+ }
2595
+ };
2596
+ }
2261
2597
  //#endregion
2262
2598
  //#region src/plugin-dev-ssr.ts
2263
2599
  const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
@@ -2307,6 +2643,7 @@ function createDevSSRMiddleware(server, options = {}) {
2307
2643
  const withDevBase = (path) => devBase === "/" || !path.startsWith("/") ? path : `${devBase}${path.slice(1)}`;
2308
2644
  let warnedDevtoolsCollision = false;
2309
2645
  let warnedLlmsTxtCollision = false;
2646
+ const agentTraffic = createAgentTrafficBuffer();
2310
2647
  if (options.llmsTxt && typeof server.config.publicDir === "string") {
2311
2648
  if (existsSync(join(server.config.publicDir, "llms.txt"))) server.config.logger.warn("[pracht] Both public/llms.txt and the pracht({ llmsTxt }) option are present. Dev serves the static public/llms.txt, but \"pracht build\" overwrites it with the generated content. Remove one to avoid a dev/production mismatch.");
2312
2649
  }
@@ -2327,6 +2664,7 @@ function createDevSSRMiddleware(server, options = {}) {
2327
2664
  server.config.logger.warn(`[pracht] An app route matches ${requestUrl.pathname}, which is reserved for the pracht devtools page in dev. The devtools page wins during development; the app route is only served in production builds.`);
2328
2665
  }
2329
2666
  await serveDevtools(server, res, {
2667
+ agentTraffic,
2330
2668
  apiRoutes: serverMod.apiRoutes ?? [],
2331
2669
  app: serverMod.resolvedApp,
2332
2670
  base: devBase,
@@ -2363,16 +2701,25 @@ function createDevSSRMiddleware(server, options = {}) {
2363
2701
  throw err;
2364
2702
  }
2365
2703
  const timings = {};
2704
+ let routeError;
2705
+ let capturedRouteError = false;
2706
+ let routeErrorContext;
2366
2707
  const response = await framework.handlePrachtRequest({
2367
2708
  app: serverMod.resolvedApp,
2368
2709
  registry: serverMod.registry,
2369
2710
  request: webRequest,
2370
2711
  debugErrors: true,
2712
+ onRouteError: (error, _requestPath, context) => {
2713
+ capturedRouteError = true;
2714
+ routeError = error;
2715
+ routeErrorContext = context;
2716
+ },
2371
2717
  clientEntryUrl: withDevBase(CLIENT_BROWSER_PATH),
2372
2718
  islandsEntryUrl: withDevBase(ISLANDS_CLIENT_BROWSER_PATH),
2373
2719
  islandsBootstrapRequired: serverMod.islandsBootstrapRequired === true,
2374
2720
  apiRoutes: serverMod.apiRoutes,
2375
- timings
2721
+ timings,
2722
+ onCapabilityAudit: agentTraffic.record
2376
2723
  });
2377
2724
  const responseContentType = response.headers.get("content-type") ?? "";
2378
2725
  if (response.status === 404 && !responseContentType.includes("application/json") && !routeMatchers.app?.notFound) return next();
@@ -2396,6 +2743,17 @@ function createDevSSRMiddleware(server, options = {}) {
2396
2743
  source.pipe(res);
2397
2744
  return;
2398
2745
  }
2746
+ if (shouldRenderDevErrorOverlay({
2747
+ capturedRouteError,
2748
+ contentType,
2749
+ exposeServerErrors: shouldExposeDevServerErrors(),
2750
+ hasErrorBoundary: routeErrorContext?.errorBoundary != null,
2751
+ status: response.status
2752
+ })) {
2753
+ const serverTiming = framework.formatServerTimingHeader(timings);
2754
+ await respondWithErrorOverlay(server, res, url, routeError, routeErrorContext, devBase, response.status, serverTiming);
2755
+ return;
2756
+ }
2399
2757
  let body = await response.text();
2400
2758
  if (contentType.includes("text/html")) body = await transformDevHtml(server, url, body, devBase);
2401
2759
  res.statusCode = response.status;
@@ -2614,27 +2972,100 @@ function escapeHtmlAttribute(value) {
2614
2972
  */
2615
2973
  async function serveDevtools(server, res, options) {
2616
2974
  const devtools = await server.ssrLoadModule("@pracht/core/devtools");
2617
- const capabilityModules = (await server.ssrLoadModule(PRACHT_SERVER_MODULE_ID)).registry?.capabilityModules;
2975
+ const serverModule = await server.ssrLoadModule(PRACHT_SERVER_MODULE_ID);
2976
+ const capabilityModules = serverModule.registry?.capabilityModules;
2977
+ const middlewareModules = serverModule.registry?.middlewareModules;
2618
2978
  const graph = await devtools.buildAppGraph({
2619
2979
  apiRoutes: options.apiRoutes,
2620
2980
  app: options.app,
2621
2981
  loadModule: async (file) => {
2622
2982
  return await resolveRegistryModule(capabilityModules, file) ?? server.ssrLoadModule(file);
2623
2983
  },
2984
+ loadSetupModule: async (file) => {
2985
+ return await resolveRegistryModule(middlewareModules, file) ?? server.ssrLoadModule(file);
2986
+ },
2987
+ verifyMcpTokenVerifier: async () => {
2988
+ const auth = options.app.agents?.mcp?.auth;
2989
+ if (!auth) return;
2990
+ await (await server.ssrLoadModule("@pracht/core/server")).loadMcpTokenVerifier(auth, serverModule.registry ?? {});
2991
+ },
2624
2992
  readSource: (file) => readFileSync(resolve(server.config.root, `.${file}`), "utf-8")
2625
2993
  });
2994
+ const agentTraffic = options.agentTraffic.snapshot();
2626
2995
  if (options.wantsJson) {
2627
2996
  res.statusCode = 200;
2628
2997
  res.setHeader("content-type", "application/json; charset=utf-8");
2629
- res.end(JSON.stringify(graph, null, 2));
2998
+ res.end(JSON.stringify({
2999
+ ...graph,
3000
+ agentTraffic
3001
+ }, null, 2));
2630
3002
  return;
2631
3003
  }
2632
- let html = devtools.buildDevtoolsHtml(graph, { base: options.base });
3004
+ let html = devtools.buildDevtoolsHtml(graph, {
3005
+ agentTraffic,
3006
+ base: options.base
3007
+ });
2633
3008
  html = await server.transformIndexHtml(options.url, html);
2634
3009
  res.statusCode = 200;
2635
3010
  res.setHeader("content-type", "text/html; charset=utf-8");
2636
3011
  res.end(html);
2637
3012
  }
3013
+ /**
3014
+ * True when a dev response should be replaced by the error overlay.
3015
+ *
3016
+ * The runtime only falls back to `text/plain` for a page render when neither
3017
+ * the route nor its shell declares an ErrorBoundary. When one does, the
3018
+ * response is the app's own error UI (`text/html`) and dev must leave it
3019
+ * alone. Route-state and capability failures are JSON and belong to the
3020
+ * client router, not to a human reading a document.
3021
+ */
3022
+ function shouldRenderDevErrorOverlay(options) {
3023
+ return options.capturedRouteError && options.exposeServerErrors && !options.hasErrorBoundary && options.status >= 500 && options.contentType.toLowerCase().startsWith("text/plain");
3024
+ }
3025
+ /**
3026
+ * The dev middleware passes `debugErrors: true` unconditionally, but the
3027
+ * runtime refuses to honor it when `NODE_ENV === "production"` (see
3028
+ * `shouldExposeServerErrors` in @pracht/core) — a dev server started inside a
3029
+ * container that exports `NODE_ENV=production` must not answer with internals.
3030
+ * The overlay is built from the raw error rather than from the runtime's
3031
+ * already-redacted body, so it has to repeat that check.
3032
+ */
3033
+ function shouldExposeDevServerErrors() {
3034
+ return (typeof process !== "undefined" ? process.env?.NODE_ENV : void 0) !== "production";
3035
+ }
3036
+ /**
3037
+ * Render a failed page render as the dev error overlay.
3038
+ *
3039
+ * `handlePrachtRequest` answers a render/loader/middleware failure with the
3040
+ * runtime's plain-text fallback whenever no ErrorBoundary claims it. In a
3041
+ * production adapter that is correct — a browser is not the audience. In dev
3042
+ * the browser *is* the audience, and the fallback is at its worst exactly when
3043
+ * it matters most: a compiler diagnostic arrives colourized for a terminal, so
3044
+ * `text/plain` renders every escape sequence literally.
3045
+ */
3046
+ async function respondWithErrorOverlay(server, res, url, error, context, base, status, serverTiming) {
3047
+ if (error instanceof Error) server.ssrFixStacktrace(error);
3048
+ const { buildErrorOverlayHtml } = await server.ssrLoadModule("@pracht/core/error-overlay");
3049
+ let html = buildErrorOverlayHtml({
3050
+ message: error instanceof Error ? error.message : String(error),
3051
+ stack: error instanceof Error ? error.stack : void 0,
3052
+ routeId: context?.routeId,
3053
+ file: context?.routeFile,
3054
+ loaderFile: context?.loaderFile,
3055
+ shellFile: context?.shellFile,
3056
+ phase: context?.phase,
3057
+ root: server.config.root,
3058
+ base
3059
+ });
3060
+ html = await server.transformIndexHtml(url, html);
3061
+ res.statusCode = status;
3062
+ res.setHeader("content-type", "text/html; charset=utf-8");
3063
+ applyDefaultSecurityHeaders(new Headers()).forEach((value, key) => {
3064
+ res.setHeader(key, value);
3065
+ });
3066
+ if (serverTiming) res.setHeader("Server-Timing", serverTiming);
3067
+ res.end(html);
3068
+ }
2638
3069
  async function handleDevError(server, req, res, next, url, error, base) {
2639
3070
  if (error instanceof Error) server.ssrFixStacktrace(error);
2640
3071
  if (req.headers["x-pracht-route-state-request"] === "1") {
@@ -2814,28 +3245,15 @@ async function nodeToWebRequest(req, maxBodySize, base = "/") {
2814
3245
  }
2815
3246
  //#endregion
2816
3247
  //#region src/index.ts
2817
- function reachesHeadBearingModule(modules, serverRoot, headHints) {
2818
- const pending = [...modules];
2819
- const seen = /* @__PURE__ */ new Set();
2820
- while (pending.length > 0) {
2821
- const module = pending.pop();
2822
- if (!module || seen.has(module)) continue;
2823
- seen.add(module);
2824
- const modulePath = module.file ?? module.id?.split("?", 1)[0];
2825
- if (modulePath) {
2826
- const normalizedPath = toPosixPath(modulePath);
2827
- if (headHints[normalizedPath.startsWith(serverRoot) ? normalizedPath.slice(serverRoot.length) : normalizedPath] === true) return true;
2828
- }
2829
- if (module.importers) pending.push(...module.importers);
2830
- }
2831
- return false;
2832
- }
2833
3248
  function pracht(options = {}) {
2834
3249
  const resolved = resolveOptions(options);
2835
3250
  const isPagesMode = !!resolved.pagesDir;
2836
3251
  let root = process.cwd();
2837
3252
  let routeFileDirs = [];
2838
3253
  let clientRouteHeadHints = {};
3254
+ let clientRouteHeadersHints = {};
3255
+ let clientRouteLoaderHints = {};
3256
+ let serverRouteLoaderHints = {};
2839
3257
  const routeFileExtensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions);
2840
3258
  let capabilityModulePaths = /* @__PURE__ */ new Set();
2841
3259
  if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
@@ -2845,6 +3263,7 @@ function pracht(options = {}) {
2845
3263
  const prachtPlugin = {
2846
3264
  name: "pracht",
2847
3265
  enforce: "pre",
3266
+ api: { llmsTxtEnabled: Boolean(resolved.llmsTxt) },
2848
3267
  config(_config, env) {
2849
3268
  const isEdge = resolved.adapter.edge === true;
2850
3269
  const isSSRBuild = env.isSsrBuild;
@@ -2854,6 +3273,9 @@ function pracht(options = {}) {
2854
3273
  const publicEnvDefine = JSON.stringify(loadEnv(env.mode, envDir, PUBLIC_ENV_PREFIX));
2855
3274
  const agentSurfaceDefine = env.command === "build" ? String(hasAgentSurface(resolved, configRoot)) : "true";
2856
3275
  const staticTargetDefine = String(env.command === "build" && resolved.adapter.staticTarget === true);
3276
+ const clientFeatureDefines = { __PRACHT_CLIENT_PREFETCH__: String(resolved.client.prefetch) };
3277
+ const clientChunkConfig = isSSRBuild || !resolved.vendorChunk ? {} : frameworkChunkConfig(_config.build?.rollupOptions?.output);
3278
+ if (clientChunkConfig.warning) console.warn(`[pracht] ${clientChunkConfig.warning}`);
2857
3279
  return {
2858
3280
  appType: "custom",
2859
3281
  envPrefix: ["VITE_", PUBLIC_ENV_PREFIX],
@@ -2861,13 +3283,12 @@ function pracht(options = {}) {
2861
3283
  define: {
2862
3284
  __PRACHT_PUBLIC_ENV__: publicEnvDefine,
2863
3285
  __PRACHT_AGENT_SURFACE__: agentSurfaceDefine,
2864
- __PRACHT_STATIC_TARGET__: staticTargetDefine
3286
+ __PRACHT_STATIC_TARGET__: staticTargetDefine,
3287
+ ...clientFeatureDefines
2865
3288
  },
2866
3289
  ...isSSRBuild ? {} : { build: { rollupOptions: {
2867
3290
  ...wantsIslandsEntry ? { input: [PRACHT_ISLANDS_CLIENT_MODULE_ID] } : {},
2868
- output: { manualChunks(id) {
2869
- if (id.includes("node_modules/preact") || id.includes("node_modules/preact-suspense")) return "vendor";
2870
- } }
3291
+ ...clientChunkConfig.output ? { output: clientChunkConfig.output } : {}
2871
3292
  } } },
2872
3293
  ...isEdge && isSSRBuild ? {
2873
3294
  ssr: {
@@ -2885,7 +3306,7 @@ function pracht(options = {}) {
2885
3306
  } } },
2886
3307
  build: { rollupOptions: { external: [/^cloudflare:/] } }
2887
3308
  } : {},
2888
- ...!isEdge && isSSRBuild ? { ssr: { noExternal: [PRACHT_SSR_NO_EXTERNAL] } } : {}
3309
+ ...!isEdge && isSSRBuild || env.command === "serve" ? { ssr: { noExternal: [PRACHT_SSR_NO_EXTERNAL] } } : {}
2889
3310
  };
2890
3311
  },
2891
3312
  configResolved(config) {
@@ -2910,6 +3331,9 @@ function pracht(options = {}) {
2910
3331
  if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved, { root });
2911
3332
  if (isClientModule(id)) {
2912
3333
  clientRouteHeadHints = createRouteHeadHintsForVirtualModules(resolved, root);
3334
+ clientRouteHeadersHints = createRouteHeadersHintsForVirtualModules(resolved, root);
3335
+ clientRouteLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, root);
3336
+ serverRouteLoaderHints = createServerLoaderHintsForHotUpdates(resolved, root);
2913
3337
  return createPrachtClientModuleSource(resolved, { root });
2914
3338
  }
2915
3339
  if (isDevModule(id)) return createPrachtDevModuleSource(resolved, {
@@ -2963,24 +3387,58 @@ function pracht(options = {}) {
2963
3387
  const normalizedFile = toPosixPath(file);
2964
3388
  const relative = normalizedFile.startsWith(serverRoot) ? normalizedFile.slice(serverRoot.length) : normalizedFile;
2965
3389
  const changesRouteHeadSource = isPagesMode ? relative.startsWith(resolved.pagesDir) : relative.startsWith(resolved.routesDir) || relative.startsWith(resolved.shellsDir);
2966
- const changesRouteHeadDependency = reachesHeadBearingModule(modules, serverRoot, clientRouteHeadHints);
2967
- let shouldReloadClientHead = changesRouteHeadDependency;
3390
+ const changesRouteLoaderSource = isPagesMode ? relative.startsWith(resolved.pagesDir) : relative.startsWith(resolved.routesDir);
3391
+ const previousServerRouteLoaderHints = serverRouteLoaderHints;
3392
+ if (!isPagesMode && relative.startsWith(resolved.serverDir)) try {
3393
+ serverRouteLoaderHints = createServerLoaderHintsForHotUpdates(resolved, root);
3394
+ } catch {}
3395
+ const loaderDependencyHints = {
3396
+ ...clientRouteLoaderHints,
3397
+ ...previousServerRouteLoaderHints,
3398
+ ...serverRouteLoaderHints
3399
+ };
3400
+ const changesRouteHeadDependency = reachesRouteHintedModule(modules, serverRoot, clientRouteHeadHints, { startAtImporters: changesRouteHeadSource });
3401
+ const changesRouteHeadersDependency = reachesRouteHintedModule(modules, serverRoot, clientRouteHeadersHints, { startAtImporters: changesRouteHeadSource });
3402
+ const changesRouteLoaderDependency = reachesRouteHintedModule(modules, serverRoot, loaderDependencyHints, { startAtImporters: changesRouteLoaderSource });
3403
+ let shouldReloadClientEntry = changesRouteHeadDependency || changesRouteHeadersDependency;
2968
3404
  let clientHeadModule;
2969
- if (changesRouteHeadSource || changesRouteHeadDependency) clientHeadModule = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
3405
+ if (changesRouteHeadSource || changesRouteHeadDependency || changesRouteHeadersDependency) clientHeadModule = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
2970
3406
  if (changesRouteHeadSource) {
2971
- const previousHint = clientRouteHeadHints[relative];
3407
+ const previousHint = clientRouteHeadHints[relative] === true;
2972
3408
  try {
2973
3409
  const nextHints = createRouteHeadHintsForVirtualModules(resolved, root);
2974
- shouldReloadClientHead ||= previousHint === true || nextHints[relative] === true;
3410
+ shouldReloadClientEntry ||= previousHint !== (nextHints[relative] === true);
2975
3411
  clientRouteHeadHints = nextHints;
2976
3412
  } catch {
2977
- shouldReloadClientHead = true;
3413
+ shouldReloadClientEntry = true;
2978
3414
  }
2979
3415
  } else if (changesRouteHeadDependency && clientHeadModule) server.moduleGraph.invalidateModule(clientHeadModule);
3416
+ if (changesRouteHeadSource) {
3417
+ const previouslyHadHeaders = clientRouteHeadersHints[relative] === true;
3418
+ try {
3419
+ const nextHints = createRouteHeadersHintsForVirtualModules(resolved, root);
3420
+ shouldReloadClientEntry ||= previouslyHadHeaders || nextHints[relative] === true;
3421
+ clientRouteHeadersHints = nextHints;
3422
+ } catch {
3423
+ shouldReloadClientEntry = true;
3424
+ }
3425
+ }
3426
+ if (changesRouteLoaderSource) {
3427
+ const previousHint = clientRouteLoaderHints[relative] === true;
3428
+ try {
3429
+ const nextHints = createRouteLoaderHintsForVirtualModules(resolved, root);
3430
+ shouldReloadClientEntry ||= previousHint !== (nextHints[relative] === true);
3431
+ clientRouteLoaderHints = nextHints;
3432
+ } catch {
3433
+ shouldReloadClientEntry = true;
3434
+ }
3435
+ }
2980
3436
  if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
2981
3437
  clearPagesAppSourceCache();
2982
3438
  invalidateVirtualModules(server);
2983
- if (!sendServerOnlyFullReload(server, file) && shouldReloadClientHead && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
3439
+ const sentFullReload = sendServerOnlyFullReload(server, file);
3440
+ if (!sentFullReload && !shouldReloadClientEntry) sendRouteDataStale(server);
3441
+ if (!sentFullReload && shouldReloadClientEntry && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
2984
3442
  return;
2985
3443
  }
2986
3444
  if (!isPagesMode && relative === resolved.appFile) {
@@ -3018,7 +3476,9 @@ function pracht(options = {}) {
3018
3476
  if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
3019
3477
  }
3020
3478
  }
3021
- if (!sendServerOnlyFullReload(server, file) && shouldReloadClientHead && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
3479
+ const sentFullReload = sendServerOnlyFullReload(server, file);
3480
+ if (!sentFullReload && shouldReloadClientEntry && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
3481
+ if (!sentFullReload && (changesRouteHeadSource || changesRouteLoaderDependency)) sendRouteDataStale(server);
3022
3482
  }
3023
3483
  };
3024
3484
  const configuredBasePlugin = {
@@ -3056,12 +3516,15 @@ function pracht(options = {}) {
3056
3516
  ...resolved.precompileSsrJsx === true ? {} : resolved.precompileSsrJsx,
3057
3517
  ssrOnly: true
3058
3518
  }) : null;
3519
+ const preactPlugins = preact();
3520
+ const clientModulePrefreshPlugin = createClientModulePrefreshPlugin(preactPlugins, { isRouteOrShellModule: (id) => isRouteOrShellFile(id, routeFileDirs, routeFileExtensions) });
3059
3521
  const plugins = [
3060
3522
  ...precompilePlugin ? [precompilePlugin] : [],
3061
- ...preact(),
3523
+ ...preactPlugins,
3062
3524
  prachtPlugin,
3063
3525
  configuredBasePlugin,
3064
3526
  clientModuleTransformPlugin,
3527
+ ...clientModulePrefreshPlugin ? [clientModulePrefreshPlugin] : [],
3065
3528
  ...edgeRuntimeSafetyPlugin ? [edgeRuntimeSafetyPlugin] : [],
3066
3529
  createEnvSafetyPlugin(resolved.envSafety)
3067
3530
  ];
@@ -3315,4 +3778,4 @@ function withTrailingSep(p) {
3315
3778
  return p.endsWith("/") ? p : `${p}/`;
3316
3779
  }
3317
3780
  //#endregion
3318
- export { PRACHT_CAPABILITIES_MODULE_ID, PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PRACHT_WEBMCP_MODULE_ID, PUBLIC_ENV_PREFIX, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtCapabilitiesClientModuleSource, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, createPrachtWebmcpModuleSource, extractCapabilities, formatEnvLeakError, pracht, scanCodeForEnvLeaks };
3781
+ export { FRAMEWORK_VENDOR_CHUNK, PRACHT_CAPABILITIES_MODULE_ID, PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PRACHT_WEBMCP_MODULE_ID, PUBLIC_ENV_PREFIX, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtCapabilitiesClientModuleSource, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, createPrachtWebmcpModuleSource, extractCapabilities, formatEnvLeakError, frameworkChunkGroups, pracht, scanCodeForEnvLeaks };
@@ -46,6 +46,7 @@ function namedDeclarationRe(exportName) {
46
46
  return new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+${exportName}\\b`);
47
47
  }
48
48
  const HEAD_DECLARATION_RE = namedDeclarationRe("head");
49
+ const HEADERS_DECLARATION_RE = namedDeclarationRe("headers");
49
50
  const STATIC_PATHS_DECLARATION_RE = namedDeclarationRe("getStaticPaths");
50
51
  const EXPORT_BLOCK_RE = /export\s*\{([^}]*)\}\s*(?:from\s*["'][^"']+["'])?/g;
51
52
  const EXPORT_ALL_RE = /export\s+\*\s+from\b/;
@@ -168,10 +169,13 @@ function exportSpecifiersInclude(specifiers, exportName) {
168
169
  * Whether `source` exports `exportName`, via a declaration, an export block,
169
170
  * or an `export *` re-export (which could expose anything, so it counts).
170
171
  *
171
- * Comments and strings are masked first, so prose or a string literal
172
- * mentioning the name cannot produce a false positive.
172
+ * Ordinary TS/JS is parsed exactly, including string-literal export names.
173
+ * Custom syntaxes fall back to masked lexical detection so prose or a string
174
+ * literal mentioning the name cannot produce a false positive.
173
175
  */
174
176
  function detectNamedExport(source, exportName, declarationRe) {
177
+ const parsedResult = inspectParsedModule(source, exportName);
178
+ if (parsedResult !== void 0) return parsedResult;
175
179
  const analysisSource = maskCommentsAndStrings(source);
176
180
  if (declarationRe.test(analysisSource) || variableDeclarationExports(analysisSource, exportName)) return true;
177
181
  for (const match of analysisSource.matchAll(EXPORT_BLOCK_RE)) if (exportSpecifiersInclude(match[1], exportName)) return true;
@@ -180,6 +184,10 @@ function detectNamedExport(source, exportName, declarationRe) {
180
184
  function detectHeadExport(source) {
181
185
  return detectNamedExport(source, "head", HEAD_DECLARATION_RE);
182
186
  }
187
+ /** Whether the route or shell module exports document response headers. */
188
+ function detectHeadersExport(source) {
189
+ return detectNamedExport(source, "headers", HEADERS_DECLARATION_RE);
190
+ }
183
191
  /**
184
192
  * Whether the route module exports `getStaticPaths()`.
185
193
  *
@@ -195,25 +203,25 @@ function detectStaticPathsExport(source) {
195
203
  function isSyntaxNode(value) {
196
204
  return typeof value === "object" && value !== null && typeof value.type === "string";
197
205
  }
198
- function bindingIncludesLoader(node) {
206
+ function bindingIncludesName(node, exportName) {
199
207
  if (!isSyntaxNode(node)) return false;
200
- if (node.type === "Identifier") return node.name === "loader";
201
- if (node.type === "AssignmentPattern") return bindingIncludesLoader(node.left);
202
- if (node.type === "RestElement") return bindingIncludesLoader(node.argument);
203
- if (node.type === "ArrayPattern") return Array.isArray(node.elements) && node.elements.some(bindingIncludesLoader);
208
+ if (node.type === "Identifier") return node.name === exportName;
209
+ if (node.type === "AssignmentPattern") return bindingIncludesName(node.left, exportName);
210
+ if (node.type === "RestElement") return bindingIncludesName(node.argument, exportName);
211
+ if (node.type === "ArrayPattern") return Array.isArray(node.elements) && node.elements.some((element) => bindingIncludesName(element, exportName));
204
212
  if (node.type === "ObjectPattern") return Array.isArray(node.properties) && node.properties.some((property) => {
205
213
  if (!isSyntaxNode(property)) return false;
206
- return property.type === "RestElement" ? bindingIncludesLoader(property.argument) : bindingIncludesLoader(property.value);
214
+ return property.type === "RestElement" ? bindingIncludesName(property.argument, exportName) : bindingIncludesName(property.value, exportName);
207
215
  });
208
216
  return false;
209
217
  }
210
- function exportedNameIsLoader(node) {
218
+ function exportedNameMatches(node, exportName) {
211
219
  if (!isSyntaxNode(node)) return false;
212
- if (node.type === "Identifier") return node.name === "loader";
213
- if (node.type === "StringLiteral") return node.value === "loader";
220
+ if (node.type === "Identifier") return node.name === exportName;
221
+ if (node.type === "StringLiteral") return node.value === exportName;
214
222
  return false;
215
223
  }
216
- function inspectParsedModule(source) {
224
+ function inspectParsedModule(source, exportName) {
217
225
  for (const plugins of [["typescript", "jsx"], ["typescript"]]) {
218
226
  let body;
219
227
  try {
@@ -230,19 +238,19 @@ function inspectParsedModule(source) {
230
238
  continue;
231
239
  }
232
240
  if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
233
- if (Array.isArray(statement.specifiers) && statement.specifiers.some((specifier) => isSyntaxNode(specifier) && specifier.exportKind !== "type" && exportedNameIsLoader(specifier.exported))) return true;
241
+ if (Array.isArray(statement.specifiers) && statement.specifiers.some((specifier) => isSyntaxNode(specifier) && specifier.exportKind !== "type" && exportedNameMatches(specifier.exported, exportName))) return true;
234
242
  const declaration = statement.declaration;
235
243
  if (!isSyntaxNode(declaration)) continue;
236
244
  if (declaration.declare === true || declaration.type.startsWith("TS")) continue;
237
245
  if (declaration.type === "VariableDeclaration") {
238
- if (Array.isArray(declaration.declarations) && declaration.declarations.some((declarator) => isSyntaxNode(declarator) && bindingIncludesLoader(declarator.id))) return true;
239
- } else if (bindingIncludesLoader(declaration.id)) return true;
246
+ if (Array.isArray(declaration.declarations) && declaration.declarations.some((declarator) => isSyntaxNode(declarator) && bindingIncludesName(declarator.id, exportName))) return true;
247
+ } else if (bindingIncludesName(declaration.id, exportName)) return true;
240
248
  }
241
249
  return false;
242
250
  }
243
251
  }
244
252
  function detectLoaderExport(source) {
245
- const parsedResult = inspectParsedModule(source);
253
+ const parsedResult = inspectParsedModule(source, "loader");
246
254
  if (parsedResult !== void 0) return parsedResult;
247
255
  try {
248
256
  const [imports, exports] = parse$1(source);
@@ -309,6 +317,26 @@ function createRouteHeadHints(routesDir, options = {}) {
309
317
  }
310
318
  return hints;
311
319
  }
320
+ function createRouteHeadersHints(routesDir, options = {}) {
321
+ const files = [];
322
+ const hints = {};
323
+ const additionalExtensions = normalizeAdditionalExtensions(options.additionalExtensions);
324
+ scanRouteFiles(routesDir, files, withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, additionalExtensions));
325
+ for (const file of files) {
326
+ const extension = extname(file);
327
+ const hasHeaders = extension === ".md" || extension === ".mdx" || additionalExtensions.includes(extension) || detectHeadersExport(readFileSync(file, "utf-8"));
328
+ const relativeToRoutesDir = toPosixPath(relative(routesDir, file));
329
+ const routeRootPrefix = options.rootRelativePrefix?.replace(/\/$/, "");
330
+ const keys = /* @__PURE__ */ new Set();
331
+ if (options.appFileDir) {
332
+ const relativeToAppFile = toPosixPath(relative(options.appFileDir, file));
333
+ keys.add(relativeToAppFile.startsWith(".") ? relativeToAppFile : `./${relativeToAppFile}`);
334
+ }
335
+ if (routeRootPrefix) keys.add(`${routeRootPrefix}/${relativeToRoutesDir}`);
336
+ for (const key of keys) hints[key] = hasHeaders;
337
+ }
338
+ return hints;
339
+ }
312
340
  /**
313
341
  * Per-route-file `getStaticPaths()` presence, keyed the same way as the loader
314
342
  * and head hints.
@@ -375,6 +403,7 @@ function scan(dir, root, pages, pageExtensions, shellExtensions, additionalExten
375
403
  const revalidate = extractRevalidateSeconds(analysisSource, rel);
376
404
  const hasLoader = detectLoaderExport(analysisSource);
377
405
  const hasHead = ext === ".md" || ext === ".mdx" || additionalExtensions.has(ext) || detectHeadExport(analysisSource);
406
+ const hasHeaders = ext === ".md" || ext === ".mdx" || additionalExtensions.has(ext) || detectHeadersExport(analysisSource);
378
407
  pages.push({
379
408
  absolutePath: abs,
380
409
  relativePath: rel,
@@ -387,7 +416,8 @@ function scan(dir, root, pages, pageExtensions, shellExtensions, additionalExten
387
416
  revalidateSeconds: revalidate.seconds,
388
417
  hasRevalidateExport: revalidate.present,
389
418
  hasLoader,
390
- hasHead
419
+ hasHead,
420
+ hasHeaders
391
421
  });
392
422
  }
393
423
  }
@@ -585,4 +615,4 @@ function generateRoutesFile(pagesDir, outputPath, options) {
585
615
  ].join("\n"), "utf-8");
586
616
  }
587
617
  //#endregion
588
- export { sortRoutes as a, createRouteStaticPathsHints as c, extensionGlob as d, normalizeAdditionalExtensions as f, scanPagesDirectory as i, DEFAULT_ROUTE_EXTENSIONS as l, generatePagesManifestSource as n, createRouteHeadHints as o, withAdditionalExtensions as p, generateRoutesFile as r, createRouteLoaderHints as s, filePathToRoutePath as t, LEGACY_BARE_ROUTE_EXTENSIONS as u };
618
+ export { sortRoutes as a, createRouteLoaderHints as c, LEGACY_BARE_ROUTE_EXTENSIONS as d, extensionGlob as f, scanPagesDirectory as i, createRouteStaticPathsHints as l, withAdditionalExtensions as m, generatePagesManifestSource as n, createRouteHeadHints as o, normalizeAdditionalExtensions as p, generateRoutesFile as r, createRouteHeadersHints as s, filePathToRoutePath as t, DEFAULT_ROUTE_EXTENSIONS as u };
@@ -12,6 +12,7 @@ interface ScannedPage {
12
12
  hasRevalidateExport?: boolean;
13
13
  hasLoader?: boolean;
14
14
  hasHead?: boolean;
15
+ hasHeaders?: boolean;
15
16
  }
16
17
  interface PagesRouterOptions {
17
18
  pagesDir: string;
@@ -1,2 +1,2 @@
1
- import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-KcCRkhnf.mjs";
1
+ import { a as sortRoutes, i as scanPagesDirectory, n as generatePagesManifestSource, r as generateRoutesFile, t as filePathToRoutePath } from "./pages-router-MA9rOl88.mjs";
2
2
  export { filePathToRoutePath, generatePagesManifestSource, generateRoutesFile, scanPagesDirectory, sortRoutes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/vite-plugin",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Vite plugin for Pracht apps with virtual modules, dev SSR, prerendering, route inspection, and multi-adapter builds.",
5
5
  "keywords": [
6
6
  "pracht",
@@ -46,10 +46,10 @@
46
46
  "@preact/preset-vite": "^2.10.5",
47
47
  "@prefresh/vite": "^2.0.0",
48
48
  "es-module-lexer": "^1.7.0",
49
- "@pracht/adapter-node": "0.4.0",
50
- "@pracht/capabilities": "0.2.0",
49
+ "@pracht/adapter-node": "0.4.2",
50
+ "@pracht/capabilities": "0.3.0",
51
51
  "@pracht/preact-ssr-precompile": "0.1.3",
52
- "@pracht/core": "0.14.0"
52
+ "@pracht/core": "0.16.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "vite": "^8.0.0"