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