@pracht/vite-plugin 0.10.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,6 +135,21 @@ 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;
|
|
138
153
|
}
|
|
139
154
|
/**
|
|
140
155
|
* Optional client-router features, compiled out of the client bundle when
|
|
@@ -157,6 +172,16 @@ interface PrachtPluginOptions {
|
|
|
157
172
|
* compiled out of the client bundle. See {@link PrachtClientOptions}.
|
|
158
173
|
*/
|
|
159
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;
|
|
160
185
|
appFile?: string;
|
|
161
186
|
routesDir?: string;
|
|
162
187
|
shellsDir?: string;
|
|
@@ -218,6 +243,50 @@ interface PrachtPluginOptions {
|
|
|
218
243
|
llmsTxt?: false | PrachtLlmsTxtOptions;
|
|
219
244
|
}
|
|
220
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
|
|
221
290
|
//#region src/plugin-codegen.d.ts
|
|
222
291
|
declare function createPrachtClientModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
223
292
|
root?: string;
|
|
@@ -244,10 +313,12 @@ interface ExtractedCapability {
|
|
|
244
313
|
name: string;
|
|
245
314
|
/** Manifest-relative module path, e.g. "./capabilities/notes-search.ts". */
|
|
246
315
|
file: string;
|
|
316
|
+
title: string;
|
|
247
317
|
description: string;
|
|
248
318
|
effect: string | null;
|
|
249
319
|
httpPath: string | null;
|
|
250
320
|
webmcp: boolean;
|
|
321
|
+
webmcpUntrustedContent: boolean;
|
|
251
322
|
inputSchema: Record<string, unknown> | null;
|
|
252
323
|
}
|
|
253
324
|
/**
|
|
@@ -276,9 +347,17 @@ declare function createPrachtCapabilitiesClientModuleSource(options?: PrachtPlug
|
|
|
276
347
|
* validation/middleware/policy stays server-side. Each dispatch carries the
|
|
277
348
|
* transport marker header so audit events can attribute it to WebMCP.
|
|
278
349
|
*
|
|
279
|
-
* Targets the
|
|
280
|
-
* (
|
|
281
|
-
*
|
|
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.
|
|
282
361
|
*/
|
|
283
362
|
declare function createPrachtWebmcpModuleSource(options?: PrachtPluginOptions, buildOptions?: {
|
|
284
363
|
root?: string;
|
|
@@ -287,4 +366,4 @@ declare function createPrachtWebmcpModuleSource(options?: PrachtPluginOptions, b
|
|
|
287
366
|
//#region src/index.d.ts
|
|
288
367
|
declare function pracht(options?: PrachtPluginOptions): Plugin[];
|
|
289
368
|
//#endregion
|
|
290
|
-
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 PrachtClientOptions, 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
|
|
@@ -1354,6 +1587,7 @@ function createDefaultNodeAdapter() {
|
|
|
1354
1587
|
const CLIENT_FEATURE_DEFAULTS = { prefetch: true };
|
|
1355
1588
|
const DEFAULTS = {
|
|
1356
1589
|
client: CLIENT_FEATURE_DEFAULTS,
|
|
1590
|
+
vendorChunk: true,
|
|
1357
1591
|
appFile: "/src/routes.ts",
|
|
1358
1592
|
middlewareDir: "/src/middleware",
|
|
1359
1593
|
routesDir: "/src/routes",
|
|
@@ -1380,6 +1614,7 @@ function resolveOptions(options) {
|
|
|
1380
1614
|
};
|
|
1381
1615
|
if (resolved.llmsTxt === void 0) resolved.llmsTxt = false;
|
|
1382
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)}.`);
|
|
1383
1618
|
resolved.additionalExtensions = normalizeAdditionalExtensions(resolved.additionalExtensions);
|
|
1384
1619
|
if (!new Set([
|
|
1385
1620
|
"spa",
|
|
@@ -1418,6 +1653,10 @@ function validateLlmsTxt(llmsTxt) {
|
|
|
1418
1653
|
if (llmsTxt.include !== void 0) {
|
|
1419
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)}.`);
|
|
1420
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
|
+
}
|
|
1421
1660
|
}
|
|
1422
1661
|
function validateBudgets(budgets) {
|
|
1423
1662
|
for (const [key, value] of Object.entries(budgets)) {
|
|
@@ -1740,16 +1979,30 @@ function createPrachtCapabilitiesClientModuleSource(options = {}, buildOptions =
|
|
|
1740
1979
|
* validation/middleware/policy stays server-side. Each dispatch carries the
|
|
1741
1980
|
* transport marker header so audit events can attribute it to WebMCP.
|
|
1742
1981
|
*
|
|
1743
|
-
* Targets the
|
|
1744
|
-
* (
|
|
1745
|
-
*
|
|
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.
|
|
1746
1993
|
*/
|
|
1747
1994
|
function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
|
|
1748
1995
|
const tools = extractCapabilities(options, buildOptions.root).filter((capability) => capability.webmcp).map((capability) => ({
|
|
1749
1996
|
name: capability.name,
|
|
1997
|
+
...capability.title ? { title: capability.title } : {},
|
|
1750
1998
|
description: capability.description,
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
+
}
|
|
1753
2006
|
}));
|
|
1754
2007
|
return [
|
|
1755
2008
|
"// Generated by @pracht/vite-plugin — WebMCP page-tool registration shim.",
|
|
@@ -1760,30 +2013,27 @@ function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
|
|
|
1760
2013
|
"",
|
|
1761
2014
|
"export function registerPrachtWebmcpTools() {",
|
|
1762
2015
|
" const modelContext =",
|
|
1763
|
-
" (typeof document !== \"undefined\" && document.modelContext) ||",
|
|
1764
|
-
" (typeof navigator !== \"undefined\" && navigator.modelContext) ||",
|
|
1765
|
-
" null;",
|
|
2016
|
+
" (typeof document !== \"undefined\" && document.modelContext) || null;",
|
|
1766
2017
|
" if (!modelContext || typeof modelContext.registerTool !== \"function\") {",
|
|
1767
2018
|
" return false;",
|
|
1768
2019
|
" }",
|
|
1769
2020
|
" for (const tool of tools) {",
|
|
1770
2021
|
" try {",
|
|
1771
2022
|
" const registration = modelContext.registerTool({",
|
|
1772
|
-
"
|
|
1773
|
-
"
|
|
1774
|
-
"
|
|
1775
|
-
"
|
|
1776
|
-
"
|
|
1777
|
-
"
|
|
1778
|
-
" 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
|
+
" });",
|
|
1779
2029
|
" },",
|
|
1780
2030
|
" });",
|
|
1781
2031
|
" if (registration && typeof registration.catch === \"function\") {",
|
|
1782
2032
|
" registration.catch(() => {});",
|
|
1783
2033
|
" }",
|
|
1784
2034
|
" } catch {",
|
|
1785
|
-
" //
|
|
1786
|
-
" // never break the page.",
|
|
2035
|
+
" // The API is still an origin-trial surface; a failed registration",
|
|
2036
|
+
" // must never break the page.",
|
|
1787
2037
|
" }",
|
|
1788
2038
|
" }",
|
|
1789
2039
|
" return true;",
|
|
@@ -1801,10 +2051,7 @@ function createPrachtWebmcpModuleSource(options = {}, buildOptions = {}) {
|
|
|
1801
2051
|
function createWebmcpBootstrapSource() {
|
|
1802
2052
|
return [
|
|
1803
2053
|
"// WebMCP page tools — loaded only when the browser exposes the API.",
|
|
1804
|
-
"if (",
|
|
1805
|
-
" typeof document !== \"undefined\" &&",
|
|
1806
|
-
" (document.modelContext || (typeof navigator !== \"undefined\" && navigator.modelContext))",
|
|
1807
|
-
") {",
|
|
2054
|
+
"if (typeof document !== \"undefined\" && document.modelContext) {",
|
|
1808
2055
|
" import(\"virtual:pracht/webmcp\").catch(() => {});",
|
|
1809
2056
|
"}",
|
|
1810
2057
|
""
|
|
@@ -1925,7 +2172,7 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
1925
2172
|
const appFilePosix = resolved.appFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1926
2173
|
const appDir = (appFilePosix.startsWith("/") ? appFilePosix : `/${appFilePosix}`).replace(/\/[^/]*$/, "") || "/";
|
|
1927
2174
|
return [
|
|
1928
|
-
"import { resolveApp, initClientRouter, readHydrationState } from \"@pracht/core/client\";",
|
|
2175
|
+
"import { resolveApp, initClientRouter, readHydrationState, DEV_ROUTE_DATA_STALE_EVENT, refreshDevRouteData } from \"@pracht/core/client\";",
|
|
1929
2176
|
appImport,
|
|
1930
2177
|
"",
|
|
1931
2178
|
`const routeLoaderHints = ${JSON.stringify(routeLoaderHints)};`,
|
|
@@ -2010,6 +2257,18 @@ function createPrachtClientModuleSource(options = {}, buildOptions = {}) {
|
|
|
2010
2257
|
" });",
|
|
2011
2258
|
"}",
|
|
2012
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
|
+
"",
|
|
2013
2272
|
...hasWebmcpCapabilities(resolved, buildOptions.root) ? createWebmcpBootstrapSource() : []
|
|
2014
2273
|
].join("\n");
|
|
2015
2274
|
}
|
|
@@ -2148,6 +2407,7 @@ function resolveLlmsTxtConfig(resolved, root = process.cwd()) {
|
|
|
2148
2407
|
if (resolved.llmsTxt.origin) config.origin = resolved.llmsTxt.origin;
|
|
2149
2408
|
if (resolved.llmsTxt.include) config.include = resolved.llmsTxt.include;
|
|
2150
2409
|
if (resolved.llmsTxt.exclude?.length) config.exclude = resolved.llmsTxt.exclude;
|
|
2410
|
+
if (resolved.llmsTxt.maxPagesPerRoute !== void 0) config.maxPagesPerRoute = resolved.llmsTxt.maxPagesPerRoute;
|
|
2151
2411
|
return config;
|
|
2152
2412
|
}
|
|
2153
2413
|
function createApplyRouteLoaderHintsSource() {
|
|
@@ -2189,6 +2449,19 @@ function createRouteHeadHintsForVirtualModules(options, root = process.cwd()) {
|
|
|
2189
2449
|
rootRelativePrefix: prefix
|
|
2190
2450
|
})));
|
|
2191
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
|
+
}
|
|
2192
2465
|
/**
|
|
2193
2466
|
* `getStaticPaths()` presence per route file. Only routes matter — a shell
|
|
2194
2467
|
* cannot enumerate paths — so unlike the head hints this skips the shells
|
|
@@ -2220,6 +2493,19 @@ function createRouteLoaderHintsForVirtualModules(options, root = process.cwd())
|
|
|
2220
2493
|
rootRelativePrefix: options.routesDir
|
|
2221
2494
|
});
|
|
2222
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
|
+
}
|
|
2223
2509
|
function createPrachtRegistryModuleSource(options = {}) {
|
|
2224
2510
|
const resolved = resolveOptions(options);
|
|
2225
2511
|
const apiGlobs = [`${resolved.apiDir}/**/*.{ts,js,tsx,jsx}`, `!${resolved.apiDir}/**/*.d.ts`];
|
|
@@ -2276,6 +2562,38 @@ function generatePagesAppInlineSource(options, root = process.cwd()) {
|
|
|
2276
2562
|
pagesAppSourceCache.set(cacheKey, source);
|
|
2277
2563
|
return source;
|
|
2278
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
|
+
}
|
|
2279
2597
|
//#endregion
|
|
2280
2598
|
//#region src/plugin-dev-ssr.ts
|
|
2281
2599
|
const BODYLESS_METHODS = new Set(["GET", "HEAD"]);
|
|
@@ -2325,6 +2643,7 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
2325
2643
|
const withDevBase = (path) => devBase === "/" || !path.startsWith("/") ? path : `${devBase}${path.slice(1)}`;
|
|
2326
2644
|
let warnedDevtoolsCollision = false;
|
|
2327
2645
|
let warnedLlmsTxtCollision = false;
|
|
2646
|
+
const agentTraffic = createAgentTrafficBuffer();
|
|
2328
2647
|
if (options.llmsTxt && typeof server.config.publicDir === "string") {
|
|
2329
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.");
|
|
2330
2649
|
}
|
|
@@ -2345,6 +2664,7 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
2345
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.`);
|
|
2346
2665
|
}
|
|
2347
2666
|
await serveDevtools(server, res, {
|
|
2667
|
+
agentTraffic,
|
|
2348
2668
|
apiRoutes: serverMod.apiRoutes ?? [],
|
|
2349
2669
|
app: serverMod.resolvedApp,
|
|
2350
2670
|
base: devBase,
|
|
@@ -2381,16 +2701,25 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
2381
2701
|
throw err;
|
|
2382
2702
|
}
|
|
2383
2703
|
const timings = {};
|
|
2704
|
+
let routeError;
|
|
2705
|
+
let capturedRouteError = false;
|
|
2706
|
+
let routeErrorContext;
|
|
2384
2707
|
const response = await framework.handlePrachtRequest({
|
|
2385
2708
|
app: serverMod.resolvedApp,
|
|
2386
2709
|
registry: serverMod.registry,
|
|
2387
2710
|
request: webRequest,
|
|
2388
2711
|
debugErrors: true,
|
|
2712
|
+
onRouteError: (error, _requestPath, context) => {
|
|
2713
|
+
capturedRouteError = true;
|
|
2714
|
+
routeError = error;
|
|
2715
|
+
routeErrorContext = context;
|
|
2716
|
+
},
|
|
2389
2717
|
clientEntryUrl: withDevBase(CLIENT_BROWSER_PATH),
|
|
2390
2718
|
islandsEntryUrl: withDevBase(ISLANDS_CLIENT_BROWSER_PATH),
|
|
2391
2719
|
islandsBootstrapRequired: serverMod.islandsBootstrapRequired === true,
|
|
2392
2720
|
apiRoutes: serverMod.apiRoutes,
|
|
2393
|
-
timings
|
|
2721
|
+
timings,
|
|
2722
|
+
onCapabilityAudit: agentTraffic.record
|
|
2394
2723
|
});
|
|
2395
2724
|
const responseContentType = response.headers.get("content-type") ?? "";
|
|
2396
2725
|
if (response.status === 404 && !responseContentType.includes("application/json") && !routeMatchers.app?.notFound) return next();
|
|
@@ -2414,6 +2743,17 @@ function createDevSSRMiddleware(server, options = {}) {
|
|
|
2414
2743
|
source.pipe(res);
|
|
2415
2744
|
return;
|
|
2416
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
|
+
}
|
|
2417
2757
|
let body = await response.text();
|
|
2418
2758
|
if (contentType.includes("text/html")) body = await transformDevHtml(server, url, body, devBase);
|
|
2419
2759
|
res.statusCode = response.status;
|
|
@@ -2632,27 +2972,100 @@ function escapeHtmlAttribute(value) {
|
|
|
2632
2972
|
*/
|
|
2633
2973
|
async function serveDevtools(server, res, options) {
|
|
2634
2974
|
const devtools = await server.ssrLoadModule("@pracht/core/devtools");
|
|
2635
|
-
const
|
|
2975
|
+
const serverModule = await server.ssrLoadModule(PRACHT_SERVER_MODULE_ID);
|
|
2976
|
+
const capabilityModules = serverModule.registry?.capabilityModules;
|
|
2977
|
+
const middlewareModules = serverModule.registry?.middlewareModules;
|
|
2636
2978
|
const graph = await devtools.buildAppGraph({
|
|
2637
2979
|
apiRoutes: options.apiRoutes,
|
|
2638
2980
|
app: options.app,
|
|
2639
2981
|
loadModule: async (file) => {
|
|
2640
2982
|
return await resolveRegistryModule(capabilityModules, file) ?? server.ssrLoadModule(file);
|
|
2641
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
|
+
},
|
|
2642
2992
|
readSource: (file) => readFileSync(resolve(server.config.root, `.${file}`), "utf-8")
|
|
2643
2993
|
});
|
|
2994
|
+
const agentTraffic = options.agentTraffic.snapshot();
|
|
2644
2995
|
if (options.wantsJson) {
|
|
2645
2996
|
res.statusCode = 200;
|
|
2646
2997
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
2647
|
-
res.end(JSON.stringify(
|
|
2998
|
+
res.end(JSON.stringify({
|
|
2999
|
+
...graph,
|
|
3000
|
+
agentTraffic
|
|
3001
|
+
}, null, 2));
|
|
2648
3002
|
return;
|
|
2649
3003
|
}
|
|
2650
|
-
let html = devtools.buildDevtoolsHtml(graph, {
|
|
3004
|
+
let html = devtools.buildDevtoolsHtml(graph, {
|
|
3005
|
+
agentTraffic,
|
|
3006
|
+
base: options.base
|
|
3007
|
+
});
|
|
2651
3008
|
html = await server.transformIndexHtml(options.url, html);
|
|
2652
3009
|
res.statusCode = 200;
|
|
2653
3010
|
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
2654
3011
|
res.end(html);
|
|
2655
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
|
+
}
|
|
2656
3069
|
async function handleDevError(server, req, res, next, url, error, base) {
|
|
2657
3070
|
if (error instanceof Error) server.ssrFixStacktrace(error);
|
|
2658
3071
|
if (req.headers["x-pracht-route-state-request"] === "1") {
|
|
@@ -2832,28 +3245,15 @@ async function nodeToWebRequest(req, maxBodySize, base = "/") {
|
|
|
2832
3245
|
}
|
|
2833
3246
|
//#endregion
|
|
2834
3247
|
//#region src/index.ts
|
|
2835
|
-
function reachesHeadBearingModule(modules, serverRoot, headHints) {
|
|
2836
|
-
const pending = [...modules];
|
|
2837
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2838
|
-
while (pending.length > 0) {
|
|
2839
|
-
const module = pending.pop();
|
|
2840
|
-
if (!module || seen.has(module)) continue;
|
|
2841
|
-
seen.add(module);
|
|
2842
|
-
const modulePath = module.file ?? module.id?.split("?", 1)[0];
|
|
2843
|
-
if (modulePath) {
|
|
2844
|
-
const normalizedPath = toPosixPath(modulePath);
|
|
2845
|
-
if (headHints[normalizedPath.startsWith(serverRoot) ? normalizedPath.slice(serverRoot.length) : normalizedPath] === true) return true;
|
|
2846
|
-
}
|
|
2847
|
-
if (module.importers) pending.push(...module.importers);
|
|
2848
|
-
}
|
|
2849
|
-
return false;
|
|
2850
|
-
}
|
|
2851
3248
|
function pracht(options = {}) {
|
|
2852
3249
|
const resolved = resolveOptions(options);
|
|
2853
3250
|
const isPagesMode = !!resolved.pagesDir;
|
|
2854
3251
|
let root = process.cwd();
|
|
2855
3252
|
let routeFileDirs = [];
|
|
2856
3253
|
let clientRouteHeadHints = {};
|
|
3254
|
+
let clientRouteHeadersHints = {};
|
|
3255
|
+
let clientRouteLoaderHints = {};
|
|
3256
|
+
let serverRouteLoaderHints = {};
|
|
2857
3257
|
const routeFileExtensions = withAdditionalExtensions(DEFAULT_ROUTE_EXTENSIONS, resolved.additionalExtensions);
|
|
2858
3258
|
let capabilityModulePaths = /* @__PURE__ */ new Set();
|
|
2859
3259
|
if (isPagesMode && options.appFile) console.warn("[pracht] Both `pagesDir` and `appFile` are set. `pagesDir` takes precedence — `appFile` will be ignored.");
|
|
@@ -2863,6 +3263,7 @@ function pracht(options = {}) {
|
|
|
2863
3263
|
const prachtPlugin = {
|
|
2864
3264
|
name: "pracht",
|
|
2865
3265
|
enforce: "pre",
|
|
3266
|
+
api: { llmsTxtEnabled: Boolean(resolved.llmsTxt) },
|
|
2866
3267
|
config(_config, env) {
|
|
2867
3268
|
const isEdge = resolved.adapter.edge === true;
|
|
2868
3269
|
const isSSRBuild = env.isSsrBuild;
|
|
@@ -2873,6 +3274,8 @@ function pracht(options = {}) {
|
|
|
2873
3274
|
const agentSurfaceDefine = env.command === "build" ? String(hasAgentSurface(resolved, configRoot)) : "true";
|
|
2874
3275
|
const staticTargetDefine = String(env.command === "build" && resolved.adapter.staticTarget === true);
|
|
2875
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}`);
|
|
2876
3279
|
return {
|
|
2877
3280
|
appType: "custom",
|
|
2878
3281
|
envPrefix: ["VITE_", PUBLIC_ENV_PREFIX],
|
|
@@ -2885,9 +3288,7 @@ function pracht(options = {}) {
|
|
|
2885
3288
|
},
|
|
2886
3289
|
...isSSRBuild ? {} : { build: { rollupOptions: {
|
|
2887
3290
|
...wantsIslandsEntry ? { input: [PRACHT_ISLANDS_CLIENT_MODULE_ID] } : {},
|
|
2888
|
-
output
|
|
2889
|
-
if (id.includes("node_modules/preact") || id.includes("node_modules/preact-suspense")) return "vendor";
|
|
2890
|
-
} }
|
|
3291
|
+
...clientChunkConfig.output ? { output: clientChunkConfig.output } : {}
|
|
2891
3292
|
} } },
|
|
2892
3293
|
...isEdge && isSSRBuild ? {
|
|
2893
3294
|
ssr: {
|
|
@@ -2930,6 +3331,9 @@ function pracht(options = {}) {
|
|
|
2930
3331
|
if (isIslandsClientModule(id)) return createPrachtIslandsClientModuleSource(resolved, { root });
|
|
2931
3332
|
if (isClientModule(id)) {
|
|
2932
3333
|
clientRouteHeadHints = createRouteHeadHintsForVirtualModules(resolved, root);
|
|
3334
|
+
clientRouteHeadersHints = createRouteHeadersHintsForVirtualModules(resolved, root);
|
|
3335
|
+
clientRouteLoaderHints = createRouteLoaderHintsForVirtualModules(resolved, root);
|
|
3336
|
+
serverRouteLoaderHints = createServerLoaderHintsForHotUpdates(resolved, root);
|
|
2933
3337
|
return createPrachtClientModuleSource(resolved, { root });
|
|
2934
3338
|
}
|
|
2935
3339
|
if (isDevModule(id)) return createPrachtDevModuleSource(resolved, {
|
|
@@ -2983,24 +3387,58 @@ function pracht(options = {}) {
|
|
|
2983
3387
|
const normalizedFile = toPosixPath(file);
|
|
2984
3388
|
const relative = normalizedFile.startsWith(serverRoot) ? normalizedFile.slice(serverRoot.length) : normalizedFile;
|
|
2985
3389
|
const changesRouteHeadSource = isPagesMode ? relative.startsWith(resolved.pagesDir) : relative.startsWith(resolved.routesDir) || relative.startsWith(resolved.shellsDir);
|
|
2986
|
-
const
|
|
2987
|
-
|
|
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;
|
|
2988
3404
|
let clientHeadModule;
|
|
2989
|
-
if (changesRouteHeadSource || changesRouteHeadDependency) clientHeadModule = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
|
|
3405
|
+
if (changesRouteHeadSource || changesRouteHeadDependency || changesRouteHeadersDependency) clientHeadModule = server.moduleGraph.getModuleById(PRACHT_CLIENT_MODULE_ID);
|
|
2990
3406
|
if (changesRouteHeadSource) {
|
|
2991
|
-
const previousHint = clientRouteHeadHints[relative];
|
|
3407
|
+
const previousHint = clientRouteHeadHints[relative] === true;
|
|
2992
3408
|
try {
|
|
2993
3409
|
const nextHints = createRouteHeadHintsForVirtualModules(resolved, root);
|
|
2994
|
-
|
|
3410
|
+
shouldReloadClientEntry ||= previousHint !== (nextHints[relative] === true);
|
|
2995
3411
|
clientRouteHeadHints = nextHints;
|
|
2996
3412
|
} catch {
|
|
2997
|
-
|
|
3413
|
+
shouldReloadClientEntry = true;
|
|
2998
3414
|
}
|
|
2999
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
|
+
}
|
|
3000
3436
|
if (isPagesMode && relative.startsWith(resolved.pagesDir)) {
|
|
3001
3437
|
clearPagesAppSourceCache();
|
|
3002
3438
|
invalidateVirtualModules(server);
|
|
3003
|
-
|
|
3439
|
+
const sentFullReload = sendServerOnlyFullReload(server, file);
|
|
3440
|
+
if (!sentFullReload && !shouldReloadClientEntry) sendRouteDataStale(server);
|
|
3441
|
+
if (!sentFullReload && shouldReloadClientEntry && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
|
|
3004
3442
|
return;
|
|
3005
3443
|
}
|
|
3006
3444
|
if (!isPagesMode && relative === resolved.appFile) {
|
|
@@ -3038,7 +3476,9 @@ function pracht(options = {}) {
|
|
|
3038
3476
|
if (capabilityMod) server.moduleGraph.invalidateModule(capabilityMod);
|
|
3039
3477
|
}
|
|
3040
3478
|
}
|
|
3041
|
-
|
|
3479
|
+
const sentFullReload = sendServerOnlyFullReload(server, file);
|
|
3480
|
+
if (!sentFullReload && shouldReloadClientEntry && clientHeadModule) return [...new Set([...modules, clientHeadModule])];
|
|
3481
|
+
if (!sentFullReload && (changesRouteHeadSource || changesRouteLoaderDependency)) sendRouteDataStale(server);
|
|
3042
3482
|
}
|
|
3043
3483
|
};
|
|
3044
3484
|
const configuredBasePlugin = {
|
|
@@ -3076,12 +3516,15 @@ function pracht(options = {}) {
|
|
|
3076
3516
|
...resolved.precompileSsrJsx === true ? {} : resolved.precompileSsrJsx,
|
|
3077
3517
|
ssrOnly: true
|
|
3078
3518
|
}) : null;
|
|
3519
|
+
const preactPlugins = preact();
|
|
3520
|
+
const clientModulePrefreshPlugin = createClientModulePrefreshPlugin(preactPlugins, { isRouteOrShellModule: (id) => isRouteOrShellFile(id, routeFileDirs, routeFileExtensions) });
|
|
3079
3521
|
const plugins = [
|
|
3080
3522
|
...precompilePlugin ? [precompilePlugin] : [],
|
|
3081
|
-
...
|
|
3523
|
+
...preactPlugins,
|
|
3082
3524
|
prachtPlugin,
|
|
3083
3525
|
configuredBasePlugin,
|
|
3084
3526
|
clientModuleTransformPlugin,
|
|
3527
|
+
...clientModulePrefreshPlugin ? [clientModulePrefreshPlugin] : [],
|
|
3085
3528
|
...edgeRuntimeSafetyPlugin ? [edgeRuntimeSafetyPlugin] : [],
|
|
3086
3529
|
createEnvSafetyPlugin(resolved.envSafety)
|
|
3087
3530
|
];
|
|
@@ -3335,4 +3778,4 @@ function withTrailingSep(p) {
|
|
|
3335
3778
|
return p.endsWith("/") ? p : `${p}/`;
|
|
3336
3779
|
}
|
|
3337
3780
|
//#endregion
|
|
3338
|
-
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.
|
|
51
|
-
"@pracht/
|
|
52
|
-
"@pracht/
|
|
49
|
+
"@pracht/adapter-node": "0.4.2",
|
|
50
|
+
"@pracht/capabilities": "0.3.0",
|
|
51
|
+
"@pracht/preact-ssr-precompile": "0.1.3",
|
|
52
|
+
"@pracht/core": "0.16.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"vite": "^8.0.0"
|