@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.
@@ -0,0 +1,218 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { gzipSync } from "node:zlib";
4
+ //#region src/bundle-report.ts
5
+ const SIZE_UNITS = {
6
+ b: 1,
7
+ kb: 1024,
8
+ mb: 1024 * 1024,
9
+ gb: 1024 * 1024 * 1024
10
+ };
11
+ const SIZE_RE = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/;
12
+ /**
13
+ * Parse a size budget value into bytes. Accepts plain numbers (bytes) or
14
+ * size strings like "120kb" / "1mb" (1kb = 1024 bytes).
15
+ */
16
+ function parseSizeToBytes(value) {
17
+ if (typeof value === "number") {
18
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`Invalid size ${JSON.stringify(value)}: expected a positive number of bytes.`);
19
+ return Math.floor(value);
20
+ }
21
+ const match = SIZE_RE.exec(value.trim().toLowerCase());
22
+ if (!match) throw new Error(`Invalid size ${JSON.stringify(value)}: expected a byte count or a size string like "120kb" or "1mb".`);
23
+ const amount = Number.parseFloat(match[1]);
24
+ const bytes = Math.round(amount * SIZE_UNITS[match[2] ?? "b"]);
25
+ if (bytes <= 0) throw new Error(`Invalid size ${JSON.stringify(value)}: expected a positive size.`);
26
+ return bytes;
27
+ }
28
+ function formatBytes(bytes) {
29
+ if (bytes < 1024) return `${bytes}b`;
30
+ const kb = bytes / 1024;
31
+ if (kb < 1024) return `${kb.toFixed(1)}kb`;
32
+ return `${(kb / 1024).toFixed(2)}mb`;
33
+ }
34
+ /** Strip leading `./` and `/` so module paths share one canonical form. */
35
+ function normalizeModulePath(path) {
36
+ return path.replace(/^\.?\//, "");
37
+ }
38
+ function buildSuffixIndex(manifest) {
39
+ const index = /* @__PURE__ */ new Map();
40
+ for (const key of Object.keys(manifest)) {
41
+ const normalized = normalizeModulePath(key);
42
+ if (!normalized) continue;
43
+ if (!index.has(normalized)) index.set(normalized, key);
44
+ for (let i = normalized.indexOf("/"); i !== -1; i = normalized.indexOf("/", i + 1)) {
45
+ const suffix = normalized.slice(i + 1);
46
+ if (suffix && !index.has(suffix)) index.set(suffix, key);
47
+ }
48
+ }
49
+ return index;
50
+ }
51
+ function resolveManifestEntries(manifest, suffixIndex, file) {
52
+ if (file in manifest) return manifest[file];
53
+ const resolved = suffixIndex.get(normalizeModulePath(file));
54
+ return resolved ? manifest[resolved] : [];
55
+ }
56
+ function collectBundleReport({ routes, jsManifest, clientEntryJs, clientDir }) {
57
+ const suffixIndex = buildSuffixIndex(jsManifest);
58
+ const chunkCache = /* @__PURE__ */ new Map();
59
+ function measureChunk(url) {
60
+ const cached = chunkCache.get(url);
61
+ if (cached) return cached;
62
+ const filePath = join(clientDir, url.replace(/^\//, ""));
63
+ let bytes = 0;
64
+ let gzipBytes = 0;
65
+ if (existsSync(filePath)) {
66
+ const contents = readFileSync(filePath);
67
+ bytes = contents.byteLength;
68
+ gzipBytes = gzipSync(contents).byteLength;
69
+ }
70
+ const chunk = {
71
+ url,
72
+ bytes,
73
+ gzipBytes
74
+ };
75
+ chunkCache.set(url, chunk);
76
+ return chunk;
77
+ }
78
+ const sharedUrls = new Set(clientEntryJs);
79
+ const sharedChunks = clientEntryJs.map(measureChunk);
80
+ const sharedBytes = sumBytes(sharedChunks);
81
+ const sharedGzipBytes = sumGzipBytes(sharedChunks);
82
+ const reportRoutes = routes.map((route) => {
83
+ const urls = /* @__PURE__ */ new Set();
84
+ if (route.shellFile) for (const url of resolveManifestEntries(jsManifest, suffixIndex, route.shellFile)) urls.add(url);
85
+ for (const url of resolveManifestEntries(jsManifest, suffixIndex, route.file)) urls.add(url);
86
+ const chunks = [...urls].filter((url) => !sharedUrls.has(url)).map(measureChunk).sort((left, right) => right.gzipBytes - left.gzipBytes);
87
+ const routeBytes = sumBytes(chunks);
88
+ const routeGzipBytes = sumGzipBytes(chunks);
89
+ return {
90
+ ...route.id ? { id: route.id } : {},
91
+ path: route.path,
92
+ render: route.render ?? "ssr",
93
+ chunks,
94
+ routeBytes,
95
+ routeGzipBytes,
96
+ totalBytes: routeBytes + sharedBytes,
97
+ totalGzipBytes: routeGzipBytes + sharedGzipBytes
98
+ };
99
+ });
100
+ reportRoutes.sort((left, right) => right.totalGzipBytes - left.totalGzipBytes || left.path.localeCompare(right.path));
101
+ return {
102
+ shared: {
103
+ chunks: [...sharedChunks].sort((left, right) => right.gzipBytes - left.gzipBytes),
104
+ bytes: sharedBytes,
105
+ gzipBytes: sharedGzipBytes
106
+ },
107
+ routes: reportRoutes
108
+ };
109
+ }
110
+ function sumBytes(chunks) {
111
+ return chunks.reduce((total, chunk) => total + chunk.bytes, 0);
112
+ }
113
+ function sumGzipBytes(chunks) {
114
+ return chunks.reduce((total, chunk) => total + chunk.gzipBytes, 0);
115
+ }
116
+ function evaluateBudgets(report, budgets) {
117
+ const defaultBudget = budgets["*"];
118
+ const explicitKeys = Object.keys(budgets).filter((key) => key !== "*");
119
+ const routePaths = new Set(report.routes.map((route) => route.path));
120
+ const unmatched = explicitKeys.filter((key) => !routePaths.has(key));
121
+ const results = [];
122
+ for (const route of report.routes) {
123
+ const source = route.path in budgets ? route.path : defaultBudget != null ? "*" : null;
124
+ if (source == null) continue;
125
+ const budget = budgets[source];
126
+ const limitBytes = parseSizeToBytes(budget);
127
+ results.push({
128
+ path: route.path,
129
+ render: route.render,
130
+ budget,
131
+ source,
132
+ limitBytes,
133
+ gzipBytes: route.totalGzipBytes,
134
+ ok: route.totalGzipBytes <= limitBytes
135
+ });
136
+ }
137
+ return {
138
+ results,
139
+ unmatched,
140
+ ok: results.every((result) => result.ok)
141
+ };
142
+ }
143
+ function shouldUseColor() {
144
+ if (process.env.NO_COLOR) return false;
145
+ return Boolean(process.stdout.isTTY);
146
+ }
147
+ function paint(text, code, color) {
148
+ return color ? `\u001b[${code}m${text}\u001b[0m` : text;
149
+ }
150
+ function formatBundleReport(report, options = {}) {
151
+ const color = options.color ?? false;
152
+ const rows = [];
153
+ for (const route of report.routes) {
154
+ rows.push({
155
+ label: `${route.path} (${route.render})`,
156
+ raw: "",
157
+ gzip: "",
158
+ kind: "header"
159
+ });
160
+ for (const chunk of route.chunks) rows.push({
161
+ label: ` ${chunk.url}`,
162
+ raw: formatBytes(chunk.bytes),
163
+ gzip: formatBytes(chunk.gzipBytes),
164
+ kind: "chunk"
165
+ });
166
+ rows.push({
167
+ label: " total (incl. shared)",
168
+ raw: formatBytes(route.totalBytes),
169
+ gzip: formatBytes(route.totalGzipBytes),
170
+ kind: "total"
171
+ });
172
+ }
173
+ rows.push({
174
+ label: "shared entry (all routes)",
175
+ raw: "",
176
+ gzip: "",
177
+ kind: "header"
178
+ });
179
+ for (const chunk of report.shared.chunks) rows.push({
180
+ label: ` ${chunk.url}`,
181
+ raw: formatBytes(chunk.bytes),
182
+ gzip: formatBytes(chunk.gzipBytes),
183
+ kind: "chunk"
184
+ });
185
+ rows.push({
186
+ label: " total",
187
+ raw: formatBytes(report.shared.bytes),
188
+ gzip: formatBytes(report.shared.gzipBytes),
189
+ kind: "total"
190
+ });
191
+ const labelWidth = Math.max(13, ...rows.map((row) => row.label.length));
192
+ const gzipWidth = Math.max(4, ...rows.map((row) => row.gzip.length));
193
+ const rawWidth = Math.max(3, ...rows.map((row) => row.raw.length));
194
+ const lines = [];
195
+ lines.push(paint(`${"Route / chunk".padEnd(labelWidth)} ${"Gzip".padStart(gzipWidth)} ${"Raw".padStart(rawWidth)}`, "1", color));
196
+ for (const row of rows) {
197
+ const line = `${row.label.padEnd(labelWidth)} ${row.gzip.padStart(gzipWidth)} ${row.raw.padStart(rawWidth)}`;
198
+ if (row.kind === "header") lines.push(paint(line.trimEnd(), "1", color));
199
+ else if (row.kind === "total") lines.push(paint(line, "36", color));
200
+ else lines.push(paint(line, "2", color));
201
+ }
202
+ return lines.join("\n");
203
+ }
204
+ function formatBudgetResults(evaluation, options = {}) {
205
+ const color = options.color ?? false;
206
+ const lines = [paint("Budgets (gzip client JS)", "1", color)];
207
+ const pathWidth = Math.max(...evaluation.results.map((result) => result.path.length), 0);
208
+ for (const result of evaluation.results) {
209
+ const status = result.ok ? paint("PASS", "32", color) : paint("FAIL", "31", color);
210
+ const comparison = result.ok ? "<=" : ">";
211
+ const suffix = result.source === "*" ? " (*)" : "";
212
+ lines.push(`${status} ${result.path.padEnd(pathWidth)} ${formatBytes(result.gzipBytes)} ${comparison} ${formatBytes(result.limitBytes)}${suffix}`);
213
+ }
214
+ for (const key of evaluation.unmatched) lines.push(paint(`WARN budget for ${JSON.stringify(key)} does not match any route.`, "33", color));
215
+ return lines.join("\n");
216
+ }
217
+ //#endregion
218
+ export { formatBytes as a, formatBundleReport as i, evaluateBudgets as n, shouldUseColor as o, formatBudgetResults as r, collectBundleReport as t };
@@ -0,0 +1,173 @@
1
+ import { t as HTTP_METHODS } from "./index.mjs";
2
+ import { defineCommand } from "citty";
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { createServer } from "vite";
6
+ //#region src/app-graph.ts
7
+ const METHOD_ORDER = [...HTTP_METHODS];
8
+ /**
9
+ * Load the resolved app graph (page routes + API routes) from a running Vite
10
+ * dev server. Shared by `pracht inspect` and the `pracht dev` startup banner.
11
+ */
12
+ async function collectAppGraph(server, root, options = {}) {
13
+ const serverModule = await server.ssrLoadModule("virtual:pracht/server");
14
+ return {
15
+ api: await collectApiRoutes(server, root, serverModule.apiRoutes, options),
16
+ routes: serializeResolvedRoutes(serverModule.resolvedApp.routes)
17
+ };
18
+ }
19
+ function serializeResolvedRoutes(routes) {
20
+ return routes.map((route) => ({
21
+ file: route.file,
22
+ id: route.id,
23
+ loaderFile: route.loaderFile ?? null,
24
+ middleware: route.middleware,
25
+ path: route.path,
26
+ render: route.render ?? null,
27
+ revalidate: route.revalidate ?? null,
28
+ shell: route.shell ?? null,
29
+ shellFile: route.shellFile ?? null
30
+ }));
31
+ }
32
+ async function collectApiRoutes(server, root, apiRoutes, options = {}) {
33
+ return Promise.all(apiRoutes.map(async (route) => ({
34
+ file: route.file,
35
+ methods: await detectApiMethods(server, root, route.file, options),
36
+ path: route.path
37
+ })));
38
+ }
39
+ async function detectApiMethods(server, root, file, options) {
40
+ const source = readFileSync(resolve(root, `.${file}`), "utf-8");
41
+ if (options.executeApiModules) try {
42
+ const module = await server.ssrLoadModule(file);
43
+ return METHOD_ORDER.filter((method) => typeof module[method] === "function");
44
+ } catch {}
45
+ return METHOD_ORDER.filter((method) => new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+${method}\\b`).test(source));
46
+ }
47
+ //#endregion
48
+ //#region src/dev-banner.ts
49
+ const ANSI = {
50
+ bold: "1",
51
+ cyan: "36",
52
+ dim: "2",
53
+ green: "32",
54
+ magenta: "35",
55
+ yellow: "33"
56
+ };
57
+ const MODE_COLORS = {
58
+ isg: ANSI.cyan,
59
+ spa: ANSI.magenta,
60
+ ssg: ANSI.green,
61
+ ssr: ANSI.yellow
62
+ };
63
+ /**
64
+ * Format the `pracht dev` startup banner: local URL(s) plus an aligned table
65
+ * of page routes (pattern, render mode, shell, middleware) and API routes.
66
+ */
67
+ function formatDevBanner(options) {
68
+ const { apiRoutes, color = false, localUrls, networkUrls = [], routes } = options;
69
+ const paint = (text, code) => color ? `\u001b[${code}m${text}\u001b[0m` : text;
70
+ const lines = [];
71
+ lines.push("");
72
+ lines.push(` ${paint("pracht dev", ANSI.bold)}`);
73
+ lines.push("");
74
+ for (const url of localUrls) lines.push(` ${paint("➜", ANSI.green)} Local: ${paint(url, `${ANSI.bold};${ANSI.cyan}`)}`);
75
+ for (const url of networkUrls) lines.push(` ${paint("➜", ANSI.green)} Network: ${paint(url, ANSI.cyan)}`);
76
+ lines.push("");
77
+ lines.push(` ${paint(`Routes (${routes.length})`, ANSI.bold)}`);
78
+ if (routes.length === 0) lines.push(" (none)");
79
+ else {
80
+ const rows = routes.map((route) => [
81
+ route.path,
82
+ route.render ?? "ssr",
83
+ route.shell ?? "-",
84
+ route.middleware.length > 0 ? route.middleware.join(", ") : "-"
85
+ ]);
86
+ const header = [
87
+ "ROUTE",
88
+ "MODE",
89
+ "SHELL",
90
+ "MIDDLEWARE"
91
+ ];
92
+ const widths = columnWidths([header, ...rows]);
93
+ lines.push(` ${paint(formatRow(header, widths), ANSI.dim)}`);
94
+ for (const row of rows) {
95
+ const [path, mode, shell, middleware] = row;
96
+ const cells = [
97
+ path.padEnd(widths[0]),
98
+ paint(mode.padEnd(widths[1]), MODE_COLORS[mode] ?? ANSI.dim),
99
+ shell.padEnd(widths[2]),
100
+ middleware
101
+ ];
102
+ lines.push(` ${cells.join(" ")}`.trimEnd());
103
+ }
104
+ }
105
+ lines.push("");
106
+ lines.push(` ${paint(`API (${apiRoutes.length})`, ANSI.bold)}`);
107
+ if (apiRoutes.length === 0) lines.push(" (none)");
108
+ else {
109
+ const rows = apiRoutes.map((route) => [route.path, route.methods.length > 0 ? route.methods.join(", ") : "-"]);
110
+ const header = ["ROUTE", "METHODS"];
111
+ const widths = columnWidths([header, ...rows]);
112
+ lines.push(` ${paint(formatRow(header, widths), ANSI.dim)}`);
113
+ for (const row of rows) lines.push(` ${formatRow(row, widths)}`);
114
+ }
115
+ lines.push("");
116
+ return lines.join("\n");
117
+ }
118
+ /** Respect NO_COLOR (https://no-color.org) and only color TTY output. */
119
+ function supportsColor(env = process.env, isTTY = Boolean(process.stdout.isTTY)) {
120
+ if (env.NO_COLOR) return false;
121
+ if (env.FORCE_COLOR) return true;
122
+ return isTTY;
123
+ }
124
+ function columnWidths(rows) {
125
+ const widths = [];
126
+ for (const row of rows) row.forEach((cell, index) => {
127
+ widths[index] = Math.max(widths[index] ?? 0, cell.length);
128
+ });
129
+ return widths;
130
+ }
131
+ function formatRow(cells, widths) {
132
+ return cells.map((cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index])).join(" ").trimEnd();
133
+ }
134
+ //#endregion
135
+ //#region src/commands/dev.ts
136
+ var dev_default = defineCommand({
137
+ meta: {
138
+ name: "dev",
139
+ description: "Start development server with HMR"
140
+ },
141
+ args: { port: {
142
+ type: "positional",
143
+ description: "Port number",
144
+ required: false
145
+ } },
146
+ async run({ args }) {
147
+ const port = parseInt(process.env.PORT || args.port || "3000", 10);
148
+ const root = process.cwd();
149
+ const server = await createServer({
150
+ root,
151
+ server: { port }
152
+ });
153
+ await server.listen();
154
+ try {
155
+ const graph = await collectAppGraph(server, root);
156
+ const urls = server.resolvedUrls ?? {
157
+ local: [],
158
+ network: []
159
+ };
160
+ console.log(formatDevBanner({
161
+ apiRoutes: graph.api,
162
+ color: supportsColor(),
163
+ localUrls: urls.local,
164
+ networkUrls: urls.network,
165
+ routes: graph.routes
166
+ }));
167
+ } catch {
168
+ server.printUrls();
169
+ }
170
+ }
171
+ });
172
+ //#endregion
173
+ export { dev_default as default };
@@ -1,4 +1,4 @@
1
- import { t as runDoctor } from "./verification-CSj-ngqk.mjs";
1
+ import { t as runDoctor } from "./verification-5w_FfRtE.mjs";
2
2
  import { defineCommand } from "citty";
3
3
  //#region src/commands/doctor.ts
4
4
  var doctor_default = defineCommand({
@@ -1,5 +1,6 @@
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
- import { a as toManifestModulePath, i as insertArrayItem, n as extractRegistryEntries, o as upsertObjectEntry, t as ensureCoreNamedImport } from "./manifest-Bs5hp3gA.mjs";
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.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-2EajzTuX.mjs";
3
+ import { a as toManifestModulePath, i as insertArrayItem, n as extractRegistryEntries, o as upsertObjectEntry, t as ensureCoreNamedImport } from "./manifest-nU7P84qF.mjs";
3
4
  import { defineCommand } from "citty";
4
5
  import { readFileSync, writeFileSync } from "node:fs";
5
6
  //#region src/commands/generate-paths.ts
@@ -141,6 +142,15 @@ function buildStaticPathsStub(params) {
141
142
  function escapeJsxText(value) {
142
143
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
143
144
  }
145
+ //#endregion
146
+ //#region src/commands/generate.ts
147
+ var generate_exports = /* @__PURE__ */ __exportAll({
148
+ default: () => generate_default,
149
+ generateApi: () => generateApi,
150
+ generateMiddleware: () => generateMiddleware,
151
+ generateRoute: () => generateRoute,
152
+ generateShell: () => generateShell
153
+ });
144
154
  var generate_default = defineCommand({
145
155
  meta: {
146
156
  name: "generate",
@@ -400,4 +410,4 @@ function generateApi(args, project) {
400
410
  };
401
411
  }
402
412
  //#endregion
403
- export { generate_default as default };
413
+ export { generate_exports as a, generateShell as i, generateMiddleware as n, generateRoute as r, generateApi as t };
package/dist/index.mjs CHANGED
@@ -41,13 +41,15 @@ runMain(defineCommand({
41
41
  description: "The pracht CLI"
42
42
  },
43
43
  subCommands: {
44
- build: () => import("./build-B7_6RUBf.mjs").then((m) => m.default),
45
- dev: () => import("./dev-BCFe3g38.mjs").then((m) => m.default),
46
- doctor: () => import("./doctor-DyOWEA42.mjs").then((m) => m.default),
47
- generate: () => import("./generate-DTMte0kd.mjs").then((m) => m.default),
48
- inspect: () => import("./inspect-WF08uLOl.mjs").then((m) => m.default),
49
- typegen: () => import("./typegen-Bl4fYQIy.mjs").then((m) => m.default),
50
- verify: () => import("./verify-FwVbdlxh.mjs").then((m) => m.default)
44
+ build: () => import("./build-CGzTcST3.mjs").then((m) => m.default),
45
+ dev: () => import("./dev-DwAbdNBf.mjs").then((m) => m.default),
46
+ doctor: () => import("./doctor-uALUogtk.mjs").then((m) => m.default),
47
+ generate: () => import("./generate-BczObays.mjs").then((n) => n.a).then((m) => m.default),
48
+ inspect: () => import("./inspect-Bxt-6Lmc.mjs").then((n) => n.t).then((m) => m.default),
49
+ mcp: () => import("./mcp-Dv9EeFtp.mjs").then((m) => m.default),
50
+ preview: () => import("./preview-B0-hgVfI.mjs").then((m) => m.default),
51
+ typegen: () => import("./typegen-KYYNjYDt.mjs").then((m) => m.default),
52
+ verify: () => import("./verify-B_HUuyEQ.mjs").then((m) => m.default)
51
53
  }
52
54
  }));
53
55
  //#endregion
@@ -1,18 +1,22 @@
1
- import { t as HTTP_METHODS } from "./index.mjs";
2
- import { t as readClientBuildAssets } from "./build-metadata-BOChHO8g.mjs";
3
- import { a as readProjectConfig, c as resolveProjectPath, p as handleCliError } from "./project-voPTS9UC.mjs";
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
+ import { t as readClientBuildAssets } from "./build-metadata-DyMzBVtV.mjs";
3
+ import { a as readProjectConfig, c as resolveProjectPath, p as handleCliError } from "./project-2EajzTuX.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
  import { createServer } from "vite";
8
+ import { serializeApiRoutes, serializeAppRoutes } from "@pracht/core";
8
9
  //#region src/commands/inspect.ts
10
+ var inspect_exports = /* @__PURE__ */ __exportAll({
11
+ default: () => inspect_default,
12
+ runInspect: () => runInspect
13
+ });
9
14
  const INSPECT_TARGETS = new Set([
10
15
  "routes",
11
16
  "api",
12
17
  "build",
13
18
  "all"
14
19
  ]);
15
- const METHOD_ORDER = [...HTTP_METHODS];
16
20
  var inspect_default = defineCommand({
17
21
  meta: {
18
22
  name: "inspect",
@@ -55,12 +59,11 @@ async function runInspect(root, { target = "all" } = {}) {
55
59
  try {
56
60
  const serverModule = await server.ssrLoadModule("virtual:pracht/server");
57
61
  const report = { mode: project.mode };
58
- if (target === "routes" || target === "all") report.routes = serializeRoutes(serverModule.resolvedApp.routes);
59
- if (target === "api" || target === "all") report.api = await Promise.all(serverModule.apiRoutes.map(async (route) => ({
60
- file: route.file,
61
- methods: await detectApiMethods(server, root, route.file),
62
- path: route.path
63
- })));
62
+ if (target === "routes" || target === "all") report.routes = serializeAppRoutes(serverModule.resolvedApp.routes);
63
+ if (target === "api" || target === "all") report.api = await serializeApiRoutes(serverModule.apiRoutes, {
64
+ loadModule: (file) => server.ssrLoadModule(file),
65
+ readSource: (file) => readFileSync(resolve(root, `.${file}`), "utf-8")
66
+ });
64
67
  if (target === "build" || target === "all") {
65
68
  const buildAssets = readClientBuildAssets(root);
66
69
  report.build = {
@@ -75,28 +78,6 @@ async function runInspect(root, { target = "all" } = {}) {
75
78
  await server.close();
76
79
  }
77
80
  }
78
- function serializeRoutes(routes) {
79
- return routes.map((route) => ({
80
- file: route.file,
81
- id: route.id,
82
- loaderFile: route.loaderFile ?? null,
83
- middleware: route.middleware,
84
- path: route.path,
85
- render: route.render ?? null,
86
- revalidate: route.revalidate ?? null,
87
- shell: route.shell ?? null,
88
- shellFile: route.shellFile ?? null
89
- }));
90
- }
91
- async function detectApiMethods(server, root, file) {
92
- const source = readFileSync(resolve(root, `.${file}`), "utf-8");
93
- try {
94
- const module = await server.ssrLoadModule(file);
95
- return METHOD_ORDER.filter((method) => typeof module[method] === "function");
96
- } catch {
97
- return METHOD_ORDER.filter((method) => new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+${method}\\b`).test(source));
98
- }
99
- }
100
81
  function printInspectReport(report) {
101
82
  console.log(`Pracht inspect (${report.mode} mode)`);
102
83
  if (report.routes) {
@@ -120,4 +101,4 @@ function printInspectReport(report) {
120
101
  }
121
102
  }
122
103
  //#endregion
123
- export { inspect_default as default, runInspect };
104
+ export { runInspect as n, inspect_exports as t };
@@ -0,0 +1,154 @@
1
+ import { r as VERSION } from "./index.mjs";
2
+ import { a as readProjectConfig } from "./project-2EajzTuX.mjs";
3
+ import { n as runVerification, t as runDoctor } from "./verification-5w_FfRtE.mjs";
4
+ import { i as generateShell, n as generateMiddleware, r as generateRoute, t as generateApi } from "./generate-BczObays.mjs";
5
+ import { n as runInspect } from "./inspect-Bxt-6Lmc.mjs";
6
+ import { defineCommand } from "citty";
7
+ import { resolve } from "node:path";
8
+ import { format } from "node:util";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { z } from "zod";
12
+ //#region src/mcp-server.ts
13
+ const cwdInput = { cwd: z.string().optional().describe("Absolute path to the pracht app root. Defaults to the server's working directory.") };
14
+ function createPrachtMcpServer() {
15
+ const server = new McpServer({
16
+ name: "pracht",
17
+ version: VERSION
18
+ });
19
+ server.registerTool("inspect_routes", {
20
+ description: "Inspect the resolved page-route graph of a pracht app: path, id, render mode, shell, middleware, loader file. Same payload as `pracht inspect routes --json`.",
21
+ inputSchema: { ...cwdInput }
22
+ }, guard(({ cwd }) => runInspect(resolveCwd(cwd), { target: "routes" })));
23
+ server.registerTool("inspect_api", {
24
+ description: "Inspect the resolved API routes of a pracht app: endpoint path, source file, exported HTTP methods. Same payload as `pracht inspect api --json`.",
25
+ inputSchema: { ...cwdInput }
26
+ }, guard(({ cwd }) => runInspect(resolveCwd(cwd), { target: "api" })));
27
+ server.registerTool("inspect_build", {
28
+ description: "Inspect build metadata of a pracht app: adapter target, client entry URL, CSS/JS manifests. Requires a prior `pracht build`. Same payload as `pracht inspect build --json`.",
29
+ inputSchema: { ...cwdInput }
30
+ }, guard(({ cwd }) => runInspect(resolveCwd(cwd), { target: "build" })));
31
+ server.registerTool("doctor", {
32
+ description: "Validate pracht app wiring (config, manifest references, adapter dependency). Same payload as `pracht doctor --json`.",
33
+ inputSchema: { ...cwdInput }
34
+ }, guard(({ cwd }) => runDoctor(resolveCwd(cwd))));
35
+ server.registerTool("verify", {
36
+ description: "Run fast framework-aware verification checks on a pracht app. Same payload as `pracht verify --json`.",
37
+ inputSchema: {
38
+ ...cwdInput,
39
+ changed: z.boolean().optional().describe("Only check files changed according to git (maps to --changed).")
40
+ }
41
+ }, guard(({ changed, cwd }) => runVerification(resolveCwd(cwd), { changed: Boolean(changed) })));
42
+ server.registerTool("generate_route", {
43
+ description: "Scaffold a pracht route module and wire it into the app (manifest apps update src/routes.ts; pages apps create the page file). Returns the files created and updated.",
44
+ inputSchema: {
45
+ ...cwdInput,
46
+ path: z.string().describe("Route path, e.g. /dashboard or /blog/:slug"),
47
+ render: z.enum([
48
+ "spa",
49
+ "ssr",
50
+ "ssg",
51
+ "isg"
52
+ ]).optional().describe("Render mode (defaults to ssr)."),
53
+ shell: z.string().optional().describe("Registered shell name (manifest apps only)."),
54
+ middleware: z.array(z.string()).optional().describe("Registered middleware names (manifest apps only)."),
55
+ loader: z.boolean().optional().describe("Include a loader export."),
56
+ errorBoundary: z.boolean().optional().describe("Include an error boundary export."),
57
+ staticPaths: z.boolean().optional().describe("Include a getStaticPaths export."),
58
+ title: z.string().optional().describe("Page title used in the head export."),
59
+ revalidate: z.number().int().positive().optional().describe("ISG revalidation window in seconds (isg render mode only).")
60
+ }
61
+ }, guard((input) => {
62
+ const project = readProjectConfig(resolveCwd(input.cwd));
63
+ return generateRoute({
64
+ "error-boundary": input.errorBoundary,
65
+ loader: input.loader,
66
+ middleware: input.middleware?.join(","),
67
+ path: input.path,
68
+ render: input.render,
69
+ revalidate: input.revalidate === void 0 ? void 0 : String(input.revalidate),
70
+ shell: input.shell,
71
+ "static-paths": input.staticPaths,
72
+ title: input.title
73
+ }, project);
74
+ }));
75
+ server.registerTool("generate_shell", {
76
+ description: "Scaffold a pracht shell component and register it in the app manifest (manifest apps only). Returns the files created and updated.",
77
+ inputSchema: {
78
+ ...cwdInput,
79
+ name: z.string().describe("Shell name, e.g. app or public")
80
+ }
81
+ }, guard(({ cwd, name }) => {
82
+ return generateShell(name, readProjectConfig(resolveCwd(cwd)));
83
+ }));
84
+ server.registerTool("generate_middleware", {
85
+ description: "Scaffold a pracht middleware function and register it in the app manifest (manifest apps only). Returns the files created and updated.",
86
+ inputSchema: {
87
+ ...cwdInput,
88
+ name: z.string().describe("Middleware name, e.g. auth")
89
+ }
90
+ }, guard(({ cwd, name }) => {
91
+ return generateMiddleware(name, readProjectConfig(resolveCwd(cwd)));
92
+ }));
93
+ server.registerTool("generate_api", {
94
+ description: "Scaffold a pracht API route with typed HTTP method handlers. Returns the files created and updated.",
95
+ inputSchema: {
96
+ ...cwdInput,
97
+ path: z.string().describe("API endpoint path, e.g. /health or /users/:id"),
98
+ methods: z.array(z.string()).optional().describe("HTTP methods to scaffold, e.g. [\"GET\", \"POST\"] (defaults to GET).")
99
+ }
100
+ }, guard(({ cwd, methods, path }) => {
101
+ const project = readProjectConfig(resolveCwd(cwd));
102
+ return generateApi({
103
+ methods: methods?.join(","),
104
+ path
105
+ }, project);
106
+ }));
107
+ return server;
108
+ }
109
+ function resolveCwd(cwd) {
110
+ return resolve(cwd ?? process.cwd());
111
+ }
112
+ function guard(handler) {
113
+ return async (input) => {
114
+ try {
115
+ const result = await handler(input);
116
+ return { content: [{
117
+ type: "text",
118
+ text: JSON.stringify(result, null, 2)
119
+ }] };
120
+ } catch (error) {
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text: error instanceof Error ? error.message : String(error)
125
+ }],
126
+ isError: true
127
+ };
128
+ }
129
+ };
130
+ }
131
+ //#endregion
132
+ //#region src/commands/mcp.ts
133
+ var mcp_default = defineCommand({
134
+ meta: {
135
+ name: "mcp",
136
+ description: "Start a Model Context Protocol server on stdio"
137
+ },
138
+ async run() {
139
+ for (const method of [
140
+ "debug",
141
+ "error",
142
+ "info",
143
+ "log",
144
+ "trace",
145
+ "warn"
146
+ ]) console[method] = (...args) => {
147
+ process.stderr.write(`${format(...args)}\n`);
148
+ };
149
+ await createPrachtMcpServer().connect(new StdioServerTransport());
150
+ process.stderr.write("pracht MCP server listening on stdio\n");
151
+ }
152
+ });
153
+ //#endregion
154
+ export { mcp_default as default };