@pracht/vite-plugin 0.6.2 → 0.7.1
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 +81 -3
- package/dist/index.mjs +628 -29
- package/package.json +4 -3
package/dist/index.d.mts
CHANGED
|
@@ -6,6 +6,8 @@ import { RenderMode, RenderMode as RenderMode$1 } from "@pracht/core";
|
|
|
6
6
|
declare const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
|
|
7
7
|
declare const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
|
|
8
8
|
declare const PRACHT_ISLANDS_CLIENT_MODULE_ID = "virtual:pracht/islands-client";
|
|
9
|
+
declare const PRACHT_CAPABILITIES_MODULE_ID = "virtual:pracht/capabilities";
|
|
10
|
+
declare const PRACHT_WEBMCP_MODULE_ID = "virtual:pracht/webmcp";
|
|
9
11
|
//#endregion
|
|
10
12
|
//#region src/env-safety.d.ts
|
|
11
13
|
/**
|
|
@@ -26,7 +28,8 @@ interface EnvLeakReference {
|
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Scans JavaScript source for references to environment variables that are
|
|
29
|
-
* neither public-prefixed, Vite built-ins, nor explicitly allowed
|
|
31
|
+
* neither public-prefixed, Vite built-ins, nor explicitly allowed, plus reads
|
|
32
|
+
* that pull in the whole `import.meta.env` object.
|
|
30
33
|
*/
|
|
31
34
|
declare function scanCodeForEnvLeaks(code: string, allow?: ReadonlySet<string>): EnvLeakReference[];
|
|
32
35
|
interface EnvLeakProblem extends EnvLeakReference {
|
|
@@ -84,6 +87,23 @@ interface PrachtAdapter {
|
|
|
84
87
|
}
|
|
85
88
|
//#endregion
|
|
86
89
|
//#region src/plugin-options.d.ts
|
|
90
|
+
type LlmsTxtSection = "pages" | "api" | "capabilities";
|
|
91
|
+
interface PrachtLlmsTxtOptions {
|
|
92
|
+
/** H1 title. Defaults to the app's package.json `name`. */
|
|
93
|
+
title?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Blockquote summary under the title. Defaults to the app's package.json
|
|
96
|
+
* `description`; omitted when neither is set.
|
|
97
|
+
*/
|
|
98
|
+
description?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Origin (e.g. "https://example.com") prepended to every link so llms.txt
|
|
101
|
+
* contains absolute URLs. Links stay root-relative when omitted.
|
|
102
|
+
*/
|
|
103
|
+
origin?: string;
|
|
104
|
+
/** Sections to emit. Defaults to ["pages", "api", "capabilities"]. */
|
|
105
|
+
include?: LlmsTxtSection[];
|
|
106
|
+
}
|
|
87
107
|
interface PrachtPluginOptions {
|
|
88
108
|
appFile?: string;
|
|
89
109
|
routesDir?: string;
|
|
@@ -96,6 +116,11 @@ interface PrachtPluginOptions {
|
|
|
96
116
|
* `hydration: "islands"` routes. Defaults to "/src/islands".
|
|
97
117
|
*/
|
|
98
118
|
islandsDir?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Directory containing capability modules registered in the app manifest
|
|
121
|
+
* via `capabilities: { ... }`. Defaults to "/src/capabilities".
|
|
122
|
+
*/
|
|
123
|
+
capabilitiesDir?: string;
|
|
99
124
|
adapter?: PrachtAdapter;
|
|
100
125
|
/** Enable file-system pages routing by pointing to the pages directory (e.g. "/src/pages"). */
|
|
101
126
|
pagesDir?: string;
|
|
@@ -125,6 +150,12 @@ interface PrachtPluginOptions {
|
|
|
125
150
|
* variables, or `false` to disable the check entirely.
|
|
126
151
|
*/
|
|
127
152
|
envSafety?: false | EnvSafetyOptions;
|
|
153
|
+
/**
|
|
154
|
+
* Opt into emitting an llms.txt file (https://llmstxt.org) generated from
|
|
155
|
+
* the resolved app graph. `pracht build` writes `dist/client/llms.txt` and
|
|
156
|
+
* the dev server serves `/llms.txt` live. Disabled by default.
|
|
157
|
+
*/
|
|
158
|
+
llmsTxt?: false | PrachtLlmsTxtOptions;
|
|
128
159
|
}
|
|
129
160
|
//#endregion
|
|
130
161
|
//#region src/plugin-codegen.d.ts
|
|
@@ -137,14 +168,61 @@ declare function createPrachtClientModuleSource(options?: PrachtPluginOptions, b
|
|
|
137
168
|
* manifest, the router, or the full client runtime: it only scans the DOM
|
|
138
169
|
* for island markers and hydrates the islands present on the page.
|
|
139
170
|
*/
|
|
140
|
-
declare function createPrachtIslandsClientModuleSource(options?: PrachtPluginOptions
|
|
171
|
+
declare function createPrachtIslandsClientModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
172
|
+
root?: string;
|
|
173
|
+
}): string;
|
|
141
174
|
declare function createPrachtServerModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
142
175
|
root?: string;
|
|
143
176
|
isBuild?: boolean;
|
|
144
177
|
}): string;
|
|
145
178
|
declare function createPrachtRegistryModuleSource(options?: PrachtPluginOptions): string;
|
|
146
179
|
//#endregion
|
|
180
|
+
//#region src/plugin-capabilities.d.ts
|
|
181
|
+
interface ExtractedCapability {
|
|
182
|
+
name: string;
|
|
183
|
+
/** Manifest-relative module path, e.g. "./capabilities/notes-search.ts". */
|
|
184
|
+
file: string;
|
|
185
|
+
description: string;
|
|
186
|
+
effect: string | null;
|
|
187
|
+
httpPath: string | null;
|
|
188
|
+
webmcp: boolean;
|
|
189
|
+
inputSchema: Record<string, unknown> | null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Extract capability registrations (name → module path) from the app
|
|
193
|
+
* manifest source and their exposure metadata from each capability source.
|
|
194
|
+
* Pages-router apps have no manifest, so capabilities are manifest-mode only.
|
|
195
|
+
*/
|
|
196
|
+
declare function extractCapabilities(options?: PrachtPluginOptions, root?: string): ExtractedCapability[];
|
|
197
|
+
/**
|
|
198
|
+
* Generate `virtual:pracht/capabilities` — the browser-side `callCapability`
|
|
199
|
+
* helper plus the endpoint map for http-exposed capabilities. Side-effect
|
|
200
|
+
* free, so it costs zero bytes unless application code imports it.
|
|
201
|
+
*
|
|
202
|
+
* After every call settles, the helper announces itself on
|
|
203
|
+
* CAPABILITY_SETTLED_EVENT with the capability's effect class; the framework
|
|
204
|
+
* runtime revalidates route data for successful non-`read` calls (opt out
|
|
205
|
+
* per call via `{ revalidate: false }`).
|
|
206
|
+
*/
|
|
207
|
+
declare function createPrachtCapabilitiesClientModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
208
|
+
root?: string;
|
|
209
|
+
}): string;
|
|
210
|
+
/**
|
|
211
|
+
* Generate `virtual:pracht/webmcp` — the disposable WebMCP registration shim.
|
|
212
|
+
* One page tool per `expose.webmcp` capability; `execute` dispatches through
|
|
213
|
+
* `callCapability`, so the user's session authenticates the call and all
|
|
214
|
+
* validation/middleware/policy stays server-side. Each dispatch carries the
|
|
215
|
+
* transport marker header so audit events can attribute it to WebMCP.
|
|
216
|
+
*
|
|
217
|
+
* Targets the Chrome origin-trial API: `document.modelContext.registerTool()`
|
|
218
|
+
* (Chrome 150+; `navigator.modelContext` is the deprecated pre-150 location
|
|
219
|
+
* and is kept as a fallback). No-ops silently when the API is absent.
|
|
220
|
+
*/
|
|
221
|
+
declare function createPrachtWebmcpModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
222
|
+
root?: string;
|
|
223
|
+
}): string;
|
|
224
|
+
//#endregion
|
|
147
225
|
//#region src/index.d.ts
|
|
148
226
|
declare function pracht(options?: PrachtPluginOptions): Plugin[];
|
|
149
227
|
//#endregion
|
|
150
|
-
export { type EnvLeakReference, type EnvSafetyOptions, PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PUBLIC_ENV_PREFIX, type PrachtAdapter, type PrachtPluginOptions, type RenderMode, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, formatEnvLeakError, pracht, scanCodeForEnvLeaks };
|
|
228
|
+
export { type EnvLeakReference, type EnvSafetyOptions, type LlmsTxtSection, PRACHT_CAPABILITIES_MODULE_ID, PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PRACHT_WEBMCP_MODULE_ID, PUBLIC_ENV_PREFIX, type PrachtAdapter, type PrachtLlmsTxtOptions, type PrachtPluginOptions, type RenderMode, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtCapabilitiesClientModuleSource, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, createPrachtWebmcpModuleSource, extractCapabilities, formatEnvLeakError, pracht, scanCodeForEnvLeaks };
|
package/dist/index.mjs
CHANGED
|
@@ -3,9 +3,12 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { preactSsrPrecompile } from "@pracht/preact-ssr-precompile";
|
|
4
4
|
import preact from "@preact/preset-vite";
|
|
5
5
|
import { dirname, extname, join, resolve } from "node:path";
|
|
6
|
-
import { parseAst } from "vite";
|
|
6
|
+
import { loadEnv, parseAst } from "vite";
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { CAPABILITY_SETTLED_EVENT, CAPABILITY_TRANSPORT_HEADER, CONFIRMATION_HEADER, capabilityHttpPath, isValidCapabilityHttpPath } from "@pracht/capabilities";
|
|
9
|
+
import { evaluateLiteral, extractCapabilityRegistrations, extractDefineCapabilityArgs, scanTopLevelProperties } from "@pracht/capabilities/static";
|
|
8
10
|
import { createNodeServerEntryModule } from "@pracht/adapter-node";
|
|
11
|
+
import { resolveRegistryModule } from "@pracht/core";
|
|
9
12
|
//#region src/client-module-query.ts
|
|
10
13
|
const CLIENT_MODULE_QUERY = "pracht-client";
|
|
11
14
|
const PRACHT_CLIENT_MODULE_QUERY = `?${CLIENT_MODULE_QUERY}`;
|
|
@@ -906,30 +909,51 @@ const VITE_BUILTIN_ENV_VARS = new Set([
|
|
|
906
909
|
const PUBLIC_ENV_PREFIX = "PRACHT_PUBLIC_";
|
|
907
910
|
/** Server-only core entry that must never resolve into client bundles. */
|
|
908
911
|
const SERVER_ENV_MODULE_ID = "@pracht/core/env/server";
|
|
909
|
-
const ENV_REFERENCE_RE = /\b(process\.env|import\.meta\.env)(
|
|
912
|
+
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;
|
|
913
|
+
const WHOLE_ENV_READ_RE = /\bimport\.meta\.env\b(?!\s*\??\.\s*[A-Za-z_$])/g;
|
|
910
914
|
/**
|
|
911
915
|
* Scans JavaScript source for references to environment variables that are
|
|
912
|
-
* neither public-prefixed, Vite built-ins, nor explicitly allowed
|
|
916
|
+
* neither public-prefixed, Vite built-ins, nor explicitly allowed, plus reads
|
|
917
|
+
* that pull in the whole `import.meta.env` object.
|
|
913
918
|
*/
|
|
914
919
|
function scanCodeForEnvLeaks(code, allow = /* @__PURE__ */ new Set()) {
|
|
915
|
-
const findings = [];
|
|
916
|
-
const seen = /* @__PURE__ */ new Set();
|
|
917
920
|
const codePositions = getCodePositionMask(code);
|
|
921
|
+
const matches = [];
|
|
918
922
|
for (const match of code.matchAll(ENV_REFERENCE_RE)) {
|
|
919
|
-
|
|
923
|
+
const index = match.index ?? -1;
|
|
924
|
+
if (!codePositions[index]) continue;
|
|
920
925
|
const accessor = match[1];
|
|
921
926
|
const name = match[2] ?? match[4];
|
|
922
927
|
if (!name) continue;
|
|
923
928
|
if (name.startsWith("PRACHT_PUBLIC_")) continue;
|
|
924
929
|
if (VITE_BUILTIN_ENV_VARS.has(name)) continue;
|
|
925
930
|
if (allow.has(name)) continue;
|
|
926
|
-
|
|
931
|
+
matches.push({
|
|
932
|
+
index,
|
|
933
|
+
reference: {
|
|
934
|
+
accessor,
|
|
935
|
+
name
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
if (!allow.has("*")) for (const match of code.matchAll(WHOLE_ENV_READ_RE)) {
|
|
940
|
+
const index = match.index ?? -1;
|
|
941
|
+
if (!codePositions[index]) continue;
|
|
942
|
+
matches.push({
|
|
943
|
+
index,
|
|
944
|
+
reference: {
|
|
945
|
+
accessor: "import.meta.env",
|
|
946
|
+
name: "*"
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
const findings = [];
|
|
951
|
+
const seen = /* @__PURE__ */ new Set();
|
|
952
|
+
for (const { reference } of matches.sort((a, b) => a.index - b.index)) {
|
|
953
|
+
const key = `${reference.accessor}.${reference.name}`;
|
|
927
954
|
if (seen.has(key)) continue;
|
|
928
955
|
seen.add(key);
|
|
929
|
-
findings.push(
|
|
930
|
-
accessor,
|
|
931
|
-
name
|
|
932
|
-
});
|
|
956
|
+
findings.push(reference);
|
|
933
957
|
}
|
|
934
958
|
return findings;
|
|
935
959
|
}
|
|
@@ -1100,12 +1124,20 @@ function isIdentifierChar(char) {
|
|
|
1100
1124
|
return !!char && /[A-Za-z0-9_$]/.test(char);
|
|
1101
1125
|
}
|
|
1102
1126
|
function formatEnvLeakError(problems) {
|
|
1127
|
+
const lines = problems.map((problem) => {
|
|
1128
|
+
const source = problem.sources.length > 0 ? ` (likely from ${problem.sources.map((file) => JSON.stringify(file)).join(", ")})` : "";
|
|
1129
|
+
return ` - ${problem.name === "*" ? "import.meta.env read as a whole object" : `${problem.accessor}.${problem.name}`} in chunk "${problem.chunk}"${source}`;
|
|
1130
|
+
});
|
|
1131
|
+
const wholeEnvGuidance = problems.some((problem) => problem.name === "*") ? [
|
|
1132
|
+
"",
|
|
1133
|
+
"A whole-object `import.meta.env` read (bare reference, destructuring, spread, or bracket access)",
|
|
1134
|
+
"is replaced at build time by an object literal containing every exposed variable — including the",
|
|
1135
|
+
"`VITE_` values Pracht does not treat as public. Read one key at a time (`import.meta.env.KEY`)."
|
|
1136
|
+
] : [];
|
|
1103
1137
|
return [
|
|
1104
1138
|
"[pracht] Environment variable leak detected in the client bundle:",
|
|
1105
|
-
...
|
|
1106
|
-
|
|
1107
|
-
return ` - ${problem.accessor}.${problem.name} in chunk "${problem.chunk}"${source}`;
|
|
1108
|
-
}),
|
|
1139
|
+
...lines,
|
|
1140
|
+
...wholeEnvGuidance,
|
|
1109
1141
|
"",
|
|
1110
1142
|
`Only PRACHT_PUBLIC_-prefixed variables may be referenced in client code (prefer publicEnv from "@pracht/core" for typed public values).`,
|
|
1111
1143
|
`Move server-only reads into loaders/API routes and access them via serverEnv from "@pracht/core/env/server",`,
|
|
@@ -1190,7 +1222,10 @@ function createEnvSafetyPlugin(envSafety) {
|
|
|
1190
1222
|
//#region src/plugin-assets.ts
|
|
1191
1223
|
const PRACHT_CLIENT_MODULE_ID = "virtual:pracht/client";
|
|
1192
1224
|
const PRACHT_SERVER_MODULE_ID = "virtual:pracht/server";
|
|
1225
|
+
const PRACHT_DEV_MODULE_ID = "virtual:pracht/dev-metadata";
|
|
1193
1226
|
const PRACHT_ISLANDS_CLIENT_MODULE_ID = "virtual:pracht/islands-client";
|
|
1227
|
+
const PRACHT_CAPABILITIES_MODULE_ID = "virtual:pracht/capabilities";
|
|
1228
|
+
const PRACHT_WEBMCP_MODULE_ID = "virtual:pracht/webmcp";
|
|
1194
1229
|
const CLIENT_BROWSER_PATH = "/@pracht/client.js";
|
|
1195
1230
|
const ISLANDS_CLIENT_BROWSER_PATH = "/@pracht/islands.js";
|
|
1196
1231
|
function readClientBuildAssets(root = process.cwd()) {
|
|
@@ -1253,9 +1288,18 @@ function isClientModule(id) {
|
|
|
1253
1288
|
function isServerModule(id) {
|
|
1254
1289
|
return id === "virtual:pracht/server" || id.endsWith("virtual:pracht/server");
|
|
1255
1290
|
}
|
|
1291
|
+
function isDevModule(id) {
|
|
1292
|
+
return id === "virtual:pracht/dev-metadata" || id.endsWith("virtual:pracht/dev-metadata");
|
|
1293
|
+
}
|
|
1256
1294
|
function isIslandsClientModule(id) {
|
|
1257
1295
|
return id === "virtual:pracht/islands-client" || id === "/@pracht/islands.js" || id.endsWith("virtual:pracht/islands-client");
|
|
1258
1296
|
}
|
|
1297
|
+
function isCapabilitiesModule(id) {
|
|
1298
|
+
return id === "virtual:pracht/capabilities" || id.endsWith("virtual:pracht/capabilities");
|
|
1299
|
+
}
|
|
1300
|
+
function isWebmcpModule(id) {
|
|
1301
|
+
return id === "virtual:pracht/webmcp" || id.endsWith("virtual:pracht/webmcp");
|
|
1302
|
+
}
|
|
1259
1303
|
//#endregion
|
|
1260
1304
|
//#region src/plugin-adapter.ts
|
|
1261
1305
|
function createDefaultNodeAdapter() {
|
|
@@ -1277,6 +1321,7 @@ const DEFAULTS = {
|
|
|
1277
1321
|
apiDir: "/src/api",
|
|
1278
1322
|
serverDir: "/src/server",
|
|
1279
1323
|
islandsDir: "/src/islands",
|
|
1324
|
+
capabilitiesDir: "/src/capabilities",
|
|
1280
1325
|
adapter: createDefaultNodeAdapter(),
|
|
1281
1326
|
pagesDir: "",
|
|
1282
1327
|
pagesDefaultRender: "ssr",
|
|
@@ -1284,18 +1329,33 @@ const DEFAULTS = {
|
|
|
1284
1329
|
maxBodySize: 1024 * 1024,
|
|
1285
1330
|
budgets: {},
|
|
1286
1331
|
precompileSsrJsx: false,
|
|
1287
|
-
envSafety: {}
|
|
1332
|
+
envSafety: {},
|
|
1333
|
+
llmsTxt: false
|
|
1288
1334
|
};
|
|
1289
1335
|
function resolveOptions(options) {
|
|
1290
1336
|
const resolved = {
|
|
1291
1337
|
...DEFAULTS,
|
|
1292
1338
|
...options
|
|
1293
1339
|
};
|
|
1340
|
+
if (resolved.llmsTxt === void 0) resolved.llmsTxt = false;
|
|
1294
1341
|
if (!Number.isInteger(resolved.prerenderConcurrency) || resolved.prerenderConcurrency <= 0) throw new Error("pracht({ prerenderConcurrency }) expects a positive integer.");
|
|
1295
1342
|
if (!Number.isInteger(resolved.maxBodySize) || resolved.maxBodySize <= 0) throw new Error("pracht({ maxBodySize }) expects a positive integer number of bytes.");
|
|
1296
1343
|
validateBudgets(resolved.budgets);
|
|
1344
|
+
validateLlmsTxt(resolved.llmsTxt);
|
|
1297
1345
|
return resolved;
|
|
1298
1346
|
}
|
|
1347
|
+
const LLMS_TXT_SECTIONS = new Set([
|
|
1348
|
+
"pages",
|
|
1349
|
+
"api",
|
|
1350
|
+
"capabilities"
|
|
1351
|
+
]);
|
|
1352
|
+
function validateLlmsTxt(llmsTxt) {
|
|
1353
|
+
if (llmsTxt === false) return;
|
|
1354
|
+
if (typeof llmsTxt !== "object" || llmsTxt === null) throw new Error("pracht({ llmsTxt }) expects false or an options object.");
|
|
1355
|
+
if (llmsTxt.include !== void 0) {
|
|
1356
|
+
if (!(Array.isArray(llmsTxt.include) && llmsTxt.include.every((section) => LLMS_TXT_SECTIONS.has(section)))) throw new Error(`pracht({ llmsTxt: { include } }) expects an array of "pages", "api", and/or "capabilities", got ${JSON.stringify(llmsTxt.include)}.`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1299
1359
|
function validateBudgets(budgets) {
|
|
1300
1360
|
for (const [key, value] of Object.entries(budgets)) {
|
|
1301
1361
|
if (key !== "*" && !key.startsWith("/")) throw new Error(`pracht({ budgets }) keys must be "*" or a route path starting with "/", got ${JSON.stringify(key)}.`);
|
|
@@ -1305,6 +1365,288 @@ function validateBudgets(budgets) {
|
|
|
1305
1365
|
}
|
|
1306
1366
|
}
|
|
1307
1367
|
//#endregion
|
|
1368
|
+
//#region src/plugin-capabilities.ts
|
|
1369
|
+
/**
|
|
1370
|
+
* Build-time capability projection for the browser.
|
|
1371
|
+
*
|
|
1372
|
+
* The client never loads capability modules (they are server-only), so the
|
|
1373
|
+
* `virtual:pracht/capabilities` and `virtual:pracht/webmcp` modules are
|
|
1374
|
+
* generated from static analysis of the app manifest and the registered
|
|
1375
|
+
* capability sources — the same approach the plugin already uses for
|
|
1376
|
+
* hydration-mode excludes. Only serializable metadata crosses the boundary:
|
|
1377
|
+
* capability names, HTTP endpoints, effects, and (for WebMCP tools)
|
|
1378
|
+
* description and input schema.
|
|
1379
|
+
*
|
|
1380
|
+
* The static analyzer itself lives in `@pracht/capabilities/static` and is
|
|
1381
|
+
* shared with `pracht verify`, so the build and verification can never
|
|
1382
|
+
* disagree about what is analyzable. Constraint it imposes: a capability's
|
|
1383
|
+
* `expose`, HTTP-projected `effect`, and WebMCP `input` values must be inline
|
|
1384
|
+
* literals (no imported constants or spreads) — the extractor parses the
|
|
1385
|
+
* literal text as data.
|
|
1386
|
+
* Extraction failures fail the build with a pointer to the offending file
|
|
1387
|
+
* rather than silently dropping an endpoint.
|
|
1388
|
+
*/
|
|
1389
|
+
/**
|
|
1390
|
+
* Extract capability registrations (name → module path) from the app
|
|
1391
|
+
* manifest source and their exposure metadata from each capability source.
|
|
1392
|
+
* Pages-router apps have no manifest, so capabilities are manifest-mode only.
|
|
1393
|
+
*/
|
|
1394
|
+
function extractCapabilities(options = {}, root = process.cwd()) {
|
|
1395
|
+
const resolved = resolveOptions(options);
|
|
1396
|
+
if (resolved.pagesDir) return [];
|
|
1397
|
+
const appFileAbs = resolve(root, resolved.appFile.replace(/^\//, ""));
|
|
1398
|
+
let manifestSource;
|
|
1399
|
+
try {
|
|
1400
|
+
manifestSource = readFileSync(appFileAbs, "utf-8");
|
|
1401
|
+
} catch {
|
|
1402
|
+
return [];
|
|
1403
|
+
}
|
|
1404
|
+
const registrations = extractCapabilityRegistrations(manifestSource);
|
|
1405
|
+
if (registrations.length === 0) return [];
|
|
1406
|
+
const appDir = dirname(appFileAbs);
|
|
1407
|
+
return registrations.map(({ name, file }) => {
|
|
1408
|
+
const capabilityFileAbs = file.startsWith("/") ? resolve(root, file.replace(/^\//, "")) : resolve(appDir, file);
|
|
1409
|
+
let source;
|
|
1410
|
+
try {
|
|
1411
|
+
source = readFileSync(capabilityFileAbs, "utf-8");
|
|
1412
|
+
} catch {
|
|
1413
|
+
throw new Error(`[pracht] Capability "${name}" references missing file ${JSON.stringify(file)}.`);
|
|
1414
|
+
}
|
|
1415
|
+
return extractCapabilityMetadata(name, file, source);
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
function extractCapabilityMetadata(name, file, source) {
|
|
1419
|
+
const args = extractDefineCapabilityArgs(source);
|
|
1420
|
+
if (!args) throw new Error(`[pracht] Capability "${name}" (${file}) does not contain a defineCapability({ ... }) call the build can analyze.`);
|
|
1421
|
+
const properties = scanTopLevelProperties(args);
|
|
1422
|
+
const exposeText = properties.get("expose");
|
|
1423
|
+
if (!exposeText) return {
|
|
1424
|
+
name,
|
|
1425
|
+
file,
|
|
1426
|
+
description: "",
|
|
1427
|
+
effect: null,
|
|
1428
|
+
httpPath: null,
|
|
1429
|
+
webmcp: false,
|
|
1430
|
+
inputSchema: null
|
|
1431
|
+
};
|
|
1432
|
+
const expose = evaluateLiteral(exposeText);
|
|
1433
|
+
if (!isPlainObject(expose)) throw new Error(`[pracht] Capability "${name}" (${file}): "expose" must be an inline object literal so the client projection can be generated at build time.`);
|
|
1434
|
+
const http = expose.http;
|
|
1435
|
+
let httpPath = null;
|
|
1436
|
+
if (http === true) httpPath = capabilityHttpPath(name);
|
|
1437
|
+
else if (isPlainObject(http)) httpPath = typeof http.path === "string" ? http.path : capabilityHttpPath(name);
|
|
1438
|
+
if (httpPath && !isValidCapabilityHttpPath(httpPath)) throw new Error(`[pracht] Capability "${name}" (${file}): HTTP exposure "path" must be an exact same-origin pathname starting with "/".`);
|
|
1439
|
+
const webmcp = expose.webmcp === true;
|
|
1440
|
+
if (webmcp && !httpPath) throw new Error(`[pracht] Capability "${name}" (${file}): expose.webmcp requires expose.http.`);
|
|
1441
|
+
let description = "";
|
|
1442
|
+
const descriptionText = properties.get("description");
|
|
1443
|
+
if (descriptionText) {
|
|
1444
|
+
const value = evaluateLiteral(descriptionText);
|
|
1445
|
+
if (typeof value === "string") description = value;
|
|
1446
|
+
}
|
|
1447
|
+
let effect = null;
|
|
1448
|
+
const effectText = properties.get("effect");
|
|
1449
|
+
if (effectText) {
|
|
1450
|
+
const value = evaluateLiteral(effectText);
|
|
1451
|
+
if (typeof value === "string") effect = value;
|
|
1452
|
+
}
|
|
1453
|
+
if (httpPath && effect !== "read" && effect !== "write" && effect !== "destructive") throw new Error(`[pracht] Capability "${name}" (${file}) is exposed via HTTP, but its "effect" could not be extracted at build time. HTTP-exposed capabilities must declare "effect" as an inline "read", "write", or "destructive" string literal.`);
|
|
1454
|
+
let inputSchema = null;
|
|
1455
|
+
if (webmcp) {
|
|
1456
|
+
const inputText = properties.get("input");
|
|
1457
|
+
const value = inputText ? evaluateLiteral(inputText) : void 0;
|
|
1458
|
+
if (!isPlainObject(value)) throw new Error(`[pracht] Capability "${name}" (${file}) is exposed via WebMCP, but its "input" schema could not be extracted at build time. WebMCP-exposed capabilities must declare their input schema as an inline object literal.`);
|
|
1459
|
+
inputSchema = value;
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
name,
|
|
1463
|
+
file,
|
|
1464
|
+
description,
|
|
1465
|
+
effect,
|
|
1466
|
+
httpPath,
|
|
1467
|
+
webmcp,
|
|
1468
|
+
inputSchema
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Generate `virtual:pracht/capabilities` — the browser-side `callCapability`
|
|
1473
|
+
* helper plus the endpoint map for http-exposed capabilities. Side-effect
|
|
1474
|
+
* free, so it costs zero bytes unless application code imports it.
|
|
1475
|
+
*
|
|
1476
|
+
* After every call settles, the helper announces itself on
|
|
1477
|
+
* CAPABILITY_SETTLED_EVENT with the capability's effect class; the framework
|
|
1478
|
+
* runtime revalidates route data for successful non-`read` calls (opt out
|
|
1479
|
+
* per call via `{ revalidate: false }`).
|
|
1480
|
+
*/
|
|
1481
|
+
function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions = {}) {
|
|
1482
|
+
const capabilities = extractCapabilities(options, buildOptions.root);
|
|
1483
|
+
const endpoints = {};
|
|
1484
|
+
for (const capability of capabilities) if (capability.httpPath) endpoints[capability.name] = {
|
|
1485
|
+
method: "POST",
|
|
1486
|
+
path: capability.httpPath,
|
|
1487
|
+
effect: capability.effect
|
|
1488
|
+
};
|
|
1489
|
+
return [
|
|
1490
|
+
"// Generated by @pracht/vite-plugin from the app manifest capability registrations.",
|
|
1491
|
+
"// Contains only http-exposed capability names, endpoints, and effects —",
|
|
1492
|
+
"// capability modules themselves are server-only and never reach the client graph.",
|
|
1493
|
+
`const endpoints = ${JSON.stringify(endpoints)};`,
|
|
1494
|
+
"",
|
|
1495
|
+
"export const capabilityEndpoints = endpoints;",
|
|
1496
|
+
"",
|
|
1497
|
+
"async function dispatchCapability(endpoint, input, opts) {",
|
|
1498
|
+
" let response;",
|
|
1499
|
+
" try {",
|
|
1500
|
+
" const headers = new Headers(opts && opts.headers);",
|
|
1501
|
+
" headers.set(\"content-type\", \"application/json\");",
|
|
1502
|
+
" if (opts && opts.confirm) {",
|
|
1503
|
+
` headers.set(${JSON.stringify(CONFIRMATION_HEADER)}, opts.confirm);`,
|
|
1504
|
+
" }",
|
|
1505
|
+
" response = await fetch(endpoint.path, {",
|
|
1506
|
+
" method: endpoint.method,",
|
|
1507
|
+
" headers,",
|
|
1508
|
+
" body: JSON.stringify(input === undefined ? {} : input),",
|
|
1509
|
+
" credentials: \"same-origin\",",
|
|
1510
|
+
" signal: opts && opts.signal,",
|
|
1511
|
+
" });",
|
|
1512
|
+
" } catch (error) {",
|
|
1513
|
+
" return {",
|
|
1514
|
+
" ok: false,",
|
|
1515
|
+
" error: { code: \"network_error\", message: String((error && error.message) || error) },",
|
|
1516
|
+
" };",
|
|
1517
|
+
" }",
|
|
1518
|
+
" try {",
|
|
1519
|
+
" return await response.json();",
|
|
1520
|
+
" } catch {",
|
|
1521
|
+
" return {",
|
|
1522
|
+
" ok: false,",
|
|
1523
|
+
" error: {",
|
|
1524
|
+
" code: \"invalid_response\",",
|
|
1525
|
+
" message: `Capability endpoint returned a non-JSON response (status ${response.status}).`,",
|
|
1526
|
+
" },",
|
|
1527
|
+
" };",
|
|
1528
|
+
" }",
|
|
1529
|
+
"}",
|
|
1530
|
+
"",
|
|
1531
|
+
"export async function callCapability(name, input, opts) {",
|
|
1532
|
+
" const endpoint = endpoints[name];",
|
|
1533
|
+
" if (!endpoint) {",
|
|
1534
|
+
" return {",
|
|
1535
|
+
" ok: false,",
|
|
1536
|
+
" error: {",
|
|
1537
|
+
" code: \"unknown_capability\",",
|
|
1538
|
+
" message: `No HTTP-exposed capability named \"${name}\" is registered.`,",
|
|
1539
|
+
" },",
|
|
1540
|
+
" };",
|
|
1541
|
+
" }",
|
|
1542
|
+
" const result = await dispatchCapability(endpoint, input, opts);",
|
|
1543
|
+
" // Announce the settled call so the route runtime can revalidate after",
|
|
1544
|
+
" // successful non-read effects. Best-effort — never breaks the call.",
|
|
1545
|
+
" try {",
|
|
1546
|
+
" if (typeof window !== \"undefined\") {",
|
|
1547
|
+
` window.dispatchEvent(new CustomEvent(${JSON.stringify(CAPABILITY_SETTLED_EVENT)}, {`,
|
|
1548
|
+
" detail: {",
|
|
1549
|
+
" name,",
|
|
1550
|
+
" effect: endpoint.effect,",
|
|
1551
|
+
" ok: result && result.ok === true,",
|
|
1552
|
+
" revalidate: opts && opts.revalidate === false ? false : undefined,",
|
|
1553
|
+
" },",
|
|
1554
|
+
" }));",
|
|
1555
|
+
" }",
|
|
1556
|
+
" } catch {}",
|
|
1557
|
+
" return result;",
|
|
1558
|
+
"}",
|
|
1559
|
+
""
|
|
1560
|
+
].join("\n");
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Generate `virtual:pracht/webmcp` — the disposable WebMCP registration shim.
|
|
1564
|
+
* One page tool per `expose.webmcp` capability; `execute` dispatches through
|
|
1565
|
+
* `callCapability`, so the user's session authenticates the call and all
|
|
1566
|
+
* validation/middleware/policy stays server-side. Each dispatch carries the
|
|
1567
|
+
* transport marker header so audit events can attribute it to WebMCP.
|
|
1568
|
+
*
|
|
1569
|
+
* Targets the Chrome origin-trial API: `document.modelContext.registerTool()`
|
|
1570
|
+
* (Chrome 150+; `navigator.modelContext` is the deprecated pre-150 location
|
|
1571
|
+
* and is kept as a fallback). No-ops silently when the API is absent.
|
|
1572
|
+
*/
|
|
1573
|
+
function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
|
|
1574
|
+
const tools = extractCapabilities(options, buildOptions.root).filter((capability) => capability.webmcp).map((capability) => ({
|
|
1575
|
+
name: capability.name,
|
|
1576
|
+
description: capability.description,
|
|
1577
|
+
effect: capability.effect,
|
|
1578
|
+
inputSchema: capability.inputSchema
|
|
1579
|
+
}));
|
|
1580
|
+
return [
|
|
1581
|
+
"// Generated by @pracht/vite-plugin — WebMCP page-tool registration shim.",
|
|
1582
|
+
"import { callCapability } from \"virtual:pracht/capabilities\";",
|
|
1583
|
+
"",
|
|
1584
|
+
`const tools = ${JSON.stringify(tools)};`,
|
|
1585
|
+
`const transportHeaders = { ${JSON.stringify(CAPABILITY_TRANSPORT_HEADER)}: "webmcp" };`,
|
|
1586
|
+
"",
|
|
1587
|
+
"export function registerPrachtWebmcpTools() {",
|
|
1588
|
+
" const modelContext =",
|
|
1589
|
+
" (typeof document !== \"undefined\" && document.modelContext) ||",
|
|
1590
|
+
" (typeof navigator !== \"undefined\" && navigator.modelContext) ||",
|
|
1591
|
+
" null;",
|
|
1592
|
+
" if (!modelContext || typeof modelContext.registerTool !== \"function\") {",
|
|
1593
|
+
" return false;",
|
|
1594
|
+
" }",
|
|
1595
|
+
" for (const tool of tools) {",
|
|
1596
|
+
" try {",
|
|
1597
|
+
" const registration = modelContext.registerTool({",
|
|
1598
|
+
" name: tool.name,",
|
|
1599
|
+
" description: tool.description,",
|
|
1600
|
+
" inputSchema: tool.inputSchema,",
|
|
1601
|
+
" annotations: { readOnlyHint: tool.effect === \"read\" },",
|
|
1602
|
+
" async execute(input) {",
|
|
1603
|
+
" const result = await callCapability(tool.name, input, { headers: transportHeaders });",
|
|
1604
|
+
" return { content: [{ type: \"text\", text: JSON.stringify(result) }] };",
|
|
1605
|
+
" },",
|
|
1606
|
+
" });",
|
|
1607
|
+
" if (registration && typeof registration.catch === \"function\") {",
|
|
1608
|
+
" registration.catch(() => {});",
|
|
1609
|
+
" }",
|
|
1610
|
+
" } catch {",
|
|
1611
|
+
" // Origin-trial API surface may shift; a failed registration must",
|
|
1612
|
+
" // never break the page.",
|
|
1613
|
+
" }",
|
|
1614
|
+
" }",
|
|
1615
|
+
" return true;",
|
|
1616
|
+
"}",
|
|
1617
|
+
"",
|
|
1618
|
+
"registerPrachtWebmcpTools();",
|
|
1619
|
+
""
|
|
1620
|
+
].join("\n");
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Snippet appended to the client entry / islands bootstrap when at least one
|
|
1624
|
+
* capability opts into WebMCP. Feature-detects before importing so browsers
|
|
1625
|
+
* without the origin trial never pay for the shim chunk.
|
|
1626
|
+
*/
|
|
1627
|
+
function createWebmcpBootstrapSource() {
|
|
1628
|
+
return [
|
|
1629
|
+
"// WebMCP page tools — loaded only when the browser exposes the API.",
|
|
1630
|
+
"if (",
|
|
1631
|
+
" typeof document !== \"undefined\" &&",
|
|
1632
|
+
" (document.modelContext || (typeof navigator !== \"undefined\" && navigator.modelContext))",
|
|
1633
|
+
") {",
|
|
1634
|
+
" import(\"virtual:pracht/webmcp\").catch(() => {});",
|
|
1635
|
+
"}",
|
|
1636
|
+
""
|
|
1637
|
+
];
|
|
1638
|
+
}
|
|
1639
|
+
function hasWebmcpCapabilities(options = {}, root = process.cwd()) {
|
|
1640
|
+
try {
|
|
1641
|
+
return extractCapabilities(options, root).some((capability) => capability.webmcp);
|
|
1642
|
+
} catch {
|
|
1643
|
+
return true;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function isPlainObject(value) {
|
|
1647
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1648
|
+
}
|
|
1649
|
+
//#endregion
|
|
1308
1650
|
//#region src/plugin-codegen.ts
|
|
1309
1651
|
const ROUTE_MODULE_EXTENSIONS = new Set([
|
|
1310
1652
|
".ts",
|
|
@@ -1500,7 +1842,8 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1500
1842
|
" findModuleKey,",
|
|
1501
1843
|
" });",
|
|
1502
1844
|
"}",
|
|
1503
|
-
""
|
|
1845
|
+
"",
|
|
1846
|
+
...hasWebmcpCapabilities(resolved, buildOptions.root) ? createWebmcpBootstrapSource() : []
|
|
1504
1847
|
].join("\n");
|
|
1505
1848
|
}
|
|
1506
1849
|
/**
|
|
@@ -1509,15 +1852,17 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1509
1852
|
* manifest, the router, or the full client runtime: it only scans the DOM
|
|
1510
1853
|
* for island markers and hydrates the islands present on the page.
|
|
1511
1854
|
*/
|
|
1512
|
-
function createPrachtIslandsClientModuleSource(options = {}) {
|
|
1513
|
-
const
|
|
1855
|
+
function createPrachtIslandsClientModuleSource(options = {}, buildOptions = {}) {
|
|
1856
|
+
const resolved = resolveOptions(options);
|
|
1857
|
+
const islandsGlob = `${resolved.islandsDir}/**/*.{ts,tsx,js,jsx}`;
|
|
1514
1858
|
return [
|
|
1515
1859
|
"import { hydrateIslands } from \"@pracht/core/islands-client\";",
|
|
1516
1860
|
"",
|
|
1517
1861
|
`const islandModules = import.meta.glob(${JSON.stringify(islandsGlob)});`,
|
|
1518
1862
|
"",
|
|
1519
1863
|
"hydrateIslands({ modules: islandModules });",
|
|
1520
|
-
""
|
|
1864
|
+
"",
|
|
1865
|
+
...hasWebmcpCapabilities(resolved, buildOptions.root) ? createWebmcpBootstrapSource() : []
|
|
1521
1866
|
].join("\n");
|
|
1522
1867
|
}
|
|
1523
1868
|
function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
@@ -1532,7 +1877,9 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
|
1532
1877
|
jsManifest: {}
|
|
1533
1878
|
};
|
|
1534
1879
|
const adapter = resolved.adapter;
|
|
1535
|
-
const
|
|
1880
|
+
const llmsTxtConfig = resolveLlmsTxtConfig(resolved, buildOptions.root);
|
|
1881
|
+
let prachtImports = adapter?.serverImports ? adapter.serverImports + "\nimport { prerenderApp } from \"@pracht/core/server\";" : "import { resolveApp, resolveApiRoutes, prerenderApp } from \"@pracht/core/server\";";
|
|
1882
|
+
if (llmsTxtConfig) prachtImports += "\nimport { buildLlmsTxt } from \"@pracht/core/server\";";
|
|
1536
1883
|
const appImport = isPagesMode ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`;
|
|
1537
1884
|
const islandsEntryUrl = buildOptions.isBuild ? clientBuild.islandsEntryUrl : ISLANDS_CLIENT_BROWSER_PATH;
|
|
1538
1885
|
const islandsGlob = `${resolved.islandsDir}/**/*.{ts,tsx,js,jsx}`;
|
|
@@ -1563,11 +1910,54 @@ function createPrachtServerModuleSource(options = {}, buildOptions = {}) {
|
|
|
1563
1910
|
`export const prerenderConcurrency = ${JSON.stringify(resolved.prerenderConcurrency)};`,
|
|
1564
1911
|
`export const budgets = ${JSON.stringify(resolved.budgets)};`,
|
|
1565
1912
|
"export { prerenderApp };",
|
|
1913
|
+
...llmsTxtConfig ? [
|
|
1914
|
+
"// llms.txt (https://llmstxt.org) generated from the resolved app graph.",
|
|
1915
|
+
"// `pracht build` writes it to dist/client/llms.txt; the dev SSR",
|
|
1916
|
+
"// middleware serves it at /llms.txt.",
|
|
1917
|
+
`const llmsTxtConfig = ${JSON.stringify(llmsTxtConfig)};`,
|
|
1918
|
+
"export const generateLlmsTxt = () =>",
|
|
1919
|
+
" buildLlmsTxt({ ...llmsTxtConfig, apiRoutes, app: resolvedApp, registry });"
|
|
1920
|
+
] : [],
|
|
1566
1921
|
""
|
|
1567
1922
|
];
|
|
1568
1923
|
if (adapter) source.push(adapter.createServerEntryModule());
|
|
1569
1924
|
return source.join("\n");
|
|
1570
1925
|
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Adapter-neutral app metadata used by development tooling. Keeping this
|
|
1928
|
+
* separate from the server entry avoids evaluating worker-only imports (for
|
|
1929
|
+
* example `cloudflare:workers`) in Vite's Node SSR environment.
|
|
1930
|
+
*/
|
|
1931
|
+
function createPrachtDevModuleSource(options = {}, buildOptions = {}) {
|
|
1932
|
+
const resolved = resolveOptions(options);
|
|
1933
|
+
return [
|
|
1934
|
+
"import { resolveApp } from \"@pracht/core/server\";",
|
|
1935
|
+
resolved.pagesDir ? generatePagesAppInlineSource(resolved, buildOptions.root) : `import { app } from ${JSON.stringify(resolved.appFile)};`,
|
|
1936
|
+
"",
|
|
1937
|
+
createPrachtRegistryModuleSource(resolved),
|
|
1938
|
+
"",
|
|
1939
|
+
"export const resolvedApp = resolveApp(app);",
|
|
1940
|
+
""
|
|
1941
|
+
].join("\n");
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Fill llms.txt title/description from the app's package.json when the user
|
|
1945
|
+
* did not set them explicitly. Returns null when the feature is disabled so
|
|
1946
|
+
* the server module codegen stays byte-for-byte unchanged.
|
|
1947
|
+
*/
|
|
1948
|
+
function resolveLlmsTxtConfig(resolved, root = process.cwd()) {
|
|
1949
|
+
if (!resolved.llmsTxt) return null;
|
|
1950
|
+
let pkg = {};
|
|
1951
|
+
try {
|
|
1952
|
+
pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf-8"));
|
|
1953
|
+
} catch {}
|
|
1954
|
+
const config = { title: resolved.llmsTxt.title ?? (typeof pkg.name === "string" && pkg.name ? pkg.name : "App") };
|
|
1955
|
+
const description = resolved.llmsTxt.description ?? (typeof pkg.description === "string" && pkg.description ? pkg.description : void 0);
|
|
1956
|
+
if (description) config.description = description;
|
|
1957
|
+
if (resolved.llmsTxt.origin) config.origin = resolved.llmsTxt.origin;
|
|
1958
|
+
if (resolved.llmsTxt.include) config.include = resolved.llmsTxt.include;
|
|
1959
|
+
return config;
|
|
1960
|
+
}
|
|
1571
1961
|
function createApplyRouteLoaderHintsSource() {
|
|
1572
1962
|
return [
|
|
1573
1963
|
"function applyRouteLoaderHints(resolvedApp, routeLoaderHints) {",
|
|
@@ -1618,6 +2008,7 @@ function createPrachtRegistryModuleSource(options = {}) {
|
|
|
1618
2008
|
`export const middlewareModules = import.meta.glob(${JSON.stringify(`${resolved.middlewareDir}/**/*.{ts,tsx,js,jsx}`)});`,
|
|
1619
2009
|
`export const apiModules = import.meta.glob(${JSON.stringify(`${resolved.apiDir}/**/*.{ts,js,tsx,jsx}`)});`,
|
|
1620
2010
|
`export const dataModules = import.meta.glob(${JSON.stringify(`${resolved.serverDir}/**/*.{ts,js,tsx,jsx}`)});`,
|
|
2011
|
+
`export const capabilityModules = import.meta.glob(${JSON.stringify(`${resolved.capabilitiesDir}/**/*.{ts,js,tsx,jsx}`)});`,
|
|
1621
2012
|
"",
|
|
1622
2013
|
"export const registry = {",
|
|
1623
2014
|
" routeModules,",
|
|
@@ -1625,6 +2016,7 @@ function createPrachtRegistryModuleSource(options = {}) {
|
|
|
1625
2016
|
" middlewareModules,",
|
|
1626
2017
|
" apiModules,",
|
|
1627
2018
|
" dataModules,",
|
|
2019
|
+
" capabilityModules,",
|
|
1628
2020
|
"};"
|
|
1629
2021
|
].join("\n");
|
|
1630
2022
|
}
|
|
@@ -1653,10 +2045,16 @@ function generatePagesAppInlineSource(options, root = process.cwd()) {
|
|
|
1653
2045
|
//#region src/plugin-dev-ssr.ts
|
|
1654
2046
|
const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
|
|
1655
2047
|
const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
|
|
2048
|
+
const CSS_MODULE_URL_RE = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
|
|
1656
2049
|
const DEVTOOLS_JSON_PATH = "/_pracht.json";
|
|
2050
|
+
const LLMS_TXT_PATH = "/llms.txt";
|
|
1657
2051
|
function createDevSSRMiddleware(server, options = {}) {
|
|
1658
2052
|
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
1659
2053
|
let warnedDevtoolsCollision = false;
|
|
2054
|
+
let warnedLlmsTxtCollision = false;
|
|
2055
|
+
if (options.llmsTxt && typeof server.config.publicDir === "string") {
|
|
2056
|
+
if (existsSync(join(server.config.publicDir, "llms.txt"))) server.config.logger.warn("[pracht] Both public/llms.txt and the pracht({ llmsTxt }) option are present. Dev serves the static public/llms.txt, but \"pracht build\" overwrites it with the generated content. Remove one to avoid a dev/production mismatch.");
|
|
2057
|
+
}
|
|
1660
2058
|
return async (req, res, next) => {
|
|
1661
2059
|
const url = req.url ?? "/";
|
|
1662
2060
|
const requestUrl = new URL(url, "http://localhost");
|
|
@@ -1681,6 +2079,17 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
1681
2079
|
});
|
|
1682
2080
|
return;
|
|
1683
2081
|
}
|
|
2082
|
+
if (options.llmsTxt && requestUrl.pathname === "/llms.txt" && BODYLESS_METHODS.has((req.method ?? "GET").toUpperCase()) && typeof serverMod.generateLlmsTxt === "function") {
|
|
2083
|
+
if (!warnedLlmsTxtCollision && matchesResolvedRoute("/llms.txt", routeMatchers)) {
|
|
2084
|
+
warnedLlmsTxtCollision = true;
|
|
2085
|
+
server.config.logger.warn(`[pracht] An app route matches ${LLMS_TXT_PATH}, which is reserved by the pracht({ llmsTxt }) option. The generated llms.txt wins; disable the option to serve the app route instead.`);
|
|
2086
|
+
}
|
|
2087
|
+
const llmsTxt = await serverMod.generateLlmsTxt();
|
|
2088
|
+
res.statusCode = 200;
|
|
2089
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2090
|
+
res.end(llmsTxt);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
1684
2093
|
if (shouldBypassDevSSR(requestUrl, req, routeMatchers)) return next();
|
|
1685
2094
|
if (isDevNotFoundRequest(requestUrl, req, routeMatchers)) return serveDevNotFound(server, res, next, url, requestUrl.pathname, routeMatchers);
|
|
1686
2095
|
let webRequest;
|
|
@@ -1704,7 +2113,8 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
1704
2113
|
apiRoutes: serverMod.apiRoutes,
|
|
1705
2114
|
timings
|
|
1706
2115
|
});
|
|
1707
|
-
|
|
2116
|
+
const responseContentType = response.headers.get("content-type") ?? "";
|
|
2117
|
+
if (response.status === 404 && !responseContentType.includes("application/json") && !routeMatchers.app?.notFound) return next();
|
|
1708
2118
|
const contentType = response.headers.get("content-type") ?? "text/html";
|
|
1709
2119
|
let body = await response.text();
|
|
1710
2120
|
if (contentType.includes("text/html")) body = await server.transformIndexHtml(url, body);
|
|
@@ -1721,15 +2131,166 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
1721
2131
|
};
|
|
1722
2132
|
}
|
|
1723
2133
|
/**
|
|
2134
|
+
* Build the development equivalent of the production CSS manifest for the
|
|
2135
|
+
* current route. Vite turns CSS imports into client-side style injection by
|
|
2136
|
+
* default; resolving the same imports through the active server environment
|
|
2137
|
+
* graphs lets pracht put real stylesheet links in the initial document and
|
|
2138
|
+
* avoid a first-paint FOUC.
|
|
2139
|
+
*/
|
|
2140
|
+
async function createDevCssManifest(server, options) {
|
|
2141
|
+
const route = options.matchAppRoute(options.app, options.pathname)?.route ?? options.app.notFound;
|
|
2142
|
+
if (!route) return {};
|
|
2143
|
+
const manifest = {};
|
|
2144
|
+
const modules = [...route.shellFile ? [{
|
|
2145
|
+
file: route.shellFile,
|
|
2146
|
+
registry: options.registry.shellModules
|
|
2147
|
+
}] : [], {
|
|
2148
|
+
file: route.file,
|
|
2149
|
+
registry: options.registry.routeModules
|
|
2150
|
+
}];
|
|
2151
|
+
const results = await Promise.all(modules.map(async ({ file, registry }) => {
|
|
2152
|
+
if (!registry) return {
|
|
2153
|
+
file,
|
|
2154
|
+
urls: []
|
|
2155
|
+
};
|
|
2156
|
+
const moduleKey = findRegistryModuleKey(registry, file);
|
|
2157
|
+
if (!moduleKey) return {
|
|
2158
|
+
file,
|
|
2159
|
+
urls: []
|
|
2160
|
+
};
|
|
2161
|
+
const entries = await Promise.all(Object.values(server.environments).map((environment) => environment.moduleGraph.getModuleByUrl(moduleKey)));
|
|
2162
|
+
return {
|
|
2163
|
+
file,
|
|
2164
|
+
urls: [...new Set(entries.flatMap((entry) => collectDevCssUrls(entry)))]
|
|
2165
|
+
};
|
|
2166
|
+
}));
|
|
2167
|
+
for (const { file, urls } of results) if (urls.length > 0) manifest[file] = urls;
|
|
2168
|
+
return manifest;
|
|
2169
|
+
}
|
|
2170
|
+
function findRegistryModuleKey(modules, file) {
|
|
2171
|
+
if (!modules) return void 0;
|
|
2172
|
+
if (file in modules) return file;
|
|
2173
|
+
const suffix = `/${file.split("?")[0].replace(/\\/g, "/").replace(/^\.?\//, "")}`;
|
|
2174
|
+
return Object.keys(modules).find((key) => key.split("?")[0].replace(/\\/g, "/").endsWith(suffix));
|
|
2175
|
+
}
|
|
2176
|
+
function collectDevCssUrls(entry) {
|
|
2177
|
+
if (!entry) return [];
|
|
2178
|
+
const urls = /* @__PURE__ */ new Set();
|
|
2179
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2180
|
+
const pending = [entry];
|
|
2181
|
+
while (pending.length > 0) {
|
|
2182
|
+
const module = pending.pop();
|
|
2183
|
+
if (visited.has(module)) continue;
|
|
2184
|
+
visited.add(module);
|
|
2185
|
+
if ((module.type === "css" || CSS_MODULE_URL_RE.test(module.url)) && !/[?&](?:inline|raw|url)(?:[=&]|$)/.test(module.url)) urls.add(module.url);
|
|
2186
|
+
pending.push(...[...module.importedModules].reverse());
|
|
2187
|
+
}
|
|
2188
|
+
return [...urls];
|
|
2189
|
+
}
|
|
2190
|
+
function injectDevCssLinks(html, manifest) {
|
|
2191
|
+
if (!html.includes("</head>")) return html;
|
|
2192
|
+
const tags = [...new Set(Object.values(manifest).flat())].map((url) => escapeHtmlAttribute(url)).filter((escapedUrl) => !html.includes(`href="${escapedUrl}"`)).map((escapedUrl) => `<link rel="stylesheet" href="${escapedUrl}">`);
|
|
2193
|
+
if (tags.length === 0) return html;
|
|
2194
|
+
return html.replace("</head>", ` ${tags.join("\n ")}\n </head>`);
|
|
2195
|
+
}
|
|
2196
|
+
async function injectDevCssForPath(server, path, html) {
|
|
2197
|
+
return injectDevCssLinks(html, await createDevCssManifest(server, await resolveDevCssContextForPath(server, path)));
|
|
2198
|
+
}
|
|
2199
|
+
async function resolveDevCssContextForPath(server, path) {
|
|
2200
|
+
const [framework, serverMod] = await Promise.all([server.ssrLoadModule("@pracht/core/server"), server.ssrLoadModule(PRACHT_DEV_MODULE_ID)]);
|
|
2201
|
+
const pathname = new URL(path, "http://localhost").pathname;
|
|
2202
|
+
return {
|
|
2203
|
+
app: serverMod.resolvedApp,
|
|
2204
|
+
matchAppRoute: framework.matchAppRoute,
|
|
2205
|
+
pathname,
|
|
2206
|
+
registry: serverMod.registry
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Adapter-owned dev servers (for example Cloudflare's worker runtime) bypass
|
|
2211
|
+
* Vite's HTML transform hooks. Install this before the adapter middleware so
|
|
2212
|
+
* document responses still receive the same parser-blocking stylesheet links.
|
|
2213
|
+
*/
|
|
2214
|
+
function createDevCssInjectionMiddleware(server) {
|
|
2215
|
+
let warned = false;
|
|
2216
|
+
return (req, res, next) => {
|
|
2217
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2218
|
+
const accept = readRequestHeader(req.headers.accept).toLowerCase();
|
|
2219
|
+
if (method !== "GET" || !accept.includes("text/html")) {
|
|
2220
|
+
next();
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
const contextPromise = resolveDevCssContextForPath(server, req.url ?? "/").catch((error) => {
|
|
2224
|
+
if (!warned) {
|
|
2225
|
+
warned = true;
|
|
2226
|
+
server.config.logger.warn(`[pracht] Could not discover development stylesheets: ${error instanceof Error ? error.message : String(error)}`);
|
|
2227
|
+
}
|
|
2228
|
+
return null;
|
|
2229
|
+
});
|
|
2230
|
+
const chunks = [];
|
|
2231
|
+
const originalEnd = res.end.bind(res);
|
|
2232
|
+
const originalWriteHead = res.writeHead.bind(res);
|
|
2233
|
+
res.writeHead = ((statusCode, ...args) => {
|
|
2234
|
+
res.removeHeader("content-length");
|
|
2235
|
+
return Reflect.apply(originalWriteHead, res, [statusCode, ...args.map(stripContentLengthHeader)]);
|
|
2236
|
+
});
|
|
2237
|
+
res.write = ((chunk, encodingOrCallback, callback) => {
|
|
2238
|
+
chunks.push(toBuffer(chunk, encodingOrCallback));
|
|
2239
|
+
(typeof encodingOrCallback === "function" ? encodingOrCallback : typeof callback === "function" ? callback : void 0)?.();
|
|
2240
|
+
return true;
|
|
2241
|
+
});
|
|
2242
|
+
res.end = ((chunk, encodingOrCallback, callback) => {
|
|
2243
|
+
if (chunk != null) chunks.push(toBuffer(chunk, encodingOrCallback));
|
|
2244
|
+
const done = typeof encodingOrCallback === "function" ? encodingOrCallback : typeof callback === "function" ? callback : void 0;
|
|
2245
|
+
(async () => {
|
|
2246
|
+
const body = Buffer.concat(chunks);
|
|
2247
|
+
if (!String(res.getHeader("content-type") ?? "").includes("text/html")) {
|
|
2248
|
+
originalEnd(body, done);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
try {
|
|
2252
|
+
const context = await contextPromise;
|
|
2253
|
+
const manifest = context ? await createDevCssManifest(server, context) : null;
|
|
2254
|
+
originalEnd(manifest ? injectDevCssLinks(body.toString("utf-8"), manifest) : body.toString("utf-8"), done);
|
|
2255
|
+
} catch {
|
|
2256
|
+
originalEnd(body, done);
|
|
2257
|
+
}
|
|
2258
|
+
})();
|
|
2259
|
+
return res;
|
|
2260
|
+
});
|
|
2261
|
+
next();
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
function toBuffer(chunk, encoding) {
|
|
2265
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
2266
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk);
|
|
2267
|
+
return Buffer.from(String(chunk), typeof encoding === "string" ? encoding : void 0);
|
|
2268
|
+
}
|
|
2269
|
+
function stripContentLengthHeader(value) {
|
|
2270
|
+
if (Array.isArray(value)) {
|
|
2271
|
+
const headers = [];
|
|
2272
|
+
for (let index = 0; index < value.length; index += 2) if (String(value[index]).toLowerCase() !== "content-length") headers.push(value[index], value[index + 1]);
|
|
2273
|
+
return headers;
|
|
2274
|
+
}
|
|
2275
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
2276
|
+
return value;
|
|
2277
|
+
}
|
|
2278
|
+
function escapeHtmlAttribute(value) {
|
|
2279
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
1724
2282
|
* Serve the dev-only `/_pracht` devtools page (or `/_pracht.json`) built from
|
|
1725
2283
|
* the same resolved app graph that `pracht inspect` reports.
|
|
1726
2284
|
*/
|
|
1727
2285
|
async function serveDevtools(server, res, options) {
|
|
1728
2286
|
const devtools = await server.ssrLoadModule("@pracht/core/devtools");
|
|
2287
|
+
const capabilityModules = (await server.ssrLoadModule(PRACHT_SERVER_MODULE_ID)).registry?.capabilityModules;
|
|
1729
2288
|
const graph = await devtools.buildAppGraph({
|
|
1730
2289
|
apiRoutes: options.apiRoutes,
|
|
1731
2290
|
app: options.app,
|
|
1732
|
-
loadModule: (file) =>
|
|
2291
|
+
loadModule: async (file) => {
|
|
2292
|
+
return await resolveRegistryModule(capabilityModules, file) ?? server.ssrLoadModule(file);
|
|
2293
|
+
},
|
|
1733
2294
|
readSource: (file) => readFileSync(resolve(server.config.root, `.${file}`), "utf-8")
|
|
1734
2295
|
});
|
|
1735
2296
|
if (options.wantsJson) {
|
|
@@ -1933,9 +2494,12 @@ function pracht(options = {}) {
|
|
|
1933
2494
|
const isSSRBuild = env.isSsrBuild;
|
|
1934
2495
|
const configRoot = _config.root ?? process.cwd();
|
|
1935
2496
|
const wantsIslandsEntry = env.command === "build" && !isSSRBuild && existsSync(resolveConfigPath(configRoot, resolved.islandsDir));
|
|
2497
|
+
const envDir = _config.envDir ? resolve(configRoot, _config.envDir) : configRoot;
|
|
2498
|
+
const publicEnvDefine = JSON.stringify(loadEnv(env.mode, envDir, PUBLIC_ENV_PREFIX));
|
|
1936
2499
|
return {
|
|
1937
2500
|
appType: "custom",
|
|
1938
2501
|
envPrefix: ["VITE_", PUBLIC_ENV_PREFIX],
|
|
2502
|
+
define: { __PRACHT_PUBLIC_ENV__: publicEnvDefine },
|
|
1939
2503
|
...isSSRBuild ? {} : { build: { rollupOptions: {
|
|
1940
2504
|
...wantsIslandsEntry ? { input: [PRACHT_ISLANDS_CLIENT_MODULE_ID] } : {},
|
|
1941
2505
|
output: { manualChunks(id) {
|
|
@@ -1959,17 +2523,23 @@ function pracht(options = {}) {
|
|
|
1959
2523
|
resolveId(id, importer, resolveIdOptions) {
|
|
1960
2524
|
if (isIslandsClientModule(id)) return PRACHT_ISLANDS_CLIENT_MODULE_ID;
|
|
1961
2525
|
if (isClientModule(id)) return PRACHT_CLIENT_MODULE_ID;
|
|
2526
|
+
if (isDevModule(id)) return PRACHT_DEV_MODULE_ID;
|
|
1962
2527
|
if (isServerModule(id)) return PRACHT_SERVER_MODULE_ID;
|
|
2528
|
+
if (isCapabilitiesModule(id)) return PRACHT_CAPABILITIES_MODULE_ID;
|
|
2529
|
+
if (isWebmcpModule(id)) return PRACHT_WEBMCP_MODULE_ID;
|
|
1963
2530
|
if (id === "@pracht/core/env/server" && !resolveIdOptions?.ssr && !resolveIdOptions?.scan) throw new Error(`[pracht] ${JSON.stringify(SERVER_ENV_MODULE_ID)} was imported by ${JSON.stringify(importer ?? "unknown module")} in client code. serverEnv is server-only — read it inside loaders, middleware, or API routes, or use publicEnv (PRACHT_PUBLIC_-prefixed variables) from "@pracht/core" instead.`);
|
|
1964
2531
|
return null;
|
|
1965
2532
|
},
|
|
1966
2533
|
load(id) {
|
|
1967
|
-
if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved);
|
|
2534
|
+
if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved, { root });
|
|
1968
2535
|
if (isClientModule(id)) return createPrachtClientModuleSource(resolved, { root });
|
|
2536
|
+
if (isDevModule(id)) return createPrachtDevModuleSource(resolved, { root });
|
|
1969
2537
|
if (isServerModule(id)) return createPrachtServerModuleSource(resolved, {
|
|
1970
2538
|
root,
|
|
1971
2539
|
isBuild
|
|
1972
2540
|
});
|
|
2541
|
+
if (isCapabilitiesModule(id)) return createPrachtCapabilitiesClientModuleSource(resolved, { root });
|
|
2542
|
+
if (isWebmcpModule(id)) return createPrachtWebmcpModuleSource(resolved, { root });
|
|
1973
2543
|
return null;
|
|
1974
2544
|
},
|
|
1975
2545
|
transform(code, id) {
|
|
@@ -1984,11 +2554,25 @@ function pracht(options = {}) {
|
|
|
1984
2554
|
},
|
|
1985
2555
|
configureServer(server) {
|
|
1986
2556
|
if (isPagesMode) watchPagesDirectory(server, resolved, root);
|
|
1987
|
-
if (resolved.adapter.ownsDevServer)
|
|
2557
|
+
if (resolved.adapter.ownsDevServer) {
|
|
2558
|
+
server.middlewares.use(createDevCssInjectionMiddleware(server));
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
1988
2561
|
return () => {
|
|
1989
|
-
server.middlewares.use(createDevSSRMiddleware(server, {
|
|
2562
|
+
server.middlewares.use(createDevSSRMiddleware(server, {
|
|
2563
|
+
llmsTxt: !!resolved.llmsTxt,
|
|
2564
|
+
maxBodySize: resolved.maxBodySize
|
|
2565
|
+
}));
|
|
1990
2566
|
};
|
|
1991
2567
|
},
|
|
2568
|
+
async transformIndexHtml(html, context) {
|
|
2569
|
+
if (isBuild || !context.server || !html.includes("</head>")) return html;
|
|
2570
|
+
try {
|
|
2571
|
+
return await injectDevCssForPath(context.server, context.path, html);
|
|
2572
|
+
} catch {
|
|
2573
|
+
return html;
|
|
2574
|
+
}
|
|
2575
|
+
},
|
|
1992
2576
|
handleHotUpdate({ file, server }) {
|
|
1993
2577
|
const serverRoot = toPosixPath(server.config.root);
|
|
1994
2578
|
const normalizedFile = toPosixPath(file);
|
|
@@ -2008,10 +2592,13 @@ function pracht(options = {}) {
|
|
|
2008
2592
|
resolved.middlewareDir,
|
|
2009
2593
|
resolved.apiDir,
|
|
2010
2594
|
resolved.serverDir,
|
|
2011
|
-
resolved.islandsDir
|
|
2595
|
+
resolved.islandsDir,
|
|
2596
|
+
resolved.capabilitiesDir
|
|
2012
2597
|
].some((dir) => relative.startsWith(dir))) {
|
|
2013
2598
|
const serverMod = server.moduleGraph.getModuleById(PRACHT_SERVER_MODULE_ID);
|
|
2014
2599
|
if (serverMod) server.moduleGraph.invalidateModule(serverMod);
|
|
2600
|
+
const devMod = server.moduleGraph.getModuleById(PRACHT_DEV_MODULE_ID);
|
|
2601
|
+
if (devMod) server.moduleGraph.invalidateModule(devMod);
|
|
2015
2602
|
if (relative.startsWith(resolved.routesDir)) {
|
|
2016
2603
|
const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
|
|
2017
2604
|
if (clientMod) server.moduleGraph.invalidateModule(clientMod);
|
|
@@ -2020,6 +2607,15 @@ function pracht(options = {}) {
|
|
|
2020
2607
|
const islandsMod = server.moduleGraph.getModuleById(PRACHT_ISLANDS_CLIENT_MODULE_ID);
|
|
2021
2608
|
if (islandsMod) server.moduleGraph.invalidateModule(islandsMod);
|
|
2022
2609
|
}
|
|
2610
|
+
if (relative.startsWith(resolved.capabilitiesDir)) for (const moduleId of [
|
|
2611
|
+
PRACHT_CAPABILITIES_MODULE_ID,
|
|
2612
|
+
PRACHT_WEBMCP_MODULE_ID,
|
|
2613
|
+
PRACHT_CLIENT_MODULE_ID,
|
|
2614
|
+
PRACHT_ISLANDS_CLIENT_MODULE_ID
|
|
2615
|
+
]) {
|
|
2616
|
+
const capabilityMod = server.moduleGraph.getModuleById(moduleId);
|
|
2617
|
+
if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
|
|
2618
|
+
}
|
|
2023
2619
|
}
|
|
2024
2620
|
}
|
|
2025
2621
|
};
|
|
@@ -2112,7 +2708,8 @@ function createPrachtOptimizeDepsEntries(resolved) {
|
|
|
2112
2708
|
`${toOptimizeDepsEntry(resolved.middlewareDir)}/**/*.${scriptExtensions}`,
|
|
2113
2709
|
`${toOptimizeDepsEntry(resolved.apiDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
2114
2710
|
`${toOptimizeDepsEntry(resolved.serverDir)}/**/*.{ts,js,tsx,jsx}`,
|
|
2115
|
-
`${toOptimizeDepsEntry(resolved.islandsDir)}/**/*.${scriptExtensions}
|
|
2711
|
+
`${toOptimizeDepsEntry(resolved.islandsDir)}/**/*.${scriptExtensions}`,
|
|
2712
|
+
`${toOptimizeDepsEntry(resolved.capabilitiesDir)}/**/*.{ts,js,tsx,jsx}`
|
|
2116
2713
|
];
|
|
2117
2714
|
return [...new Set(entries.filter(Boolean))];
|
|
2118
2715
|
}
|
|
@@ -2140,8 +2737,10 @@ function watchPagesDirectory(server, resolved, root) {
|
|
|
2140
2737
|
function invalidateVirtualModules(server) {
|
|
2141
2738
|
const clientMod = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
|
|
2142
2739
|
const serverMod = server.moduleGraph.getModuleById(PRACHT_SERVER_MODULE_ID);
|
|
2740
|
+
const devMod = server.moduleGraph.getModuleById(PRACHT_DEV_MODULE_ID);
|
|
2143
2741
|
if (clientMod) server.moduleGraph.invalidateModule(clientMod);
|
|
2144
2742
|
if (serverMod) server.moduleGraph.invalidateModule(serverMod);
|
|
2743
|
+
if (devMod) server.moduleGraph.invalidateModule(devMod);
|
|
2145
2744
|
}
|
|
2146
2745
|
const ROUTE_FILE_EXTENSIONS = new Set([
|
|
2147
2746
|
".ts",
|
|
@@ -2180,4 +2779,4 @@ function withTrailingSep(p) {
|
|
|
2180
2779
|
return p.endsWith("/") ? p : `${p}/`;
|
|
2181
2780
|
}
|
|
2182
2781
|
//#endregion
|
|
2183
|
-
export { PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PUBLIC_ENV_PREFIX, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, formatEnvLeakError, pracht, scanCodeForEnvLeaks };
|
|
2782
|
+
export { PRACHT_CAPABILITIES_MODULE_ID, PRACHT_CLIENT_MODULE_ID, PRACHT_ISLANDS_CLIENT_MODULE_ID, PRACHT_SERVER_MODULE_ID, PRACHT_WEBMCP_MODULE_ID, PUBLIC_ENV_PREFIX, VITE_BUILTIN_ENV_VARS, createEnvSafetyPlugin, createPrachtCapabilitiesClientModuleSource, createPrachtClientModuleSource, createPrachtIslandsClientModuleSource, createPrachtRegistryModuleSource, createPrachtServerModuleSource, createPrachtWebmcpModuleSource, extractCapabilities, formatEnvLeakError, pracht, scanCodeForEnvLeaks };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pracht/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Vite plugin for Pracht apps with virtual modules, dev SSR, prerendering, route inspection, and multi-adapter builds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pracht",
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@preact/preset-vite": "^2.10.5",
|
|
46
46
|
"@prefresh/vite": "^2.0.0",
|
|
47
|
-
"@pracht/adapter-node": "0.3.
|
|
48
|
-
"@pracht/core": "0.
|
|
47
|
+
"@pracht/adapter-node": "0.3.4",
|
|
48
|
+
"@pracht/core": "0.11.1",
|
|
49
|
+
"@pracht/capabilities": "0.1.0",
|
|
49
50
|
"@pracht/preact-ssr-precompile": "0.1.2"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|