@pracht/cli 1.5.1 → 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 +69 -0
- package/README.md +48 -1
- package/dist/build-CGzTcST3.mjs +324 -0
- package/dist/{build-metadata-BOChHO8g.mjs → build-metadata-DyMzBVtV.mjs} +2 -0
- package/dist/bundle-report-DxTtmipa.mjs +218 -0
- package/dist/dev-DwAbdNBf.mjs +173 -0
- package/dist/{doctor-DyOWEA42.mjs → doctor-uALUogtk.mjs} +1 -1
- package/dist/{generate-DTMte0kd.mjs → generate-BczObays.mjs} +13 -3
- package/dist/index.mjs +9 -7
- package/dist/{inspect-WF08uLOl.mjs → inspect-Bxt-6Lmc.mjs} +14 -33
- package/dist/mcp-Dv9EeFtp.mjs +154 -0
- package/dist/prerender-module-hooks.mjs +46 -0
- package/dist/preview-B0-hgVfI.mjs +149 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/{typegen-Bl4fYQIy.mjs → typegen-KYYNjYDt.mjs} +25 -5
- package/dist/{verification-CSj-ngqk.mjs → verification-5w_FfRtE.mjs} +23 -2
- package/dist/{verify-FwVbdlxh.mjs → verify-B_HUuyEQ.mjs} +1 -1
- package/package.json +4 -2
- package/dist/build-B7_6RUBf.mjs +0 -234
- package/dist/dev-BCFe3g38.mjs +0 -25
- /package/dist/{manifest-Bs5hp3gA.mjs → manifest-nU7P84qF.mjs} +0 -0
- /package/dist/{project-voPTS9UC.mjs → project-2EajzTuX.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
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
|
+
|
|
3
72
|
## 1.5.1
|
|
4
73
|
|
|
5
74
|
### Patch 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
|
};
|