@pracht/cli 0.0.1 → 1.1.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,52 @@
1
1
  # @pracht/cli
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#70](https://github.com/JoviDeCroock/pracht/pull/70) [`ddd50a1`](https://github.com/JoviDeCroock/pracht/commit/ddd50a1edf82a6884881a91ce7172d87ec571cde) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add `pracht inspect` as a machine-readable app graph command.
8
+
9
+ The CLI can now emit resolved routes, API handlers, and build metadata via:
10
+
11
+ - `pracht inspect routes --json`
12
+ - `pracht inspect api --json`
13
+ - `pracht inspect build --json`
14
+ - `pracht inspect --json`
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [[`0d33c3d`](https://github.com/JoviDeCroock/pracht/commit/0d33c3dee00bf3940dc56bef3a171249a3d73e21), [`ba1eaea`](https://github.com/JoviDeCroock/pracht/commit/ba1eaeaf68ab63b47b08411fbdafae2fd98e5f09)]:
19
+ - @pracht/core@0.2.0
20
+
21
+ ## 1.0.0
22
+
23
+ ### Major Changes
24
+
25
+ - [#58](https://github.com/JoviDeCroock/pracht/pull/58) [`6bf6738`](https://github.com/JoviDeCroock/pracht/commit/6bf6738469c8533db2890a89b4edcb92bbbb1011) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add framework-native `pracht generate route|shell|middleware|api` scaffolding commands, add `pracht doctor` with optional JSON output, and remove the Node-specific `pracht preview` command.
26
+
27
+ ### Minor Changes
28
+
29
+ - [#69](https://github.com/JoviDeCroock/pracht/pull/69) [`527e030`](https://github.com/JoviDeCroock/pracht/commit/527e030017f269b7cff51e96a0bcb98bbd1bff3d) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Add a fast `pracht verify` command with optional `--changed` and `--json`
30
+ output for framework-aware manifest, pages-router, and API route validation.
31
+
32
+ ### Patch Changes
33
+
34
+ - [#63](https://github.com/JoviDeCroock/pracht/pull/63) [`cf71d67`](https://github.com/JoviDeCroock/pracht/commit/cf71d6781012cc5f79bf5e557658c9fb9112832e) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Separate HTML and route-state cache variants across framework responses and build outputs.
35
+
36
+ Page responses now vary on `x-pracht-route-state-request`, framework-generated
37
+ route-state responses default to `Cache-Control: no-store`, and Node/preview
38
+ cached HTML paths no longer intercept route-state fetches. Vercel build output
39
+ now routes route-state requests to the edge function before static rewrites.
40
+
41
+ - [#62](https://github.com/JoviDeCroock/pracht/pull/62) [`4017a4a`](https://github.com/JoviDeCroock/pracht/commit/4017a4a59ef702de14a3eb835b0d7bf0967509f8) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Serve static assets directly from the Node adapter with proper Cache-Control headers. Hashed assets under /assets/ get immutable caching; HTML gets must-revalidate. Preview server now mirrors production caching behavior.
42
+
43
+ - [#51](https://github.com/JoviDeCroock/pracht/pull/51) [`db5f6d0`](https://github.com/JoviDeCroock/pracht/commit/db5f6d0a6770cd36fbcdaea708d2f161d2be23d3) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Apply default security headers to static asset responses across adapters
44
+
45
+ Cloudflare static assets now inherit the same permissions-policy, referrer-policy, x-content-type-options, and x-frame-options headers that dynamic responses already receive. Vercel build output config now emits a headers section so static files served by Vercel's CDN also get the baseline security headers.
46
+
47
+ - Updated dependencies [[`b34695f`](https://github.com/JoviDeCroock/pracht/commit/b34695f8e6cfaf2e00b77c451395351565ff3b7c), [`bb9480e`](https://github.com/JoviDeCroock/pracht/commit/bb9480ee6a22b3bbb744f174e9132fd8dda446b4), [`4c885be`](https://github.com/JoviDeCroock/pracht/commit/4c885be049049fe2f1b0bbcfe3a39aa63f7364c0), [`cf71d67`](https://github.com/JoviDeCroock/pracht/commit/cf71d6781012cc5f79bf5e557658c9fb9112832e), [`8b71a9f`](https://github.com/JoviDeCroock/pracht/commit/8b71a9f3a7d6fd8d43bea6767d59bfa2d5b28abb), [`4e9b705`](https://github.com/JoviDeCroock/pracht/commit/4e9b7053b5bedadedd39e6343e7a887864e094dd), [`9fc392f`](https://github.com/JoviDeCroock/pracht/commit/9fc392f132b5d34ee9da72f389c6ac15fe2f1161), [`12829ec`](https://github.com/JoviDeCroock/pracht/commit/12829ec075d269e2511387543c4ad592ae5d8c2a)]:
48
+ - @pracht/core@0.1.0
49
+
3
50
  ## 0.0.1
4
51
 
5
52
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @pracht/cli
2
2
 
3
- Command-line tool for developing and building pracht apps.
3
+ Command-line tool for developing, building, validating, and scaffolding pracht apps.
4
4
 
5
5
  ## Install
6
6
 
@@ -18,6 +18,73 @@ 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
- ### `pracht preview`
21
+ For Node.js targets, run the built server with:
22
22
 
23
- Run a production smoke test against the built output.
23
+ ```bash
24
+ node dist/server/server.js
25
+ ```
26
+
27
+ ### `pracht verify`
28
+
29
+ Run fast framework-aware verification checks without paying for a full build or
30
+ test loop. Use `--changed` to focus on changed manifest-managed files and
31
+ `--json` for machine-readable output.
32
+
33
+ ```bash
34
+ pracht verify
35
+ pracht verify --changed
36
+ pracht verify --json
37
+ ```
38
+
39
+ ### `pracht generate route`
40
+
41
+ Create a new route module. In manifest apps this also updates `src/routes.ts`.
42
+
43
+ ```bash
44
+ pracht generate route --path /dashboard --render ssr --shell app --middleware auth
45
+ ```
46
+
47
+ ### `pracht generate shell`
48
+
49
+ Create a shell module and register it in the app manifest.
50
+
51
+ ```bash
52
+ pracht generate shell --name app
53
+ ```
54
+
55
+ ### `pracht generate middleware`
56
+
57
+ Create a middleware module and register it in the app manifest.
58
+
59
+ ```bash
60
+ pracht generate middleware --name auth
61
+ ```
62
+
63
+ ### `pracht generate api`
64
+
65
+ Create an API route under `src/api/`.
66
+
67
+ ```bash
68
+ pracht generate api --path /health --methods GET,POST
69
+ ```
70
+
71
+ ### `pracht inspect`
72
+
73
+ Inspect the resolved app graph. Use `--json` for agent/tool consumption.
74
+
75
+ ```bash
76
+ pracht inspect --json
77
+ pracht inspect routes --json
78
+ pracht inspect api --json
79
+ pracht inspect build --json
80
+ ```
81
+
82
+ ### `pracht doctor`
83
+
84
+ Validate the local app wiring across the whole project. Use `--json` for
85
+ machine-readable output.
86
+
87
+ ```bash
88
+ pracht doctor
89
+ pracht doctor --json
90
+ ```
package/bin/pracht.js CHANGED
@@ -1,29 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createServer as createHttpServer } from "node:http";
4
- import { resolve, join, dirname, extname } from "node:path";
5
- import {
6
- existsSync,
7
- statSync,
8
- mkdirSync,
9
- writeFileSync,
10
- createReadStream,
11
- readFileSync,
12
- rmSync,
13
- cpSync,
14
- } from "node:fs";
15
- import { createServer, build as viteBuild } from "vite";
16
-
17
- const DEFAULT_SECURITY_HEADERS = {
18
- "permissions-policy":
19
- "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()",
20
- "referrer-policy": "strict-origin-when-cross-origin",
21
- "x-content-type-options": "nosniff",
22
- "x-frame-options": "SAMEORIGIN",
23
- };
24
-
25
- const VERSION = "0.0.0";
26
- const command = process.argv[2];
3
+ import { doctorCommand } from "../lib/commands/doctor.js";
4
+ import { buildCommand } from "../lib/commands/build.js";
5
+ import { devCommand } from "../lib/commands/dev.js";
6
+ import { generateCommand } from "../lib/commands/generate.js";
7
+ import { verifyCommand } from "../lib/commands/verify.js";
8
+ import { inspectCommand } from "../lib/commands/inspect.js";
9
+ import { handleCliError, printHelp } from "../lib/cli.js";
10
+ import { VERSION } from "../lib/constants.js";
11
+
12
+ const argv = process.argv.slice(2);
13
+ const command = argv[0];
14
+ const jsonOutput = argv.includes("--json");
27
15
 
28
16
  if (!command || command === "help" || command === "--help" || command === "-h") {
29
17
  printHelp();
@@ -35,435 +23,19 @@ if (command === "--version" || command === "-v") {
35
23
  process.exit(0);
36
24
  }
37
25
 
38
- const handlers = { dev, build, preview };
26
+ const handlers = {
27
+ build: buildCommand,
28
+ dev: devCommand,
29
+ doctor: doctorCommand,
30
+ generate: generateCommand,
31
+ verify: verifyCommand,
32
+ inspect: inspectCommand,
33
+ };
39
34
 
40
35
  if (!(command in handlers)) {
41
- console.error(`Unknown pracht command: ${command}`);
42
- printHelp();
43
- process.exit(1);
36
+ handleCliError(new Error(`Unknown pracht command: ${command}`), { json: false });
44
37
  }
45
38
 
46
- handlers[command]().catch((err) => {
47
- console.error(err);
48
- process.exit(1);
39
+ handlers[command](argv.slice(1)).catch((error) => {
40
+ handleCliError(error, { json: jsonOutput });
49
41
  });
50
-
51
- // ---------------------------------------------------------------------------
52
- // Commands
53
- // ---------------------------------------------------------------------------
54
-
55
- async function dev() {
56
- const port = parseInt(process.env.PORT || process.argv[3] || "3000", 10);
57
-
58
- const server = await createServer({
59
- root: process.cwd(),
60
- server: { port },
61
- });
62
-
63
- await server.listen();
64
- server.printUrls();
65
- }
66
-
67
- async function build() {
68
- // 1. Client build
69
- // Use outDir "dist" — Vite 8's environment API (used by @cloudflare/vite-plugin)
70
- // outputs the client environment to <outDir>/client/, so "dist" → "dist/client/".
71
- console.log("\n Building client...\n");
72
- await viteBuild({
73
- root: process.cwd(),
74
- build: {
75
- outDir: "dist",
76
- manifest: true,
77
- rollupOptions: {
78
- input: "virtual:pracht/client",
79
- },
80
- },
81
- });
82
-
83
- // 2. SSR build
84
- console.log("\n Building server...\n");
85
- await viteBuild({
86
- root: process.cwd(),
87
- build: {
88
- ssr: "virtual:pracht/server",
89
- outDir: "dist/server",
90
- },
91
- });
92
-
93
- // 3. SSG prerendering
94
- const root = process.cwd();
95
- const serverEntry = resolve(root, "dist/server/server.js");
96
- const clientDir = resolve(root, "dist/client");
97
-
98
- if (existsSync(serverEntry)) {
99
- const serverMod = await import(serverEntry);
100
- // Use prerenderApp from the server bundle to share the same Preact context
101
- // instances as route/shell modules — avoids dual-copy issues during SSG.
102
- const { prerenderApp } = serverMod;
103
-
104
- // Read the Vite manifest for asset URLs
105
- const manifestPath = resolve(clientDir, ".vite/manifest.json");
106
- const viteManifest = existsSync(manifestPath)
107
- ? JSON.parse(readFileSync(manifestPath, "utf-8"))
108
- : {};
109
-
110
- const clientEntry = viteManifest["virtual:pracht/client"];
111
- const clientEntryUrl = clientEntry ? `/${clientEntry.file}` : undefined;
112
-
113
- // Build per-source-file CSS manifest by walking static imports transitively.
114
- // This ensures each page gets only the CSS it actually needs.
115
- function collectTransitiveCss(key) {
116
- const css = new Set();
117
- const visited = new Set();
118
- function collect(k) {
119
- if (visited.has(k)) return;
120
- visited.add(k);
121
- const entry = viteManifest[k];
122
- if (!entry) return;
123
- for (const c of entry.css ?? []) css.add(c);
124
- for (const imp of entry.imports ?? []) collect(imp);
125
- }
126
- collect(key);
127
- return [...css];
128
- }
129
-
130
- const cssManifest = {};
131
- for (const [key, entry] of Object.entries(viteManifest)) {
132
- if (!entry.src) continue;
133
- const css = collectTransitiveCss(key);
134
- if (css.length > 0) {
135
- cssManifest[key] = css.map((f) => `/${f}`);
136
- }
137
- }
138
-
139
- const { pages, isgManifest } = await prerenderApp({
140
- app: serverMod.resolvedApp,
141
- registry: serverMod.registry,
142
- clientEntryUrl,
143
- cssManifest,
144
- withISGManifest: true,
145
- });
146
-
147
- if (pages.length > 0) {
148
- console.log(`\n Prerendering ${pages.length} SSG/ISG route(s)...\n`);
149
- for (const page of pages) {
150
- const filePath =
151
- page.path === "/"
152
- ? join(clientDir, "index.html")
153
- : join(clientDir, page.path, "index.html");
154
-
155
- mkdirSync(dirname(filePath), { recursive: true });
156
- writeFileSync(filePath, page.html, "utf-8");
157
- console.log(` ${page.path} → ${filePath.replace(root + "/", "")}`);
158
- }
159
- }
160
-
161
- // Write ISG manifest for the preview/production server
162
- if (Object.keys(isgManifest).length > 0) {
163
- const isgManifestPath = resolve(root, "dist/server/isg-manifest.json");
164
- writeFileSync(isgManifestPath, JSON.stringify(isgManifest, null, 2), "utf-8");
165
- console.log(
166
- `\n ISG manifest → dist/server/isg-manifest.json (${Object.keys(isgManifest).length} route(s))\n`,
167
- );
168
- }
169
-
170
- if (serverMod.buildTarget === "cloudflare") {
171
- console.log(`\n Cloudflare worker → dist/server/server.js\n`);
172
- console.log(` Deploy with: wrangler deploy\n`);
173
- }
174
-
175
- if (serverMod.buildTarget === "vercel") {
176
- const outputPath = writeVercelBuildOutput({
177
- functionName: serverMod.vercelFunctionName,
178
- regions: serverMod.vercelRegions,
179
- root,
180
- staticRoutes: pages.map((page) => page.path).filter((path) => !(path in isgManifest)),
181
- isgRoutes: Object.keys(isgManifest),
182
- });
183
-
184
- console.log(`\n Vercel build output → ${outputPath}\n`);
185
- }
186
- }
187
-
188
- console.log("\n Build complete.\n");
189
- }
190
-
191
- async function preview() {
192
- const root = process.cwd();
193
- const clientDir = resolve(root, "dist/client");
194
- const serverEntry = resolve(root, "dist/server/server.js");
195
-
196
- if (!existsSync(serverEntry)) {
197
- console.error("Server build not found at dist/server/. Run `pracht build` first.");
198
- process.exit(1);
199
- }
200
-
201
- const serverMod = await import(serverEntry);
202
- const { handlePrachtRequest } = await import("@pracht/core");
203
-
204
- // Load ISG manifest if it exists
205
- const isgManifestPath = resolve(root, "dist/server/isg-manifest.json");
206
- const isgManifest = existsSync(isgManifestPath)
207
- ? JSON.parse(readFileSync(isgManifestPath, "utf-8"))
208
- : {};
209
-
210
- // Read the Vite manifest for asset URLs
211
- const manifestPath = resolve(clientDir, ".vite/manifest.json");
212
- const viteManifest = existsSync(manifestPath)
213
- ? JSON.parse(readFileSync(manifestPath, "utf-8"))
214
- : {};
215
-
216
- // Find the client entry asset from the manifest
217
- const clientEntry = viteManifest["virtual:pracht/client"];
218
- const clientEntryUrl = clientEntry ? `/${clientEntry.file}` : undefined;
219
- const cssUrls = (clientEntry?.css ?? []).map((f) => `/${f}`);
220
-
221
- const MIME_TYPES = {
222
- ".html": "text/html",
223
- ".js": "application/javascript",
224
- ".css": "text/css",
225
- ".json": "application/json",
226
- ".png": "image/png",
227
- ".jpg": "image/jpeg",
228
- ".svg": "image/svg+xml",
229
- ".woff": "font/woff",
230
- ".woff2": "font/woff2",
231
- };
232
-
233
- const port = parseInt(process.argv[3] || "3000", 10);
234
-
235
- const server = createHttpServer(async (req, res) => {
236
- const url = req.url ?? "/";
237
-
238
- const parsedUrl = new URL(url, "http://localhost");
239
-
240
- // ISG stale-while-revalidate check
241
- if (req.method === "GET" && parsedUrl.pathname in isgManifest) {
242
- const entry = isgManifest[parsedUrl.pathname];
243
- const htmlPath =
244
- parsedUrl.pathname === "/"
245
- ? join(clientDir, "index.html")
246
- : join(clientDir, parsedUrl.pathname, "index.html");
247
-
248
- if (existsSync(htmlPath) && statSync(htmlPath).isFile()) {
249
- const stat = statSync(htmlPath);
250
- const ageMs = Date.now() - stat.mtimeMs;
251
- const isStale = entry.revalidate.kind === "time" && ageMs > entry.revalidate.seconds * 1000;
252
-
253
- setDefaultSecurityHeaders(res, {
254
- "content-type": "text/html; charset=utf-8",
255
- "x-pracht-isg": isStale ? "stale" : "fresh",
256
- });
257
- createReadStream(htmlPath).pipe(res);
258
-
259
- if (isStale) {
260
- // Background regeneration
261
- const regenRequest = new Request(new URL(parsedUrl.pathname, "http://localhost"), {
262
- method: "GET",
263
- });
264
- handlePrachtRequest({
265
- app: serverMod.resolvedApp,
266
- registry: serverMod.registry,
267
- request: regenRequest,
268
- clientEntryUrl,
269
- cssUrls,
270
- })
271
- .then(async (response) => {
272
- if (response.status === 200) {
273
- const { mkdirSync, writeFileSync } = await import("node:fs");
274
- mkdirSync(dirname(htmlPath), { recursive: true });
275
- writeFileSync(htmlPath, await response.text(), "utf-8");
276
- }
277
- })
278
- .catch((err) => {
279
- console.error(`ISG regeneration failed for ${parsedUrl.pathname}:`, err);
280
- });
281
- }
282
- return;
283
- }
284
- }
285
-
286
- // Try static file first
287
- const filePath = resolve(clientDir, "." + url);
288
- if (!filePath.startsWith(clientDir + "/") && filePath !== clientDir) {
289
- res.statusCode = 403;
290
- res.end("Forbidden");
291
- return;
292
- }
293
- if (existsSync(filePath) && statSync(filePath).isFile()) {
294
- const ext = extname(filePath);
295
- const headers = {
296
- "content-type": MIME_TYPES[ext] || "application/octet-stream",
297
- };
298
- if (ext === ".html") {
299
- setDefaultSecurityHeaders(res, headers);
300
- } else {
301
- for (const [key, value] of Object.entries(headers)) {
302
- res.setHeader(key, value);
303
- }
304
- }
305
- createReadStream(filePath).pipe(res);
306
- return;
307
- }
308
-
309
- // SSR fallback
310
- try {
311
- const headers = new Headers();
312
- for (const [key, value] of Object.entries(req.headers)) {
313
- if (value === undefined) continue;
314
- if (Array.isArray(value)) {
315
- for (const v of value) headers.append(key, v);
316
- } else {
317
- headers.set(key, value);
318
- }
319
- }
320
-
321
- const protocol = req.headers["x-forwarded-proto"] || "http";
322
- const host = req.headers.host || "localhost";
323
- const webRequest = new Request(new URL(url, `${protocol}://${host}`), {
324
- method: req.method,
325
- headers,
326
- });
327
-
328
- const response = await handlePrachtRequest({
329
- app: serverMod.resolvedApp,
330
- registry: serverMod.registry,
331
- request: webRequest,
332
- clientEntryUrl,
333
- cssUrls,
334
- apiRoutes: serverMod.apiRoutes,
335
- });
336
-
337
- res.statusCode = response.status;
338
- response.headers.forEach((value, key) => {
339
- res.setHeader(key, value);
340
- });
341
-
342
- if (!response.body) {
343
- res.end();
344
- return;
345
- }
346
-
347
- const body = Buffer.from(await response.arrayBuffer());
348
- res.end(body);
349
- } catch (err) {
350
- console.error("SSR error:", err);
351
- res.statusCode = 500;
352
- res.end("Internal Server Error");
353
- }
354
- });
355
-
356
- server.listen(port, () => {
357
- console.log(`\n pracht preview server running at http://localhost:${port}\n`);
358
- });
359
- }
360
-
361
- function printHelp() {
362
- console.log(`pracht ${VERSION}
363
-
364
- Usage:
365
- pracht dev Start development server with HMR
366
- pracht build Production build (client + server)
367
- pracht preview Preview the production build
368
- `);
369
- }
370
-
371
- function setDefaultSecurityHeaders(res, headers = {}) {
372
- for (const [key, value] of Object.entries({
373
- ...DEFAULT_SECURITY_HEADERS,
374
- ...headers,
375
- })) {
376
- res.setHeader(key, value);
377
- }
378
- }
379
-
380
- function writeVercelBuildOutput({ functionName, regions, root, staticRoutes, isgRoutes }) {
381
- const outputDir = join(root, ".vercel/output");
382
- const staticDir = join(outputDir, "static");
383
- const functionDir = join(outputDir, "functions", `${functionName || "render"}.func`);
384
-
385
- rmSync(outputDir, { force: true, recursive: true });
386
- mkdirSync(outputDir, { recursive: true });
387
- cpSync(join(root, "dist/client"), staticDir, { recursive: true });
388
- cpSync(join(root, "dist/server"), functionDir, { recursive: true });
389
-
390
- writeFileSync(
391
- join(outputDir, "config.json"),
392
- `${JSON.stringify(createVercelOutputConfig({ functionName, staticRoutes, isgRoutes }), null, 2)}\n`,
393
- "utf-8",
394
- );
395
- writeFileSync(
396
- join(functionDir, ".vc-config.json"),
397
- `${JSON.stringify(createVercelFunctionConfig({ regions }), null, 2)}\n`,
398
- "utf-8",
399
- );
400
-
401
- return ".vercel/output";
402
- }
403
-
404
- function createVercelOutputConfig({ functionName, staticRoutes, isgRoutes }) {
405
- const target = `/${functionName || "render"}`;
406
- const routes = [];
407
-
408
- for (const route of sortStaticRoutes(staticRoutes)) {
409
- routes.push({
410
- src: routeToRouteExpression(route),
411
- dest: routeToStaticHtmlPath(route),
412
- });
413
- }
414
-
415
- for (const route of isgRoutes) {
416
- routes.push({
417
- src: routeToRouteExpression(route),
418
- dest: target,
419
- });
420
- }
421
-
422
- routes.push({ handle: "filesystem" });
423
- routes.push({ src: "/(.*)", dest: target });
424
-
425
- return {
426
- version: 3,
427
- routes,
428
- framework: {
429
- version: VERSION,
430
- },
431
- };
432
- }
433
-
434
- function createVercelFunctionConfig({ regions }) {
435
- const config = {
436
- runtime: "edge",
437
- entrypoint: "server.js",
438
- };
439
-
440
- if (regions) {
441
- config.regions = regions;
442
- }
443
-
444
- return config;
445
- }
446
-
447
- function sortStaticRoutes(routes) {
448
- return [...new Set(routes)].sort((left, right) => right.length - left.length);
449
- }
450
-
451
- function routeToRouteExpression(route) {
452
- if (route === "/") {
453
- return "^/$";
454
- }
455
-
456
- return `^${escapeRegex(route)}/?$`;
457
- }
458
-
459
- function routeToStaticHtmlPath(route) {
460
- if (route === "/") {
461
- return "/index.html";
462
- }
463
-
464
- return `${route}/index.html`;
465
- }
466
-
467
- function escapeRegex(value) {
468
- return value.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
469
- }
@@ -0,0 +1,73 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ const MANIFEST_PATHS = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"];
5
+
6
+ export function readClientBuildAssets(root = process.cwd()) {
7
+ const manifestPath = MANIFEST_PATHS.map((candidate) => resolve(root, candidate)).find((path) =>
8
+ existsSync(path),
9
+ );
10
+
11
+ if (!manifestPath) {
12
+ return {
13
+ clientEntryUrl: null,
14
+ cssManifest: {},
15
+ jsManifest: {},
16
+ };
17
+ }
18
+
19
+ const rawManifest = readFileSync(manifestPath, "utf-8");
20
+ const manifest = JSON.parse(rawManifest);
21
+ const clientEntry = manifest["virtual:pracht/client"];
22
+
23
+ function collectTransitiveDeps(key) {
24
+ const css = new Set();
25
+ const js = new Set();
26
+ const visited = new Set();
27
+
28
+ function collect(currentKey) {
29
+ if (visited.has(currentKey)) return;
30
+ visited.add(currentKey);
31
+
32
+ const entry = manifest[currentKey];
33
+ if (!entry) return;
34
+
35
+ for (const cssFile of entry.css ?? []) {
36
+ css.add(cssFile);
37
+ }
38
+
39
+ js.add(entry.file);
40
+
41
+ for (const importedKey of entry.imports ?? []) {
42
+ collect(importedKey);
43
+ }
44
+ }
45
+
46
+ collect(key);
47
+ return {
48
+ css: [...css],
49
+ js: [...js],
50
+ };
51
+ }
52
+
53
+ const cssManifest = {};
54
+ const jsManifest = {};
55
+
56
+ for (const [key, entry] of Object.entries(manifest)) {
57
+ if (!entry.src) continue;
58
+
59
+ const deps = collectTransitiveDeps(key);
60
+ if (deps.css.length > 0) {
61
+ cssManifest[key] = deps.css.map((file) => `/${file}`);
62
+ }
63
+ if (deps.js.length > 0) {
64
+ jsManifest[key] = deps.js.map((file) => `/${file}`);
65
+ }
66
+ }
67
+
68
+ return {
69
+ clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
70
+ cssManifest,
71
+ jsManifest,
72
+ };
73
+ }