@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.d.mts +37 -37
- package/dist/index.mjs +918 -537
- package/dist/pages-router-BI34Cani.mjs +906 -0
- package/dist/pages-router.d.mts +105 -1
- package/dist/pages-router.mjs +2 -2
- package/package.json +13 -6
- package/virtual.d.ts +255 -0
- package/dist/pages-router-MA9rOl88.mjs +0 -618
package/dist/index.mjs
CHANGED
|
@@ -1,83 +1,17 @@
|
|
|
1
|
-
import {
|
|
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 || !
|
|
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) =>
|
|
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
|
|
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 ===
|
|
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)
|
|
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 = {
|
|
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.
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
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 =
|
|
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
|
|
1769
|
-
* manifest
|
|
1770
|
-
*
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1565
|
+
manifestSource = readAppManifestSource(resolved, root);
|
|
1816
1566
|
} catch {
|
|
1817
1567
|
return [];
|
|
1818
1568
|
}
|
|
1819
|
-
const appDir =
|
|
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
|
|
1977
|
-
* One page tool per `expose.webmcp` capability;
|
|
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
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1988
|
-
*
|
|
1989
|
-
*
|
|
1990
|
-
*
|
|
1991
|
-
*
|
|
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
|
|
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
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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
|
-
"
|
|
2016
|
-
"
|
|
2017
|
-
"
|
|
2018
|
-
"
|
|
2019
|
-
"
|
|
2020
|
-
"
|
|
2021
|
-
"
|
|
2022
|
-
"
|
|
2023
|
-
"
|
|
2024
|
-
"
|
|
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
|
-
"
|
|
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 —
|
|
2054
|
-
"
|
|
2055
|
-
"
|
|
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
|
|
2160
|
-
const
|
|
2161
|
-
const
|
|
2162
|
-
const
|
|
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(
|
|
2187
|
-
` ...import.meta.glob(${JSON.stringify(
|
|
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
|
-
...
|
|
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
|
|
2299
|
-
const
|
|
2300
|
-
const
|
|
2301
|
-
const
|
|
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
|
|
2372
|
-
const
|
|
2373
|
-
const
|
|
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
|
-
*
|
|
2467
|
-
*
|
|
2468
|
-
*
|
|
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
|
|
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
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
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
|
-
|
|
2490
|
-
|
|
2491
|
-
additionalExtensions
|
|
2492
|
-
|
|
2493
|
-
|
|
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}
|
|
2517
|
-
const additionalShellGlob = isPagesMode ? `${resolved.pagesDir}/**/_app.${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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2927
|
-
|
|
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 =
|
|
3186
|
+
const done = readNodeWriteCallback(encodingOrCallback, callback);
|
|
2933
3187
|
(async () => {
|
|
2934
|
-
|
|
2935
|
-
if (
|
|
2936
|
-
originalEnd(
|
|
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
|
|
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: {
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3356
|
-
|
|
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
|
-
...
|
|
3753
|
+
...clientRouteHints.loader,
|
|
3397
3754
|
...previousServerRouteLoaderHints,
|
|
3398
3755
|
...serverRouteLoaderHints
|
|
3399
3756
|
};
|
|
3400
|
-
const changesRouteHeadDependency = reachesRouteHintedModule(modules, serverRoot,
|
|
3401
|
-
const changesRouteHeadersDependency = reachesRouteHintedModule(modules, serverRoot,
|
|
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
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
shouldReloadClientEntry ||=
|
|
3411
|
-
|
|
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
|
-
|
|
3427
|
-
|
|
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))
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
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
|
-
|
|
3499
|
-
|
|
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
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
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 };
|