@pracht/cli 1.5.0 → 1.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # @pracht/cli
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#179](https://github.com/JoviDeCroock/pracht/pull/179) [`67bc60b`](https://github.com/JoviDeCroock/pracht/commit/67bc60b5a0439beb91fc7332ea6bac9520108d70) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add `pracht build --analyze` and per-route client JS budgets.
8
+
9
+ `pracht build --analyze` prints a per-route report of the client JavaScript each route loads: the transitive chunks (route module + shell) with raw and gzip sizes, a total row per route, and the shared entry chunks broken out. `--json` emits the same data as machine-readable JSON. Output respects `NO_COLOR` and routes are sorted by total gzip size, descending.
10
+
11
+ The pracht plugin accepts a new `budgets` option (e.g. `budgets: { "*": "120kb", "/dashboard": "200kb" }`) declaring per-route gzip client-JS ceilings; `"*"` applies to every route and explicit route paths override it. `pracht build` evaluates budgets after every build, prints pass/fail per route, writes `dist/server/budget-report.json`, and exits non-zero on exceeded budgets unless `--no-budget-fail` is passed. `pracht verify` and `pracht doctor` surface the last build's budget results when the report file is present.
12
+
13
+ - [#183](https://github.com/JoviDeCroock/pracht/pull/183) [`9db0a58`](https://github.com/JoviDeCroock/pracht/commit/9db0a5897216eb049cc99f0d53adb5dad34314b9) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - `pracht build` for the Cloudflare target now writes a thin deploy entry at
14
+ `dist/server/worker.js` that re-exports only the default handler and the
15
+ `workerExportsFrom` entrypoint classes. workerd validates every named export
16
+ of the deployed entry module and rejects the build metadata (`buildTarget`,
17
+ asset manifests, `resolvedApp`, ...) that `dist/server/server.js` exports for
18
+ the SSG prerender pass, so pointing `wrangler.jsonc`'s `main` at `server.js`
19
+ failed to boot with `Incorrect type for map entry 'buildTarget'`. Point `main`
20
+ at `dist/server/worker.js` instead. The generated server entry now also
21
+ exports `cloudflareWorkerEntrypointNames` so the CLI knows which classes to
22
+ re-export.
23
+
24
+ - [#178](https://github.com/JoviDeCroock/pracht/pull/178) [`d27b96a`](https://github.com/JoviDeCroock/pracht/commit/d27b96a68354b69d06cdfdd9667956631283ce1a) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add a dev-server startup banner and a rich dev-only 404 page.
25
+
26
+ `pracht dev` now prints a route table on startup — every page route with its
27
+ render mode, shell, and middleware, plus API routes with their HTTP methods —
28
+ alongside the local URL. The banner reuses the resolved-app-graph logic shared
29
+ with `pracht inspect` and respects `NO_COLOR`.
30
+
31
+ In dev mode, document navigations that match no page route and no API route now
32
+ render a styled standalone 404 page (new `@pracht/core/dev-404` entry, same
33
+ self-contained approach as the error overlay) listing all registered routes
34
+ with render modes and links plus the requested path. The module is only loaded
35
+ by the dev middleware; production 404 behavior is unchanged.
36
+
37
+ - [#173](https://github.com/JoviDeCroock/pracht/pull/173) [`004e429`](https://github.com/JoviDeCroock/pracht/commit/004e4295db64bea56a283848db352b3c29909a45) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add `pracht mcp`, a stdio Model Context Protocol server built into the CLI. It exposes the existing command internals as native MCP tools for coding agents: `inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify` (with optional `changed` scope), and `generate_route` / `generate_shell` / `generate_middleware` / `generate_api`. Every tool accepts an optional `cwd`, returns the same JSON payloads as the corresponding `--json` CLI flags, and surfaces failures as `isError` results instead of crashing the server. See docs/MCP.md for registration instructions and the tool reference.
38
+
39
+ - [#175](https://github.com/JoviDeCroock/pracht/pull/175) [`439bc22`](https://github.com/JoviDeCroock/pracht/commit/439bc22a7a92baf2e450ecf6c9fa9b6e0d43b22d) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add `pracht preview` to serve the production build locally with one command. It runs `pracht build` first (skippable with `--skip-build`) and then serves the output for the configured adapter: Node targets run `dist/server/server.js` as a child process (`--port <n>`, `$PORT`, default 3000), Cloudflare targets delegate to `wrangler dev` against the built worker (with an actionable error when wrangler or its config is missing), and Vercel targets print guidance towards `vercel build`/`vercel dev` since there is no faithful local production runtime. Scaffolded Node and Cloudflare starters now include a `preview` script.
40
+
41
+ - [#177](https://github.com/JoviDeCroock/pracht/pull/177) [`c1b22c4`](https://github.com/JoviDeCroock/pracht/commit/c1b22c4e786a485c969143de48cd2be7f5f03fe8) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add zero-generic typed loader data keyed by route id.
42
+
43
+ `pracht typegen` now registers each route's loader data type on
44
+ `Register["routes"]` in the generated `src/pracht-routes.d.ts`, pointing at the
45
+ route module (or the separate loader module wired via the manifest, which wins
46
+ over an inline loader like at runtime). `@pracht/core` gains a
47
+ `RouteLoaderData<TModule, TFallbackModule?>` utility type, a
48
+ `RouteDataFor<TRouteId>` helper, and a new `useRouteData(routeId)` overload
49
+ that returns the mapped loader data with route-id autocomplete — no generic
50
+ needed. The existing `useRouteData<typeof loader>()` form keeps working as the
51
+ fallback for projects that do not run typegen. In development, passing a route
52
+ id that is not the active route logs a warning.
53
+
54
+ ### Patch Changes
55
+
56
+ - [#185](https://github.com/JoviDeCroock/pracht/pull/185) [`b83f5b7`](https://github.com/JoviDeCroock/pracht/commit/b83f5b7d6d92f22c982bad4fb62a9be00dd56a97) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - `pracht build` now stubs `cloudflare:*` platform modules (via Node module
57
+ hooks) while importing the built server bundle for SSG prerendering. Edge
58
+ server bundles keep these imports external because they only exist inside
59
+ workerd, so any app whose worker graph imports `cloudflare:workers` or
60
+ `cloudflare:email` previously failed the prerender pass with
61
+ `ERR_UNSUPPORTED_ESM_URL_SCHEME`.
62
+
63
+ - [#180](https://github.com/JoviDeCroock/pracht/pull/180) [`ab693d5`](https://github.com/JoviDeCroock/pracht/commit/ab693d5ac04a1c7b3815c70396ab2e9a3a258072) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add a dev-only `/_pracht` devtools page and `Server-Timing` phase headers.
64
+
65
+ - The dev server now serves a self-contained devtools page at `/_pracht` listing every page route (pattern, render mode, shell, middleware chain, source file) and API route (path, methods, source file), with the same data available as JSON at `/_pracht.json`. The path is reserved in dev only — a colliding user route logs a warning in dev and still wins in production.
66
+ - Dev SSR responses now carry a standards-compliant `Server-Timing` header (e.g. `mw;dur=1.2, loader;dur=14.8, render;dur=3.1`) so middleware/loader/render phase durations show up in the browser Network panel. The runtime only records timings when the new `HandlePrachtRequestOptions.timings` collector is passed; production requests skip all timing work.
67
+ - `@pracht/core` gains a shared app-graph module (`buildAppGraph`, `serializeAppRoutes`, `serializeApiRoutes`, `detectApiMethods`, and a new `@pracht/core/devtools` entry) that both `pracht inspect` and the devtools page use, so the CLI and the page report the same graph.
68
+
69
+ - Updated dependencies [[`d27b96a`](https://github.com/JoviDeCroock/pracht/commit/d27b96a68354b69d06cdfdd9667956631283ce1a), [`ab693d5`](https://github.com/JoviDeCroock/pracht/commit/ab693d5ac04a1c7b3815c70396ab2e9a3a258072), [`54b1070`](https://github.com/JoviDeCroock/pracht/commit/54b1070e3c73075689ae7d40ceb7716da412e077), [`a6b120b`](https://github.com/JoviDeCroock/pracht/commit/a6b120b8b79082adbdb54dbeb1920ba3703079c8), [`8862f51`](https://github.com/JoviDeCroock/pracht/commit/8862f51505bdbba8afd7ebf8570d461b233d66f9), [`c1b22c4`](https://github.com/JoviDeCroock/pracht/commit/c1b22c4e786a485c969143de48cd2be7f5f03fe8)]:
70
+ - @pracht/core@0.9.0
71
+
72
+ ## 1.5.1
73
+
74
+ ### Patch Changes
75
+
76
+ - Updated dependencies [[`9b089c6`](https://github.com/JoviDeCroock/pracht/commit/9b089c65a51ff724737fffce18f6b08259cfb76e), [`a1c44ab`](https://github.com/JoviDeCroock/pracht/commit/a1c44ab966bcf1afafc33d26d846a1f91a15011e), [`c656bbd`](https://github.com/JoviDeCroock/pracht/commit/c656bbd622f73567f38c02e4346039d2595568b7), [`b3be9a0`](https://github.com/JoviDeCroock/pracht/commit/b3be9a0563f3f66df1f18cc91929b9191b834646)]:
77
+ - @pracht/core@0.8.1
78
+
3
79
  ## 1.5.0
4
80
 
5
81
  ### Minor Changes
package/README.md CHANGED
@@ -18,17 +18,53 @@ Start the local development server with SSR and HMR.
18
18
 
19
19
  Create a production build with client/server output and SSG/ISG prerendering.
20
20
 
21
+ ```bash
22
+ pracht build
23
+ pracht build --analyze # per-route client JS report (gzip + raw)
24
+ pracht build --json # same report as JSON (agent-friendly)
25
+ pracht build --no-budget-fail # report exceeded budgets without failing
26
+ ```
27
+
28
+ `--analyze` prints, per route (pattern + render mode), the transitive client
29
+ chunks it loads with raw and gzip sizes, a total row, and the shared entry
30
+ chunks broken out. Output respects `NO_COLOR`.
31
+
32
+ When the pracht plugin config declares `budgets` (e.g.
33
+ `budgets: { "*": "120kb", "/dashboard": "200kb" }`), every build evaluates the
34
+ per-route gzip client-JS ceilings, writes `dist/server/budget-report.json`, and
35
+ exits non-zero on exceeded budgets unless `--no-budget-fail` is passed. See
36
+ [docs/PERFORMANCE.md](https://github.com/JoviDeCroock/pracht/blob/main/docs/PERFORMANCE.md).
37
+
21
38
  For Node.js targets, run the built server with:
22
39
 
23
40
  ```bash
24
41
  node dist/server/server.js
25
42
  ```
26
43
 
44
+ ### `pracht preview`
45
+
46
+ Serve the production build locally. Runs `pracht build` first (skip with
47
+ `--skip-build`), then serves the output for the configured adapter:
48
+
49
+ - **Node**: runs `dist/server/server.js` on `--port` (or `$PORT`, default 3000).
50
+ - **Cloudflare**: delegates to `wrangler dev` against the built worker; requires
51
+ wrangler in `node_modules` or on your PATH plus a wrangler config.
52
+ - **Vercel**: there is no faithful local production runtime — the command points
53
+ you at `vercel build` / `vercel dev` instead.
54
+
55
+ ```bash
56
+ pracht preview
57
+ pracht preview --port 4000
58
+ pracht preview --skip-build
59
+ ```
60
+
27
61
  ### `pracht verify`
28
62
 
29
63
  Run fast framework-aware verification checks without paying for a full build or
30
64
  test loop. Use `--changed` to focus on changed manifest-managed files and
31
- `--json` for machine-readable output.
65
+ `--json` for machine-readable output. When `dist/server/budget-report.json`
66
+ exists (written by `pracht build` when budgets are configured), the last
67
+ build's client JS budget results are surfaced as checks.
32
68
 
33
69
  ```bash
34
70
  pracht verify
@@ -99,3 +135,14 @@ machine-readable output.
99
135
  pracht doctor
100
136
  pracht doctor --json
101
137
  ```
138
+
139
+ ### `pracht mcp`
140
+
141
+ Start a Model Context Protocol server on stdio that exposes inspect, doctor,
142
+ verify, and generate as native tools for coding agents. See
143
+ [docs/MCP.md](https://github.com/JoviDeCroock/pracht/blob/main/docs/MCP.md)
144
+ for client registration and the tool reference.
145
+
146
+ ```bash
147
+ pracht mcp
148
+ ```
@@ -0,0 +1,324 @@
1
+ import { r as VERSION } from "./index.mjs";
2
+ import { t as readClientBuildAssets } from "./build-metadata-DyMzBVtV.mjs";
3
+ import { a as formatBytes, i as formatBundleReport, n as evaluateBudgets, o as shouldUseColor, r as formatBudgetResults, t as collectBundleReport } from "./bundle-report-DxTtmipa.mjs";
4
+ import { register } from "node:module";
5
+ import { defineCommand } from "citty";
6
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
7
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import { build } from "vite";
10
+ //#region src/build-shared.ts
11
+ const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
12
+ function writeVercelBuildOutput({ functionName, headersManifest = {}, isgRoutes, regions, root, staticRoutes }) {
13
+ const outputDir = join(root, ".vercel/output");
14
+ const staticDir = join(outputDir, "static");
15
+ const functionDir = join(outputDir, "functions", `${functionName || "render"}.func`);
16
+ rmSync(outputDir, {
17
+ force: true,
18
+ recursive: true
19
+ });
20
+ mkdirSync(outputDir, { recursive: true });
21
+ cpSync(join(root, "dist/client"), staticDir, { recursive: true });
22
+ cpSync(join(root, "dist/server"), functionDir, { recursive: true });
23
+ writeFileSync(join(outputDir, "config.json"), `${JSON.stringify(createVercelOutputConfig({
24
+ functionName,
25
+ headersManifest,
26
+ staticRoutes,
27
+ isgRoutes
28
+ }), null, 2)}\n`, "utf-8");
29
+ writeFileSync(join(functionDir, ".vc-config.json"), `${JSON.stringify(createVercelFunctionConfig({ regions }), null, 2)}\n`, "utf-8");
30
+ return ".vercel/output";
31
+ }
32
+ function createVercelOutputConfig({ functionName, headersManifest, staticRoutes, isgRoutes }) {
33
+ const target = `/${functionName || "render"}`;
34
+ const routes = [{
35
+ dest: target,
36
+ has: [{
37
+ type: "header",
38
+ key: ROUTE_STATE_REQUEST_HEADER,
39
+ value: "1"
40
+ }],
41
+ src: "/(.*)"
42
+ }, {
43
+ dest: target,
44
+ has: [{
45
+ type: "query",
46
+ key: "_data",
47
+ value: "1"
48
+ }],
49
+ src: "/(.*)"
50
+ }];
51
+ for (const route of sortStaticRoutes(staticRoutes)) routes.push({
52
+ dest: routeToStaticHtmlPath(route),
53
+ src: routeToRouteExpression(route)
54
+ });
55
+ for (const route of isgRoutes) routes.push({
56
+ dest: target,
57
+ src: routeToRouteExpression(route)
58
+ });
59
+ routes.push({ handle: "filesystem" });
60
+ routes.push({
61
+ dest: target,
62
+ src: "/(.*)"
63
+ });
64
+ const headers = [{
65
+ headers: [
66
+ {
67
+ key: "permissions-policy",
68
+ value: "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
69
+ },
70
+ {
71
+ key: "referrer-policy",
72
+ value: "strict-origin-when-cross-origin"
73
+ },
74
+ {
75
+ key: "x-content-type-options",
76
+ value: "nosniff"
77
+ },
78
+ {
79
+ key: "x-frame-options",
80
+ value: "SAMEORIGIN"
81
+ }
82
+ ],
83
+ source: "/(.*)"
84
+ }];
85
+ for (const route of sortStaticRoutes(staticRoutes)) {
86
+ const routeHeaders = headersManifest[route];
87
+ if (!routeHeaders) continue;
88
+ headers.push({
89
+ headers: Object.entries(routeHeaders).map(([key, value]) => ({
90
+ key,
91
+ value
92
+ })),
93
+ source: routeToHeaderSource(route)
94
+ });
95
+ }
96
+ return {
97
+ headers,
98
+ framework: { version: VERSION },
99
+ routes,
100
+ version: 3
101
+ };
102
+ }
103
+ function createVercelFunctionConfig({ regions }) {
104
+ const config = {
105
+ entrypoint: "server.js",
106
+ runtime: "edge"
107
+ };
108
+ if (regions) config.regions = regions;
109
+ return config;
110
+ }
111
+ function sortStaticRoutes(routes) {
112
+ return [...new Set(routes)].sort((left, right) => right.length - left.length);
113
+ }
114
+ function routeToRouteExpression(route) {
115
+ if (route === "/") return "^/$";
116
+ return `^${escapeRegex(route)}/?$`;
117
+ }
118
+ function routeToStaticHtmlPath(route) {
119
+ if (route === "/") return "/index.html";
120
+ return `${route}/index.html`;
121
+ }
122
+ function routeToHeaderSource(route) {
123
+ return route === "/" ? "/" : route;
124
+ }
125
+ function escapeRegex(value) {
126
+ return value.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
127
+ }
128
+ //#endregion
129
+ //#region src/commands/build.ts
130
+ let prerenderHooksRegistered = false;
131
+ function registerPrerenderModuleHooks() {
132
+ if (prerenderHooksRegistered) return;
133
+ prerenderHooksRegistered = true;
134
+ try {
135
+ register("./prerender-module-hooks.mjs", import.meta.url);
136
+ } catch {
137
+ register("../prerender-module-hooks.ts", import.meta.url);
138
+ }
139
+ }
140
+ var build_default = defineCommand({
141
+ meta: {
142
+ name: "build",
143
+ description: "Production build (client + server)"
144
+ },
145
+ args: {
146
+ analyze: {
147
+ type: "boolean",
148
+ description: "Print a per-route client JavaScript report after the build"
149
+ },
150
+ json: {
151
+ type: "boolean",
152
+ description: "Output the analyze report as JSON (implies --analyze)"
153
+ },
154
+ "budget-fail": {
155
+ type: "boolean",
156
+ default: true,
157
+ description: "Fail the build when a client JS budget is exceeded (--no-budget-fail to disable)"
158
+ }
159
+ },
160
+ async run({ args }) {
161
+ await runBuild(process.cwd(), {
162
+ analyze: Boolean(args.analyze),
163
+ analyzeJson: Boolean(args.json),
164
+ budgetFail: Boolean(args["budget-fail"])
165
+ });
166
+ }
167
+ });
168
+ function indentBlock(block) {
169
+ return block.split("\n").map((line) => line ? ` ${line}` : line).join("\n");
170
+ }
171
+ async function runBuild(root, options = {}) {
172
+ const analyzeJson = Boolean(options.analyzeJson);
173
+ const analyze = Boolean(options.analyze) || analyzeJson;
174
+ const budgetFail = options.budgetFail ?? true;
175
+ const logLevel = analyzeJson ? "silent" : void 0;
176
+ const log = (message) => {
177
+ if (!analyzeJson) console.log(message);
178
+ };
179
+ log("\n Building client...\n");
180
+ await build({
181
+ root,
182
+ logLevel,
183
+ build: {
184
+ outDir: "dist",
185
+ manifest: true,
186
+ rollupOptions: { input: "virtual:pracht/client" }
187
+ }
188
+ });
189
+ log("\n Building server...\n");
190
+ await build({
191
+ root,
192
+ logLevel,
193
+ build: {
194
+ outDir: "dist/server",
195
+ rollupOptions: { input: "virtual:pracht/server" },
196
+ ssr: true
197
+ }
198
+ });
199
+ const serverEntry = resolve(root, "dist/server/server.js");
200
+ let clientDir;
201
+ if (existsSync(resolve(root, "dist/client/.vite/manifest.json"))) clientDir = resolve(root, "dist/client");
202
+ else {
203
+ clientDir = resolve(root, "dist/client");
204
+ const distRoot = resolve(root, "dist");
205
+ mkdirSync(clientDir, { recursive: true });
206
+ for (const entry of readdirSync(distRoot)) {
207
+ if (entry === "server" || entry === "client") continue;
208
+ const sourcePath = join(distRoot, entry);
209
+ cpSync(sourcePath, join(clientDir, entry), { recursive: true });
210
+ rmSync(sourcePath, {
211
+ force: true,
212
+ recursive: true
213
+ });
214
+ }
215
+ }
216
+ const publicDir = resolve(root, "public");
217
+ if (existsSync(publicDir)) cpSync(publicDir, clientDir, { recursive: true });
218
+ let buildTarget = null;
219
+ if (existsSync(serverEntry)) {
220
+ registerPrerenderModuleHooks();
221
+ const serverMod = await import(pathToFileURL(serverEntry).href);
222
+ buildTarget = typeof serverMod.buildTarget === "string" ? serverMod.buildTarget : null;
223
+ const { prerenderApp } = serverMod;
224
+ const { clientEntryUrl, clientEntryJs, cssManifest, jsManifest } = readClientBuildAssets(root);
225
+ const { pages, isgManifest } = await prerenderApp({
226
+ app: serverMod.resolvedApp,
227
+ clientEntryUrl: clientEntryUrl ?? void 0,
228
+ cssManifest,
229
+ jsManifest,
230
+ registry: serverMod.registry,
231
+ withISGManifest: true,
232
+ concurrency: serverMod.prerenderConcurrency
233
+ });
234
+ const headersManifest = Object.fromEntries(pages.map((page) => [page.path, page.headers ?? {}]));
235
+ if (pages.length > 0) {
236
+ log(`\n Prerendering ${pages.length} SSG/ISG route(s)...\n`);
237
+ for (const page of pages) {
238
+ const filePath = resolvePrerenderOutputPath(clientDir, page.path);
239
+ mkdirSync(dirname(filePath), { recursive: true });
240
+ writeFileSync(filePath, page.html, "utf-8");
241
+ log(` ${page.path} → ${filePath.replace(root + "/", "")}`);
242
+ }
243
+ }
244
+ if (Object.keys(headersManifest).length > 0) {
245
+ const headersManifestJson = `${JSON.stringify(headersManifest, null, 2)}\n`;
246
+ writeFileSync(resolve(root, "dist/server/headers-manifest.json"), headersManifestJson, "utf-8");
247
+ mkdirSync(resolve(clientDir, "_pracht"), { recursive: true });
248
+ writeFileSync(resolve(clientDir, "_pracht/headers.json"), headersManifestJson, "utf-8");
249
+ }
250
+ if (Object.keys(isgManifest).length > 0) {
251
+ writeFileSync(resolve(root, "dist/server/isg-manifest.json"), JSON.stringify(isgManifest, null, 2), "utf-8");
252
+ log(`\n ISG manifest → dist/server/isg-manifest.json (${Object.keys(isgManifest).length} route(s))\n`);
253
+ }
254
+ if (serverMod.buildTarget === "cloudflare") {
255
+ if (Object.keys(isgManifest).length > 0) console.warn("\n Warning: Cloudflare adapter currently serves prerendered ISG HTML as static assets and does not perform runtime revalidation. Use SSR/SSG on Cloudflare, or deploy ISG routes to Node until Cloudflare ISG support is added.\n");
256
+ const entrypointNames = Array.isArray(serverMod.cloudflareWorkerEntrypointNames) ? serverMod.cloudflareWorkerEntrypointNames : [];
257
+ const deployEntryLines = [
258
+ ...entrypointNames.length > 0 ? [`export { ${entrypointNames.join(", ")} } from "./server.js";`] : [],
259
+ "export { default } from \"./server.js\";",
260
+ ""
261
+ ];
262
+ writeFileSync(resolve(root, "dist/server/worker.js"), deployEntryLines.join("\n"), "utf-8");
263
+ log("\n Cloudflare worker → dist/server/worker.js\n");
264
+ log(" Deploy with: wrangler deploy\n");
265
+ }
266
+ if (serverMod.buildTarget === "vercel") log(`\n Vercel build output → ${writeVercelBuildOutput({
267
+ functionName: serverMod.vercelFunctionName,
268
+ isgRoutes: Object.keys(isgManifest),
269
+ headersManifest,
270
+ regions: serverMod.vercelRegions,
271
+ root,
272
+ staticRoutes: pages.map((page) => page.path).filter((path) => !(path in isgManifest))
273
+ })}\n`);
274
+ const budgets = serverMod.budgets ?? {};
275
+ const hasBudgets = Object.keys(budgets).length > 0;
276
+ if (analyze || hasBudgets) {
277
+ const report = collectBundleReport({
278
+ routes: serverMod.resolvedApp?.routes ?? [],
279
+ jsManifest,
280
+ clientEntryJs,
281
+ clientDir
282
+ });
283
+ const evaluation = hasBudgets ? evaluateBudgets(report, budgets) : null;
284
+ const color = shouldUseColor();
285
+ if (analyzeJson) console.log(JSON.stringify({
286
+ shared: report.shared,
287
+ routes: report.routes,
288
+ ...evaluation ? { budgets: evaluation } : {}
289
+ }, null, 2));
290
+ else if (analyze) console.log(`\n${indentBlock(formatBundleReport(report, { color }))}\n`);
291
+ if (evaluation) {
292
+ writeFileSync(resolve(root, "dist/server/budget-report.json"), `${JSON.stringify({
293
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
294
+ budgets,
295
+ results: evaluation.results,
296
+ unmatched: evaluation.unmatched,
297
+ ok: evaluation.ok
298
+ }, null, 2)}\n`, "utf-8");
299
+ if (!analyzeJson) console.log(`\n${indentBlock(formatBudgetResults(evaluation, { color }))}\n`);
300
+ if (!evaluation.ok) {
301
+ const summary = evaluation.results.filter((result) => !result.ok).map((result) => `${result.path} (${formatBytes(result.gzipBytes)} gzip > ${formatBytes(result.limitBytes)})`).join(", ");
302
+ if (budgetFail) {
303
+ console.error(`\n Build failed: client JS budget exceeded for ${summary}.\n`);
304
+ process.exitCode = 1;
305
+ return { buildTarget };
306
+ }
307
+ if (!analyzeJson) console.warn(`\n Warning: client JS budget exceeded for ${summary} (--no-budget-fail).\n`);
308
+ }
309
+ }
310
+ }
311
+ }
312
+ log("\n Build complete.\n");
313
+ return { buildTarget };
314
+ }
315
+ function resolvePrerenderOutputPath(clientDir, routePath) {
316
+ if (routePath.includes("\0")) throw new Error(`Refusing to write prerendered route "${routePath}" with a NUL byte.`);
317
+ const root = resolve(clientDir);
318
+ const filePath = routePath === "/" ? resolve(root, "index.html") : resolve(root, `.${routePath}`, "index.html");
319
+ const relativePath = relative(root, filePath);
320
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) throw new Error(`Refusing to write prerendered route "${routePath}" outside dist/client (${filePath}).`);
321
+ return filePath;
322
+ }
323
+ //#endregion
324
+ export { build_default as default, runBuild };
@@ -7,6 +7,7 @@ function readClientBuildAssets(root = process.cwd()) {
7
7
  const manifestPath = MANIFEST_PATHS.map((candidate) => resolve(root, candidate)).find((path) => existsSync(path));
8
8
  if (!manifestPath) return {
9
9
  clientEntryUrl: null,
10
+ clientEntryJs: [],
10
11
  cssManifest: {},
11
12
  jsManifest: {}
12
13
  };
@@ -43,6 +44,7 @@ function readClientBuildAssets(root = process.cwd()) {
43
44
  }
44
45
  return {
45
46
  clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
47
+ clientEntryJs: clientEntry ? collectTransitiveDeps("virtual:pracht/client").js.map((file) => `/${file}`) : [],
46
48
  cssManifest,
47
49
  jsManifest
48
50
  };