@pracht/vite-plugin 0.11.0 → 0.12.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.mjs CHANGED
@@ -1,83 +1,17 @@
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";
1
+ import { C as withAdditionalExtensions, D as isPrefreshCompatibleId, E as isPrachtClientModuleId, O as stripPrachtClientModuleQuery, S as normalizeAdditionalExtensions, T as getRolldownLang, _ as createRouteLoaderHints, b as LEGACY_BARE_ROUTE_EXTENSIONS, d as generatePagesManifestSource, g as createRouteHints, k as toPrachtClientPrefreshId, m as scanPagesDirectory, t as GENERATED_PAGES_LAYOUT_EXPORT, v as DEFAULT_ROUTE_EXTENSIONS, w as PRACHT_CLIENT_MODULE_QUERY, x as extensionGlob, y as DEFAULT_SHELL_EXTENSIONS } from "./pages-router-BI34Cani.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";
5
5
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
6
6
  import { dirname, extname, join, resolve } from "node:path";
7
- import { loadEnv, parseAst } from "vite";
7
+ import { loadEnv, parseAst, runnerImport } from "vite";
8
8
  import { PRACHT_GRAPH_ONLY_ENV } from "@pracht/core/server";
9
+ import { PUBLIC_ENV_PREFIX, VITE_BUILTIN_ENV_VARS, WHOLE_ENV_READ, extractCapabilityProjection, extractCapabilityRegistrations, extractDefineAppObjectBody, scanCodeForEnvLeaks, scanCodeForEnvLeaks as scanCodeForEnvLeaks$1, scanTopLevelProperties } from "@pracht/capabilities/static";
9
10
  import { DEV_ROUTE_DATA_STALE_EVENT } from "@pracht/core/client";
10
11
  import { CAPABILITY_SETTLED_EVENT, CAPABILITY_TRANSPORT_HEADER, CONFIRMATION_HEADER } from "@pracht/capabilities";
11
- import { extractCapabilityProjection, extractCapabilityRegistrations, extractDefineAppObjectBody, scanTopLevelProperties } from "@pracht/capabilities/static";
12
12
  import { createNodeServerEntryModule } from "@pracht/adapter-node";
13
13
  import { Readable } from "node:stream";
14
- import { applyDefaultSecurityHeaders, resolveRegistryModule } from "@pracht/core";
15
- //#region src/client-module-query.ts
16
- const CLIENT_MODULE_QUERY = "pracht-client";
17
- const PRACHT_CLIENT_MODULE_QUERY = `?${CLIENT_MODULE_QUERY}`;
18
- function isPrachtClientModuleId(id) {
19
- const queryStart = id.indexOf("?");
20
- if (queryStart === -1) return false;
21
- return id.slice(queryStart + 1).split("&").includes(CLIENT_MODULE_QUERY);
22
- }
23
- function stripPrachtClientModuleQuery(id) {
24
- const queryStart = id.indexOf("?");
25
- if (queryStart === -1) return id;
26
- const path = id.slice(0, queryStart);
27
- const query = id.slice(queryStart + 1).split("&").filter((part) => part !== CLIENT_MODULE_QUERY);
28
- return query.length > 0 ? `${path}?${query.join("&")}` : path;
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
- }
71
- function getRolldownLang(id) {
72
- const path = stripPrachtClientModuleQuery(id).split("?")[0];
73
- if (/\.(c|m)?tsx$/i.test(path)) return "tsx";
74
- if (/\.(c|m)?ts$/i.test(path)) return "ts";
75
- if (/\.(c|m)?jsx$/i.test(path)) return "jsx";
76
- if (/\.mdx?$/i.test(path)) return "jsx";
77
- if (/\.(c|m)?js$/i.test(path)) return "js";
78
- return "tsx";
79
- }
80
- //#endregion
14
+ import { applyDefaultSecurityHeaders, normalizeResponseHeaders, resolveRegistryModule } from "@pracht/core";
81
15
  //#region src/scope-analysis-types.ts
82
16
  const JSX_COMPONENT_RE = /^[A-Z]/;
83
17
  const SKIPPED_KEYS = new Set([
@@ -692,15 +626,19 @@ const SERVER_ONLY_EXPORTS = new Set([
692
626
  "getStaticPaths",
693
627
  "markdown"
694
628
  ]);
695
- function stripServerOnlyExportsForClient(code, id = "pracht-client-route.tsx") {
629
+ function stripServerOnlyExportsForClient(code, id = "pracht-client-route.tsx", options = {}) {
696
630
  const states = createStatementStates(parseAst(code, { lang: getRolldownLang(id) }));
631
+ if (options.middleware === true) {
632
+ for (const state of states) state.removed = true;
633
+ return renderProgram(code, states);
634
+ }
697
635
  const initialBindingNames = collectCurrentTopLevelBindingNames(states);
698
- const { changed, candidates } = removeServerOnlyExports(states, initialBindingNames);
636
+ const { changed, candidates } = removeServerOnlyExports(states, initialBindingNames, SERVER_ONLY_EXPORTS);
699
637
  if (!changed) return code;
700
638
  pruneDeadBindings(states, initialBindingNames, candidates);
701
639
  return renderProgram(code, states);
702
640
  }
703
- function removeServerOnlyExports(states, initialBindingNames) {
641
+ function removeServerOnlyExports(states, initialBindingNames, serverOnlyExports) {
704
642
  let changed = false;
705
643
  const candidates = /* @__PURE__ */ new Set();
706
644
  for (const state of states) {
@@ -709,14 +647,14 @@ function removeServerOnlyExports(states, initialBindingNames) {
709
647
  const declaration = statement.declaration;
710
648
  if (declaration?.type === "FunctionDeclaration") {
711
649
  const name = declaration.id?.name;
712
- if (!name || !SERVER_ONLY_EXPORTS.has(name)) continue;
650
+ if (!name || !serverOnlyExports.has(name)) continue;
713
651
  changed = true;
714
652
  state.removed = true;
715
653
  enqueueDependencies(candidates, collectTopLevelReferences(declaration, initialBindingNames, new Set([name])));
716
654
  continue;
717
655
  }
718
656
  if (declaration?.type === "VariableDeclaration") {
719
- const removable = getRemainingDeclaratorIndices(state).filter((index) => collectBindingNamesFromPattern(declaration.declarations[index].id).some((name) => SERVER_ONLY_EXPORTS.has(name)));
657
+ const removable = getRemainingDeclaratorIndices(state).filter((index) => collectBindingNamesFromPattern(declaration.declarations[index].id).some((name) => serverOnlyExports.has(name)));
720
658
  if (removable.length === 0) continue;
721
659
  changed = true;
722
660
  for (const index of removable) {
@@ -733,7 +671,7 @@ function removeServerOnlyExports(states, initialBindingNames) {
733
671
  if (specifier.type !== "ExportSpecifier" || specifier.exportKind === "type") return false;
734
672
  const localName = getIdentifierName(specifier.local);
735
673
  const exportedName = getIdentifierName(specifier.exported);
736
- return SERVER_ONLY_EXPORTS.has(localName ?? "") || SERVER_ONLY_EXPORTS.has(exportedName ?? "");
674
+ return serverOnlyExports.has(localName ?? "") || serverOnlyExports.has(exportedName ?? "");
737
675
  });
738
676
  if (removableSpecifiers.length === 0) continue;
739
677
  changed = true;
@@ -1016,243 +954,14 @@ function isRecord(value) {
1016
954
  }
1017
955
  //#endregion
1018
956
  //#region src/env-safety.ts
1019
- /**
1020
- * Env vars Vite defines on `import.meta.env` in every bundle, plus NODE_ENV
1021
- * which Vite's define pass statically replaces at build time (so it can never
1022
- * leak and is referenced by countless dependencies).
1023
- */
1024
- const VITE_BUILTIN_ENV_VARS = new Set([
1025
- "MODE",
1026
- "DEV",
1027
- "PROD",
1028
- "SSR",
1029
- "BASE_URL",
1030
- "NODE_ENV"
1031
- ]);
1032
- /** Prefix that marks an env var as intentionally public. */
1033
- const PUBLIC_ENV_PREFIX = "PRACHT_PUBLIC_";
1034
957
  /** Server-only core entry that must never resolve into client bundles. */
1035
958
  const SERVER_ENV_MODULE_ID = "@pracht/core/env/server";
1036
- const ENV_REFERENCE_RE = /\b(process\.env|import\.meta\.env)(?:\??\.([A-Za-z_$][A-Za-z0-9_$]*)|(?:\?\.)?\[\s*(["'])([A-Za-z_$][A-Za-z0-9_$]*)\3\s*\])/g;
1037
- const WHOLE_ENV_READ_RE = /\bimport\.meta\.env\b(?!\s*\??\.\s*[A-Za-z_$])/g;
1038
- /**
1039
- * Scans JavaScript source for references to environment variables that are
1040
- * neither public-prefixed, Vite built-ins, nor explicitly allowed, plus reads
1041
- * that pull in the whole `import.meta.env` object.
1042
- */
1043
- function scanCodeForEnvLeaks(code, allow = /* @__PURE__ */ new Set()) {
1044
- const codePositions = getCodePositionMask(code);
1045
- const matches = [];
1046
- for (const match of code.matchAll(ENV_REFERENCE_RE)) {
1047
- const index = match.index ?? -1;
1048
- if (!codePositions[index]) continue;
1049
- const accessor = match[1];
1050
- const name = match[2] ?? match[4];
1051
- if (!name) continue;
1052
- if (name.startsWith("PRACHT_PUBLIC_")) continue;
1053
- if (VITE_BUILTIN_ENV_VARS.has(name)) continue;
1054
- if (allow.has(name)) continue;
1055
- matches.push({
1056
- index,
1057
- reference: {
1058
- accessor,
1059
- name
1060
- }
1061
- });
1062
- }
1063
- if (!allow.has("*")) for (const match of code.matchAll(WHOLE_ENV_READ_RE)) {
1064
- const index = match.index ?? -1;
1065
- if (!codePositions[index]) continue;
1066
- matches.push({
1067
- index,
1068
- reference: {
1069
- accessor: "import.meta.env",
1070
- name: "*"
1071
- }
1072
- });
1073
- }
1074
- const findings = [];
1075
- const seen = /* @__PURE__ */ new Set();
1076
- for (const { reference } of matches.sort((a, b) => a.index - b.index)) {
1077
- const key = `${reference.accessor}.${reference.name}`;
1078
- if (seen.has(key)) continue;
1079
- seen.add(key);
1080
- findings.push(reference);
1081
- }
1082
- return findings;
1083
- }
1084
- function getCodePositionMask(code) {
1085
- const mask = new Uint8Array(code.length);
1086
- const templateExpressionDepths = [];
1087
- let mode = "code";
1088
- let regexCharClass = false;
1089
- let i = 0;
1090
- while (i < code.length) {
1091
- const char = code[i];
1092
- const next = code[i + 1];
1093
- if (mode === "line-comment") {
1094
- if (char === "\n" || char === "\r") {
1095
- mode = "code";
1096
- mask[i] = 1;
1097
- }
1098
- i++;
1099
- continue;
1100
- }
1101
- if (mode === "block-comment") {
1102
- if (char === "*" && next === "/") {
1103
- mode = "code";
1104
- i += 2;
1105
- } else i++;
1106
- continue;
1107
- }
1108
- if (mode === "single" || mode === "double") {
1109
- const quote = mode === "single" ? "'" : "\"";
1110
- if (char === "\\") {
1111
- i += 2;
1112
- continue;
1113
- }
1114
- if (char === quote || char === "\n" || char === "\r") mode = "code";
1115
- i++;
1116
- continue;
1117
- }
1118
- if (mode === "regex") {
1119
- if (char === "\\") {
1120
- i += 2;
1121
- continue;
1122
- }
1123
- if (char === "[") {
1124
- regexCharClass = true;
1125
- i++;
1126
- continue;
1127
- }
1128
- if (char === "]") {
1129
- regexCharClass = false;
1130
- i++;
1131
- continue;
1132
- }
1133
- if (char === "/" && !regexCharClass) {
1134
- regexCharClass = false;
1135
- i++;
1136
- while (i < code.length && isIdentifierChar(code[i])) i++;
1137
- mode = "code";
1138
- continue;
1139
- }
1140
- if (char === "\n" || char === "\r") {
1141
- regexCharClass = false;
1142
- mode = "code";
1143
- }
1144
- i++;
1145
- continue;
1146
- }
1147
- if (mode === "template") {
1148
- if (char === "\\") {
1149
- i += 2;
1150
- continue;
1151
- }
1152
- if (char === "`") {
1153
- mode = "code";
1154
- i++;
1155
- continue;
1156
- }
1157
- if (char === "$" && next === "{") {
1158
- mask[i] = 1;
1159
- mask[i + 1] = 1;
1160
- templateExpressionDepths.push(1);
1161
- mode = "code";
1162
- i += 2;
1163
- continue;
1164
- }
1165
- i++;
1166
- continue;
1167
- }
1168
- mask[i] = 1;
1169
- if (char === "/" && next === "/") {
1170
- mask[i + 1] = 1;
1171
- mode = "line-comment";
1172
- i += 2;
1173
- continue;
1174
- }
1175
- if (char === "/" && next === "*") {
1176
- mask[i + 1] = 1;
1177
- mode = "block-comment";
1178
- i += 2;
1179
- continue;
1180
- }
1181
- if (char === "/" && isRegexLiteralStart(code, i)) {
1182
- mode = "regex";
1183
- regexCharClass = false;
1184
- i++;
1185
- continue;
1186
- }
1187
- if (char === "'") {
1188
- mode = "single";
1189
- i++;
1190
- continue;
1191
- }
1192
- if (char === "\"") {
1193
- mode = "double";
1194
- i++;
1195
- continue;
1196
- }
1197
- if (char === "`") {
1198
- mode = "template";
1199
- i++;
1200
- continue;
1201
- }
1202
- if (templateExpressionDepths.length > 0) {
1203
- const top = templateExpressionDepths.length - 1;
1204
- if (char === "{") templateExpressionDepths[top]++;
1205
- else if (char === "}") {
1206
- templateExpressionDepths[top]--;
1207
- if (templateExpressionDepths[top] === 0) {
1208
- templateExpressionDepths.pop();
1209
- mode = "template";
1210
- }
1211
- }
1212
- }
1213
- i++;
1214
- }
1215
- return mask;
1216
- }
1217
- function isRegexLiteralStart(code, slashIndex) {
1218
- let i = slashIndex - 1;
1219
- while (i >= 0 && /\s/.test(code[i])) i--;
1220
- if (i < 0) return true;
1221
- const previous = code[i];
1222
- if (previous === ">" && code[i - 1] === "=") return true;
1223
- if ("([{=,:;!?&|^~<>*%+-".includes(previous)) return true;
1224
- if (isIdentifierChar(previous)) {
1225
- let start = i;
1226
- while (start >= 0 && isIdentifierChar(code[start])) start--;
1227
- const word = code.slice(start + 1, i + 1);
1228
- return new Set([
1229
- "await",
1230
- "case",
1231
- "delete",
1232
- "do",
1233
- "else",
1234
- "in",
1235
- "instanceof",
1236
- "new",
1237
- "of",
1238
- "return",
1239
- "throw",
1240
- "typeof",
1241
- "void",
1242
- "yield"
1243
- ]).has(word);
1244
- }
1245
- return false;
1246
- }
1247
- function isIdentifierChar(char) {
1248
- return !!char && /[A-Za-z0-9_$]/.test(char);
1249
- }
1250
959
  function formatEnvLeakError(problems) {
1251
960
  const lines = problems.map((problem) => {
1252
961
  const source = problem.sources.length > 0 ? ` (likely from ${problem.sources.map((file) => JSON.stringify(file)).join(", ")})` : "";
1253
- return ` - ${problem.name === "*" ? "import.meta.env read as a whole object" : `${problem.accessor}.${problem.name}`} in chunk "${problem.chunk}"${source}`;
962
+ return ` - ${problem.name === WHOLE_ENV_READ ? "import.meta.env read as a whole object" : `${problem.accessor}.${problem.name}`} in chunk "${problem.chunk}"${source}`;
1254
963
  });
1255
- const wholeEnvGuidance = problems.some((problem) => problem.name === "*") ? [
964
+ const wholeEnvGuidance = problems.some((problem) => problem.name === WHOLE_ENV_READ) ? [
1256
965
  "",
1257
966
  "A whole-object `import.meta.env` read (bare reference, destructuring, spread, or bracket access)",
1258
967
  "is replaced at build time by an object literal containing every exposed variable — including the",
@@ -1293,7 +1002,7 @@ function createEnvSafetyPlugin(envSafety) {
1293
1002
  if (transformOptions?.ssr) return null;
1294
1003
  const moduleId = stripIdQuery(id);
1295
1004
  if (moduleId.includes("node_modules")) return null;
1296
- const findings = scanCodeForEnvLeaks(code, allow);
1005
+ const findings = scanCodeForEnvLeaks$1(code, allow);
1297
1006
  if (findings.length > 0) moduleEnvReferences.set(moduleId, findings);
1298
1007
  return null;
1299
1008
  },
@@ -1321,7 +1030,7 @@ function createEnvSafetyPlugin(envSafety) {
1321
1030
  sources: [moduleId]
1322
1031
  });
1323
1032
  }
1324
- for (const finding of scanCodeForEnvLeaks(output.code, allow)) {
1033
+ for (const finding of scanCodeForEnvLeaks$1(output.code, allow)) {
1325
1034
  const sources = moduleIds.filter((moduleId) => moduleEnvReferences.get(moduleId)?.some((reference) => reference.name === finding.name));
1326
1035
  addProblem({
1327
1036
  ...finding,
@@ -1499,12 +1208,13 @@ const ISLANDS_CLIENT_BROWSER_PATH = "/@pracht/islands.js";
1499
1208
  function assetUrl(file, base) {
1500
1209
  return `${base}${file}`;
1501
1210
  }
1502
- function readClientBuildAssets(root = process.cwd(), base = "/") {
1211
+ function readClientBuildAssets(root = process.cwd(), base = "/", inlineCss = false) {
1503
1212
  const manifestPath = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"].map((candidate) => resolve(root, candidate)).find((candidate) => existsSync(candidate));
1504
1213
  if (!manifestPath) return {
1505
1214
  clientEntryUrl: null,
1506
1215
  islandsEntryUrl: null,
1507
1216
  cssManifest: {},
1217
+ cssContentManifest: {},
1508
1218
  jsManifest: {}
1509
1219
  };
1510
1220
  const rawManifest = readFileSync(manifestPath, "utf-8");
@@ -1512,12 +1222,20 @@ function readClientBuildAssets(root = process.cwd(), base = "/") {
1512
1222
  const clientEntry = manifest[PRACHT_CLIENT_MODULE_ID];
1513
1223
  const islandsEntry = manifest[PRACHT_ISLANDS_CLIENT_MODULE_ID];
1514
1224
  const cssManifest = {};
1225
+ const cssContentManifest = {};
1515
1226
  const jsManifest = {};
1227
+ const clientOutDir = dirname(dirname(manifestPath));
1516
1228
  for (const [key, entry] of Object.entries(manifest)) {
1517
1229
  if (!entry.src) continue;
1518
1230
  const deps = collectTransitiveDeps(manifest, key);
1519
1231
  const manifestKey = stripPrachtClientModuleQuery(entry.src);
1520
- if (deps.css.length > 0) cssManifest[manifestKey] = deps.css.map((f) => assetUrl(f, base));
1232
+ if (deps.css.length > 0) {
1233
+ cssManifest[manifestKey] = deps.css.map((f) => assetUrl(f, base));
1234
+ if (inlineCss) for (const file of deps.css) {
1235
+ const url = assetUrl(file, base);
1236
+ cssContentManifest[url] ??= readFileSync(resolve(clientOutDir, file), "utf-8");
1237
+ }
1238
+ }
1521
1239
  if (deps.js.length > 0) jsManifest[manifestKey] = deps.js.map((f) => assetUrl(f, base));
1522
1240
  }
1523
1241
  addEntryDeps(manifest, jsManifest, PRACHT_CLIENT_MODULE_ID, clientEntry, base);
@@ -1526,6 +1244,7 @@ function readClientBuildAssets(root = process.cwd(), base = "/") {
1526
1244
  clientEntryUrl: clientEntry ? assetUrl(clientEntry.file, base) : null,
1527
1245
  islandsEntryUrl: islandsEntry ? assetUrl(islandsEntry.file, base) : null,
1528
1246
  cssManifest,
1247
+ cssContentManifest,
1529
1248
  jsManifest
1530
1249
  };
1531
1250
  }
@@ -1584,10 +1303,14 @@ function createDefaultNodeAdapter() {
1584
1303
  }
1585
1304
  //#endregion
1586
1305
  //#region src/plugin-options.ts
1587
- const CLIENT_FEATURE_DEFAULTS = { prefetch: true };
1306
+ const CLIENT_FEATURE_DEFAULTS = {
1307
+ prefetch: true,
1308
+ navigationGuards: true
1309
+ };
1588
1310
  const DEFAULTS = {
1589
1311
  client: CLIENT_FEATURE_DEFAULTS,
1590
1312
  vendorChunk: true,
1313
+ inlineCss: false,
1591
1314
  appFile: "/src/routes.ts",
1592
1315
  middlewareDir: "/src/middleware",
1593
1316
  routesDir: "/src/routes",
@@ -1615,6 +1338,7 @@ function resolveOptions(options) {
1615
1338
  if (resolved.llmsTxt === void 0) resolved.llmsTxt = false;
1616
1339
  resolved.client = resolveClientOptions(options.client);
1617
1340
  if (typeof resolved.vendorChunk !== "boolean") throw new Error(`pracht({ vendorChunk }) expects a boolean, got ${JSON.stringify(resolved.vendorChunk)}.`);
1341
+ if (typeof resolved.inlineCss !== "boolean") throw new Error(`pracht({ inlineCss }) expects a boolean, got ${JSON.stringify(resolved.inlineCss)}.`);
1618
1342
  resolved.additionalExtensions = normalizeAdditionalExtensions(resolved.additionalExtensions);
1619
1343
  if (!new Set([
1620
1344
  "spa",
@@ -1677,18 +1401,50 @@ function validateBudgets(budgets) {
1677
1401
  * capability sources — the same approach the plugin already uses for
1678
1402
  * hydration-mode excludes. Only serializable metadata crosses the boundary:
1679
1403
  * capability names, HTTP endpoints, effects, and (for WebMCP tools)
1680
- * description and input schema.
1404
+ * description and input schema. Inline JSON Schema remains a zero-execution
1405
+ * fast path; imported Standard JSON Schemas are resolved by loading the
1406
+ * server-only capability module during WebMCP code generation and serializing
1407
+ * only the converted schema.
1681
1408
  *
1682
1409
  * The static analyzer itself lives in `@pracht/capabilities/static` and is
1683
1410
  * shared with `pracht verify`, so the build and verification can never
1684
- * disagree about what is analyzable. Constraint it imposes: a capability's
1685
- * `expose`, HTTP-projected `effect`, and WebMCP `input` values must be inline
1686
- * literals (no imported constants or spreads) the extractor parses the
1687
- * literal text as data.
1411
+ * disagree about what is analyzable. A capability's `expose` and
1412
+ * HTTP-projected `effect` stay inline literals because they decide which
1413
+ * client endpoints exist. WebMCP `input` may instead be a Standard JSON
1414
+ * Schema: codegen resolves it through the server module without putting the
1415
+ * validator or capability implementation in the client graph.
1688
1416
  * Extraction failures fail the build with a pointer to the offending file
1689
1417
  * rather than silently dropping an endpoint.
1690
1418
  */
1691
1419
  /**
1420
+ * The app manifest source these analyzers read.
1421
+ *
1422
+ * In pages mode there is no manifest file, so the generated one is
1423
+ * synthesized here — the exact source the virtual module serves. Analyzing the
1424
+ * generated text rather than re-deriving the registry from the file system is
1425
+ * what keeps pages mode and manifest mode from ever disagreeing about which
1426
+ * capabilities exist and how they are exposed.
1427
+ *
1428
+ * Throws when the manifest cannot be read or the pages tree is invalid; each
1429
+ * caller decides whether that means "assume the worst" or "report nothing".
1430
+ */
1431
+ function readAppManifestSource(resolved, root) {
1432
+ if (!resolved.pagesDir) return readFileSync(resolve(root, resolved.appFile.replace(/^\//, "")), "utf-8");
1433
+ const pagesDir = resolve(root, resolved.pagesDir.replace(/^\//, ""));
1434
+ return generatePagesManifestSource(scanPagesDirectory(pagesDir, [...resolved.additionalExtensions]), {
1435
+ additionalExtensions: resolved.additionalExtensions,
1436
+ capabilitiesDir: resolve(root, resolved.capabilitiesDir.replace(/^\//, "")),
1437
+ capabilitiesDirPrefix: resolved.capabilitiesDir,
1438
+ pagesDir,
1439
+ pagesDefaultRender: resolved.pagesDefaultRender,
1440
+ pagesDirPrefix: resolved.pagesDir
1441
+ }).replace("const app = defineApp(", "export const app = defineApp(");
1442
+ }
1443
+ /** The directory manifest-relative module refs resolve against. */
1444
+ function appManifestDir(resolved, root) {
1445
+ return resolved.pagesDir ? resolve(root, resolved.pagesDir.replace(/^\//, ""), "..") : dirname(resolve(root, resolved.appFile.replace(/^\//, "")));
1446
+ }
1447
+ /**
1692
1448
  * Whether the app can reach the agent surface at all — registered capabilities
1693
1449
  * or a `defineApp({ agents })` config. Drives the `__PRACHT_AGENT_SURFACE__`
1694
1450
  * define, which lets the bundler drop the capability and Web Bot Auth runtimes
@@ -1703,11 +1459,9 @@ function validateBudgets(budgets) {
1703
1459
  */
1704
1460
  function hasAgentSurface(options = {}, root = process.cwd()) {
1705
1461
  const resolved = resolveOptions(options);
1706
- if (resolved.pagesDir) return false;
1707
- const appFileAbs = resolve(root, resolved.appFile.replace(/^\//, ""));
1708
1462
  let manifestSource;
1709
1463
  try {
1710
- manifestSource = readFileSync(appFileAbs, "utf-8");
1464
+ manifestSource = readAppManifestSource(resolved, root);
1711
1465
  } catch {
1712
1466
  return true;
1713
1467
  }
@@ -1765,23 +1519,21 @@ function hasOpaqueTopLevelProperty(objectBody) {
1765
1519
  return false;
1766
1520
  }
1767
1521
  /**
1768
- * Extract capability registrations (name → module path) from the app
1769
- * manifest source and their exposure metadata from each capability source.
1770
- * Pages-router apps have no manifest, so capabilities are manifest-mode only.
1522
+ * Extract capability registrations (name → module path) from a manifest app
1523
+ * or the pages router's generated manifest, then read exposure metadata from
1524
+ * each capability source.
1771
1525
  */
1772
1526
  function extractCapabilities(options = {}, root = process.cwd()) {
1773
1527
  const resolved = resolveOptions(options);
1774
- if (resolved.pagesDir) return [];
1775
- const appFileAbs = resolve(root, resolved.appFile.replace(/^\//, ""));
1776
1528
  let manifestSource;
1777
1529
  try {
1778
- manifestSource = readFileSync(appFileAbs, "utf-8");
1530
+ manifestSource = readAppManifestSource(resolved, root);
1779
1531
  } catch {
1780
1532
  return [];
1781
1533
  }
1782
1534
  const registrations = extractCapabilityRegistrations(manifestSource);
1783
1535
  if (registrations.length === 0) return [];
1784
- const appDir = dirname(appFileAbs);
1536
+ const appDir = appManifestDir(resolved, root);
1785
1537
  return registrations.map(({ name, file }) => {
1786
1538
  const capabilityFileAbs = file.startsWith("/") ? resolve(root, file.replace(/^\//, "")) : resolve(appDir, file);
1787
1539
  let source;
@@ -1808,15 +1560,13 @@ function extractCapabilities(options = {}, root = process.cwd()) {
1808
1560
  */
1809
1561
  function resolveCapabilityModulePaths(options = {}, root = process.cwd()) {
1810
1562
  const resolved = resolveOptions(options);
1811
- if (resolved.pagesDir) return [];
1812
- const appFileAbs = resolve(root, resolved.appFile.replace(/^\//, ""));
1813
1563
  let manifestSource;
1814
1564
  try {
1815
- manifestSource = readFileSync(appFileAbs, "utf-8");
1565
+ manifestSource = readAppManifestSource(resolved, root);
1816
1566
  } catch {
1817
1567
  return [];
1818
1568
  }
1819
- const appDir = dirname(appFileAbs);
1569
+ const appDir = appManifestDir(resolved, root);
1820
1570
  return extractCapabilityRegistrations(manifestSource).map(({ file }) => file.startsWith("/") ? resolve(root, file.replace(/^\//, "")) : resolve(appDir, file));
1821
1571
  }
1822
1572
  function extractCapabilityMetadata(name, file, source) {
@@ -1973,73 +1723,110 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
1973
1723
  ].join("\n");
1974
1724
  }
1975
1725
  /**
1976
- * Generate `virtual:pracht/webmcp` — the disposable WebMCP registration shim.
1977
- * One page tool per `expose.webmcp` capability; `execute` dispatches through
1726
+ * Generate `virtual:pracht/webmcp` — the WebMCP registration shim.
1727
+ * One page tool per `expose.webmcp` capability; dispatch goes through
1978
1728
  * `callCapability`, so the user's session authenticates the call and all
1979
1729
  * validation/middleware/policy stays server-side. Each dispatch carries the
1980
1730
  * transport marker header so audit events can attribute it to WebMCP.
1981
1731
  *
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.
1732
+ * The registration runtime feature detection, `registerTool()` calls, and
1733
+ * the WebMCP annotation policy lives in
1734
+ * `@pracht/capabilities/webmcp` (published, so non-pracht sites register tools
1735
+ * with identical semantics); this module only
1736
+ * contributes the statically extracted tool metadata and the app-specific
1737
+ * dispatch. The registrar import is resolved from *this plugin's* copy of
1738
+ * `@pracht/capabilities` at codegen time: the virtual module has no importer
1739
+ * on disk, so a bare specifier would resolve against the app root — where an
1740
+ * older installed copy may predate the `./webmcp` entry point. The registrar
1741
+ * is stateless, so using the plugin's copy cannot split state.
1993
1742
  */
1994
1743
  function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
1995
- const tools = extractCapabilities(options, buildOptions.root).filter((capability) => capability.webmcp).map((capability) => ({
1744
+ const capabilities = extractCapabilities(options, buildOptions.root).filter((capability) => capability.webmcp);
1745
+ const unresolved = capabilities.find((capability) => !capability.inputSchema);
1746
+ if (unresolved) throw new Error(`[pracht] Capability ${JSON.stringify(unresolved.name)} (${unresolved.file}) uses a non-literal WebMCP input schema. Vite resolves it from the server module during normal codegen; callers of createPrachtWebmcpModuleSource() must supply inline JSON Schema.`);
1747
+ return buildPrachtWebmcpModuleSource(capabilities.map(webmcpTool));
1748
+ }
1749
+ async function createPrachtWebmcpModuleSourceAsync(options = {}, buildOptions = {}) {
1750
+ const capabilities = extractCapabilities(options, buildOptions.root).filter((capability) => capability.webmcp);
1751
+ const root = buildOptions.root ?? process.cwd();
1752
+ const appDir = appManifestDir(resolveOptions(options), root);
1753
+ const loadCapability = buildOptions.loadCapability ?? (async (file) => {
1754
+ return (await runnerImport(file, {
1755
+ ...buildOptions.runnerConfig,
1756
+ root,
1757
+ logLevel: "silent"
1758
+ })).module.default;
1759
+ });
1760
+ return buildPrachtWebmcpModuleSource(await Promise.all(capabilities.map(async (capability) => {
1761
+ let title = capability.title;
1762
+ let description = capability.description;
1763
+ let inputSchema = capability.inputSchema;
1764
+ if (!inputSchema) {
1765
+ const file = capability.file.startsWith("/") ? resolve(root, capability.file.replace(/^\//, "")) : resolve(appDir, capability.file);
1766
+ let loaded;
1767
+ try {
1768
+ loaded = await loadCapability(file);
1769
+ } catch (error) {
1770
+ throw new Error(`[pracht] Capability ${JSON.stringify(capability.name)} (${capability.file}) uses a non-literal WebMCP input schema, but its server module could not be loaded to derive that schema: ${error instanceof Error ? error.message : String(error)}. Keep the input as inline JSON Schema, or make the schema module loadable in the Node build environment.`);
1771
+ }
1772
+ if (!loaded || typeof loaded !== "object" || Array.isArray(loaded)) throw new Error(`[pracht] Capability ${JSON.stringify(capability.name)} (${capability.file}) did not default-export a capability object while deriving its WebMCP input schema.`);
1773
+ const runtime = loaded;
1774
+ if (!runtime.input || typeof runtime.input !== "object" || Array.isArray(runtime.input)) throw new Error(`[pracht] Capability ${JSON.stringify(capability.name)} (${capability.file}) did not resolve its WebMCP input to a JSON Schema object.`);
1775
+ inputSchema = runtime.input;
1776
+ if (typeof runtime.title === "string") title = runtime.title;
1777
+ if (typeof runtime.description === "string") description = runtime.description;
1778
+ }
1779
+ return webmcpTool({
1780
+ ...capability,
1781
+ title,
1782
+ description,
1783
+ inputSchema
1784
+ });
1785
+ })));
1786
+ }
1787
+ function webmcpTool(capability) {
1788
+ return {
1996
1789
  name: capability.name,
1997
1790
  ...capability.title ? { title: capability.title } : {},
1998
1791
  description: capability.description,
1999
1792
  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
- }
2006
- }));
1793
+ ...capability.effect ? { effect: capability.effect } : {},
1794
+ ...capability.webmcpUntrustedContent ? { untrustedContent: true } : {}
1795
+ };
1796
+ }
1797
+ function buildPrachtWebmcpModuleSource(tools) {
2007
1798
  return [
2008
1799
  "// Generated by @pracht/vite-plugin — WebMCP page-tool registration shim.",
1800
+ `import { registerWebmcpTools } from ${JSON.stringify(resolveWebmcpRegistrarSpecifier())};`,
2009
1801
  "import { callCapability } from \"virtual:pracht/capabilities\";",
2010
1802
  "",
2011
1803
  `const tools = ${JSON.stringify(tools)};`,
1804
+ "const toolsByName = new Map(tools.map((tool) => [tool.name, tool]));",
2012
1805
  `const transportHeaders = { ${JSON.stringify(CAPABILITY_TRANSPORT_HEADER)}: "webmcp" };`,
1806
+ "let registrationController;",
1807
+ "let activeNames = import.meta.hot?.data.activeNames ?? [];",
2013
1808
  "",
2014
- "export function registerPrachtWebmcpTools() {",
2015
- " const modelContext =",
2016
- " (typeof document !== \"undefined\" && document.modelContext) || null;",
2017
- " if (!modelContext || typeof modelContext.registerTool !== \"function\") {",
2018
- " return false;",
2019
- " }",
2020
- " for (const tool of tools) {",
2021
- " try {",
2022
- " const registration = modelContext.registerTool({",
2023
- " ...tool,",
2024
- " async execute(input, { signal } = {}) {",
2025
- " return callCapability(tool.name, input, {",
2026
- " headers: transportHeaders,",
2027
- " signal,",
2028
- " });",
2029
- " },",
2030
- " });",
2031
- " if (registration && typeof registration.catch === \"function\") {",
2032
- " registration.catch(() => {});",
2033
- " }",
2034
- " } catch {",
2035
- " // The API is still an origin-trial surface; a failed registration",
2036
- " // must never break the page.",
2037
- " }",
2038
- " }",
2039
- " return true;",
1809
+ "export function registerPrachtWebmcpTools(names) {",
1810
+ " activeNames = [...new Set(names)];",
1811
+ " registrationController?.abort();",
1812
+ " registrationController = new AbortController();",
1813
+ " return registerWebmcpTools(activeNames.flatMap((name) => {",
1814
+ " const tool = toolsByName.get(name);",
1815
+ " return tool ? [tool] : [];",
1816
+ " }), (name, input, { signal } = {}) =>",
1817
+ " callCapability(name, input, { headers: transportHeaders, signal }),",
1818
+ " { signal: registrationController.signal },",
1819
+ " );",
2040
1820
  "}",
2041
1821
  "",
2042
- "registerPrachtWebmcpTools();",
1822
+ "if (import.meta.hot) {",
1823
+ " import.meta.hot.dispose((data) => {",
1824
+ " data.activeNames = activeNames;",
1825
+ " registrationController?.abort();",
1826
+ " });",
1827
+ "}",
1828
+ "",
1829
+ "if (activeNames.length > 0) registerPrachtWebmcpTools(activeNames);",
2043
1830
  ""
2044
1831
  ].join("\n");
2045
1832
  }
@@ -2050,13 +1837,36 @@ function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
2050
1837
  */
2051
1838
  function createWebmcpBootstrapSource() {
2052
1839
  return [
2053
- "// WebMCP page tools — loaded only when the browser exposes the API.",
2054
- "if (typeof document !== \"undefined\" && document.modelContext) {",
2055
- " import(\"virtual:pracht/webmcp\").catch(() => {});",
1840
+ "// WebMCP page tools — the active route owns the registration lifetime.",
1841
+ "let webmcpRouteGeneration = 0;",
1842
+ "let webmcpModulePromise;",
1843
+ "function syncPrachtWebmcpTools(capabilities) {",
1844
+ " const generation = ++webmcpRouteGeneration;",
1845
+ " if (typeof document === \"undefined\" || !document.modelContext) return;",
1846
+ " if (capabilities.length === 0 && !webmcpModulePromise) return;",
1847
+ " webmcpModulePromise ??= import(\"virtual:pracht/webmcp\");",
1848
+ " webmcpModulePromise.then((module) => {",
1849
+ " if (generation === webmcpRouteGeneration) {",
1850
+ " module.registerPrachtWebmcpTools(capabilities);",
1851
+ " }",
1852
+ " }).catch(() => {});",
2056
1853
  "}",
2057
1854
  ""
2058
1855
  ];
2059
1856
  }
1857
+ /**
1858
+ * Absolute module specifier for `@pracht/capabilities/webmcp`, resolved from
1859
+ * this plugin so the generated shim never depends on the app root carrying a
1860
+ * new-enough copy. Falls back to the bare specifier if resolution fails
1861
+ * (tests with unusual layouts); Vite then resolves it from the app root.
1862
+ */
1863
+ function resolveWebmcpRegistrarSpecifier() {
1864
+ try {
1865
+ return createRequire(import.meta.url).resolve("@pracht/capabilities/webmcp").split("\\").join("/");
1866
+ } catch {
1867
+ return "@pracht/capabilities/webmcp";
1868
+ }
1869
+ }
2060
1870
  function hasWebmcpCapabilities(options = {}, root = process.cwd()) {
2061
1871
  try {
2062
1872
  return extractCapabilities(options, root).some((capability) => capability.webmcp);
@@ -2156,19 +1966,29 @@ function createNonFullHydrationExcludes(resolved, root = process.cwd()) {
2156
1966
  function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
2157
1967
  const resolved = resolveOptions(options);
2158
1968
  const isPagesMode = !!resolved.pagesDir;
2159
- const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
2160
- const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
2161
- const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
2162
- const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
1969
+ const routeHints = createRouteHintsForVirtualModules(resolved, buildOptions.root);
1970
+ const routeLoaderHints = routeHints.loader;
1971
+ const routeHeadHints = routeHints.head;
1972
+ const routeStaticPathsHints = routeHints.staticPaths;
1973
+ const webmcpEnabled = hasWebmcpCapabilities(resolved, buildOptions.root);
1974
+ const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root, "client") : `import { app } from ${JSON.stringify(resolved.appFile)};`;
2163
1975
  const bareRouteExtensions = [...withAdditionalExtensions(LEGACY_BARE_ROUTE_EXTENSIONS, resolved.additionalExtensions)];
2164
1976
  const dirPrefix = isPagesMode ? resolved.pagesDir : resolved.routesDir;
2165
1977
  const routeGlob = `${dirPrefix}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2166
1978
  const additionalRouteGlob = `${dirPrefix}/**/*.${extensionGlob(bareRouteExtensions)}`;
2167
1979
  const routeExcludes = createNonFullHydrationExcludes(resolved, buildOptions.root);
1980
+ const usesEjectedPagesLayout = isEjectedPagesLayout(resolved, buildOptions.root ?? process.cwd());
1981
+ if (isPagesMode || usesEjectedPagesLayout) routeExcludes.push(...createUnderscoreReservedExcludes(isPagesMode ? resolved.pagesDir : resolved.routesDir));
2168
1982
  const routeGlobPattern = routeExcludes.length > 0 ? [routeGlob, ...routeExcludes] : routeGlob;
2169
1983
  const additionalRouteGlobPattern = additionalRouteGlob && routeExcludes.length > 0 ? [additionalRouteGlob, ...routeExcludes] : additionalRouteGlob;
2170
1984
  const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2171
1985
  const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}` : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
1986
+ const root = buildOptions.root ?? process.cwd();
1987
+ const usesEjectedPagesShellLayout = usesEjectedPagesLayout && (sameConfigDirectory(resolved.shellsDir, resolved.routesDir, root) || hasRootPagesAppShell(resolved.shellsDir, root, resolved.additionalExtensions));
1988
+ const shellExcludes = isPagesMode ? createReservedSubtreeExcludes(resolved.pagesDir) : usesEjectedPagesShellLayout ? createUnderscoreReservedExcludes(resolved.shellsDir) : [];
1989
+ const shellGlobPattern = shellExcludes.length > 0 ? [shellGlob, ...shellExcludes] : shellGlob;
1990
+ const additionalShellGlobPattern = shellExcludes.length > 0 ? [additionalShellGlob, ...shellExcludes] : additionalShellGlob;
1991
+ const ejectedPagesAppShellSources = usesEjectedPagesShellLayout ? [` ...import.meta.glob(${JSON.stringify([`${resolved.shellsDir}/**/_app.{ts,tsx,js,jsx}`, ...createReservedSubtreeExcludes(resolved.shellsDir)])}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`, ` ...import.meta.glob(${JSON.stringify([`${resolved.shellsDir}/**/_app.${extensionGlob(bareRouteExtensions)}`, ...createReservedSubtreeExcludes(resolved.shellsDir)])}),`] : [];
2172
1992
  const appFilePosix = resolved.appFile.replace(/\\/g, "/").replace(/^\.\//, "");
2173
1993
  const appDir = (appFilePosix.startsWith("/") ? appFilePosix : `/${appFilePosix}`).replace(/\/[^/]*$/, "") || "/";
2174
1994
  return [
@@ -2183,8 +2003,9 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
2183
2003
  ` ...import.meta.glob(${JSON.stringify(additionalRouteGlobPattern)}),`,
2184
2004
  `};`,
2185
2005
  `const shellModules = {`,
2186
- ` ...import.meta.glob(${JSON.stringify(shellGlob)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
2187
- ` ...import.meta.glob(${JSON.stringify(additionalShellGlob)}),`,
2006
+ ` ...import.meta.glob(${JSON.stringify(shellGlobPattern)}, { query: ${JSON.stringify(PRACHT_CLIENT_MODULE_QUERY)} }),`,
2007
+ ` ...import.meta.glob(${JSON.stringify(additionalShellGlobPattern)}),`,
2008
+ ...ejectedPagesAppShellSources,
2188
2009
  `};`,
2189
2010
  "",
2190
2011
  "const resolvedApp = resolveApp(app);",
@@ -2244,6 +2065,7 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
2244
2065
  " return null;",
2245
2066
  "}",
2246
2067
  "",
2068
+ ...webmcpEnabled ? createWebmcpBootstrapSource() : [],
2247
2069
  "const state = readHydrationState();",
2248
2070
  "const root = document.getElementById(\"pracht-root\");",
2249
2071
  "if (state && root) {",
@@ -2254,6 +2076,7 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
2254
2076
  " initialState: state,",
2255
2077
  " root,",
2256
2078
  " findModuleKey,",
2079
+ ...webmcpEnabled ? [" onRouteChange: syncPrachtWebmcpTools,"] : [],
2257
2080
  " });",
2258
2081
  "}",
2259
2082
  "",
@@ -2268,10 +2091,110 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
2268
2091
  "if (import.meta.hot) {",
2269
2092
  " import.meta.hot.on(DEV_ROUTE_DATA_STALE_EVENT, refreshDevRouteData);",
2270
2093
  "}",
2271
- "",
2272
- ...hasWebmcpCapabilities(resolved, buildOptions.root) ? createWebmcpBootstrapSource() : []
2094
+ ""
2273
2095
  ].join("\n");
2274
2096
  }
2097
+ function sameConfigDirectory(left, right, root) {
2098
+ const canonicalize = (value) => {
2099
+ const absolute = resolve(root, value.replace(/^[/\\]+/, ""));
2100
+ try {
2101
+ return realpathSync.native(absolute);
2102
+ } catch {
2103
+ return absolute;
2104
+ }
2105
+ };
2106
+ return canonicalize(left) === canonicalize(right);
2107
+ }
2108
+ function hasRootPagesAppShell(directory, root, additionalExtensions) {
2109
+ const absoluteDirectory = resolve(root, directory.replace(/^[/\\]+/, ""));
2110
+ const extensions = withAdditionalExtensions(DEFAULT_SHELL_EXTENSIONS, additionalExtensions);
2111
+ for (const extension of extensions) try {
2112
+ if (statSync(join(absoluteDirectory, `_app${extension}`)).isFile()) return true;
2113
+ } catch {}
2114
+ return false;
2115
+ }
2116
+ function isEjectedPagesLayout(resolved, root) {
2117
+ if (resolved.pagesDir) return false;
2118
+ try {
2119
+ const appFile = resolve(root, resolved.appFile.replace(/^\//, ""));
2120
+ return hasTrueConstExport(readFileSync(appFile, "utf-8"), appFile, GENERATED_PAGES_LAYOUT_EXPORT);
2121
+ } catch {
2122
+ return false;
2123
+ }
2124
+ }
2125
+ function hasTrueConstExport(source, file, name) {
2126
+ const program = parseAst(source, { lang: getRolldownLang(file) });
2127
+ const statements = (Array.isArray(program.body) ? program.body : []).map(asStaticProgramNode).filter((statement) => statement !== null);
2128
+ const trueConstBindings = /* @__PURE__ */ new Set();
2129
+ for (const statement of statements) {
2130
+ const declaration = statement.type === "ExportNamedDeclaration" ? asStaticProgramNode(statement.declaration) : statement;
2131
+ if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") continue;
2132
+ const declarators = Array.isArray(declaration.declarations) ? declaration.declarations : [];
2133
+ for (const declaratorValue of declarators) {
2134
+ const declarator = asStaticProgramNode(declaratorValue);
2135
+ if (declarator?.type !== "VariableDeclarator") continue;
2136
+ const identifier = asStaticProgramNode(declarator.id);
2137
+ if (identifier?.type !== "Identifier" || typeof identifier.name !== "string") continue;
2138
+ const initializer = unwrapStaticExpression(declarator.init);
2139
+ if ((initializer?.type === "BooleanLiteral" || initializer?.type === "Literal") && initializer.value === true) trueConstBindings.add(identifier.name);
2140
+ }
2141
+ }
2142
+ for (const statement of statements) {
2143
+ if (statement.type !== "ExportNamedDeclaration") continue;
2144
+ const declaration = asStaticProgramNode(statement.declaration);
2145
+ if (declaration?.type === "VariableDeclaration") {
2146
+ if ((Array.isArray(declaration.declarations) ? declaration.declarations : []).some((declaratorValue) => {
2147
+ const identifier = asStaticProgramNode(asStaticProgramNode(declaratorValue)?.id);
2148
+ return identifier?.type === "Identifier" && identifier.name === name;
2149
+ }) && trueConstBindings.has(name)) return true;
2150
+ continue;
2151
+ }
2152
+ if (statement.source) continue;
2153
+ const specifiers = Array.isArray(statement.specifiers) ? statement.specifiers : [];
2154
+ for (const specifierValue of specifiers) {
2155
+ const specifier = asStaticProgramNode(specifierValue);
2156
+ if (specifier?.type !== "ExportSpecifier" || specifier.exportKind === "type") continue;
2157
+ const local = asStaticProgramNode(specifier.local);
2158
+ const exported = asStaticProgramNode(specifier.exported);
2159
+ const localName = getStaticProgramName(local);
2160
+ if (localName !== null && getStaticProgramName(exported) === name && trueConstBindings.has(localName)) return true;
2161
+ }
2162
+ }
2163
+ return false;
2164
+ }
2165
+ function unwrapStaticExpression(value) {
2166
+ let node = asStaticProgramNode(value);
2167
+ while (node && new Set([
2168
+ "ParenthesizedExpression",
2169
+ "TSAsExpression",
2170
+ "TSNonNullExpression",
2171
+ "TSSatisfiesExpression",
2172
+ "TSTypeAssertion",
2173
+ "TypeCastExpression"
2174
+ ]).has(node.type)) node = asStaticProgramNode(node.expression);
2175
+ return node;
2176
+ }
2177
+ function asStaticProgramNode(value) {
2178
+ if (!value || typeof value !== "object" || !("type" in value)) return null;
2179
+ return typeof value.type === "string" ? value : null;
2180
+ }
2181
+ function getStaticProgramName(value) {
2182
+ if (value?.type === "Identifier" && typeof value.name === "string") return value.name;
2183
+ if (value?.type === "Literal" && typeof value.value === "string") return value.value;
2184
+ return null;
2185
+ }
2186
+ function createUnderscoreReservedExcludes(directory) {
2187
+ return [`!${directory}/**/_*`, ...createReservedSubtreeExcludes(directory)];
2188
+ }
2189
+ /**
2190
+ * Exclude the contents of underscore-reserved directory trees without
2191
+ * excluding underscore-prefixed files themselves. The shell registry needs
2192
+ * this narrower form: `_app.tsx` is a reserved *name* that must stay in the
2193
+ * registry, while `_components/_app.tsx` is a helper that must not.
2194
+ */
2195
+ function createReservedSubtreeExcludes(directory) {
2196
+ return [`!${directory}/**/_*/**`];
2197
+ }
2275
2198
  /**
2276
2199
  * Source of `virtual:pracht/islands-client` — the tiny bootstrap loaded by
2277
2200
  * `hydration: "islands"` routes. It deliberately does NOT import the app
@@ -2281,6 +2204,7 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
2281
2204
  function createPrachtIslandsClientModuleSource(options = {}, buildOptions = {}) {
2282
2205
  const resolved = resolveOptions(options);
2283
2206
  const islandsGlob = `${resolved.islandsDir}/**/*.{ts,tsx,js,jsx}`;
2207
+ const webmcpEnabled = hasWebmcpCapabilities(resolved, buildOptions.root);
2284
2208
  return [
2285
2209
  "import { hydrateIslands } from \"@pracht/core/islands-client\";",
2286
2210
  "",
@@ -2288,20 +2212,28 @@ function createPrachtIslandsClientModuleSource(options = {}, buildOptions = {})
2288
2212
  "",
2289
2213
  "hydrateIslands({ modules: islandModules });",
2290
2214
  "",
2291
- ...hasWebmcpCapabilities(resolved, buildOptions.root) ? createWebmcpBootstrapSource() : []
2215
+ ...webmcpEnabled ? [
2216
+ ...createWebmcpBootstrapSource(),
2217
+ "const webmcpEntry = document.querySelector(\"script[data-pracht-webmcp-tools]\");",
2218
+ "const webmcpCapabilities = webmcpEntry?.getAttribute(\"data-pracht-webmcp-tools\")?.split(\",\").filter(Boolean) ?? [];",
2219
+ "syncPrachtWebmcpTools(webmcpCapabilities);",
2220
+ ""
2221
+ ] : []
2292
2222
  ].join("\n");
2293
2223
  }
2294
2224
  function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2295
2225
  const resolved = resolveOptions(options);
2296
2226
  const isPagesMode = !!resolved.pagesDir;
2297
2227
  const registrySource = createPrachtRegistryModuleSource(resolved);
2298
- const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
2299
- const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
2300
- const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
2301
- const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root, buildOptions.base ?? "/") : {
2228
+ const routeHints = createRouteHintsForVirtualModules(resolved, buildOptions.root);
2229
+ const routeLoaderHints = routeHints.loader;
2230
+ const routeHeadHints = routeHints.head;
2231
+ const routeStaticPathsHints = routeHints.staticPaths;
2232
+ const clientBuild = buildOptions.isBuild ? readClientBuildAssets(buildOptions.root, buildOptions.base ?? "/", resolved.inlineCss) : {
2302
2233
  clientEntryUrl: null,
2303
2234
  islandsEntryUrl: null,
2304
2235
  cssManifest: {},
2236
+ cssContentManifest: {},
2305
2237
  jsManifest: {}
2306
2238
  };
2307
2239
  const adapter = resolved.adapter;
@@ -2344,6 +2276,7 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2344
2276
  `export const islandsEntryUrl = ${JSON.stringify(islandsEntryUrl ?? null)};`,
2345
2277
  `export const islandsBootstrapRequired = ${JSON.stringify(islandsBootstrapRequired)};`,
2346
2278
  `export const cssManifest = ${JSON.stringify(clientBuild.cssManifest)};`,
2279
+ `export const cssContentManifest = ${JSON.stringify(clientBuild.cssContentManifest)};`,
2347
2280
  `export const jsManifest = ${JSON.stringify(clientBuild.jsManifest)};`,
2348
2281
  `export const prerenderConcurrency = ${JSON.stringify(resolved.prerenderConcurrency)};`,
2349
2282
  `export const budgets = ${JSON.stringify(resolved.budgets)};`,
@@ -2368,9 +2301,10 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
2368
2301
  */
2369
2302
  function createPrachtDevModuleSource(options = {}, buildOptions = {}) {
2370
2303
  const resolved = resolveOptions(options);
2371
- const routeLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, buildOptions.root);
2372
- const routeHeadHints = createRouteHeadHintsForVirtualModules(resolved, buildOptions.root);
2373
- const routeStaticPathsHints = createRouteStaticPathsHintsForVirtualModules(resolved, buildOptions.root);
2304
+ const routeHints = createRouteHintsForVirtualModules(resolved, buildOptions.root);
2305
+ const routeLoaderHints = routeHints.loader;
2306
+ const routeHeadHints = routeHints.head;
2307
+ const routeStaticPathsHints = routeHints.staticPaths;
2374
2308
  return [
2375
2309
  "import { resolveApp, resolveApiRoutes } from \"@pracht/core/server\";",
2376
2310
  resolved.pagesDir ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`,
@@ -2440,58 +2374,46 @@ function createApplyRouteLoaderHintsSource() {
2440
2374
  ""
2441
2375
  ];
2442
2376
  }
2443
- function createRouteHeadHintsForVirtualModules(options, root = process.cwd()) {
2444
- const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2445
- const directories = options.pagesDir ? [[options.pagesDir, resolve(root, options.pagesDir.slice(1))]] : [[options.routesDir, resolve(root, options.routesDir.slice(1))], [options.shellsDir, resolve(root, options.shellsDir.slice(1))]];
2446
- return Object.assign({}, ...directories.map(([prefix, directory]) => createRouteHeadHints(directory, {
2447
- additionalExtensions: options.additionalExtensions,
2448
- appFileDir,
2449
- rootRelativePrefix: prefix
2450
- })));
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
- }
2465
2377
  /**
2466
- * `getStaticPaths()` presence per route file. Only routes matter a shell
2467
- * cannot enumerate paths so unlike the head hints this skips the shells
2468
- * directory.
2378
+ * Every route hint table the generated client entry bakes in, resolved against
2379
+ * the plugin's configured directories and keyed the way the app manifest names
2380
+ * its modules.
2381
+ *
2382
+ * One call, one walk per directory, one parse per route file. The four tables
2383
+ * used to be built independently, which walked the routes directory four times
2384
+ * and re-parsed every route module four times — on each file of each save.
2469
2385
  */
2470
- function createRouteStaticPathsHintsForVirtualModules(options, root = process.cwd()) {
2386
+ function createRouteHintsForVirtualModules(options, root = process.cwd()) {
2471
2387
  const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2472
2388
  const routesPrefix = options.pagesDir || options.routesDir;
2473
- return createRouteStaticPathsHints(resolve(root, routesPrefix.slice(1)), {
2474
- additionalExtensions: options.additionalExtensions,
2475
- appFileDir,
2476
- rootRelativePrefix: routesPrefix
2477
- });
2478
- }
2479
- function createRouteLoaderHintsForVirtualModules(options, root = process.cwd()) {
2480
- if (options.pagesDir) {
2481
- const pages = scanPagesDirectory(resolve(root, options.pagesDir.slice(1)), options.additionalExtensions);
2482
- const hints = {};
2483
- for (const page of pages) {
2484
- const key = `${options.pagesDir}/${page.relativePath.replace(/\\/g, "/")}`;
2485
- hints[key] = !!page.hasLoader;
2389
+ const directories = options.pagesDir ? [[options.pagesDir, resolve(root, options.pagesDir.slice(1))]] : [[options.routesDir, resolve(root, options.routesDir.slice(1))], [options.shellsDir, resolve(root, options.shellsDir.slice(1))]];
2390
+ const hints = {
2391
+ capabilities: {},
2392
+ head: {},
2393
+ headers: {},
2394
+ incomplete: false,
2395
+ loader: {},
2396
+ staticPaths: {}
2397
+ };
2398
+ for (const [prefix, directory] of directories) {
2399
+ const scanned = createRouteHints(directory, {
2400
+ additionalExtensions: options.additionalExtensions,
2401
+ appFileDir,
2402
+ rootRelativePrefix: prefix
2403
+ });
2404
+ hints.incomplete ||= scanned.incomplete;
2405
+ Object.assign(hints.head, scanned.head);
2406
+ Object.assign(hints.headers, scanned.headers);
2407
+ if (prefix === routesPrefix) {
2408
+ Object.assign(hints.loader, scanned.loader);
2409
+ Object.assign(hints.staticPaths, scanned.staticPaths);
2486
2410
  }
2487
- return hints;
2488
2411
  }
2489
- const appFileDir = dirname(resolve(root, options.appFile.slice(1)));
2490
- return createRouteLoaderHints(resolve(root, options.routesDir.slice(1)), {
2491
- additionalExtensions: options.additionalExtensions,
2492
- appFileDir,
2493
- rootRelativePrefix: options.routesDir
2494
- });
2412
+ if (options.pagesDir) {
2413
+ hints.loader = {};
2414
+ for (const page of scanPagesDirectory(resolve(root, options.pagesDir.slice(1)), options.additionalExtensions)) hints.loader[`${options.pagesDir}/${page.relativePath.replace(/\\/g, "/")}`] = !!page.hasLoader;
2415
+ }
2416
+ return hints;
2495
2417
  }
2496
2418
  /**
2497
2419
  * Server data modules that can own a separately wired route loader.
@@ -2513,8 +2435,8 @@ function createPrachtRegistryModuleSource(options = {}) {
2513
2435
  const bareRouteExtensions = [...withAdditionalExtensions(LEGACY_BARE_ROUTE_EXTENSIONS, resolved.additionalExtensions)];
2514
2436
  const routeGlob = isPagesMode ? `${resolved.pagesDir}/**/*.{ts,tsx,js,jsx,md,mdx}` : `${resolved.routesDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2515
2437
  const additionalRouteGlob = `${isPagesMode ? resolved.pagesDir : resolved.routesDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2516
- const shellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}` : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2517
- const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}` : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2438
+ const shellGlob = isPagesMode ? [`${resolved.pagesDir}/**/_app.{ts,tsx,js,jsx}`, ...createReservedSubtreeExcludes(resolved.pagesDir)] : `${resolved.shellsDir}/**/*.{ts,tsx,js,jsx,md,mdx}`;
2439
+ const additionalShellGlob = isPagesMode ? [`${resolved.pagesDir}/**/_app.${extensionGlob(bareRouteExtensions)}`, ...createReservedSubtreeExcludes(resolved.pagesDir)] : `${resolved.shellsDir}/**/*.${extensionGlob(bareRouteExtensions)}`;
2518
2440
  return [
2519
2441
  `export const routeModules = {`,
2520
2442
  ` ...import.meta.glob(${JSON.stringify(routeGlob)}),`,
@@ -2524,7 +2446,12 @@ function createPrachtRegistryModuleSource(options = {}) {
2524
2446
  ` ...import.meta.glob(${JSON.stringify(shellGlob)}),`,
2525
2447
  ` ...import.meta.glob(${JSON.stringify(additionalShellGlob)}),`,
2526
2448
  `};`,
2527
- `export const middlewareModules = import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx}`)});`,
2449
+ ...isPagesMode ? [
2450
+ `export const middlewareModules = {`,
2451
+ ` ...import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx}`)}),`,
2452
+ ` ...import.meta.glob(${JSON.stringify(`${resolved.pagesDir}/_middleware.{ts,tsx,js,jsx}`)}),`,
2453
+ `};`
2454
+ ] : [`export const middlewareModules = import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx}`)});`],
2528
2455
  `export const apiModules = import.meta.glob(${JSON.stringify(apiGlobs)});`,
2529
2456
  `export const dataModules = import.meta.glob(${JSON.stringify(`${resolved.serverDir}/**/*.{ts,js,tsx,jsx}`)});`,
2530
2457
  `export const capabilityModules = import.meta.glob(${JSON.stringify(`${resolved.capabilitiesDir}/**/*.{ts,js,tsx,jsx}`)});`,
@@ -2543,21 +2470,36 @@ const pagesAppSourceCache = /* @__PURE__ */ new Map();
2543
2470
  function clearPagesAppSourceCache() {
2544
2471
  pagesAppSourceCache.clear();
2545
2472
  }
2546
- function generatePagesAppInlineSource(options, root = process.cwd()) {
2473
+ /**
2474
+ * The generated pages manifest, inlined into a virtual module.
2475
+ *
2476
+ * The client target drops `agents` and `constraints` — the two keys only the
2477
+ * server reads — along with the `_app.config.ts` import that supplies them.
2478
+ * Without that split the browser entry would import the config module, and
2479
+ * every Web Bot Auth key in it would ship to visitors.
2480
+ */
2481
+ function generatePagesAppInlineSource(options, root = process.cwd(), target = "server") {
2547
2482
  const absPagesDir = resolve(root, options.pagesDir.slice(1));
2483
+ const absCapabilitiesDir = resolve(root, options.capabilitiesDir.slice(1));
2548
2484
  const cacheKey = JSON.stringify({
2549
2485
  additionalExtensions: options.additionalExtensions,
2486
+ absCapabilitiesDir,
2550
2487
  absPagesDir,
2488
+ capabilitiesDirPrefix: options.capabilitiesDir,
2551
2489
  pagesDefaultRender: options.pagesDefaultRender,
2552
- pagesDirPrefix: options.pagesDir
2490
+ pagesDirPrefix: options.pagesDir,
2491
+ target
2553
2492
  });
2554
2493
  const cached = pagesAppSourceCache.get(cacheKey);
2555
2494
  if (cached) return cached;
2556
2495
  const source = generatePagesManifestSource(scanPagesDirectory(absPagesDir, options.additionalExtensions), {
2557
2496
  additionalExtensions: options.additionalExtensions,
2497
+ capabilitiesDir: absCapabilitiesDir,
2498
+ capabilitiesDirPrefix: options.capabilitiesDir,
2558
2499
  pagesDir: absPagesDir,
2559
2500
  pagesDefaultRender: options.pagesDefaultRender,
2560
- pagesDirPrefix: options.pagesDir
2501
+ pagesDirPrefix: options.pagesDir,
2502
+ target
2561
2503
  });
2562
2504
  pagesAppSourceCache.set(cacheKey, source);
2563
2505
  return source;
@@ -2578,6 +2520,10 @@ function createAgentTrafficBuffer(limit = 200) {
2578
2520
  outcome: event.outcome,
2579
2521
  status: event.status,
2580
2522
  durationMs: event.durationMs,
2523
+ tokenAuth: event.tokenAuth ? {
2524
+ subject: event.tokenAuth.subject,
2525
+ clientId: event.tokenAuth.clientId ?? null
2526
+ } : null,
2581
2527
  agent: event.agent ? {
2582
2528
  agentDomain: event.agent.agentDomain,
2583
2529
  keyId: event.agent.keyId
@@ -2650,6 +2596,8 @@ function createDevSSRMiddleware(server, options = {}) {
2650
2596
  return async (req, res, next) => {
2651
2597
  const url = req.url ?? "/";
2652
2598
  const requestUrl = new URL(url, "http://localhost");
2599
+ let reportedError;
2600
+ let hasReportedError = false;
2653
2601
  try {
2654
2602
  const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core/server"), server.ssrLoadModule(PRACHT_SERVER_MODULE_ID)]);
2655
2603
  const routeMatchers = {
@@ -2704,7 +2652,7 @@ function createDevSSRMiddleware(server, options = {}) {
2704
2652
  let routeError;
2705
2653
  let capturedRouteError = false;
2706
2654
  let routeErrorContext;
2707
- const response = await framework.handlePrachtRequest({
2655
+ const response = normalizeResponseHeaders(await framework.handlePrachtRequest({
2708
2656
  app: serverMod.resolvedApp,
2709
2657
  registry: serverMod.registry,
2710
2658
  request: webRequest,
@@ -2713,6 +2661,22 @@ function createDevSSRMiddleware(server, options = {}) {
2713
2661
  capturedRouteError = true;
2714
2662
  routeError = error;
2715
2663
  routeErrorContext = context;
2664
+ reportedError = error;
2665
+ hasReportedError = true;
2666
+ logDevRequestError(server, {
2667
+ context,
2668
+ error,
2669
+ path: requestUrl.pathname
2670
+ });
2671
+ },
2672
+ onApiError: (error, _requestPath, context) => {
2673
+ reportedError = error;
2674
+ hasReportedError = true;
2675
+ logDevRequestError(server, {
2676
+ context,
2677
+ error,
2678
+ path: requestUrl.pathname
2679
+ });
2716
2680
  },
2717
2681
  clientEntryUrl: withDevBase(CLIENT_BROWSER_PATH),
2718
2682
  islandsEntryUrl: withDevBase(ISLANDS_CLIENT_BROWSER_PATH),
@@ -2720,15 +2684,13 @@ function createDevSSRMiddleware(server, options = {}) {
2720
2684
  apiRoutes: serverMod.apiRoutes,
2721
2685
  timings,
2722
2686
  onCapabilityAudit: agentTraffic.record
2723
- });
2687
+ }));
2724
2688
  const responseContentType = response.headers.get("content-type") ?? "";
2725
2689
  if (response.status === 404 && !responseContentType.includes("application/json") && !routeMatchers.app?.notFound) return next();
2726
2690
  const contentType = response.headers.get("content-type") ?? "";
2727
2691
  if (response.body && isEventStreamContentType(contentType)) {
2728
2692
  res.statusCode = response.status;
2729
- response.headers.forEach((value, key) => {
2730
- res.setHeader(key, value);
2731
- });
2693
+ writeDevResponseHeaders(res, response.headers);
2732
2694
  const source = Readable.fromWeb(response.body);
2733
2695
  if (res.destroyed || res.writableEnded) {
2734
2696
  source.destroy();
@@ -2754,21 +2716,254 @@ function createDevSSRMiddleware(server, options = {}) {
2754
2716
  await respondWithErrorOverlay(server, res, url, routeError, routeErrorContext, devBase, response.status, serverTiming);
2755
2717
  return;
2756
2718
  }
2757
- let body = await response.text();
2758
- if (contentType.includes("text/html")) body = await transformDevHtml(server, url, body, devBase);
2759
- res.statusCode = response.status;
2760
- response.headers.forEach((value, key) => {
2761
- res.setHeader(key, value);
2762
- });
2763
2719
  const serverTiming = framework.formatServerTimingHeader(timings);
2720
+ if (shouldStreamDevHtmlResponse(framework, response, contentType)) {
2721
+ await streamDevHtmlResponse(server, res, url, response, devBase, serverTiming);
2722
+ return;
2723
+ }
2724
+ if (contentType.includes("text/html")) {
2725
+ const html = await transformDevHtml(server, url, await response.text(), devBase);
2726
+ res.statusCode = response.status;
2727
+ writeDevResponseHeaders(res, response.headers);
2728
+ res.removeHeader("content-length");
2729
+ if (serverTiming) res.setHeader("Server-Timing", serverTiming);
2730
+ res.end(html);
2731
+ return;
2732
+ }
2733
+ res.statusCode = response.status;
2734
+ writeDevResponseHeaders(res, response.headers);
2764
2735
  if (serverTiming) res.setHeader("Server-Timing", serverTiming);
2765
- res.end(body);
2736
+ if (!response.body) {
2737
+ res.end();
2738
+ return;
2739
+ }
2740
+ const source = Readable.fromWeb(response.body);
2741
+ if (res.destroyed || res.writableEnded) {
2742
+ source.destroy();
2743
+ return;
2744
+ }
2745
+ res.on("close", () => {
2746
+ if (!res.writableFinished) source.destroy();
2747
+ });
2748
+ source.on("error", (streamError) => {
2749
+ logDevRequestError(server, {
2750
+ error: streamError,
2751
+ path: requestUrl.pathname
2752
+ });
2753
+ res.destroy();
2754
+ });
2755
+ source.pipe(res);
2766
2756
  } catch (error) {
2757
+ if (!hasReportedError || error !== reportedError) logDevRequestError(server, {
2758
+ error,
2759
+ path: requestUrl.pathname
2760
+ });
2767
2761
  await handleDevError(server, req, res, next, url, error, devBase);
2768
2762
  }
2769
2763
  };
2770
2764
  }
2771
2765
  /**
2766
+ * Print one line per dev-server failure, in the terminal running `pracht dev`.
2767
+ *
2768
+ * The browser overlay only reaches a document navigation. Everything else that
2769
+ * can fail — a route-state fetch during client-side navigation, `curl`, an
2770
+ * end-to-end test — used to get a 500 and no server-side trace of why.
2771
+ */
2772
+ function formatDevRequestErrorLine(options) {
2773
+ const route = options.routeId ? ` in route "${options.routeId}"` : "";
2774
+ const file = options.file ? ` (${options.file})` : "";
2775
+ return `[pracht] ${options.phase ?? "request"} error${route}${file} at ${options.path}: ${options.message}`;
2776
+ }
2777
+ /**
2778
+ * A `throw notFound()` that reaches `onRouteError` is a routing outcome, not a
2779
+ * crash: the app simply declares no not-found page. Redirects never reach it
2780
+ * at all — the runtime returns a thrown `Response` before the error path.
2781
+ */
2782
+ function shouldLogDevRequestError(error) {
2783
+ return devErrorStatus(error) >= 500;
2784
+ }
2785
+ function devErrorStatus(error) {
2786
+ if (error instanceof Error && error.name === "PrachtHttpError" && typeof error.status === "number") return error.status;
2787
+ return 500;
2788
+ }
2789
+ function logDevRequestError(server, options) {
2790
+ if (!shouldLogDevRequestError(options.error)) return;
2791
+ const { error } = options;
2792
+ const line = formatDevRequestErrorLine({
2793
+ file: describeAnnotatedUserModule(error, server.config.root) ?? describeContextUserModule(options.context),
2794
+ message: error instanceof Error ? error.message : String(error),
2795
+ path: options.path,
2796
+ phase: options.context?.phase,
2797
+ routeId: options.context?.routeId
2798
+ });
2799
+ const stack = error instanceof Error ? error.stack : void 0;
2800
+ const wantsStack = shouldIncludeDevErrorStack({
2801
+ context: options.context,
2802
+ debug: Boolean(process.env?.DEBUG),
2803
+ error,
2804
+ root: server.config.root
2805
+ });
2806
+ server.config.logger.error(wantsStack && stack ? `${line}\n${stack}` : line, { timestamp: true });
2807
+ }
2808
+ /** Source modules the runtime matched before a handled request failure. */
2809
+ function describeContextUserModule(context) {
2810
+ if (!context) return void 0;
2811
+ if (context.phase === "middleware" && context.middlewareFiles?.length) return context.middlewareFiles.join(", ");
2812
+ if (context.phase === "loader" && context.loaderFile) return context.loaderFile;
2813
+ return context.routeFile;
2814
+ }
2815
+ /**
2816
+ * Whether the logged line should carry the stack trace.
2817
+ *
2818
+ * A failure the developer can locate — a route module, a loader, a Vite
2819
+ * transform error that names the file it could not compile — is already
2820
+ * pinpointed by the message and, for a document navigation, by the overlay.
2821
+ * Repeating the trace for every one of those (a failing route-state poll fires
2822
+ * on each navigation) buries the terminal. A failure that names no user module
2823
+ * is a framework or module-loading fault where the trace is the only clue, so
2824
+ * it always gets one; `DEBUG` opts back in for everything.
2825
+ */
2826
+ function shouldIncludeDevErrorStack(options) {
2827
+ if (options.debug) return true;
2828
+ return !isAttributableToUserModule(options.context, options.error, options.root);
2829
+ }
2830
+ function isAttributableToUserModule(context, error, root) {
2831
+ if (context?.routeFile || context?.loaderFile || context?.shellFile) return true;
2832
+ const annotatedFile = readAnnotatedFile(error);
2833
+ if (annotatedFile !== void 0) return isUserModulePath(annotatedFile, root);
2834
+ const stack = error instanceof Error ? error.stack : void 0;
2835
+ if (!stack) return false;
2836
+ return stack.split("\n").slice(1).some((frame) => {
2837
+ const match = /(?:\(|\bat\s)([^()\s]+):\d+:\d+\)?\s*$/.exec(frame.trim());
2838
+ return match ? isUserModulePath(match[1], root) : false;
2839
+ });
2840
+ }
2841
+ /** The module a Vite or Rollup build error blames, when it names one. */
2842
+ function readAnnotatedFile(error) {
2843
+ const annotated = error;
2844
+ if (typeof annotated?.id === "string") return annotated.id;
2845
+ if (typeof annotated?.loc?.file === "string") return annotated.loc.file;
2846
+ }
2847
+ /**
2848
+ * The annotated module as `path/to/file.tsx:line:column`, relative to the
2849
+ * project root, or `undefined` when the error blames nothing of the user's.
2850
+ */
2851
+ function describeAnnotatedUserModule(error, root) {
2852
+ const file = readAnnotatedFile(error);
2853
+ if (file === void 0 || !isUserModulePath(file, root)) return void 0;
2854
+ const rootPrefix = root ? `${root.replace(/\/$/, "")}/` : void 0;
2855
+ const label = rootPrefix && file.startsWith(rootPrefix) ? file.slice(rootPrefix.length) : file;
2856
+ const loc = error?.loc;
2857
+ if (typeof loc?.line !== "number") return label;
2858
+ return typeof loc.column === "number" ? `${label}:${loc.line}:${loc.column}` : `${label}:${loc.line}`;
2859
+ }
2860
+ function isUserModulePath(candidate, root) {
2861
+ const path = candidate.replace(/^file:\/\//, "").replace(/^\/@fs/, "").split("?")[0];
2862
+ if (path.includes("/node_modules/") || path.startsWith("node:")) return false;
2863
+ if (path.startsWith("\0") || path.startsWith("/@")) return false;
2864
+ if (!root) return path.startsWith("/src/");
2865
+ return path.startsWith(`${root.replace(/\/$/, "")}/`) || path.startsWith("/src/");
2866
+ }
2867
+ function shouldStreamDevHtmlResponse(framework, response, contentType) {
2868
+ return response.body !== null && contentType.includes("text/html") && framework.isStreamingHtmlResponse?.(response) === true;
2869
+ }
2870
+ async function streamDevHtmlResponse(server, res, url, response, base, serverTiming) {
2871
+ const reader = response.body.getReader();
2872
+ let committed = false;
2873
+ const cancel = () => {
2874
+ if (!res.writableFinished) reader.cancel().catch(() => void 0);
2875
+ };
2876
+ res.on("close", cancel);
2877
+ try {
2878
+ const first = await reader.read();
2879
+ const transformed = await transformStreamingDevHtmlPrefix(server, url, first.done ? "" : new TextDecoder().decode(first.value), base);
2880
+ res.statusCode = response.status;
2881
+ writeDevResponseHeaders(res, response.headers);
2882
+ res.removeHeader("content-length");
2883
+ if (serverTiming) res.setHeader("Server-Timing", serverTiming);
2884
+ committed = true;
2885
+ if (transformed.prefix) await writeDevResponseChunk(res, transformed.prefix);
2886
+ let bodyEndInjected = transformed.beforeBodyClose === "";
2887
+ const decoder = new TextDecoder();
2888
+ while (!first.done) {
2889
+ const next = await reader.read();
2890
+ if (next.done) break;
2891
+ let chunk = next.value;
2892
+ if (!bodyEndInjected) {
2893
+ const injection = injectStreamingBodyEnd(decoder.decode(next.value), transformed.beforeBodyClose);
2894
+ chunk = injection.html;
2895
+ bodyEndInjected = injection.injected;
2896
+ }
2897
+ await writeDevResponseChunk(res, chunk);
2898
+ }
2899
+ res.end();
2900
+ } catch (error) {
2901
+ if (committed || res.headersSent) {
2902
+ res.destroy(error instanceof Error ? error : new Error(String(error)));
2903
+ return;
2904
+ }
2905
+ throw error;
2906
+ } finally {
2907
+ res.removeListener("close", cancel);
2908
+ reader.releaseLock();
2909
+ }
2910
+ }
2911
+ const STREAM_PREFIX_END_MARKER = "<template data-pracht-stream-prefix-end=\"\"></template>";
2912
+ const STREAM_BODY_END_MARKER = "<template data-pracht-stream-body-end=\"\"></template>";
2913
+ /**
2914
+ * Run Vite's HTML hooks against a syntactically complete document, then split
2915
+ * their output back into the prefix that can be committed now and tags that
2916
+ * belong immediately before `</body>` once the stream finishes.
2917
+ */
2918
+ async function transformStreamingDevHtmlPrefix(server, url, prefix, base) {
2919
+ const transformed = await transformDevHtml(server, url, `${prefix}${STREAM_PREFIX_END_MARKER}</div>${STREAM_BODY_END_MARKER}</body></html>`, base);
2920
+ const prefixEnd = transformed.indexOf(STREAM_PREFIX_END_MARKER);
2921
+ const bodyMarker = transformed.indexOf(STREAM_BODY_END_MARKER, prefixEnd);
2922
+ const bodyClose = transformed.indexOf("</body>", bodyMarker);
2923
+ if (prefixEnd < 0 || bodyMarker < 0 || bodyClose < 0) throw new Error("A Vite transform removed Pracht's streaming HTML markers; the document cannot be streamed safely in development.");
2924
+ return {
2925
+ prefix: transformed.slice(0, prefixEnd),
2926
+ beforeBodyClose: transformed.slice(bodyMarker + 52, bodyClose)
2927
+ };
2928
+ }
2929
+ /** Insert deferred Vite `body` tags into the runtime-owned closing chunk. */
2930
+ function injectStreamingBodyEnd(html, beforeBodyClose) {
2931
+ const bodyClose = html.indexOf("</body>");
2932
+ if (bodyClose < 0) return {
2933
+ html,
2934
+ injected: false
2935
+ };
2936
+ return {
2937
+ html: `${html.slice(0, bodyClose)}${beforeBodyClose}${html.slice(bodyClose)}`,
2938
+ injected: true
2939
+ };
2940
+ }
2941
+ async function writeDevResponseChunk(res, chunk) {
2942
+ if (res.destroyed || res.writableEnded || res.write(chunk)) return;
2943
+ await new Promise((resolve, reject) => {
2944
+ const cleanup = () => {
2945
+ res.removeListener("close", onClose);
2946
+ res.removeListener("drain", onDrain);
2947
+ res.removeListener("error", onError);
2948
+ };
2949
+ const onClose = () => {
2950
+ cleanup();
2951
+ resolve();
2952
+ };
2953
+ const onDrain = () => {
2954
+ cleanup();
2955
+ resolve();
2956
+ };
2957
+ const onError = (error) => {
2958
+ cleanup();
2959
+ reject(error);
2960
+ };
2961
+ res.once("close", onClose);
2962
+ res.once("drain", onDrain);
2963
+ res.once("error", onError);
2964
+ });
2965
+ }
2966
+ /**
2772
2967
  * Vite's HTML transform adds `config.base` to root-absolute asset attributes.
2773
2968
  * Pracht's runtime has already added it to URLs produced by `withBase()` — the
2774
2969
  * client entry, route-state preloads, image endpoints, and user-authored asset
@@ -2825,7 +3020,7 @@ function protectRootAbsoluteAssetAttributes(html) {
2825
3020
  * avoid a first-paint FOUC.
2826
3021
  */
2827
3022
  async function createDevCssManifest(server, options) {
2828
- const route = options.pathname === null ? void 0 : options.matchAppRoute(options.app, options.pathname)?.route ?? options.app.notFound;
3023
+ const route = Object.hasOwn(options, "route") ? options.route : options.pathname === null ? void 0 : options.matchAppRoute(options.app, options.pathname)?.route ?? options.app.notFound;
2829
3024
  if (!route) return {};
2830
3025
  const manifest = {};
2831
3026
  const modules = [...route.shellFile ? [{
@@ -2887,11 +3082,14 @@ async function resolveDevCssContextForPath(server, path, options = {}) {
2887
3082
  const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core/server"), server.ssrLoadModule(PRACHT_DEV_MODULE_ID)]);
2888
3083
  const publicPathname = new URL(path, "http://localhost").pathname;
2889
3084
  const pathname = options.basePathRetained ? framework.stripBase(publicPathname) : publicPathname;
3085
+ const route = pathname === null ? null : framework.matchAppRoute(serverMod.resolvedApp, pathname)?.route ?? serverMod.resolvedApp.notFound ?? null;
2890
3086
  return {
2891
3087
  app: serverMod.resolvedApp,
2892
3088
  matchAppRoute: framework.matchAppRoute,
2893
3089
  pathname,
2894
- registry: serverMod.registry
3090
+ registry: serverMod.registry,
3091
+ route,
3092
+ streaming: route?.streaming === true
2895
3093
  };
2896
3094
  }
2897
3095
  /**
@@ -2901,6 +3099,7 @@ async function resolveDevCssContextForPath(server, path, options = {}) {
2901
3099
  */
2902
3100
  function createDevCssInjectionMiddleware(server) {
2903
3101
  let warned = false;
3102
+ let warnedInjectionFailure = false;
2904
3103
  return (req, res, next) => {
2905
3104
  const method = (req.method ?? "GET").toUpperCase();
2906
3105
  const accept = readRequestHeader(req.headers.accept).toLowerCase();
@@ -2916,31 +3115,91 @@ function createDevCssInjectionMiddleware(server) {
2916
3115
  return null;
2917
3116
  });
2918
3117
  const chunks = [];
3118
+ let buffered = 0;
3119
+ let mode = "pending";
3120
+ const originalWrite = res.write.bind(res);
2919
3121
  const originalEnd = res.end.bind(res);
2920
3122
  const originalWriteHead = res.writeHead.bind(res);
3123
+ const releasePatches = () => {
3124
+ res.write = originalWrite;
3125
+ res.end = originalEnd;
3126
+ res.writeHead = originalWriteHead;
3127
+ };
3128
+ const decide = (writeHeadArgs) => {
3129
+ if (mode === "pending") {
3130
+ mode = isHtmlContentType(readWriteHeadHeader(writeHeadArgs, "content-type") ?? res.getHeader("content-type")) ? "buffer" : "passthrough";
3131
+ if (mode === "passthrough") releasePatches();
3132
+ else res.removeHeader("content-length");
3133
+ }
3134
+ return mode;
3135
+ };
3136
+ const spillToPassthrough = () => {
3137
+ mode = "passthrough";
3138
+ releasePatches();
3139
+ for (const chunk of chunks) originalWrite(chunk);
3140
+ chunks.length = 0;
3141
+ buffered = 0;
3142
+ };
3143
+ let streamingFlush;
3144
+ const flushStreamingPrefix = () => {
3145
+ if (streamingFlush || mode !== "buffer") return;
3146
+ streamingFlush = (async () => {
3147
+ const context = await contextPromise;
3148
+ if (!context?.streaming || mode !== "buffer") return;
3149
+ if (!Buffer.concat(chunks).includes("</head>")) return;
3150
+ const manifest = await createDevCssManifest(server, context);
3151
+ if (mode !== "buffer") return;
3152
+ const html = injectDevCssLinks(Buffer.concat(chunks).toString("utf-8"), manifest, server.config.base || "/");
3153
+ chunks.length = 0;
3154
+ buffered = 0;
3155
+ mode = "passthrough";
3156
+ releasePatches();
3157
+ originalWrite(html);
3158
+ })().catch((error) => {
3159
+ server.config.logger.error(`[pracht] Could not inject streaming development stylesheets: ${String(error)}`);
3160
+ if (mode === "buffer") spillToPassthrough();
3161
+ }).finally(() => {
3162
+ streamingFlush = void 0;
3163
+ });
3164
+ };
2921
3165
  res.writeHead = ((statusCode, ...args) => {
2922
- res.removeHeader("content-length");
3166
+ if (decide(args) === "passthrough") return Reflect.apply(originalWriteHead, res, [statusCode, ...args]);
2923
3167
  return Reflect.apply(originalWriteHead, res, [statusCode, ...args.map(stripContentLengthHeader)]);
2924
3168
  });
2925
3169
  res.write = ((chunk, encodingOrCallback, callback) => {
2926
- chunks.push(toBuffer(chunk, encodingOrCallback));
2927
- (typeof encodingOrCallback === "function" ? encodingOrCallback : typeof callback === "function" ? callback : void 0)?.();
3170
+ if (decide() === "passthrough") return originalWrite(chunk, encodingOrCallback, callback);
3171
+ const done = readNodeWriteCallback(encodingOrCallback, callback);
3172
+ const buffer = toBuffer(chunk, encodingOrCallback);
3173
+ if (buffered + buffer.length > 8388608) {
3174
+ spillToPassthrough();
3175
+ return originalWrite(buffer, done);
3176
+ }
3177
+ chunks.push(buffer);
3178
+ buffered += buffer.length;
3179
+ flushStreamingPrefix();
3180
+ done?.();
2928
3181
  return true;
2929
3182
  });
2930
3183
  res.end = ((chunk, encodingOrCallback, callback) => {
3184
+ if (decide() === "passthrough") return originalEnd(chunk, encodingOrCallback, callback);
2931
3185
  if (chunk != null) chunks.push(toBuffer(chunk, encodingOrCallback));
2932
- const done = typeof encodingOrCallback === "function" ? encodingOrCallback : typeof callback === "function" ? callback : void 0;
3186
+ const done = readNodeWriteCallback(encodingOrCallback, callback);
2933
3187
  (async () => {
2934
- const body = Buffer.concat(chunks);
2935
- if (!String(res.getHeader("content-type") ?? "").includes("text/html")) {
2936
- originalEnd(body, done);
3188
+ await streamingFlush;
3189
+ if (mode === "passthrough") {
3190
+ originalEnd(void 0, done);
2937
3191
  return;
2938
3192
  }
3193
+ const body = Buffer.concat(chunks);
2939
3194
  try {
2940
3195
  const context = await contextPromise;
2941
3196
  const manifest = context ? await createDevCssManifest(server, context) : null;
2942
3197
  originalEnd(manifest ? injectDevCssLinks(body.toString("utf-8"), manifest, server.config.base || "/") : body.toString("utf-8"), done);
2943
- } catch {
3198
+ } catch (error) {
3199
+ if (!warnedInjectionFailure) {
3200
+ warnedInjectionFailure = true;
3201
+ server.config.logger.error(`[pracht] Could not inject development stylesheets: ${error instanceof Error ? error.message : String(error)}`, { timestamp: true });
3202
+ }
2944
3203
  originalEnd(body, done);
2945
3204
  }
2946
3205
  })();
@@ -2949,6 +3208,22 @@ function createDevCssInjectionMiddleware(server) {
2949
3208
  next();
2950
3209
  };
2951
3210
  }
3211
+ function isHtmlContentType(value) {
3212
+ return String(value ?? "").toLowerCase().includes("text/html");
3213
+ }
3214
+ /** Read a header out of the `writeHead(status[, statusMessage][, headers])` tail. */
3215
+ function readWriteHeadHeader(args, name) {
3216
+ if (!args) return void 0;
3217
+ for (const arg of args) if (Array.isArray(arg)) {
3218
+ for (let index = 0; index < arg.length; index += 2) if (String(arg[index]).toLowerCase() === name) return String(arg[index + 1]);
3219
+ } else if (arg && typeof arg === "object") {
3220
+ for (const [key, value] of Object.entries(arg)) if (key.toLowerCase() === name) return String(value);
3221
+ }
3222
+ }
3223
+ function readNodeWriteCallback(encodingOrCallback, callback) {
3224
+ if (typeof encodingOrCallback === "function") return encodingOrCallback;
3225
+ if (typeof callback === "function") return callback;
3226
+ }
2952
3227
  function toBuffer(chunk, encoding) {
2953
3228
  if (Buffer.isBuffer(chunk)) return chunk;
2954
3229
  if (chunk instanceof Uint8Array) return Buffer.from(chunk);
@@ -3044,6 +3319,8 @@ function shouldExposeDevServerErrors() {
3044
3319
  * `text/plain` renders every escape sequence literally.
3045
3320
  */
3046
3321
  async function respondWithErrorOverlay(server, res, url, error, context, base, status, serverTiming) {
3322
+ if (finishAlreadySentResponse(res)) return;
3323
+ discardPendingResponseHeaders(res);
3047
3324
  if (error instanceof Error) server.ssrFixStacktrace(error);
3048
3325
  const { buildErrorOverlayHtml } = await server.ssrLoadModule("@pracht/core/error-overlay");
3049
3326
  let html = buildErrorOverlayHtml({
@@ -3066,7 +3343,43 @@ async function respondWithErrorOverlay(server, res, url, error, context, base, s
3066
3343
  if (serverTiming) res.setHeader("Server-Timing", serverTiming);
3067
3344
  res.end(html);
3068
3345
  }
3346
+ /**
3347
+ * True when the response has already reached the wire, so no error page can
3348
+ * replace it. Writing a second status line throws `ERR_HTTP_HEADERS_SENT`,
3349
+ * which would then be the error the developer sees instead of the real one —
3350
+ * a failure *after* `res.end()` (a rejected body stream, a late throw) is
3351
+ * exactly when that happens.
3352
+ */
3353
+ /**
3354
+ * Headers that describe the body being abandoned. A `content-length` measuring
3355
+ * the response the error replaced would truncate the error page written in its
3356
+ * place, and a stale `content-type`/`content-encoding` would have the browser
3357
+ * decode HTML as something else.
3358
+ */
3359
+ const ABANDONED_BODY_HEADERS = new Set([
3360
+ "content-disposition",
3361
+ "content-encoding",
3362
+ "content-length",
3363
+ "content-type",
3364
+ "transfer-encoding"
3365
+ ]);
3366
+ /**
3367
+ * Drop the headers that described the response being replaced, and only those.
3368
+ * Everything else staged on `res` belongs to a different concern — Vite's cors
3369
+ * middleware has already put `access-control-allow-origin` there, and clearing
3370
+ * it would turn a cross-origin 500 into a CORS failure with no overlay to read.
3371
+ */
3372
+ function discardPendingResponseHeaders(res) {
3373
+ for (const name of res.getHeaderNames()) if (ABANDONED_BODY_HEADERS.has(name.toLowerCase())) res.removeHeader(name);
3374
+ }
3375
+ function finishAlreadySentResponse(res) {
3376
+ if (!res.headersSent && !res.writableEnded) return false;
3377
+ if (!res.writableEnded && !res.destroyed) res.end();
3378
+ return true;
3379
+ }
3069
3380
  async function handleDevError(server, req, res, next, url, error, base) {
3381
+ if (finishAlreadySentResponse(res)) return;
3382
+ discardPendingResponseHeaders(res);
3070
3383
  if (error instanceof Error) server.ssrFixStacktrace(error);
3071
3384
  if (req.headers["x-pracht-route-state-request"] === "1") {
3072
3385
  res.statusCode = 500;
@@ -3160,6 +3473,24 @@ function readRequestHeader(value) {
3160
3473
  if (Array.isArray(value)) return value.join(", ");
3161
3474
  return value ?? "";
3162
3475
  }
3476
+ /**
3477
+ * Copy a `Response`'s headers onto a Node response the way the production
3478
+ * adapters do.
3479
+ *
3480
+ * `headers.forEach()` yields `set-cookie` once, joined with `, ` — and
3481
+ * `res.setHeader()` replaces rather than appends — so a loader or API route
3482
+ * that sets two cookies used to emit a single corrupted header in dev while
3483
+ * production (see `writeNodeResponseHeaders` in @pracht/adapter-node) sent
3484
+ * both. `getSetCookie()` is the only accessor that keeps them apart.
3485
+ */
3486
+ function writeDevResponseHeaders(res, headers) {
3487
+ const setCookieHeaders = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : [];
3488
+ headers.forEach((value, key) => {
3489
+ if (key.toLowerCase() === "set-cookie" && setCookieHeaders.length > 0) return;
3490
+ res.setHeader(key, value);
3491
+ });
3492
+ if (setCookieHeaders.length > 0) res.setHeader("set-cookie", setCookieHeaders);
3493
+ }
3163
3494
  function hasKnownAssetExtension(pathname) {
3164
3495
  const fileName = pathname.split("/").pop() ?? "";
3165
3496
  const extensionIndex = fileName.lastIndexOf(".");
@@ -3245,17 +3576,29 @@ async function nodeToWebRequest(req, maxBodySize, base = "/") {
3245
3576
  }
3246
3577
  //#endregion
3247
3578
  //#region src/index.ts
3579
+ function emptyRouteHints() {
3580
+ return {
3581
+ capabilities: {},
3582
+ head: {},
3583
+ headers: {},
3584
+ incomplete: false,
3585
+ loader: {},
3586
+ staticPaths: {}
3587
+ };
3588
+ }
3248
3589
  function pracht(options = {}) {
3249
3590
  const resolved = resolveOptions(options);
3250
3591
  const isPagesMode = !!resolved.pagesDir;
3251
3592
  let root = process.cwd();
3252
3593
  let routeFileDirs = [];
3253
- let clientRouteHeadHints = {};
3254
- let clientRouteHeadersHints = {};
3255
- let clientRouteLoaderHints = {};
3594
+ let clientRouteHints = emptyRouteHints();
3256
3595
  let serverRouteLoaderHints = {};
3596
+ let emittedRouteHints = emptyRouteHints();
3597
+ let routeHintsNeedResync = false;
3257
3598
  const routeFileExtensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions);
3258
3599
  let capabilityModulePaths = /* @__PURE__ */ new Set();
3600
+ let capabilityRunnerConfig = {};
3601
+ let usesEjectedPagesLayout = false;
3259
3602
  if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
3260
3603
  let isBuild = false;
3261
3604
  let base = "/";
@@ -3263,7 +3606,10 @@ function pracht(options = {}) {
3263
3606
  const prachtPlugin = {
3264
3607
  name: "pracht",
3265
3608
  enforce: "pre",
3266
- api: { llmsTxtEnabled: Boolean(resolved.llmsTxt) },
3609
+ api: {
3610
+ llmsTxtEnabled: Boolean(resolved.llmsTxt),
3611
+ pracht: { staticTarget: resolved.adapter.staticTarget === true }
3612
+ },
3267
3613
  config(_config, env) {
3268
3614
  const isEdge = resolved.adapter.edge === true;
3269
3615
  const isSSRBuild = env.isSsrBuild;
@@ -3273,7 +3619,10 @@ function pracht(options = {}) {
3273
3619
  const publicEnvDefine = JSON.stringify(loadEnv(env.mode, envDir, PUBLIC_ENV_PREFIX));
3274
3620
  const agentSurfaceDefine = env.command === "build" ? String(hasAgentSurface(resolved, configRoot)) : "true";
3275
3621
  const staticTargetDefine = String(env.command === "build" && resolved.adapter.staticTarget === true);
3276
- const clientFeatureDefines = { __PRACHT_CLIENT_PREFETCH__: String(resolved.client.prefetch) };
3622
+ const clientFeatureDefines = {
3623
+ __PRACHT_CLIENT_BLOCKER__: String(resolved.client.navigationGuards),
3624
+ __PRACHT_CLIENT_PREFETCH__: String(resolved.client.prefetch)
3625
+ };
3277
3626
  const clientChunkConfig = isSSRBuild || !resolved.vendorChunk ? {} : frameworkChunkConfig(_config.build?.rollupOptions?.output);
3278
3627
  if (clientChunkConfig.warning) console.warn(`[pracht] ${clientChunkConfig.warning}`);
3279
3628
  return {
@@ -3316,6 +3665,8 @@ function pracht(options = {}) {
3316
3665
  base = config.base;
3317
3666
  routeFileDirs = computeRouteFileDirs(root, resolved);
3318
3667
  capabilityModulePaths = new Set(resolveCapabilityModulePaths(resolved, root).map(canonicalFilePath));
3668
+ usesEjectedPagesLayout = isEjectedPagesLayout(resolved, root);
3669
+ capabilityRunnerConfig = { resolve: { alias: config.resolve?.alias } };
3319
3670
  },
3320
3671
  resolveId(id, importer, resolveIdOptions) {
3321
3672
  if (isIslandsClientModule(id)) return PRACHT_ISLANDS_CLIENT_MODULE_ID;
@@ -3330,10 +3681,10 @@ function pracht(options = {}) {
3330
3681
  load(id) {
3331
3682
  if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved, { root });
3332
3683
  if (isClientModule(id)) {
3333
- clientRouteHeadHints = createRouteHeadHintsForVirtualModules(resolved, root);
3334
- clientRouteHeadersHints = createRouteHeadersHintsForVirtualModules(resolved, root);
3335
- clientRouteLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, root);
3684
+ clientRouteHints = createRouteHintsForVirtualModules(resolved, root);
3336
3685
  serverRouteLoaderHints = createServerLoaderHintsForHotUpdates(resolved, root);
3686
+ emittedRouteHints = clientRouteHints;
3687
+ routeHintsNeedResync = clientRouteHints.incomplete;
3337
3688
  return createPrachtClientModuleSource(resolved, { root });
3338
3689
  }
3339
3690
  if (isDevModule(id)) return createPrachtDevModuleSource(resolved, {
@@ -3347,13 +3698,19 @@ function pracht(options = {}) {
3347
3698
  configuredBase
3348
3699
  });
3349
3700
  if (isCapabilitiesModule(id)) return createPrachtCapabilitiesClientModuleSource(resolved, { root });
3350
- if (isWebmcpModule(id)) return createPrachtWebmcpModuleSource(resolved, { root });
3701
+ if (isWebmcpModule(id)) return createPrachtWebmcpModuleSourceAsync(resolved, {
3702
+ root,
3703
+ runnerConfig: capabilityRunnerConfig
3704
+ });
3351
3705
  return null;
3352
3706
  },
3353
3707
  transform(code, id) {
3708
+ const normalizedId = canonicalFilePath(id.split("?")[0]);
3354
3709
  const appFileAbs = canonicalFilePath(resolveConfigPath(root, resolved.appFile));
3355
- if (canonicalFilePath(id.split("?")[0]) !== appFileAbs) return null;
3356
- const transformed = rewriteManifestCoreImports(code.replace(/\(\)\s*=>\s*import\(\s*(['"])([^'"]+)\1\s*\)/g, "$1$2$1"));
3710
+ const isPagesAppConfig = isPagesMode && isPagesAppConfigModule(normalizedId, root, resolved);
3711
+ if (normalizedId !== appFileAbs && !isPagesAppConfig) return null;
3712
+ const withStringModuleRefs = code.replace(/\(\)\s*=>\s*import\(\s*(['"])([^'"]+)\1\s*\)/g, "$1$2$1");
3713
+ const transformed = isPagesAppConfig ? withStringModuleRefs : rewriteManifestCoreImports(withStringModuleRefs);
3357
3714
  if (transformed === code) return null;
3358
3715
  return {
3359
3716
  code: transformed,
@@ -3393,46 +3750,35 @@ function pracht(options = {}) {
3393
3750
  serverRouteLoaderHints = createServerLoaderHintsForHotUpdates(resolved, root);
3394
3751
  } catch {}
3395
3752
  const loaderDependencyHints = {
3396
- ...clientRouteLoaderHints,
3753
+ ...clientRouteHints.loader,
3397
3754
  ...previousServerRouteLoaderHints,
3398
3755
  ...serverRouteLoaderHints
3399
3756
  };
3400
- const changesRouteHeadDependency = reachesRouteHintedModule(modules, serverRoot, clientRouteHeadHints, { startAtImporters: changesRouteHeadSource });
3401
- const changesRouteHeadersDependency = reachesRouteHintedModule(modules, serverRoot, clientRouteHeadersHints, { startAtImporters: changesRouteHeadSource });
3757
+ const changesRouteHeadDependency = reachesRouteHintedModule(modules, serverRoot, clientRouteHints.head, { startAtImporters: changesRouteHeadSource });
3758
+ const changesRouteHeadersDependency = reachesRouteHintedModule(modules, serverRoot, clientRouteHints.headers, { startAtImporters: changesRouteHeadSource });
3402
3759
  const changesRouteLoaderDependency = reachesRouteHintedModule(modules, serverRoot, loaderDependencyHints, { startAtImporters: changesRouteLoaderSource });
3403
- let shouldReloadClientEntry = changesRouteHeadDependency || changesRouteHeadersDependency;
3760
+ let shouldReloadClientEntry = changesRouteHeadDependency || changesRouteHeadersDependency || routeHintsNeedResync && (changesRouteHeadSource || changesRouteLoaderSource);
3404
3761
  let clientHeadModule;
3405
3762
  if (changesRouteHeadSource || changesRouteHeadDependency || changesRouteHeadersDependency) clientHeadModule = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
3406
- if (changesRouteHeadSource) {
3407
- const previousHint = clientRouteHeadHints[relative] === true;
3408
- try {
3409
- const nextHints = createRouteHeadHintsForVirtualModules(resolved, root);
3410
- shouldReloadClientEntry ||= previousHint !== (nextHints[relative] === true);
3411
- clientRouteHeadHints = nextHints;
3412
- } catch {
3413
- shouldReloadClientEntry = true;
3414
- }
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;
3763
+ if (changesRouteHeadSource || changesRouteLoaderSource) try {
3764
+ const nextHints = createRouteHintsForVirtualModules(resolved, root);
3765
+ if (changesRouteHeadSource) {
3766
+ shouldReloadClientEntry ||= JSON.stringify(emittedRouteHints.capabilities[relative] ?? []) !== JSON.stringify(nextHints.capabilities[relative] ?? []);
3767
+ shouldReloadClientEntry ||= emittedRouteHints.head[relative] === true !== (nextHints.head[relative] === true);
3768
+ shouldReloadClientEntry ||= emittedRouteHints.headers[relative] === true || nextHints.headers[relative] === true;
3424
3769
  }
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;
3770
+ if (changesRouteLoaderSource) {
3771
+ shouldReloadClientEntry ||= emittedRouteHints.loader[relative] === true !== (nextHints.loader[relative] === true);
3772
+ shouldReloadClientEntry ||= emittedRouteHints.staticPaths[relative] === true !== (nextHints.staticPaths[relative] === true);
3434
3773
  }
3774
+ shouldReloadClientEntry ||= nextHints.incomplete;
3775
+ routeHintsNeedResync ||= nextHints.incomplete;
3776
+ clientRouteHints = nextHints;
3777
+ } catch {
3778
+ shouldReloadClientEntry = true;
3779
+ routeHintsNeedResync = true;
3435
3780
  }
3781
+ else if (changesRouteHeadDependency && clientHeadModule) server.moduleGraph.invalidateModule(clientHeadModule);
3436
3782
  if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
3437
3783
  clearPagesAppSourceCache();
3438
3784
  invalidateVirtualModules(server);
@@ -3466,14 +3812,17 @@ function pracht(options = {}) {
3466
3812
  const islandsMod = server.moduleGraph.getModuleById(PRACHT_ISLANDS_CLIENT_MODULE_ID);
3467
3813
  if (islandsMod) server.moduleGraph.invalidateModule(islandsMod);
3468
3814
  }
3469
- if (relative.startsWith(resolved.capabilitiesDir)) for (const moduleId of [
3470
- PRACHT_CAPABILITIES_MODULE_ID,
3471
- PRACHT_WEBMCP_MODULE_ID,
3472
- PRACHT_CLIENT_MODULE_ID,
3473
- PRACHT_ISLANDS_CLIENT_MODULE_ID
3474
- ]) {
3475
- const capabilityMod = server.moduleGraph.getModuleById(moduleId);
3476
- if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
3815
+ if (relative.startsWith(resolved.capabilitiesDir)) {
3816
+ if (isPagesMode) clearPagesAppSourceCache();
3817
+ for (const moduleId of [
3818
+ PRACHT_CAPABILITIES_MODULE_ID,
3819
+ PRACHT_WEBMCP_MODULE_ID,
3820
+ PRACHT_CLIENT_MODULE_ID,
3821
+ PRACHT_ISLANDS_CLIENT_MODULE_ID
3822
+ ]) {
3823
+ const capabilityMod = server.moduleGraph.getModuleById(moduleId);
3824
+ if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
3825
+ }
3477
3826
  }
3478
3827
  }
3479
3828
  const sentFullReload = sendServerOnlyFullReload(server, file);
@@ -3495,8 +3844,9 @@ function pracht(options = {}) {
3495
3844
  enforce: "post",
3496
3845
  transform(code, id, transformOptions) {
3497
3846
  if (!transformOptions?.ssr && isCapabilityModule(id, capabilityModulePaths)) throw new Error(`[pracht] Capability module ${JSON.stringify(toPosixPath(id))} was imported by client code. Capability modules are server-only — their run() implementation and its imports would be bundled for every visitor. Call the capability instead: \`callCapability\`/\`capabilities\` from "virtual:pracht/capabilities" in the browser, or \`invokeCapability\` from "@pracht/core/server" in loaders, middleware, and API routes.`);
3498
- if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs, routeFileExtensions))) return null;
3499
- const transformed = stripServerOnlyExportsForClient(code, id);
3847
+ const isPagesMiddlewareModule = !transformOptions?.ssr && (isPagesMode || usesEjectedPagesLayout) && isRootMiddlewareModule(id, root, resolved);
3848
+ if (!(isPrachtClientModuleId(id) || !transformOptions?.ssr && isRouteOrShellFile(id, routeFileDirs, routeFileExtensions) || isPagesMiddlewareModule)) return null;
3849
+ const transformed = stripServerOnlyExportsForClient(code, id, { middleware: isPagesMiddlewareModule });
3500
3850
  if (transformed === code) return null;
3501
3851
  return {
3502
3852
  code: transformed,
@@ -3700,19 +4050,28 @@ function mergeOptimizeDepsEntries(userEntries, prachtEntries) {
3700
4050
  function toOptimizeDepsEntry(path) {
3701
4051
  return toPosixPath(path).replace(/^\.\//, "").replace(/^\//, "").replace(/\/$/, "");
3702
4052
  }
4053
+ /**
4054
+ * Restart the dev server when the pages graph gains or loses a file.
4055
+ *
4056
+ * `capabilitiesDir` is watched alongside `pagesDir` because the generated
4057
+ * manifest registers capabilities from it: without this, adding
4058
+ * `src/capabilities/x.ts` leaves its endpoint 404ing until some unrelated page
4059
+ * changes, and deleting one leaves a manifest pointing at a module that is
4060
+ * gone — a 500 on every capability endpoint. The cached manifest source is
4061
+ * cleared first so the restart regenerates it. Edits (as opposed to
4062
+ * add/unlink) go through `handleHotUpdate`, which clears the same cache
4063
+ * without a restart.
4064
+ */
3703
4065
  function watchPagesDirectory(server, resolved, root) {
3704
- const abs = resolveConfigPath(root, resolved.pagesDir);
3705
- server.watcher.on("add", (f) => {
3706
- if (toPosixPath(f).startsWith(toPosixPath(abs))) {
3707
- clearPagesAppSourceCache();
3708
- server.restart();
3709
- }
3710
- });
3711
- server.watcher.on("unlink", (f) => {
3712
- if (toPosixPath(f).startsWith(toPosixPath(abs))) {
3713
- clearPagesAppSourceCache();
3714
- server.restart();
3715
- }
4066
+ const watched = [toPosixPath(resolveConfigPath(root, resolved.pagesDir)), toPosixPath(resolveConfigPath(root, resolved.capabilitiesDir))];
4067
+ const isWatched = (file) => {
4068
+ const path = toPosixPath(file);
4069
+ return watched.some((dir) => path === dir || path.startsWith(`${dir}/`));
4070
+ };
4071
+ for (const event of ["add", "unlink"]) server.watcher.on(event, (file) => {
4072
+ if (!isWatched(file)) return;
4073
+ clearPagesAppSourceCache();
4074
+ server.restart();
3716
4075
  });
3717
4076
  }
3718
4077
  function invalidateVirtualModules(server) {
@@ -3740,6 +4099,28 @@ function isCapabilityModule(id, capabilityModulePaths) {
3740
4099
  if (path.startsWith("\0") || path.startsWith("virtual:")) return false;
3741
4100
  return capabilityModulePaths.has(canonicalFilePath(path));
3742
4101
  }
4102
+ /** Whether `modulePath` is the pages directory's root `_app.config` module. */
4103
+ function isPagesAppConfigModule(modulePath, root, resolved) {
4104
+ return [
4105
+ ".ts",
4106
+ ".tsx",
4107
+ ".js",
4108
+ ".jsx"
4109
+ ].some((extension) => modulePath === canonicalFilePath(resolveConfigPath(root, `${resolved.pagesDir}/_app.config${extension}`)));
4110
+ }
4111
+ function isRootMiddlewareModule(id, root, resolved) {
4112
+ const queryStart = id.indexOf("?");
4113
+ const path = queryStart === -1 ? id : id.slice(0, queryStart);
4114
+ if (path.startsWith("\0") || path.startsWith("virtual:")) return false;
4115
+ const middlewareDir = resolved.pagesDir || resolved.middlewareDir;
4116
+ const modulePath = canonicalFilePath(path);
4117
+ return [
4118
+ ".ts",
4119
+ ".tsx",
4120
+ ".js",
4121
+ ".jsx"
4122
+ ].some((extension) => modulePath === canonicalFilePath(resolveConfigPath(root, `${middlewareDir}/_middleware${extension}`)));
4123
+ }
3743
4124
  /**
3744
4125
  * Match Vite's canonical module ids even when the manifest path crosses a
3745
4126
  * symlink (including macOS' /var -> /private/var alias). Missing paths keep
@@ -3778,4 +4159,4 @@ function withTrailingSep(p) {
3778
4159
  return p.endsWith("/") ? p : `${p}/`;
3779
4160
  }
3780
4161
  //#endregion
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 };
4162
+ 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, createPrachtWebmcpModuleSourceAsync, extractCapabilities, formatEnvLeakError, frameworkChunkGroups, pracht, scanCodeForEnvLeaks };