@pracht/cli 1.2.0 → 1.2.2

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,90 @@
1
1
  # @pracht/cli
2
2
 
3
+ ## 1.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#124](https://github.com/JoviDeCroock/pracht/pull/124) [`8f662c0`](https://github.com/JoviDeCroock/pracht/commit/8f662c0b78b1911a7534ffd7aa4e919cf22a3a42) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Internal refactor: split several large modules into smaller, focused files to improve maintainability. Public APIs are unchanged.
8
+
9
+ - [#132](https://github.com/JoviDeCroock/pracht/pull/132) [`30d867f`](https://github.com/JoviDeCroock/pracht/commit/30d867f4a4cd41107a1ed60c607afe0d51848c3b) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Follow-up security hardening after the main audit fixes.
10
+
11
+ - `@pracht/adapter-node` now supports `canonicalOrigin` so apps can pin
12
+ `request.url` to a known public origin instead of depending on untrusted
13
+ `Host` values. The adapter also treats both `x-pracht-route-state-request`
14
+ and `?_data=1` as route-state transports before any static/ISG HTML serving,
15
+ and ISG regeneration now uses a clean HTML request instead of replaying the
16
+ triggering user's cookies or authorization headers.
17
+ - `@pracht/adapter-cloudflare` now bypasses static asset serving for both
18
+ route-state transports (`x-pracht-route-state-request` and `?_data=1`).
19
+ - `@pracht/cli` now emits a Vercel Build Output rule that sends `?_data=1`
20
+ requests to the render function before static rewrites can serve prerendered
21
+ HTML.
22
+
23
+ - [#131](https://github.com/JoviDeCroock/pracht/pull/131) [`015e987`](https://github.com/JoviDeCroock/pracht/commit/015e987a2de471980fab557e3dbf3d52937ad0ac) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Security hardening across request handling, redirects, and build output.
24
+
25
+ **Framework (`@pracht/core`)**
26
+
27
+ - **Middleware/loader redirects are now validated.** `javascript:`, `data:`,
28
+ `vbscript:`, `blob:`, and `file:` targets are refused server-side (they
29
+ were already refused on the client) and CR/LF in the `Location` value
30
+ throws instead of producing a split response. Non-safe-method redirects
31
+ now default to **303 See Other** rather than 302 so browsers don't
32
+ resend the POST body to the redirect target. `MiddlewareResult`'s
33
+ `redirect` form now accepts an optional `status` override.
34
+ - **CSRF protection for mutating API routes.** Non-GET API requests are
35
+ rejected with 403 unless the browser signals a same-origin/same-site
36
+ fetch (`Sec-Fetch-Site`) or the `Origin` header matches the request
37
+ URL's origin. Opt out per-app via `defineApp({ api: { requireSameOrigin: false } })`.
38
+ - **`_data=1` route-state bypass is now gated.** The query-param form of
39
+ the route-state endpoint now requires `Sec-Fetch-Site: same-origin`/
40
+ `same-site` (or a matching `Origin`). The explicit
41
+ `x-pracht-route-state-request` header is still accepted unconditionally
42
+ (CORS-protected).
43
+ - **Catch-all path traversal at build time is closed.**
44
+ `buildPathFromSegments` now percent-encodes catch-all components
45
+ individually and explicitly neutralises `.` / `..` segments, so a
46
+ `getStaticPaths` returning `{ "*": "../../etc/passwd" }` can no longer
47
+ escape `dist/client/` at SSG/ISG write time.
48
+ - **`headers()` values are validated for CR/LF.** `applyHeaders` now
49
+ throws a consistent framework error on response-splitting attempts,
50
+ regardless of adapter-specific Headers implementation behaviour.
51
+ - **`debugErrors` is ignored in production.** When `NODE_ENV=production`,
52
+ `debugErrors: true` is refused (with a one-shot console warning) so a
53
+ misconfigured deploy cannot leak stack traces and module paths.
54
+
55
+ **Adapter (`@pracht/adapter-node`)**
56
+
57
+ - **Symlinks are no longer followed by the static server.** `resolveStaticFile`
58
+ now uses `lstat` and rejects files whose inode is a symlink, preventing
59
+ a malicious build artifact from exposing files outside `dist/client/`.
60
+ - **ISG cache is path-contained.** The on-disk write path is now
61
+ `resolve()`-checked against the static root, rejecting any URL path
62
+ that would escape via `..`, encoded separators, or NUL bytes.
63
+ - **ISG skips the on-disk cache when the response is user-specific.**
64
+ Responses that set `Cache-Control: no-store`/`private`, `Set-Cookie`,
65
+ or a `Vary` covering `cookie`/`authorization`/`*` are served through
66
+ but not written to disk, closing a per-user cache-poisoning window.
67
+
68
+ **Packaging**
69
+
70
+ - `@pracht/cli` now has an explicit `files` allowlist so future
71
+ workdir additions can't accidentally ship in the npm tarball.
72
+ - `create-pracht`'s bin entry is now executable in the repository.
73
+
74
+ - Updated dependencies [[`caae3cb`](https://github.com/JoviDeCroock/pracht/commit/caae3cb53e0b6136ef78c3ac189a0d0ab82e4df7), [`8f662c0`](https://github.com/JoviDeCroock/pracht/commit/8f662c0b78b1911a7534ffd7aa4e919cf22a3a42), [`901ef5b`](https://github.com/JoviDeCroock/pracht/commit/901ef5b7958e4066d5382f836d098bded8bfe320), [`015e987`](https://github.com/JoviDeCroock/pracht/commit/015e987a2de471980fab557e3dbf3d52937ad0ac)]:
75
+ - @pracht/core@0.3.0
76
+
77
+ ## 1.2.1
78
+
79
+ ### Patch Changes
80
+
81
+ - [#116](https://github.com/JoviDeCroock/pracht/pull/116) [`411da18`](https://github.com/JoviDeCroock/pracht/commit/411da18d0fa8bbc20270729584c6677376be7f24) Thanks [@kinngh](https://github.com/kinngh)! - Strip server-only route and shell exports from client module imports so inline loaders can statically import server-only dependencies without evaluating them in browser bundles.
82
+
83
+ - [#117](https://github.com/JoviDeCroock/pracht/pull/117) [`39a226d`](https://github.com/JoviDeCroock/pracht/commit/39a226d1023317c357df8b72e020034a2c68d896) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Copy public/ folder contents to dist/client/ during build so that static assets like favicons and robots.txt are available for deployment platforms
84
+
85
+ - Updated dependencies [[`f0ed41e`](https://github.com/JoviDeCroock/pracht/commit/f0ed41e4b886e751fbdfd29ae10f880c3aa364d4), [`49732fc`](https://github.com/JoviDeCroock/pracht/commit/49732fc78a776cbaabe9579e5a7f2fb154497479), [`d88c9e4`](https://github.com/JoviDeCroock/pracht/commit/d88c9e4b8347c4d3ecacdbc5f7674ee38af0092e), [`7ee2a93`](https://github.com/JoviDeCroock/pracht/commit/7ee2a936357a0f0b4ff7f5a7f6f3206b070f3890), [`00c4014`](https://github.com/JoviDeCroock/pracht/commit/00c401410b13c2d904c0beafc4da62dfb8f0f91e), [`f0ed41e`](https://github.com/JoviDeCroock/pracht/commit/f0ed41e4b886e751fbdfd29ae10f880c3aa364d4)]:
86
+ - @pracht/core@0.2.7
87
+
3
88
  ## 1.2.0
4
89
 
5
90
  ### Minor Changes
@@ -1,5 +1,5 @@
1
1
  import { r as VERSION } from "./index.mjs";
2
- import { t as readClientBuildAssets } from "./build-metadata-IABPFFKk.mjs";
2
+ import { t as readClientBuildAssets } from "./build-metadata-BOChHO8g.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
@@ -36,6 +36,14 @@ function createVercelOutputConfig({ functionName, headersManifest, staticRoutes,
36
36
  value: "1"
37
37
  }],
38
38
  src: "/(.*)"
39
+ }, {
40
+ dest: target,
41
+ has: [{
42
+ type: "query",
43
+ key: "_data",
44
+ value: "1"
45
+ }],
46
+ src: "/(.*)"
39
47
  }];
40
48
  for (const route of sortStaticRoutes(staticRoutes)) routes.push({
41
49
  dest: routeToStaticHtmlPath(route),
@@ -157,6 +165,8 @@ var build_default = defineCommand({
157
165
  });
158
166
  }
159
167
  }
168
+ const publicDir = resolve(root, "public");
169
+ if (existsSync(publicDir)) cpSync(publicDir, clientDir, { recursive: true });
160
170
  if (existsSync(serverEntry)) {
161
171
  const serverMod = await import(serverEntry);
162
172
  const { prerenderApp } = serverMod;
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  //#region src/build-metadata.ts
4
4
  const MANIFEST_PATHS = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"];
5
+ const PRACHT_CLIENT_MODULE_QUERY = "pracht-client";
5
6
  function readClientBuildAssets(root = process.cwd()) {
6
7
  const manifestPath = MANIFEST_PATHS.map((candidate) => resolve(root, candidate)).find((path) => existsSync(path));
7
8
  if (!manifestPath) return {
@@ -36,8 +37,9 @@ function readClientBuildAssets(root = process.cwd()) {
36
37
  for (const [key, entry] of Object.entries(manifest)) {
37
38
  if (!entry.src) continue;
38
39
  const deps = collectTransitiveDeps(key);
39
- if (deps.css.length > 0) cssManifest[key] = deps.css.map((file) => `/${file}`);
40
- if (deps.js.length > 0) jsManifest[key] = deps.js.map((file) => `/${file}`);
40
+ const manifestKey = stripPrachtClientModuleQuery(entry.src);
41
+ if (deps.css.length > 0) cssManifest[manifestKey] = deps.css.map((file) => `/${file}`);
42
+ if (deps.js.length > 0) jsManifest[manifestKey] = deps.js.map((file) => `/${file}`);
41
43
  }
42
44
  return {
43
45
  clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
@@ -45,5 +47,12 @@ function readClientBuildAssets(root = process.cwd()) {
45
47
  jsManifest
46
48
  };
47
49
  }
50
+ function stripPrachtClientModuleQuery(id) {
51
+ const queryStart = id.indexOf("?");
52
+ if (queryStart === -1) return id;
53
+ const path = id.slice(0, queryStart);
54
+ const query = id.slice(queryStart + 1).split("&").filter((part) => part !== PRACHT_CLIENT_MODULE_QUERY);
55
+ return query.length > 0 ? `${path}?${query.join("&")}` : path;
56
+ }
48
57
  //#endregion
49
58
  export { readClientBuildAssets as t };
@@ -1,4 +1,4 @@
1
- import { t as runDoctor } from "./verification-Dfl3X4Zo.mjs";
1
+ import { t as runDoctor } from "./verification-Dwf1ZuNn.mjs";
2
2
  import { defineCommand } from "citty";
3
3
  //#region src/commands/doctor.ts
4
4
  var doctor_default = defineCommand({
@@ -1,7 +1,146 @@
1
- import { a as toManifestModulePath, i as insertArrayItem, n as extractRegistryEntries, o as upsertObjectEntry, t as ensureCoreNamedImport } from "./manifest-DGq1n5LT.mjs";
2
- import { _ as requireEnum, a as readProjectConfig, c as resolveProjectPath, d as writeGeneratedFile, f as ensureTrailingNewline, g as quote, h as parseCommaList, l as resolveRouteModulePath, m as parseApiMethods, n as displayPath, o as resolveApiModulePath, s as resolvePagesRouteModulePath, t as assertFileExists, u as resolveScopedFile, v as requirePositiveInteger } from "./project-DydjqBoS.mjs";
1
+ import { _ as requireEnum, a as readProjectConfig, c as resolveProjectPath, d as writeGeneratedFile, f as ensureTrailingNewline, g as quote, h as parseCommaList, l as resolveRouteModulePath, m as parseApiMethods, n as displayPath, o as resolveApiModulePath, s as resolvePagesRouteModulePath, t as assertFileExists, u as resolveScopedFile, v as requirePositiveInteger } from "./project-BdMiN3s7.mjs";
2
+ import { a as toManifestModulePath, i as insertArrayItem, n as extractRegistryEntries, o as upsertObjectEntry, t as ensureCoreNamedImport } from "./manifest-Bs5hp3gA.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { readFileSync, writeFileSync } from "node:fs";
5
+ //#region src/commands/generate-paths.ts
6
+ function normalizeRoutePathString(value) {
7
+ if (!value || value === "/") return "/";
8
+ const normalized = `/${value}`.replace(/\/+/g, "/");
9
+ return normalized !== "/" && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
10
+ }
11
+ function normalizeApiPath(value) {
12
+ return normalizeRoutePathString(value).replace(/^\/api(?=\/|$)/, "") || "/";
13
+ }
14
+ function hasDynamicSegments(routePath) {
15
+ return routePath.split("/").some((segment) => segment.startsWith(":") || segment === "*");
16
+ }
17
+ function dynamicParamNames(routePath) {
18
+ return routePath.split("/").filter(Boolean).map((segment) => {
19
+ if (segment.startsWith(":")) return segment.slice(1);
20
+ if (segment === "*") return "slug";
21
+ return null;
22
+ }).filter((s) => s !== null);
23
+ }
24
+ function routeIdFromPath(routePath) {
25
+ if (routePath === "/") return "index";
26
+ return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\*/g, "splat")).join("-");
27
+ }
28
+ function titleFromPath(routePath) {
29
+ if (routePath === "/") return "Home";
30
+ return titleCase((routePath.split("/").filter(Boolean).at(-1) ?? "Page").replace(/^:/, "").replace(/\*/g, "slug"));
31
+ }
32
+ function titleCase(value) {
33
+ return value.split(/[-_/]/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
34
+ }
35
+ //#endregion
36
+ //#region src/commands/generate-source.ts
37
+ function buildManifestRouteModuleSource(opts) {
38
+ const sections = buildRouteModuleSections(opts);
39
+ const componentIdx = sections.findIndex((s) => s.startsWith("export function Component"));
40
+ const insertAt = componentIdx === -1 ? sections.length : componentIdx;
41
+ sections.splice(insertAt, 0, "export function head() {", ` return { title: ${quote(opts.title)} };`, "}", "");
42
+ return `${sections.join("\n")}\n`;
43
+ }
44
+ function buildPagesRouteModuleSource(opts) {
45
+ const sections = buildRouteModuleSections(opts);
46
+ const firstExportIdx = sections.findIndex((s) => s.startsWith("export"));
47
+ const insertAt = firstExportIdx === -1 ? sections.length : firstExportIdx;
48
+ sections.splice(insertAt, 0, `export const RENDER_MODE = ${quote(opts.render)};`, "");
49
+ return `${sections.join("\n")}\n`;
50
+ }
51
+ function buildShellModuleSource(name) {
52
+ const title = titleCase(name);
53
+ return [
54
+ "import type { ShellProps } from \"@pracht/core\";",
55
+ "",
56
+ "export function Shell({ children }: ShellProps) {",
57
+ " return (",
58
+ ` <div class=${quote(`${name}-shell`)}>`,
59
+ " <main>{children}</main>",
60
+ " </div>",
61
+ " );",
62
+ "}",
63
+ "",
64
+ "export function head() {",
65
+ ` return { title: ${quote(title)} };`,
66
+ "}",
67
+ ""
68
+ ].join("\n");
69
+ }
70
+ function buildMiddlewareModuleSource() {
71
+ return [
72
+ "import type { MiddlewareFn } from \"@pracht/core\";",
73
+ "",
74
+ "export const middleware: MiddlewareFn = async (_args) => {",
75
+ " return;",
76
+ "};",
77
+ ""
78
+ ].join("\n");
79
+ }
80
+ function buildApiRouteSource({ endpointPath, methods }) {
81
+ return [
82
+ "import type { BaseRouteArgs } from \"@pracht/core\";",
83
+ "",
84
+ ...methods.flatMap((method, index) => {
85
+ const lines = buildApiMethodSource(method, methods, endpointPath);
86
+ if (index === methods.length - 1) return lines;
87
+ return [...lines, ""];
88
+ }),
89
+ ""
90
+ ].join("\n");
91
+ }
92
+ function buildRouteModuleSections(opts) {
93
+ const { includeErrorBoundary, includeLoader, includeStaticPaths, routePath, title } = opts;
94
+ const params = dynamicParamNames(routePath);
95
+ const imports = [];
96
+ const sections = [];
97
+ if (includeLoader) imports.push("LoaderArgs", "RouteComponentProps");
98
+ if (includeErrorBoundary) imports.push("ErrorBoundaryProps");
99
+ if (imports.length > 0) {
100
+ sections.push(`import type { ${imports.join(", ")} } from "@pracht/core";`);
101
+ sections.push("");
102
+ }
103
+ if (includeLoader) sections.push("export async function loader(_args: LoaderArgs) {", ` return { message: ${quote(`Welcome to ${title}.`)} };`, "}", "");
104
+ if (includeStaticPaths) sections.push("export function getStaticPaths() {", ` return [${buildStaticPathsStub(params)}];`, "}", "");
105
+ if (includeLoader) sections.push("export function Component({ data }: RouteComponentProps<typeof loader>) {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " <p>{data.message}</p>", " </section>", " );", "}");
106
+ else sections.push("export function Component() {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " </section>", " );", "}");
107
+ if (includeErrorBoundary) sections.push("", "export function ErrorBoundary({ error }: ErrorBoundaryProps) {", " return <p>{error.message}</p>;", "}");
108
+ return sections;
109
+ }
110
+ function buildApiMethodSource(method, methods, endpointPath) {
111
+ if (method === "DELETE" || method === "HEAD") return [
112
+ `export function ${method}(_args: BaseRouteArgs) {`,
113
+ " return new Response(null, { status: 204 });",
114
+ "}"
115
+ ];
116
+ if (method === "OPTIONS") return [
117
+ `export function ${method}(_args: BaseRouteArgs) {`,
118
+ " return new Response(null, {",
119
+ ` headers: { allow: ${quote(methods.join(", "))} },`,
120
+ " status: 204,",
121
+ " });",
122
+ "}"
123
+ ];
124
+ if (method === "GET") return [
125
+ `export function ${method}(_args: BaseRouteArgs) {`,
126
+ ` return Response.json({ endpoint: ${quote(`/api${endpointPath}`)}, ok: true });`,
127
+ "}"
128
+ ];
129
+ const status = method === "POST" ? 201 : 200;
130
+ return [
131
+ `export async function ${method}({ request }: BaseRouteArgs) {`,
132
+ " const body = await request.json();",
133
+ ` return Response.json({ body, ok: true }, { status: ${status} });`,
134
+ "}"
135
+ ];
136
+ }
137
+ function buildStaticPathsStub(params) {
138
+ if (params.length === 0) return "{}";
139
+ return `{ ${params.map((name) => `${name}: ${quote(`example-${name}`)}`).join(", ")} }`;
140
+ }
141
+ function escapeJsxText(value) {
142
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
143
+ }
5
144
  var generate_default = defineCommand({
6
145
  meta: {
7
146
  name: "generate",
@@ -260,145 +399,5 @@ function generateApi(args, project) {
260
399
  updated: []
261
400
  };
262
401
  }
263
- function buildManifestRouteModuleSource({ includeErrorBoundary, includeLoader, includeStaticPaths, routePath, title }) {
264
- const params = dynamicParamNames(routePath);
265
- const imports = [];
266
- const sections = [];
267
- if (includeLoader) imports.push("LoaderArgs", "RouteComponentProps");
268
- if (includeErrorBoundary) imports.push("ErrorBoundaryProps");
269
- if (imports.length > 0) {
270
- sections.push(`import type { ${imports.join(", ")} } from "@pracht/core";`);
271
- sections.push("");
272
- }
273
- if (includeLoader) sections.push("export async function loader(_args: LoaderArgs) {", ` return { message: ${quote(`Welcome to ${title}.`)} };`, "}", "");
274
- if (includeStaticPaths) sections.push("export function getStaticPaths() {", ` return [${buildStaticPathsStub(params)}];`, "}", "");
275
- sections.push("export function head() {", ` return { title: ${quote(title)} };`, "}", "");
276
- if (includeLoader) sections.push("export function Component({ data }: RouteComponentProps<typeof loader>) {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " <p>{data.message}</p>", " </section>", " );", "}");
277
- else sections.push("export function Component() {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " </section>", " );", "}");
278
- if (includeErrorBoundary) sections.push("", "export function ErrorBoundary({ error }: ErrorBoundaryProps) {", " return <p>{error.message}</p>;", "}");
279
- return `${sections.join("\n")}\n`;
280
- }
281
- function buildPagesRouteModuleSource({ includeErrorBoundary, includeLoader, includeStaticPaths, render, routePath, title }) {
282
- const params = dynamicParamNames(routePath);
283
- const imports = [];
284
- const sections = [];
285
- if (includeLoader) imports.push("LoaderArgs", "RouteComponentProps");
286
- if (includeErrorBoundary) imports.push("ErrorBoundaryProps");
287
- if (imports.length > 0) {
288
- sections.push(`import type { ${imports.join(", ")} } from "@pracht/core";`);
289
- sections.push("");
290
- }
291
- sections.push(`export const RENDER_MODE = ${quote(render)};`, "");
292
- if (includeLoader) sections.push("export async function loader(_args: LoaderArgs) {", ` return { message: ${quote(`Welcome to ${title}.`)} };`, "}", "");
293
- if (includeStaticPaths) sections.push("export function getStaticPaths() {", ` return [${buildStaticPathsStub(params)}];`, "}", "");
294
- if (includeLoader) sections.push("export function Component({ data }: RouteComponentProps<typeof loader>) {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " <p>{data.message}</p>", " </section>", " );", "}");
295
- else sections.push("export function Component() {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " </section>", " );", "}");
296
- if (includeErrorBoundary) sections.push("", "export function ErrorBoundary({ error }: ErrorBoundaryProps) {", " return <p>{error.message}</p>;", "}");
297
- return `${sections.join("\n")}\n`;
298
- }
299
- function buildShellModuleSource(name) {
300
- const title = titleCase(name);
301
- return [
302
- "import type { ShellProps } from \"@pracht/core\";",
303
- "",
304
- "export function Shell({ children }: ShellProps) {",
305
- " return (",
306
- ` <div class=${quote(`${name}-shell`)}>`,
307
- " <main>{children}</main>",
308
- " </div>",
309
- " );",
310
- "}",
311
- "",
312
- "export function head() {",
313
- ` return { title: ${quote(title)} };`,
314
- "}",
315
- ""
316
- ].join("\n");
317
- }
318
- function buildMiddlewareModuleSource() {
319
- return [
320
- "import type { MiddlewareFn } from \"@pracht/core\";",
321
- "",
322
- "export const middleware: MiddlewareFn = async (_args) => {",
323
- " return;",
324
- "};",
325
- ""
326
- ].join("\n");
327
- }
328
- function buildApiRouteSource({ endpointPath, methods }) {
329
- return [
330
- "import type { BaseRouteArgs } from \"@pracht/core\";",
331
- "",
332
- ...methods.flatMap((method, index) => {
333
- const lines = buildApiMethodSource(method, methods, endpointPath);
334
- if (index === methods.length - 1) return lines;
335
- return [...lines, ""];
336
- }),
337
- ""
338
- ].join("\n");
339
- }
340
- function buildApiMethodSource(method, methods, endpointPath) {
341
- if (method === "DELETE" || method === "HEAD") return [
342
- `export function ${method}(_args: BaseRouteArgs) {`,
343
- " return new Response(null, { status: 204 });",
344
- "}"
345
- ];
346
- if (method === "OPTIONS") return [
347
- `export function ${method}(_args: BaseRouteArgs) {`,
348
- " return new Response(null, {",
349
- ` headers: { allow: ${quote(methods.join(", "))} },`,
350
- " status: 204,",
351
- " });",
352
- "}"
353
- ];
354
- if (method === "GET") return [
355
- `export function ${method}(_args: BaseRouteArgs) {`,
356
- ` return Response.json({ endpoint: ${quote(`/api${endpointPath}`)}, ok: true });`,
357
- "}"
358
- ];
359
- const status = method === "POST" ? 201 : 200;
360
- return [
361
- `export async function ${method}({ request }: BaseRouteArgs) {`,
362
- " const body = await request.json();",
363
- ` return Response.json({ body, ok: true }, { status: ${status} });`,
364
- "}"
365
- ];
366
- }
367
- function normalizeRoutePathString(value) {
368
- if (!value || value === "/") return "/";
369
- const normalized = `/${value}`.replace(/\/+/g, "/");
370
- return normalized !== "/" && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
371
- }
372
- function normalizeApiPath(value) {
373
- return normalizeRoutePathString(value).replace(/^\/api(?=\/|$)/, "") || "/";
374
- }
375
- function hasDynamicSegments(routePath) {
376
- return routePath.split("/").some((segment) => segment.startsWith(":") || segment === "*");
377
- }
378
- function dynamicParamNames(routePath) {
379
- return routePath.split("/").filter(Boolean).map((segment) => {
380
- if (segment.startsWith(":")) return segment.slice(1);
381
- if (segment === "*") return "slug";
382
- return null;
383
- }).filter((s) => s !== null);
384
- }
385
- function routeIdFromPath(routePath) {
386
- if (routePath === "/") return "index";
387
- return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\*/g, "splat")).join("-");
388
- }
389
- function titleFromPath(routePath) {
390
- if (routePath === "/") return "Home";
391
- return titleCase((routePath.split("/").filter(Boolean).at(-1) ?? "Page").replace(/^:/, "").replace(/\*/g, "slug"));
392
- }
393
- function titleCase(value) {
394
- return value.split(/[-_/]/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
395
- }
396
- function buildStaticPathsStub(params) {
397
- if (params.length === 0) return "{}";
398
- return `{ ${params.map((name) => `${name}: ${quote(`example-${name}`)}`).join(", ")} }`;
399
- }
400
- function escapeJsxText(value) {
401
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
402
- }
403
402
  //#endregion
404
403
  export { generate_default as default };
package/dist/index.mjs CHANGED
@@ -29,12 +29,12 @@ runMain(defineCommand({
29
29
  description: "The pracht CLI"
30
30
  },
31
31
  subCommands: {
32
- build: () => import("./build-BzQAQjuy.mjs").then((m) => m.default),
32
+ build: () => import("./build-DkP2ffu9.mjs").then((m) => m.default),
33
33
  dev: () => import("./dev-BCFe3g38.mjs").then((m) => m.default),
34
- doctor: () => import("./doctor-BcoqyBk-.mjs").then((m) => m.default),
35
- generate: () => import("./generate-BXkePCIx.mjs").then((m) => m.default),
36
- inspect: () => import("./inspect-DIjjCfs3.mjs").then((m) => m.default),
37
- verify: () => import("./verify-CObGxWtW.mjs").then((m) => m.default)
34
+ doctor: () => import("./doctor-CwQJjJ83.mjs").then((m) => m.default),
35
+ generate: () => import("./generate-C9AMSGsv.mjs").then((m) => m.default),
36
+ inspect: () => import("./inspect-MseHPU9s.mjs").then((m) => m.default),
37
+ verify: () => import("./verify-o0G5Xuff.mjs").then((m) => m.default)
38
38
  }
39
39
  }));
40
40
  //#endregion
@@ -1,8 +1,8 @@
1
1
  import { t as HTTP_METHODS } from "./index.mjs";
2
- import { t as readClientBuildAssets } from "./build-metadata-IABPFFKk.mjs";
3
- import { a as readProjectConfig, c as resolveProjectPath, p as handleCliError } from "./project-DydjqBoS.mjs";
2
+ import { t as readClientBuildAssets } from "./build-metadata-BOChHO8g.mjs";
3
+ import { a as readProjectConfig, c as resolveProjectPath, p as handleCliError } from "./project-BdMiN3s7.mjs";
4
4
  import { defineCommand } from "citty";
5
- import { readFileSync } from "node:fs";
5
+ import { existsSync, readFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
  import { createServer } from "vite";
8
8
  //#region src/commands/inspect.ts
@@ -45,12 +45,7 @@ async function runInspect(root, { target = "all" } = {}) {
45
45
  if (!project.configFile) throw new Error("Missing vite config. `pracht inspect` requires a project with pracht configured.");
46
46
  if (!project.hasPrachtPlugin) throw new Error("vite.config does not appear to register the pracht plugin.");
47
47
  if (project.mode === "manifest") {
48
- const manifestPath = resolveProjectPath(project.root, project.appFile);
49
- try {
50
- readFileSync(manifestPath, "utf-8");
51
- } catch {
52
- throw new Error(`App manifest is missing at ${project.appFile}.`);
53
- }
48
+ if (!existsSync(resolveProjectPath(project.root, project.appFile))) throw new Error(`App manifest is missing at ${project.appFile}.`);
54
49
  }
55
50
  const server = await createServer({
56
51
  root,
@@ -67,7 +67,7 @@ function resolveScopedFile(root, configDir, fileName) {
67
67
  return resolve(resolveProjectPath(root, configDir), fileName);
68
68
  }
69
69
  function resolveRouteModulePath(project, routePath, extension) {
70
- const segments = segmentsFromRoutePath(routePath);
70
+ const segments = segmentsFromPath(routePath);
71
71
  const relativePath = segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
72
72
  return {
73
73
  absolutePath: resolve(resolveProjectPath(project.root, project.routesDir), relativePath),
@@ -75,7 +75,7 @@ function resolveRouteModulePath(project, routePath, extension) {
75
75
  };
76
76
  }
77
77
  function resolvePagesRouteModulePath(project, routePath, extension) {
78
- const segments = segmentsFromRoutePath(routePath);
78
+ const segments = segmentsFromPath(routePath);
79
79
  const relativePath = segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
80
80
  return {
81
81
  absolutePath: resolve(resolveProjectPath(project.root, project.pagesDir), relativePath),
@@ -83,7 +83,7 @@ function resolvePagesRouteModulePath(project, routePath, extension) {
83
83
  };
84
84
  }
85
85
  function resolveApiModulePath(project, endpointPath) {
86
- const segments = segmentsFromApiPath(endpointPath);
86
+ const segments = segmentsFromPath(endpointPath);
87
87
  const relativePath = segments.length === 0 ? "index.ts" : `${segments.join("/")}.ts`;
88
88
  return {
89
89
  absolutePath: resolve(resolveProjectPath(project.root, project.apiDir), relativePath),
@@ -137,15 +137,8 @@ function normalizeConfigPath(value) {
137
137
  if (!value) return value;
138
138
  return value.startsWith("/") ? value : `/${value}`;
139
139
  }
140
- function segmentsFromRoutePath(routePath) {
141
- return routePath.split("/").filter(Boolean).map((segment) => {
142
- if (segment.startsWith(":")) return `[${segment.slice(1)}]`;
143
- if (segment === "*") return "[...slug]";
144
- return segment;
145
- });
146
- }
147
- function segmentsFromApiPath(endpointPath) {
148
- return endpointPath.split("/").filter(Boolean).map((segment) => {
140
+ function segmentsFromPath(path) {
141
+ return path.split("/").filter(Boolean).map((segment) => {
149
142
  if (segment.startsWith(":")) return `[${segment.slice(1)}]`;
150
143
  if (segment === "*") return "[...slug]";
151
144
  return segment;