@pracht/cli 1.1.4 → 1.2.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,155 @@
1
+ import { n as PROJECT_DEFAULTS, t as HTTP_METHODS } from "./index.mjs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, relative, resolve } from "node:path";
4
+ //#region src/utils.ts
5
+ function quote(value) {
6
+ return JSON.stringify(value);
7
+ }
8
+ function ensureTrailingNewline(value) {
9
+ return value.endsWith("\n") ? value : `${value}\n`;
10
+ }
11
+ function parseCommaList(value) {
12
+ if (!value || typeof value === "boolean") return [];
13
+ return (Array.isArray(value) ? value : [value]).flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter(Boolean);
14
+ }
15
+ function parseApiMethods(value) {
16
+ const methods = parseCommaList(value);
17
+ const normalized = methods.length === 0 ? ["GET"] : methods.map((entry) => entry.toUpperCase());
18
+ for (const method of normalized) if (!HTTP_METHODS.has(method)) throw new Error(`Unsupported HTTP method "${method}".`);
19
+ return [...new Set(normalized)];
20
+ }
21
+ function requireEnum(value, key, allowed, fallback) {
22
+ const val = value ?? fallback;
23
+ if (!allowed.includes(val)) throw new Error(`Invalid value for --${key}. Expected one of ${allowed.join(", ")}.`);
24
+ return val;
25
+ }
26
+ function requirePositiveInteger(value, key, fallback) {
27
+ const parsed = value == null ? fallback : Number.parseInt(value, 10);
28
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`--${key} must be a positive integer.`);
29
+ return parsed;
30
+ }
31
+ function handleCliError(error, { json }) {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ if (json) console.error(JSON.stringify({
34
+ ok: false,
35
+ error: message
36
+ }, null, 2));
37
+ else {
38
+ console.error(message);
39
+ if (error instanceof Error && error.stack && process.env.DEBUG) console.error(error.stack);
40
+ }
41
+ process.exit(1);
42
+ }
43
+ //#endregion
44
+ //#region src/project.ts
45
+ function readProjectConfig(root) {
46
+ const configFile = findConfigFile(root);
47
+ const rawConfig = configFile ? readFileSync(configFile, "utf-8") : "";
48
+ const config = {
49
+ ...PROJECT_DEFAULTS,
50
+ configFile,
51
+ hasPrachtPlugin: /\bpracht\s*\(/.test(rawConfig),
52
+ mode: "manifest",
53
+ rawConfig,
54
+ root
55
+ };
56
+ for (const key of Object.keys(PROJECT_DEFAULTS)) {
57
+ const value = readQuotedConfigValue(rawConfig, key);
58
+ if (typeof value === "string") config[key] = normalizeConfigPath(value);
59
+ }
60
+ config.mode = config.pagesDir ? "pages" : "manifest";
61
+ return config;
62
+ }
63
+ function resolveProjectPath(root, configPath) {
64
+ return resolve(root, `.${configPath}`);
65
+ }
66
+ function resolveScopedFile(root, configDir, fileName) {
67
+ return resolve(resolveProjectPath(root, configDir), fileName);
68
+ }
69
+ function resolveRouteModulePath(project, routePath, extension) {
70
+ const segments = segmentsFromRoutePath(routePath);
71
+ const relativePath = segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
72
+ return {
73
+ absolutePath: resolve(resolveProjectPath(project.root, project.routesDir), relativePath),
74
+ relativePath
75
+ };
76
+ }
77
+ function resolvePagesRouteModulePath(project, routePath, extension) {
78
+ const segments = segmentsFromRoutePath(routePath);
79
+ const relativePath = segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
80
+ return {
81
+ absolutePath: resolve(resolveProjectPath(project.root, project.pagesDir), relativePath),
82
+ relativePath
83
+ };
84
+ }
85
+ function resolveApiModulePath(project, endpointPath) {
86
+ const segments = segmentsFromApiPath(endpointPath);
87
+ const relativePath = segments.length === 0 ? "index.ts" : `${segments.join("/")}.ts`;
88
+ return {
89
+ absolutePath: resolve(resolveProjectPath(project.root, project.apiDir), relativePath),
90
+ relativePath
91
+ };
92
+ }
93
+ function displayPath(root, filePath) {
94
+ return relative(root, filePath) || ".";
95
+ }
96
+ function writeGeneratedFile(filePath, source) {
97
+ if (existsSync(filePath)) throw new Error(`Refusing to overwrite existing file ${filePath}.`);
98
+ mkdirSync(dirname(filePath), { recursive: true });
99
+ writeFileSync(filePath, ensureTrailingNewline(source), "utf-8");
100
+ }
101
+ function assertFileExists(filePath, message) {
102
+ if (!existsSync(filePath)) throw new Error(message);
103
+ }
104
+ function listFilesRecursively(dir) {
105
+ const files = [];
106
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
107
+ const fullPath = resolve(dir, entry.name);
108
+ if (entry.isDirectory()) files.push(...listFilesRecursively(fullPath));
109
+ else files.push(fullPath);
110
+ }
111
+ return files;
112
+ }
113
+ function hasPagesAppShell(filePath) {
114
+ return /^_app\.(ts|tsx|js|jsx)$/.test(basename(filePath));
115
+ }
116
+ function findConfigFile(root) {
117
+ for (const name of [
118
+ "vite.config.ts",
119
+ "vite.config.mts",
120
+ "vite.config.js",
121
+ "vite.config.mjs",
122
+ "vite.config.cjs",
123
+ "vite.config.cts"
124
+ ]) {
125
+ const file = resolve(root, name);
126
+ if (existsSync(file)) return file;
127
+ }
128
+ return null;
129
+ }
130
+ function readQuotedConfigValue(source, key) {
131
+ if (!source) return null;
132
+ const pattern = new RegExp(`${key}\\s*:\\s*(["'\\\`])([^"'\\\`]+)\\1`);
133
+ const match = source.match(pattern);
134
+ return match ? match[2] : null;
135
+ }
136
+ function normalizeConfigPath(value) {
137
+ if (!value) return value;
138
+ return value.startsWith("/") ? value : `/${value}`;
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) => {
149
+ if (segment.startsWith(":")) return `[${segment.slice(1)}]`;
150
+ if (segment === "*") return "[...slug]";
151
+ return segment;
152
+ });
153
+ }
154
+ //#endregion
155
+ 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 };
@@ -0,0 +1,372 @@
1
+ import { n as extractRegistryEntries, r as extractRelativeModulePaths } from "./manifest-DGq1n5LT.mjs";
2
+ import { a as readProjectConfig, c as resolveProjectPath, i as listFilesRecursively, n as displayPath, r as hasPagesAppShell } from "./project-DydjqBoS.mjs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { basename, dirname, relative, resolve } from "node:path";
5
+ import { execFileSync } from "node:child_process";
6
+ //#region src/verification.ts
7
+ const CONFIG_FILE_NAMES = new Set([
8
+ "vite.config.ts",
9
+ "vite.config.mts",
10
+ "vite.config.js",
11
+ "vite.config.mjs",
12
+ "vite.config.cjs",
13
+ "vite.config.cts"
14
+ ]);
15
+ const MODULE_SOURCE_RE = /\.(ts|tsx|js|jsx)$/;
16
+ const PAGE_SOURCE_RE = /\.(ts|tsx|js|jsx|md|mdx)$/;
17
+ function runDoctor(root) {
18
+ const report = runVerification(root);
19
+ return {
20
+ checks: report.checks,
21
+ configFile: report.configFile,
22
+ mode: report.mode,
23
+ ok: report.ok
24
+ };
25
+ }
26
+ function runVerification(root, options = {}) {
27
+ const project = readProjectConfig(root);
28
+ const checks = [];
29
+ const packageJsonPath = resolve(project.root, "package.json");
30
+ const configDisplayPath = project.configFile ? displayPath(root, project.configFile) : "vite.config.*";
31
+ const requestedScope = options.changed ? "changed" : "full";
32
+ collectConfigChecks(project, checks, configDisplayPath);
33
+ let changedInfo = {
34
+ files: [],
35
+ warning: null
36
+ };
37
+ if (options.changed) {
38
+ changedInfo = collectChangedFiles(project.root);
39
+ if (changedInfo.warning) checks.push(createCheck("warning", changedInfo.warning));
40
+ }
41
+ const frameworkFiles = options.changed ? filterFrameworkFiles(project, changedInfo.files, packageJsonPath) : [];
42
+ const scope = options.changed && !changedInfo.warning && !requiresFullVerification(project, frameworkFiles) ? "changed" : "full";
43
+ if (project.mode === "pages") collectPagesVerification(project, checks, {
44
+ changedFiles: frameworkFiles,
45
+ scope
46
+ });
47
+ else collectManifestVerification(project, checks, {
48
+ changedFiles: frameworkFiles,
49
+ scope
50
+ });
51
+ collectApiVerification(project, checks, {
52
+ changedFiles: frameworkFiles,
53
+ scope
54
+ });
55
+ collectPackageChecks(project, checks, packageJsonPath);
56
+ if (options.changed && frameworkFiles.length === 0 && !changedInfo.warning) checks.push(createCheck("ok", "No changed framework files were detected in the current project scope."));
57
+ return {
58
+ checks,
59
+ configFile: project.configFile ? displayPath(root, project.configFile) : null,
60
+ mode: project.mode,
61
+ ok: !checks.some((check) => check.status === "error"),
62
+ requestedScope,
63
+ scope,
64
+ changedFiles: changedInfo.files.map((file) => displayPath(project.root, file)),
65
+ frameworkFiles: frameworkFiles.map((file) => displayPath(project.root, file))
66
+ };
67
+ }
68
+ function collectConfigChecks(project, checks, configDisplayPath) {
69
+ if (!project.configFile) checks.push(createCheck("error", "Missing vite config."));
70
+ else checks.push(createCheck("ok", `Found ${configDisplayPath}.`));
71
+ if (!project.hasPrachtPlugin) checks.push(createCheck("error", "vite.config does not appear to register the pracht plugin."));
72
+ else checks.push(createCheck("ok", "Vite config registers the pracht plugin."));
73
+ }
74
+ function collectManifestVerification(project, checks, { changedFiles, scope }) {
75
+ const manifestPath = resolveProjectPath(project.root, project.appFile);
76
+ if (!existsSync(manifestPath)) {
77
+ checks.push(createCheck("error", `App manifest is missing at ${project.appFile}.`));
78
+ return;
79
+ }
80
+ const source = readFileSync(manifestPath, "utf-8");
81
+ const relativeModules = [...extractRelativeModulePaths(source)];
82
+ const routeCount = (source.match(/\broute\s*\(/g) ?? []).length;
83
+ if (scope === "full") {
84
+ checks.push(createCheck("ok", `Found app manifest at ${project.appFile}.`));
85
+ if (routeCount === 0) checks.push(createCheck("warning", "No routes were found in the app manifest."));
86
+ else checks.push(createCheck("ok", `App manifest defines ${routeCount} route${routeCount === 1 ? "" : "s"}.`));
87
+ const shellEntries = extractRegistryEntries(source, "shells");
88
+ const middlewareEntries = extractRegistryEntries(source, "middleware");
89
+ if (shellEntries.length > 0) checks.push(createCheck("ok", `Registered ${shellEntries.length} shell${shellEntries.length === 1 ? "" : "s"}.`));
90
+ if (middlewareEntries.length > 0) checks.push(createCheck("ok", `Registered ${middlewareEntries.length} middleware module${middlewareEntries.length === 1 ? "" : "s"}.`));
91
+ const missingModules = relativeModules.map((modulePath) => ({
92
+ display: modulePath,
93
+ exists: existsSync(resolve(dirname(manifestPath), modulePath))
94
+ })).filter((entry) => !entry.exists).map((entry) => entry.display);
95
+ if (missingModules.length > 0) checks.push(createCheck("error", `Manifest references missing files: ${missingModules.map((item) => JSON.stringify(item)).join(", ")}.`));
96
+ else checks.push(createCheck("ok", `All ${relativeModules.length} manifest module path${relativeModules.length === 1 ? "" : "s"} resolve.`));
97
+ } else collectChangedManifestModuleChecks(project, checks, manifestPath, relativeModules, changedFiles);
98
+ }
99
+ function collectChangedManifestModuleChecks(project, checks, manifestPath, relativeModules, changedFiles) {
100
+ const manifestDir = dirname(manifestPath);
101
+ const referencedModules = new Set(relativeModules.map(normalizePath));
102
+ const moduleDirectories = [
103
+ {
104
+ dir: resolveProjectPath(project.root, project.routesDir),
105
+ label: "route module"
106
+ },
107
+ {
108
+ dir: resolveProjectPath(project.root, project.shellsDir),
109
+ label: "shell module"
110
+ },
111
+ {
112
+ dir: resolveProjectPath(project.root, project.middlewareDir),
113
+ label: "middleware module"
114
+ },
115
+ {
116
+ dir: resolveProjectPath(project.root, project.serverDir),
117
+ label: "server module"
118
+ }
119
+ ];
120
+ for (const file of changedFiles) {
121
+ const directory = moduleDirectories.find((entry) => isWithinDirectory(file, entry.dir));
122
+ if (!directory) continue;
123
+ if (!MODULE_SOURCE_RE.test(file)) continue;
124
+ const display = displayPath(project.root, file);
125
+ const modulePath = normalizePath(toModuleSpecifier(manifestDir, file));
126
+ const exists = existsSync(file);
127
+ if (referencedModules.has(modulePath)) {
128
+ if (exists) checks.push(createCheck("ok", `Changed ${directory.label} ${JSON.stringify(display)} is referenced by the app manifest.`));
129
+ else checks.push(createCheck("error", `Changed ${directory.label} ${JSON.stringify(display)} was removed but is still referenced by the app manifest.`));
130
+ continue;
131
+ }
132
+ if (exists) checks.push(createCheck("warning", `Changed ${directory.label} ${JSON.stringify(display)} is not referenced by the app manifest.`));
133
+ }
134
+ }
135
+ function collectPagesVerification(project, checks, { changedFiles, scope }) {
136
+ const pagesDir = resolveProjectPath(project.root, project.pagesDir);
137
+ if (!existsSync(pagesDir)) {
138
+ checks.push(createCheck("error", `Pages directory is missing at ${project.pagesDir}.`));
139
+ return;
140
+ }
141
+ const pages = scanPagesDirectory(pagesDir);
142
+ const routes = pages.filter((page) => page.kind === "route");
143
+ const duplicates = collectDuplicateRoutePaths(routes).map((entry) => ({
144
+ ...entry,
145
+ files: entry.files.map((file) => displayPath(project.root, file))
146
+ }));
147
+ if (scope === "full") {
148
+ checks.push(createCheck("ok", `Found pages directory at ${project.pagesDir}.`));
149
+ if (routes.length === 0) checks.push(createCheck("warning", "Pages router app does not contain any route files yet."));
150
+ else checks.push(createCheck("ok", `Found ${routes.length} page route${routes.length === 1 ? "" : "s"}.`));
151
+ if (!pages.some((page) => page.kind === "shell")) checks.push(createCheck("warning", "No `_app` shell was found in the pages directory."));
152
+ else checks.push(createCheck("ok", "Found a pages-router `_app` shell."));
153
+ } else collectChangedPagesChecks(project, checks, pagesDir, changedFiles);
154
+ if (duplicates.length > 0) checks.push(createCheck("error", `Pages router resolves duplicate paths: ${duplicates.map((entry) => `${JSON.stringify(entry.path)} from ${entry.files.map((file) => JSON.stringify(file)).join(", ")}`).join("; ")}.`));
155
+ else if (scope === "full" && routes.length > 0) checks.push(createCheck("ok", `Pages router resolved ${routes.length} route${routes.length === 1 ? "" : "s"} without path collisions.`));
156
+ }
157
+ function collectChangedPagesChecks(project, checks, pagesDir, changedFiles) {
158
+ for (const file of changedFiles) {
159
+ if (!isWithinDirectory(file, pagesDir)) continue;
160
+ if (!PAGE_SOURCE_RE.test(file)) continue;
161
+ const display = displayPath(project.root, file);
162
+ if (!existsSync(file)) {
163
+ checks.push(createCheck("ok", `Removed page file ${JSON.stringify(display)} is no longer auto-discovered.`));
164
+ continue;
165
+ }
166
+ const page = describePagesFile(pagesDir, file);
167
+ if (page.kind === "shell") {
168
+ checks.push(createCheck("ok", `Changed pages shell ${JSON.stringify(display)} will wrap auto-discovered routes.`));
169
+ continue;
170
+ }
171
+ if (page.kind === "ignored") {
172
+ checks.push(createCheck("warning", `Changed pages file ${JSON.stringify(display)} is ignored by the pages router.`));
173
+ continue;
174
+ }
175
+ checks.push(createCheck("ok", `Changed page route ${JSON.stringify(display)} resolves to ${JSON.stringify(page.routePath)}.`));
176
+ }
177
+ }
178
+ function collectApiVerification(project, checks, { changedFiles, scope }) {
179
+ const apiDir = resolveProjectPath(project.root, project.apiDir);
180
+ const changedApiFiles = changedFiles.filter((file) => isWithinDirectory(file, apiDir));
181
+ if (scope === "changed" && changedApiFiles.length === 0) return;
182
+ if (!existsSync(apiDir)) {
183
+ if (scope === "full") checks.push(createCheck("ok", `No API directory was found at ${project.apiDir}; skipping API discovery.`));
184
+ return;
185
+ }
186
+ const apiFiles = listFilesRecursively(apiDir).filter((file) => MODULE_SOURCE_RE.test(file));
187
+ const routeMap = /* @__PURE__ */ new Map();
188
+ for (const file of apiFiles) {
189
+ const routePath = resolveApiRoutePath(apiDir, file);
190
+ const display = displayPath(project.root, file);
191
+ const entries = routeMap.get(routePath) ?? [];
192
+ entries.push(display);
193
+ routeMap.set(routePath, entries);
194
+ }
195
+ const duplicates = [...routeMap.entries()].filter(([, files]) => files.length > 1).map(([path, files]) => ({
196
+ files,
197
+ path
198
+ }));
199
+ if (duplicates.length > 0) checks.push(createCheck("error", `API route discovery resolves duplicate paths: ${duplicates.map((entry) => `${JSON.stringify(entry.path)} from ${entry.files.map((file) => JSON.stringify(file)).join(", ")}`).join("; ")}.`));
200
+ else if (scope === "full") checks.push(createCheck("ok", `API route discovery resolved ${apiFiles.length} route${apiFiles.length === 1 ? "" : "s"}.`));
201
+ for (const file of changedApiFiles) {
202
+ if (!MODULE_SOURCE_RE.test(file)) continue;
203
+ const display = displayPath(project.root, file);
204
+ if (!existsSync(file)) {
205
+ checks.push(createCheck("ok", `Removed API route ${JSON.stringify(display)} is no longer auto-discovered.`));
206
+ continue;
207
+ }
208
+ checks.push(createCheck("ok", `Changed API route ${JSON.stringify(display)} resolves to ${JSON.stringify(resolveApiRoutePath(apiDir, file))}.`));
209
+ }
210
+ }
211
+ function collectPackageChecks(project, checks, packageJsonPath) {
212
+ if (!existsSync(packageJsonPath)) {
213
+ checks.push(createCheck("warning", "No package.json found in the current app root."));
214
+ return;
215
+ }
216
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
217
+ const deps = {
218
+ ...packageJson.dependencies,
219
+ ...packageJson.devDependencies
220
+ };
221
+ if (!("@pracht/cli" in deps)) checks.push(createCheck("warning", "`@pracht/cli` is not listed in package.json dependencies."));
222
+ const adapterPackages = Object.keys(deps).filter((name) => name.startsWith("@pracht/adapter-"));
223
+ if (adapterPackages.length === 0) checks.push(createCheck("warning", "No built-in pracht adapter dependency was found in package.json."));
224
+ else checks.push(createCheck("ok", `Found adapter dependency ${adapterPackages.map((name) => JSON.stringify(name)).join(", ")}.`));
225
+ }
226
+ function collectChangedFiles(root) {
227
+ try {
228
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
229
+ cwd: root,
230
+ encoding: "utf-8"
231
+ }).trim();
232
+ const prefix = execFileSync("git", ["rev-parse", "--show-prefix"], {
233
+ cwd: root,
234
+ encoding: "utf-8"
235
+ }).trim();
236
+ const output = execFileSync("git", [
237
+ "status",
238
+ "--porcelain",
239
+ "--untracked-files=all"
240
+ ], {
241
+ cwd: repoRoot,
242
+ encoding: "utf-8"
243
+ });
244
+ const files = /* @__PURE__ */ new Set();
245
+ for (const line of output.split(/\r?\n/).filter(Boolean)) {
246
+ const record = line.slice(3);
247
+ if (!record) continue;
248
+ if (record.includes(" -> ")) {
249
+ const [from, to] = record.split(" -> ");
250
+ addChangedFile(files, repoRoot, prefix, from);
251
+ addChangedFile(files, repoRoot, prefix, to);
252
+ } else addChangedFile(files, repoRoot, prefix, record);
253
+ }
254
+ return {
255
+ files: [...files],
256
+ warning: null
257
+ };
258
+ } catch {
259
+ return {
260
+ files: [],
261
+ warning: "Unable to determine changed files from git; ran full verification instead."
262
+ };
263
+ }
264
+ }
265
+ function addChangedFile(files, repoRoot, prefix, repoRelativePath) {
266
+ if (prefix && !repoRelativePath.startsWith(prefix)) return;
267
+ const projectRelativePath = prefix ? repoRelativePath.slice(prefix.length) : repoRelativePath;
268
+ if (!projectRelativePath) return;
269
+ files.add(resolve(repoRoot, projectRelativePath));
270
+ }
271
+ function filterFrameworkFiles(project, files, packageJsonPath) {
272
+ const appFile = resolveProjectPath(project.root, project.appFile);
273
+ const routesDir = resolveProjectPath(project.root, project.routesDir);
274
+ const shellsDir = resolveProjectPath(project.root, project.shellsDir);
275
+ const middlewareDir = resolveProjectPath(project.root, project.middlewareDir);
276
+ const serverDir = resolveProjectPath(project.root, project.serverDir);
277
+ const apiDir = resolveProjectPath(project.root, project.apiDir);
278
+ const pagesDir = project.pagesDir ? resolveProjectPath(project.root, project.pagesDir) : null;
279
+ return files.filter((file) => {
280
+ if (CONFIG_FILE_NAMES.has(basename(file))) return true;
281
+ if (normalizePath(file) === normalizePath(packageJsonPath)) return true;
282
+ if (project.mode === "manifest" && normalizePath(file) === normalizePath(appFile)) return true;
283
+ if (isWithinDirectory(file, routesDir) && MODULE_SOURCE_RE.test(file)) return true;
284
+ if (isWithinDirectory(file, shellsDir) && MODULE_SOURCE_RE.test(file)) return true;
285
+ if (isWithinDirectory(file, middlewareDir) && MODULE_SOURCE_RE.test(file)) return true;
286
+ if (isWithinDirectory(file, serverDir) && MODULE_SOURCE_RE.test(file)) return true;
287
+ if (isWithinDirectory(file, apiDir) && MODULE_SOURCE_RE.test(file)) return true;
288
+ if (pagesDir && isWithinDirectory(file, pagesDir) && PAGE_SOURCE_RE.test(file)) return true;
289
+ return false;
290
+ });
291
+ }
292
+ function requiresFullVerification(project, changedFiles) {
293
+ const packageJsonPath = resolve(project.root, "package.json");
294
+ const appFile = resolveProjectPath(project.root, project.appFile);
295
+ return changedFiles.some((file) => {
296
+ const normalized = normalizePath(file);
297
+ if (CONFIG_FILE_NAMES.has(basename(file))) return true;
298
+ if (normalized === normalizePath(packageJsonPath)) return true;
299
+ if (project.mode === "manifest" && normalized === normalizePath(appFile)) return true;
300
+ return false;
301
+ });
302
+ }
303
+ function scanPagesDirectory(pagesDir) {
304
+ return listFilesRecursively(pagesDir).filter((file) => PAGE_SOURCE_RE.test(file)).map((file) => describePagesFile(pagesDir, file));
305
+ }
306
+ function describePagesFile(pagesDir, file) {
307
+ const routePath = relative(pagesDir, file).replace(/\\/g, "/").replace(/\.(tsx?|jsx?|mdx?)$/, "");
308
+ const name = basename(routePath);
309
+ if (hasPagesAppShell(file)) return {
310
+ file,
311
+ kind: "shell"
312
+ };
313
+ if (name.startsWith("_")) return {
314
+ file,
315
+ kind: "ignored"
316
+ };
317
+ if (routePath === "index") return {
318
+ file,
319
+ kind: "route",
320
+ routePath: "/"
321
+ };
322
+ return {
323
+ file,
324
+ kind: "route",
325
+ routePath: normalizeRoutePath(`/${routePath.replace(/\/index$/, "").replace(/\[\.\.\.([^\]]+)\]/g, "*").replace(/\[([^\].]+)\]/g, ":$1")}`)
326
+ };
327
+ }
328
+ function collectDuplicateRoutePaths(routes) {
329
+ const routeMap = /* @__PURE__ */ new Map();
330
+ for (const route of routes) {
331
+ const files = routeMap.get(route.routePath) ?? [];
332
+ files.push(route.file);
333
+ routeMap.set(route.routePath, files);
334
+ }
335
+ return [...routeMap.entries()].filter(([, files]) => files.length > 1).map(([path, files]) => ({
336
+ files,
337
+ path
338
+ }));
339
+ }
340
+ function resolveApiRoutePath(apiDir, file) {
341
+ let relativePath = relative(apiDir, file).replace(/\\/g, "/");
342
+ relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, "");
343
+ if (relativePath === "index") relativePath = "";
344
+ else relativePath = relativePath.replace(/\/index$/, "");
345
+ relativePath = relativePath.replace(/\[([^\]]+)\]/g, ":$1");
346
+ return normalizeRoutePath(relativePath ? `/api/${relativePath}` : "/api");
347
+ }
348
+ function toModuleSpecifier(fromDir, filePath) {
349
+ const relativePath = relative(fromDir, filePath).replace(/\\/g, "/");
350
+ if (relativePath.startsWith(".")) return relativePath;
351
+ return `./${relativePath}`;
352
+ }
353
+ function normalizeRoutePath(path) {
354
+ if (!path || path === "/") return "/";
355
+ const collapsed = (path.startsWith("/") ? path : `/${path}`).replace(/\/{2,}/g, "/");
356
+ return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
357
+ }
358
+ function isWithinDirectory(filePath, directoryPath) {
359
+ const relativePath = relative(directoryPath, filePath);
360
+ return relativePath === "" || !relativePath.startsWith("..") && !relativePath.startsWith("../");
361
+ }
362
+ function normalizePath(value) {
363
+ return value.replace(/\\/g, "/");
364
+ }
365
+ function createCheck(status, message) {
366
+ return {
367
+ message,
368
+ status
369
+ };
370
+ }
371
+ //#endregion
372
+ export { runVerification as n, runDoctor as t };
@@ -0,0 +1,31 @@
1
+ import { n as runVerification } from "./verification-Dfl3X4Zo.mjs";
2
+ import { defineCommand } from "citty";
3
+ //#region src/commands/verify.ts
4
+ var verify_default = defineCommand({
5
+ meta: {
6
+ name: "verify",
7
+ description: "Fast framework-aware verification"
8
+ },
9
+ args: {
10
+ changed: {
11
+ type: "boolean",
12
+ description: "Only check changed files"
13
+ },
14
+ json: {
15
+ type: "boolean",
16
+ description: "Output as JSON"
17
+ }
18
+ },
19
+ async run({ args }) {
20
+ const report = runVerification(process.cwd(), { changed: Boolean(args.changed) });
21
+ if (args.json) console.log(JSON.stringify(report, null, 2));
22
+ else {
23
+ console.log(`Pracht verify (${report.mode} mode, ${report.scope} scope)`);
24
+ for (const check of report.checks) console.log(`${check.status.toUpperCase().padEnd(7)} ${check.message}`);
25
+ console.log(report.ok ? "\nNo blocking issues found." : "\nBlocking issues found.");
26
+ }
27
+ if (!report.ok) process.exitCode = 1;
28
+ }
29
+ });
30
+ //#endregion
31
+ export { verify_default as default };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@pracht/cli",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
+ "license": "MIT",
4
5
  "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/cli",
5
6
  "bugs": {
6
7
  "url": "https://github.com/JoviDeCroock/pracht/issues"
@@ -18,6 +19,10 @@
18
19
  "provenance": true
19
20
  },
20
21
  "dependencies": {
21
- "@pracht/core": "0.2.4"
22
+ "citty": "^0.1.6",
23
+ "@pracht/core": "0.2.6"
24
+ },
25
+ "scripts": {
26
+ "build": "tsdown"
22
27
  }
23
28
  }
@@ -3,7 +3,20 @@ import { resolve } from "node:path";
3
3
 
4
4
  const MANIFEST_PATHS = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"];
5
5
 
6
- export function readClientBuildAssets(root = process.cwd()) {
6
+ interface ViteManifestEntry {
7
+ css?: string[];
8
+ file: string;
9
+ imports?: string[];
10
+ src?: string;
11
+ }
12
+
13
+ export interface ClientBuildAssets {
14
+ clientEntryUrl: string | null;
15
+ cssManifest: Record<string, string[]>;
16
+ jsManifest: Record<string, string[]>;
17
+ }
18
+
19
+ export function readClientBuildAssets(root: string = process.cwd()): ClientBuildAssets {
7
20
  const manifestPath = MANIFEST_PATHS.map((candidate) => resolve(root, candidate)).find((path) =>
8
21
  existsSync(path),
9
22
  );
@@ -17,15 +30,15 @@ export function readClientBuildAssets(root = process.cwd()) {
17
30
  }
18
31
 
19
32
  const rawManifest = readFileSync(manifestPath, "utf-8");
20
- const manifest = JSON.parse(rawManifest);
33
+ const manifest: Record<string, ViteManifestEntry> = JSON.parse(rawManifest);
21
34
  const clientEntry = manifest["virtual:pracht/client"];
22
35
 
23
- function collectTransitiveDeps(key) {
24
- const css = new Set();
25
- const js = new Set();
26
- const visited = new Set();
36
+ function collectTransitiveDeps(key: string): { css: string[]; js: string[] } {
37
+ const css = new Set<string>();
38
+ const js = new Set<string>();
39
+ const visited = new Set<string>();
27
40
 
28
- function collect(currentKey) {
41
+ function collect(currentKey: string): void {
29
42
  if (visited.has(currentKey)) return;
30
43
  visited.add(currentKey);
31
44
 
@@ -50,8 +63,8 @@ export function readClientBuildAssets(root = process.cwd()) {
50
63
  };
51
64
  }
52
65
 
53
- const cssManifest = {};
54
- const jsManifest = {};
66
+ const cssManifest: Record<string, string[]> = {};
67
+ const jsManifest: Record<string, string[]> = {};
55
68
 
56
69
  for (const [key, entry] of Object.entries(manifest)) {
57
70
  if (!entry.src) continue;