@pracht/cli 1.3.0 → 1.3.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,25 @@
1
1
  # @pracht/cli
2
2
 
3
+ ## 1.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`0bd717f`](https://github.com/JoviDeCroock/pracht/commit/0bd717f280bc69a65efa6c4cb3142140ec88c9ac) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Tighten framework and deployment DX after the framework review: add shell-level error boundaries and clearer debug errors without route boundaries, fix pages-router route specificity and `.tsrx` server discovery, correct the dev error overlay import, expose generated-entry context factories for built-in adapters, add configurable Node/dev request body limits, fix CLI version reporting, refresh starter defaults, and align docs/onboarding examples with the current package names and adapter APIs.
8
+
9
+ - [`e7be45d`](https://github.com/JoviDeCroock/pracht/commit/e7be45da86eb8d04d2e5dc6c1c76547c2491cd2d) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Tighten prerender path safety by rejecting dynamic dot segments and unsafe static route segments, and by bounding SSG/ISG writes to `dist/client`. Deduplicate the default Node adapter entry generation and preserve multiple `Set-Cookie` headers in Node responses.
10
+
11
+ - Updated dependencies [[`0bd717f`](https://github.com/JoviDeCroock/pracht/commit/0bd717f280bc69a65efa6c4cb3142140ec88c9ac), [`e7be45d`](https://github.com/JoviDeCroock/pracht/commit/e7be45da86eb8d04d2e5dc6c1c76547c2491cd2d)]:
12
+ - @pracht/core@0.6.0
13
+
14
+ ## 1.3.1
15
+
16
+ ### Patch Changes
17
+
18
+ - [#137](https://github.com/JoviDeCroock/pracht/pull/137) [`ac32c2c`](https://github.com/JoviDeCroock/pracht/commit/ac32c2cb9ce5e86a38cde1167269e368f41dea0e) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Harden same-origin request checks and HTML head rendering, improve client prefetch/navigation behavior, fix cross-platform path handling, stream and conditionally revalidate Node static responses, de-document Cloudflare runtime ISG revalidation, and align starter/docs with the current CLI/runtime behavior.
19
+
20
+ - Updated dependencies [[`ac32c2c`](https://github.com/JoviDeCroock/pracht/commit/ac32c2cb9ce5e86a38cde1167269e368f41dea0e), [`49d6348`](https://github.com/JoviDeCroock/pracht/commit/49d6348bc984464cdb0e8c54c5ef9ba5cdec911e)]:
21
+ - @pracht/core@0.5.0
22
+
3
23
  ## 1.3.0
4
24
 
5
25
  ### Minor Changes
@@ -2,7 +2,8 @@ import { r as VERSION } from "./index.mjs";
2
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
- import { dirname, join, resolve } from "node:path";
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { pathToFileURL } from "node:url";
6
7
  import { build } from "vite";
7
8
  //#region src/build-shared.ts
8
9
  const ROUTE_STATE_REQUEST_HEADER = "x-pracht-route-state-request";
@@ -145,7 +146,8 @@ var build_default = defineCommand({
145
146
  root,
146
147
  build: {
147
148
  outDir: "dist/server",
148
- ssr: "virtual:pracht/server"
149
+ rollupOptions: { input: "virtual:pracht/server" },
150
+ ssr: true
149
151
  }
150
152
  });
151
153
  const serverEntry = resolve(root, "dist/server/server.js");
@@ -168,7 +170,7 @@ var build_default = defineCommand({
168
170
  const publicDir = resolve(root, "public");
169
171
  if (existsSync(publicDir)) cpSync(publicDir, clientDir, { recursive: true });
170
172
  if (existsSync(serverEntry)) {
171
- const serverMod = await import(serverEntry);
173
+ const serverMod = await import(pathToFileURL(serverEntry).href);
172
174
  const { prerenderApp } = serverMod;
173
175
  const { clientEntryUrl, cssManifest, jsManifest } = readClientBuildAssets(root);
174
176
  const { pages, isgManifest } = await prerenderApp({
@@ -177,13 +179,14 @@ var build_default = defineCommand({
177
179
  cssManifest,
178
180
  jsManifest,
179
181
  registry: serverMod.registry,
180
- withISGManifest: true
182
+ withISGManifest: true,
183
+ concurrency: serverMod.prerenderConcurrency
181
184
  });
182
185
  const headersManifest = Object.fromEntries(pages.map((page) => [page.path, page.headers ?? {}]));
183
186
  if (pages.length > 0) {
184
187
  console.log(`\n Prerendering ${pages.length} SSG/ISG route(s)...\n`);
185
188
  for (const page of pages) {
186
- const filePath = page.path === "/" ? join(clientDir, "index.html") : join(clientDir, page.path, "index.html");
189
+ const filePath = resolvePrerenderOutputPath(clientDir, page.path);
187
190
  mkdirSync(dirname(filePath), { recursive: true });
188
191
  writeFileSync(filePath, page.html, "utf-8");
189
192
  console.log(` ${page.path} → ${filePath.replace(root + "/", "")}`);
@@ -200,6 +203,7 @@ var build_default = defineCommand({
200
203
  console.log(`\n ISG manifest → dist/server/isg-manifest.json (${Object.keys(isgManifest).length} route(s))\n`);
201
204
  }
202
205
  if (serverMod.buildTarget === "cloudflare") {
206
+ 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");
203
207
  console.log("\n Cloudflare worker → dist/server/server.js\n");
204
208
  console.log(" Deploy with: wrangler deploy\n");
205
209
  }
@@ -218,5 +222,13 @@ var build_default = defineCommand({
218
222
  console.log("\n Build complete.\n");
219
223
  }
220
224
  });
225
+ function resolvePrerenderOutputPath(clientDir, routePath) {
226
+ if (routePath.includes("\0")) throw new Error(`Refusing to write prerendered route "${routePath}" with a NUL byte.`);
227
+ const root = resolve(clientDir);
228
+ const filePath = routePath === "/" ? resolve(root, "index.html") : resolve(root, `.${routePath}`, "index.html");
229
+ const relativePath = relative(root, filePath);
230
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) throw new Error(`Refusing to write prerendered route "${routePath}" outside dist/client (${filePath}).`);
231
+ return filePath;
232
+ }
221
233
  //#endregion
222
234
  export { build_default as default };
@@ -1,4 +1,4 @@
1
- import { t as runDoctor } from "./verification-WP-KTb41.mjs";
1
+ import { t as runDoctor } from "./verification-CSj-ngqk.mjs";
2
2
  import { defineCommand } from "citty";
3
3
  //#region src/commands/doctor.ts
4
4
  var doctor_default = defineCommand({
@@ -1,4 +1,4 @@
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";
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-voPTS9UC.mjs";
2
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";
package/dist/index.mjs CHANGED
@@ -1,6 +1,14 @@
1
1
  import { defineCommand, runMain } from "citty";
2
+ import { readFileSync } from "node:fs";
2
3
  //#region src/constants.ts
3
- const VERSION = "0.0.0";
4
+ const VERSION = readPackageVersion();
5
+ function readPackageVersion() {
6
+ try {
7
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version ?? "0.0.0";
8
+ } catch {
9
+ return "0.0.0";
10
+ }
11
+ }
4
12
  const PROJECT_DEFAULTS = {
5
13
  apiDir: "/src/api",
6
14
  appFile: "/src/routes.ts",
@@ -22,6 +30,10 @@ const HTTP_METHODS = new Set([
22
30
  ]);
23
31
  //#endregion
24
32
  //#region src/index.ts
33
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
34
+ console.log(VERSION);
35
+ process.exit(0);
36
+ }
25
37
  runMain(defineCommand({
26
38
  meta: {
27
39
  name: "pracht",
@@ -29,12 +41,12 @@ runMain(defineCommand({
29
41
  description: "The pracht CLI"
30
42
  },
31
43
  subCommands: {
32
- build: () => import("./build-DkP2ffu9.mjs").then((m) => m.default),
44
+ build: () => import("./build-B7_6RUBf.mjs").then((m) => m.default),
33
45
  dev: () => import("./dev-BCFe3g38.mjs").then((m) => m.default),
34
- doctor: () => import("./doctor-B5qYLJF8.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-Do8bG-fq.mjs").then((m) => m.default)
46
+ doctor: () => import("./doctor-DyOWEA42.mjs").then((m) => m.default),
47
+ generate: () => import("./generate-V7pQExlW.mjs").then((m) => m.default),
48
+ inspect: () => import("./inspect-CFD3-dna.mjs").then((m) => m.default),
49
+ verify: () => import("./verify-D0pGwcyf.mjs").then((m) => m.default)
38
50
  }
39
51
  }));
40
52
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { t as HTTP_METHODS } from "./index.mjs";
2
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";
3
+ import { a as readProjectConfig, c as resolveProjectPath, p as handleCliError } from "./project-voPTS9UC.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
@@ -1,6 +1,6 @@
1
1
  import { n as PROJECT_DEFAULTS, t as HTTP_METHODS } from "./index.mjs";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, relative, resolve } from "node:path";
3
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
4
4
  //#region src/utils.ts
5
5
  function quote(value) {
6
6
  return JSON.stringify(value);
@@ -64,34 +64,47 @@ function resolveProjectPath(root, configPath) {
64
64
  return resolve(root, `.${configPath}`);
65
65
  }
66
66
  function resolveScopedFile(root, configDir, fileName) {
67
- return resolve(resolveProjectPath(root, configDir), fileName);
67
+ assertSafePathSegment(fileName.replace(/\.(ts|tsx|js|jsx)$/, ""));
68
+ const baseDir = resolveProjectPath(root, configDir);
69
+ const filePath = resolve(baseDir, fileName);
70
+ assertInsideDirectory(baseDir, filePath);
71
+ return filePath;
68
72
  }
69
73
  function resolveRouteModulePath(project, routePath, extension) {
70
74
  const segments = segmentsFromPath(routePath);
71
75
  const relativePath = segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
76
+ const baseDir = resolveProjectPath(project.root, project.routesDir);
77
+ const absolutePath = resolve(baseDir, relativePath);
78
+ assertInsideDirectory(baseDir, absolutePath);
72
79
  return {
73
- absolutePath: resolve(resolveProjectPath(project.root, project.routesDir), relativePath),
80
+ absolutePath,
74
81
  relativePath
75
82
  };
76
83
  }
77
84
  function resolvePagesRouteModulePath(project, routePath, extension) {
78
85
  const segments = segmentsFromPath(routePath);
79
86
  const relativePath = segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
87
+ const baseDir = resolveProjectPath(project.root, project.pagesDir);
88
+ const absolutePath = resolve(baseDir, relativePath);
89
+ assertInsideDirectory(baseDir, absolutePath);
80
90
  return {
81
- absolutePath: resolve(resolveProjectPath(project.root, project.pagesDir), relativePath),
91
+ absolutePath,
82
92
  relativePath
83
93
  };
84
94
  }
85
95
  function resolveApiModulePath(project, endpointPath) {
86
96
  const segments = segmentsFromPath(endpointPath);
87
97
  const relativePath = segments.length === 0 ? "index.ts" : `${segments.join("/")}.ts`;
98
+ const baseDir = resolveProjectPath(project.root, project.apiDir);
99
+ const absolutePath = resolve(baseDir, relativePath);
100
+ assertInsideDirectory(baseDir, absolutePath);
88
101
  return {
89
- absolutePath: resolve(resolveProjectPath(project.root, project.apiDir), relativePath),
102
+ absolutePath,
90
103
  relativePath
91
104
  };
92
105
  }
93
106
  function displayPath(root, filePath) {
94
- return relative(root, filePath) || ".";
107
+ return (relative(root, filePath) || ".").replace(/\\/g, "/");
95
108
  }
96
109
  function writeGeneratedFile(filePath, source) {
97
110
  if (existsSync(filePath)) throw new Error(`Refusing to overwrite existing file ${filePath}.`);
@@ -111,7 +124,7 @@ function listFilesRecursively(dir) {
111
124
  return files;
112
125
  }
113
126
  function hasPagesAppShell(filePath) {
114
- return /^_app\.(ts|tsx|js|jsx)$/.test(basename(filePath));
127
+ return /^_app\.(ts|tsx|tsrx|js|jsx)$/.test(basename(filePath));
115
128
  }
116
129
  function findConfigFile(root) {
117
130
  for (const name of [
@@ -139,10 +152,23 @@ function normalizeConfigPath(value) {
139
152
  }
140
153
  function segmentsFromPath(path) {
141
154
  return path.split("/").filter(Boolean).map((segment) => {
142
- if (segment.startsWith(":")) return `[${segment.slice(1)}]`;
155
+ assertSafePathSegment(segment);
156
+ if (segment.startsWith(":")) {
157
+ const name = segment.endsWith("*") ? segment.slice(1, -1) : segment.slice(1);
158
+ assertSafePathSegment(name);
159
+ return segment.endsWith("*") ? `[...${name || "slug"}]` : `[${name}]`;
160
+ }
143
161
  if (segment === "*") return "[...slug]";
144
162
  return segment;
145
163
  });
146
164
  }
165
+ function assertSafePathSegment(segment) {
166
+ if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\") || segment.includes("\0")) throw new Error(`Unsafe path segment: ${JSON.stringify(segment)}.`);
167
+ }
168
+ function assertInsideDirectory(baseDir, filePath) {
169
+ const relativePath = relative(baseDir, filePath);
170
+ if (relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath)) return;
171
+ throw new Error(`Refusing to write outside ${baseDir}.`);
172
+ }
147
173
  //#endregion
148
174
  export { requireEnum as _, readProjectConfig as a, resolveProjectPath as c, writeGeneratedFile as d, ensureTrailingNewline as f, quote as g, parseCommaList as h, listFilesRecursively as i, resolveRouteModulePath as l, parseApiMethods as m, displayPath as n, resolveApiModulePath as o, handleCliError as p, hasPagesAppShell as r, resolvePagesRouteModulePath as s, assertFileExists as t, resolveScopedFile as u, requirePositiveInteger as v };
@@ -1,7 +1,7 @@
1
- import { a as readProjectConfig, c as resolveProjectPath, i as listFilesRecursively, n as displayPath, r as hasPagesAppShell } from "./project-BdMiN3s7.mjs";
1
+ import { a as readProjectConfig, c as resolveProjectPath, i as listFilesRecursively, n as displayPath, r as hasPagesAppShell } from "./project-voPTS9UC.mjs";
2
2
  import { n as extractRegistryEntries, r as extractRelativeModulePaths } from "./manifest-Bs5hp3gA.mjs";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
- import { basename, dirname, relative, resolve } from "node:path";
4
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  //#region src/verification-helpers.ts
7
7
  const CONFIG_FILE_NAMES = new Set([
@@ -12,8 +12,8 @@ const CONFIG_FILE_NAMES = new Set([
12
12
  "vite.config.cjs",
13
13
  "vite.config.cts"
14
14
  ]);
15
- const MODULE_SOURCE_RE = /\.(ts|tsx|js|jsx)$/;
16
- const PAGE_SOURCE_RE = /\.(ts|tsx|js|jsx|md|mdx)$/;
15
+ const MODULE_SOURCE_RE = /\.(ts|tsx|tsrx|js|jsx)$/;
16
+ const PAGE_SOURCE_RE = /\.(ts|tsx|tsrx|js|jsx|md|mdx)$/;
17
17
  function createCheck(status, message) {
18
18
  return {
19
19
  message,
@@ -22,7 +22,7 @@ function createCheck(status, message) {
22
22
  }
23
23
  function isWithinDirectory(filePath, directoryPath) {
24
24
  const relativePath = relative(directoryPath, filePath);
25
- return relativePath === "" || !relativePath.startsWith("..") && !relativePath.startsWith("../");
25
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
26
26
  }
27
27
  function normalizePath(value) {
28
28
  return value.replace(/\\/g, "/");
@@ -52,7 +52,7 @@ function scanPagesDirectory(pagesDir) {
52
52
  return listFilesRecursively(pagesDir).filter((file) => PAGE_SOURCE_RE.test(file)).map((file) => describePagesFile(pagesDir, file));
53
53
  }
54
54
  function describePagesFile(pagesDir, file) {
55
- const routePath = relative(pagesDir, file).replace(/\\/g, "/").replace(/\.(tsx?|jsx?|mdx?)$/, "");
55
+ const routePath = relative(pagesDir, file).replace(/\\/g, "/").replace(/\.(tsx?|tsrx|jsx?|mdx?)$/, "");
56
56
  const name = basename(routePath);
57
57
  if (hasPagesAppShell(file)) return {
58
58
  file,
@@ -271,9 +271,9 @@ function collectChangedFiles(root) {
271
271
  if (!record) continue;
272
272
  if (record.includes(" -> ")) {
273
273
  const [from, to] = record.split(" -> ");
274
- addChangedFile(files, repoRoot, prefix, from);
275
- addChangedFile(files, repoRoot, prefix, to);
276
- } else addChangedFile(files, repoRoot, prefix, record);
274
+ addChangedFile(files, root, prefix, from);
275
+ addChangedFile(files, root, prefix, to);
276
+ } else addChangedFile(files, root, prefix, record);
277
277
  }
278
278
  return {
279
279
  files: [...files],
@@ -286,11 +286,11 @@ function collectChangedFiles(root) {
286
286
  };
287
287
  }
288
288
  }
289
- function addChangedFile(files, repoRoot, prefix, repoRelativePath) {
289
+ function addChangedFile(files, projectRoot, prefix, repoRelativePath) {
290
290
  if (prefix && !repoRelativePath.startsWith(prefix)) return;
291
291
  const projectRelativePath = prefix ? repoRelativePath.slice(prefix.length) : repoRelativePath;
292
292
  if (!projectRelativePath) return;
293
- files.add(resolve(repoRoot, projectRelativePath));
293
+ files.add(resolve(projectRoot, projectRelativePath));
294
294
  }
295
295
  function filterFrameworkFiles(project, files, packageJsonPath) {
296
296
  const appFile = resolveProjectPath(project.root, project.appFile);
@@ -1,4 +1,4 @@
1
- import { n as runVerification } from "./verification-WP-KTb41.mjs";
1
+ import { n as runVerification } from "./verification-CSj-ngqk.mjs";
2
2
  import { defineCommand } from "citty";
3
3
  //#region src/commands/verify.ts
4
4
  var verify_default = defineCommand({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "license": "MIT",
5
5
  "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/cli",
6
6
  "bugs": {
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "citty": "^0.1.6",
30
- "@pracht/core": "0.4.0"
30
+ "@pracht/core": "0.6.0"
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsdown"