@pracht/cli 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/dist/{build-DlABFPbG.mjs → build-DkP2ffu9.mjs} +8 -0
- package/dist/{doctor-CQq64tfW.mjs → doctor-B5qYLJF8.mjs} +1 -1
- package/dist/{generate-sHtRHRAH.mjs → generate-C9AMSGsv.mjs} +141 -138
- package/dist/index.mjs +5 -5
- package/dist/{inspect-DP-h3vLw.mjs → inspect-MseHPU9s.mjs} +1 -1
- package/dist/{verification-CId4crSF.mjs → verification-WP-KTb41.mjs} +117 -108
- package/dist/{verify-D7Bg1Su6.mjs → verify-Do8bG-fq.mjs} +1 -1
- package/package.json +9 -2
- package/src/build-metadata.ts +0 -101
- package/src/build-shared.ts +0 -163
- package/src/commands/build.ts +0 -137
- package/src/commands/dev.ts +0 -27
- package/src/commands/doctor.ts +0 -33
- package/src/commands/generate.ts +0 -616
- package/src/commands/inspect.ts +0 -212
- package/src/commands/verify.ts +0 -37
- package/src/constants.ts +0 -26
- package/src/index.ts +0 -21
- package/src/manifest.ts +0 -166
- package/src/project.ts +0 -160
- package/src/utils.ts +0 -72
- package/src/verification.ts +0 -710
- package/test/pracht-cli.test.js +0 -633
- package/tsdown.config.ts +0 -8
- /package/dist/{manifest-DGq1n5LT.mjs → manifest-Bs5hp3gA.mjs} +0 -0
- /package/dist/{project-DUyH_7eJ.mjs → project-BdMiN3s7.mjs} +0 -0
package/src/verification.ts
DELETED
|
@@ -1,710 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { basename, dirname, relative, resolve } from "node:path";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
import { extractRegistryEntries, extractRelativeModulePaths } from "./manifest.js";
|
|
6
|
-
import {
|
|
7
|
-
displayPath,
|
|
8
|
-
hasPagesAppShell,
|
|
9
|
-
listFilesRecursively,
|
|
10
|
-
readProjectConfig,
|
|
11
|
-
resolveProjectPath,
|
|
12
|
-
type ProjectConfig,
|
|
13
|
-
} from "./project.js";
|
|
14
|
-
|
|
15
|
-
const CONFIG_FILE_NAMES = new Set([
|
|
16
|
-
"vite.config.ts",
|
|
17
|
-
"vite.config.mts",
|
|
18
|
-
"vite.config.js",
|
|
19
|
-
"vite.config.mjs",
|
|
20
|
-
"vite.config.cjs",
|
|
21
|
-
"vite.config.cts",
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
const MODULE_SOURCE_RE = /\.(ts|tsx|js|jsx)$/;
|
|
25
|
-
const PAGE_SOURCE_RE = /\.(ts|tsx|js|jsx|md|mdx)$/;
|
|
26
|
-
|
|
27
|
-
export interface Check {
|
|
28
|
-
message: string;
|
|
29
|
-
status: "ok" | "warning" | "error";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface DoctorReport {
|
|
33
|
-
checks: Check[];
|
|
34
|
-
configFile: string | null;
|
|
35
|
-
mode: "manifest" | "pages";
|
|
36
|
-
ok: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface VerificationReport {
|
|
40
|
-
changedFiles: string[];
|
|
41
|
-
checks: Check[];
|
|
42
|
-
configFile: string | null;
|
|
43
|
-
frameworkFiles: string[];
|
|
44
|
-
mode: "manifest" | "pages";
|
|
45
|
-
ok: boolean;
|
|
46
|
-
requestedScope: string;
|
|
47
|
-
scope: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function runDoctor(root: string): DoctorReport {
|
|
51
|
-
const report = runVerification(root);
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
checks: report.checks,
|
|
55
|
-
configFile: report.configFile,
|
|
56
|
-
mode: report.mode,
|
|
57
|
-
ok: report.ok,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function runVerification(
|
|
62
|
-
root: string,
|
|
63
|
-
options: { changed?: boolean } = {},
|
|
64
|
-
): VerificationReport {
|
|
65
|
-
const project = readProjectConfig(root);
|
|
66
|
-
const checks: Check[] = [];
|
|
67
|
-
const packageJsonPath = resolve(project.root, "package.json");
|
|
68
|
-
const configDisplayPath = project.configFile
|
|
69
|
-
? displayPath(root, project.configFile)
|
|
70
|
-
: "vite.config.*";
|
|
71
|
-
const requestedScope = options.changed ? "changed" : "full";
|
|
72
|
-
|
|
73
|
-
collectConfigChecks(project, checks, configDisplayPath);
|
|
74
|
-
|
|
75
|
-
let changedInfo: { files: string[]; warning: string | null } = {
|
|
76
|
-
files: [],
|
|
77
|
-
warning: null,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
if (options.changed) {
|
|
81
|
-
changedInfo = collectChangedFiles(project.root);
|
|
82
|
-
if (changedInfo.warning) {
|
|
83
|
-
checks.push(createCheck("warning", changedInfo.warning));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const frameworkFiles = options.changed
|
|
88
|
-
? filterFrameworkFiles(project, changedInfo.files, packageJsonPath)
|
|
89
|
-
: [];
|
|
90
|
-
const scope =
|
|
91
|
-
options.changed && !changedInfo.warning && !requiresFullVerification(project, frameworkFiles)
|
|
92
|
-
? "changed"
|
|
93
|
-
: "full";
|
|
94
|
-
|
|
95
|
-
if (project.mode === "pages") {
|
|
96
|
-
collectPagesVerification(project, checks, { changedFiles: frameworkFiles, scope });
|
|
97
|
-
} else {
|
|
98
|
-
collectManifestVerification(project, checks, { changedFiles: frameworkFiles, scope });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
collectApiVerification(project, checks, { changedFiles: frameworkFiles, scope });
|
|
102
|
-
collectPackageChecks(project, checks, packageJsonPath);
|
|
103
|
-
|
|
104
|
-
if (options.changed && frameworkFiles.length === 0 && !changedInfo.warning) {
|
|
105
|
-
checks.push(
|
|
106
|
-
createCheck("ok", "No changed framework files were detected in the current project scope."),
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
checks,
|
|
112
|
-
configFile: project.configFile ? displayPath(root, project.configFile) : null,
|
|
113
|
-
mode: project.mode,
|
|
114
|
-
ok: !checks.some((check) => check.status === "error"),
|
|
115
|
-
requestedScope,
|
|
116
|
-
scope,
|
|
117
|
-
changedFiles: changedInfo.files.map((file) => displayPath(project.root, file)),
|
|
118
|
-
frameworkFiles: frameworkFiles.map((file) => displayPath(project.root, file)),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function collectConfigChecks(
|
|
123
|
-
project: ProjectConfig,
|
|
124
|
-
checks: Check[],
|
|
125
|
-
configDisplayPath: string,
|
|
126
|
-
): void {
|
|
127
|
-
if (!project.configFile) {
|
|
128
|
-
checks.push(createCheck("error", "Missing vite config."));
|
|
129
|
-
} else {
|
|
130
|
-
checks.push(createCheck("ok", `Found ${configDisplayPath}.`));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!project.hasPrachtPlugin) {
|
|
134
|
-
checks.push(createCheck("error", "vite.config does not appear to register the pracht plugin."));
|
|
135
|
-
} else {
|
|
136
|
-
checks.push(createCheck("ok", "Vite config registers the pracht plugin."));
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function collectManifestVerification(
|
|
141
|
-
project: ProjectConfig,
|
|
142
|
-
checks: Check[],
|
|
143
|
-
{ changedFiles, scope }: { changedFiles: string[]; scope: string },
|
|
144
|
-
): void {
|
|
145
|
-
const manifestPath = resolveProjectPath(project.root, project.appFile);
|
|
146
|
-
if (!existsSync(manifestPath)) {
|
|
147
|
-
checks.push(createCheck("error", `App manifest is missing at ${project.appFile}.`));
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const source = readFileSync(manifestPath, "utf-8");
|
|
152
|
-
const relativeModules = [...extractRelativeModulePaths(source)];
|
|
153
|
-
const routeCount = (source.match(/\broute\s*\(/g) ?? []).length;
|
|
154
|
-
|
|
155
|
-
if (scope === "full") {
|
|
156
|
-
checks.push(createCheck("ok", `Found app manifest at ${project.appFile}.`));
|
|
157
|
-
|
|
158
|
-
if (routeCount === 0) {
|
|
159
|
-
checks.push(createCheck("warning", "No routes were found in the app manifest."));
|
|
160
|
-
} else {
|
|
161
|
-
checks.push(
|
|
162
|
-
createCheck(
|
|
163
|
-
"ok",
|
|
164
|
-
`App manifest defines ${routeCount} route${routeCount === 1 ? "" : "s"}.`,
|
|
165
|
-
),
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const shellEntries = extractRegistryEntries(source, "shells");
|
|
170
|
-
const middlewareEntries = extractRegistryEntries(source, "middleware");
|
|
171
|
-
|
|
172
|
-
if (shellEntries.length > 0) {
|
|
173
|
-
checks.push(
|
|
174
|
-
createCheck(
|
|
175
|
-
"ok",
|
|
176
|
-
`Registered ${shellEntries.length} shell${shellEntries.length === 1 ? "" : "s"}.`,
|
|
177
|
-
),
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (middlewareEntries.length > 0) {
|
|
182
|
-
checks.push(
|
|
183
|
-
createCheck(
|
|
184
|
-
"ok",
|
|
185
|
-
`Registered ${middlewareEntries.length} middleware module${middlewareEntries.length === 1 ? "" : "s"}.`,
|
|
186
|
-
),
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const missingModules = relativeModules
|
|
191
|
-
.map((modulePath) => ({
|
|
192
|
-
display: modulePath,
|
|
193
|
-
exists: existsSync(resolve(dirname(manifestPath), modulePath)),
|
|
194
|
-
}))
|
|
195
|
-
.filter((entry) => !entry.exists)
|
|
196
|
-
.map((entry) => entry.display);
|
|
197
|
-
|
|
198
|
-
if (missingModules.length > 0) {
|
|
199
|
-
checks.push(
|
|
200
|
-
createCheck(
|
|
201
|
-
"error",
|
|
202
|
-
`Manifest references missing files: ${missingModules.map((item) => JSON.stringify(item)).join(", ")}.`,
|
|
203
|
-
),
|
|
204
|
-
);
|
|
205
|
-
} else {
|
|
206
|
-
checks.push(
|
|
207
|
-
createCheck(
|
|
208
|
-
"ok",
|
|
209
|
-
`All ${relativeModules.length} manifest module path${relativeModules.length === 1 ? "" : "s"} resolve.`,
|
|
210
|
-
),
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
} else {
|
|
214
|
-
collectChangedManifestModuleChecks(
|
|
215
|
-
project,
|
|
216
|
-
checks,
|
|
217
|
-
manifestPath,
|
|
218
|
-
relativeModules,
|
|
219
|
-
changedFiles,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function collectChangedManifestModuleChecks(
|
|
225
|
-
project: ProjectConfig,
|
|
226
|
-
checks: Check[],
|
|
227
|
-
manifestPath: string,
|
|
228
|
-
relativeModules: string[],
|
|
229
|
-
changedFiles: string[],
|
|
230
|
-
): void {
|
|
231
|
-
const manifestDir = dirname(manifestPath);
|
|
232
|
-
const referencedModules = new Set(relativeModules.map(normalizePath));
|
|
233
|
-
const moduleDirectories = [
|
|
234
|
-
{ dir: resolveProjectPath(project.root, project.routesDir), label: "route module" },
|
|
235
|
-
{ dir: resolveProjectPath(project.root, project.shellsDir), label: "shell module" },
|
|
236
|
-
{ dir: resolveProjectPath(project.root, project.middlewareDir), label: "middleware module" },
|
|
237
|
-
{ dir: resolveProjectPath(project.root, project.serverDir), label: "server module" },
|
|
238
|
-
];
|
|
239
|
-
|
|
240
|
-
for (const file of changedFiles) {
|
|
241
|
-
const directory = moduleDirectories.find((entry) => isWithinDirectory(file, entry.dir));
|
|
242
|
-
if (!directory) continue;
|
|
243
|
-
if (!MODULE_SOURCE_RE.test(file)) continue;
|
|
244
|
-
|
|
245
|
-
const display = displayPath(project.root, file);
|
|
246
|
-
const modulePath = normalizePath(toModuleSpecifier(manifestDir, file));
|
|
247
|
-
const exists = existsSync(file);
|
|
248
|
-
|
|
249
|
-
if (referencedModules.has(modulePath)) {
|
|
250
|
-
if (exists) {
|
|
251
|
-
checks.push(
|
|
252
|
-
createCheck(
|
|
253
|
-
"ok",
|
|
254
|
-
`Changed ${directory.label} ${JSON.stringify(display)} is referenced by the app manifest.`,
|
|
255
|
-
),
|
|
256
|
-
);
|
|
257
|
-
} else {
|
|
258
|
-
checks.push(
|
|
259
|
-
createCheck(
|
|
260
|
-
"error",
|
|
261
|
-
`Changed ${directory.label} ${JSON.stringify(display)} was removed but is still referenced by the app manifest.`,
|
|
262
|
-
),
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (exists) {
|
|
269
|
-
checks.push(
|
|
270
|
-
createCheck(
|
|
271
|
-
"warning",
|
|
272
|
-
`Changed ${directory.label} ${JSON.stringify(display)} is not referenced by the app manifest.`,
|
|
273
|
-
),
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function collectPagesVerification(
|
|
280
|
-
project: ProjectConfig,
|
|
281
|
-
checks: Check[],
|
|
282
|
-
{ changedFiles, scope }: { changedFiles: string[]; scope: string },
|
|
283
|
-
): void {
|
|
284
|
-
const pagesDir = resolveProjectPath(project.root, project.pagesDir);
|
|
285
|
-
if (!existsSync(pagesDir)) {
|
|
286
|
-
checks.push(createCheck("error", `Pages directory is missing at ${project.pagesDir}.`));
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const pages = scanPagesDirectory(pagesDir);
|
|
291
|
-
const routes = pages.filter((page) => page.kind === "route");
|
|
292
|
-
const duplicates = collectDuplicateRoutePaths(routes as PagesRoute[]).map((entry) => ({
|
|
293
|
-
...entry,
|
|
294
|
-
files: entry.files.map((file) => displayPath(project.root, file)),
|
|
295
|
-
}));
|
|
296
|
-
|
|
297
|
-
if (scope === "full") {
|
|
298
|
-
checks.push(createCheck("ok", `Found pages directory at ${project.pagesDir}.`));
|
|
299
|
-
|
|
300
|
-
if (routes.length === 0) {
|
|
301
|
-
checks.push(createCheck("warning", "Pages router app does not contain any route files yet."));
|
|
302
|
-
} else {
|
|
303
|
-
checks.push(
|
|
304
|
-
createCheck("ok", `Found ${routes.length} page route${routes.length === 1 ? "" : "s"}.`),
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const hasAppShell = pages.some((page) => page.kind === "shell");
|
|
309
|
-
if (!hasAppShell) {
|
|
310
|
-
checks.push(createCheck("warning", "No `_app` shell was found in the pages directory."));
|
|
311
|
-
} else {
|
|
312
|
-
checks.push(createCheck("ok", "Found a pages-router `_app` shell."));
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
collectChangedPagesChecks(project, checks, pagesDir, changedFiles);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (duplicates.length > 0) {
|
|
319
|
-
checks.push(
|
|
320
|
-
createCheck(
|
|
321
|
-
"error",
|
|
322
|
-
`Pages router resolves duplicate paths: ${duplicates
|
|
323
|
-
.map(
|
|
324
|
-
(entry) =>
|
|
325
|
-
`${JSON.stringify(entry.path)} from ${entry.files.map((file) => JSON.stringify(file)).join(", ")}`,
|
|
326
|
-
)
|
|
327
|
-
.join("; ")}.`,
|
|
328
|
-
),
|
|
329
|
-
);
|
|
330
|
-
} else if (scope === "full" && routes.length > 0) {
|
|
331
|
-
checks.push(
|
|
332
|
-
createCheck(
|
|
333
|
-
"ok",
|
|
334
|
-
`Pages router resolved ${routes.length} route${routes.length === 1 ? "" : "s"} without path collisions.`,
|
|
335
|
-
),
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function collectChangedPagesChecks(
|
|
341
|
-
project: ProjectConfig,
|
|
342
|
-
checks: Check[],
|
|
343
|
-
pagesDir: string,
|
|
344
|
-
changedFiles: string[],
|
|
345
|
-
): void {
|
|
346
|
-
for (const file of changedFiles) {
|
|
347
|
-
if (!isWithinDirectory(file, pagesDir)) continue;
|
|
348
|
-
if (!PAGE_SOURCE_RE.test(file)) continue;
|
|
349
|
-
|
|
350
|
-
const display = displayPath(project.root, file);
|
|
351
|
-
if (!existsSync(file)) {
|
|
352
|
-
checks.push(
|
|
353
|
-
createCheck(
|
|
354
|
-
"ok",
|
|
355
|
-
`Removed page file ${JSON.stringify(display)} is no longer auto-discovered.`,
|
|
356
|
-
),
|
|
357
|
-
);
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const page = describePagesFile(pagesDir, file);
|
|
362
|
-
if (page.kind === "shell") {
|
|
363
|
-
checks.push(
|
|
364
|
-
createCheck(
|
|
365
|
-
"ok",
|
|
366
|
-
`Changed pages shell ${JSON.stringify(display)} will wrap auto-discovered routes.`,
|
|
367
|
-
),
|
|
368
|
-
);
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (page.kind === "ignored") {
|
|
373
|
-
checks.push(
|
|
374
|
-
createCheck(
|
|
375
|
-
"warning",
|
|
376
|
-
`Changed pages file ${JSON.stringify(display)} is ignored by the pages router.`,
|
|
377
|
-
),
|
|
378
|
-
);
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
checks.push(
|
|
383
|
-
createCheck(
|
|
384
|
-
"ok",
|
|
385
|
-
`Changed page route ${JSON.stringify(display)} resolves to ${JSON.stringify(page.routePath)}.`,
|
|
386
|
-
),
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function collectApiVerification(
|
|
392
|
-
project: ProjectConfig,
|
|
393
|
-
checks: Check[],
|
|
394
|
-
{ changedFiles, scope }: { changedFiles: string[]; scope: string },
|
|
395
|
-
): void {
|
|
396
|
-
const apiDir = resolveProjectPath(project.root, project.apiDir);
|
|
397
|
-
const changedApiFiles = changedFiles.filter((file) => isWithinDirectory(file, apiDir));
|
|
398
|
-
if (scope === "changed" && changedApiFiles.length === 0) {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (!existsSync(apiDir)) {
|
|
403
|
-
if (scope === "full") {
|
|
404
|
-
checks.push(
|
|
405
|
-
createCheck(
|
|
406
|
-
"ok",
|
|
407
|
-
`No API directory was found at ${project.apiDir}; skipping API discovery.`,
|
|
408
|
-
),
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const apiFiles = listFilesRecursively(apiDir).filter((file) => MODULE_SOURCE_RE.test(file));
|
|
415
|
-
const routeMap = new Map<string, string[]>();
|
|
416
|
-
|
|
417
|
-
for (const file of apiFiles) {
|
|
418
|
-
const routePath = resolveApiRoutePath(apiDir, file);
|
|
419
|
-
const display = displayPath(project.root, file);
|
|
420
|
-
const entries = routeMap.get(routePath) ?? [];
|
|
421
|
-
entries.push(display);
|
|
422
|
-
routeMap.set(routePath, entries);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const duplicates = [...routeMap.entries()]
|
|
426
|
-
.filter(([, files]) => files.length > 1)
|
|
427
|
-
.map(([path, files]) => ({ files, path }));
|
|
428
|
-
|
|
429
|
-
if (duplicates.length > 0) {
|
|
430
|
-
checks.push(
|
|
431
|
-
createCheck(
|
|
432
|
-
"error",
|
|
433
|
-
`API route discovery resolves duplicate paths: ${duplicates
|
|
434
|
-
.map(
|
|
435
|
-
(entry) =>
|
|
436
|
-
`${JSON.stringify(entry.path)} from ${entry.files.map((file) => JSON.stringify(file)).join(", ")}`,
|
|
437
|
-
)
|
|
438
|
-
.join("; ")}.`,
|
|
439
|
-
),
|
|
440
|
-
);
|
|
441
|
-
} else if (scope === "full") {
|
|
442
|
-
checks.push(
|
|
443
|
-
createCheck(
|
|
444
|
-
"ok",
|
|
445
|
-
`API route discovery resolved ${apiFiles.length} route${apiFiles.length === 1 ? "" : "s"}.`,
|
|
446
|
-
),
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
for (const file of changedApiFiles) {
|
|
451
|
-
if (!MODULE_SOURCE_RE.test(file)) continue;
|
|
452
|
-
|
|
453
|
-
const display = displayPath(project.root, file);
|
|
454
|
-
if (!existsSync(file)) {
|
|
455
|
-
checks.push(
|
|
456
|
-
createCheck(
|
|
457
|
-
"ok",
|
|
458
|
-
`Removed API route ${JSON.stringify(display)} is no longer auto-discovered.`,
|
|
459
|
-
),
|
|
460
|
-
);
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
checks.push(
|
|
465
|
-
createCheck(
|
|
466
|
-
"ok",
|
|
467
|
-
`Changed API route ${JSON.stringify(display)} resolves to ${JSON.stringify(resolveApiRoutePath(apiDir, file))}.`,
|
|
468
|
-
),
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function collectPackageChecks(
|
|
474
|
-
project: ProjectConfig,
|
|
475
|
-
checks: Check[],
|
|
476
|
-
packageJsonPath: string,
|
|
477
|
-
): void {
|
|
478
|
-
if (!existsSync(packageJsonPath)) {
|
|
479
|
-
checks.push(createCheck("warning", "No package.json found in the current app root."));
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
484
|
-
const deps: Record<string, string> = {
|
|
485
|
-
...packageJson.dependencies,
|
|
486
|
-
...packageJson.devDependencies,
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
if (!("@pracht/cli" in deps)) {
|
|
490
|
-
checks.push(
|
|
491
|
-
createCheck("warning", "`@pracht/cli` is not listed in package.json dependencies."),
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const adapterPackages = Object.keys(deps).filter((name) => name.startsWith("@pracht/adapter-"));
|
|
496
|
-
if (adapterPackages.length === 0) {
|
|
497
|
-
checks.push(
|
|
498
|
-
createCheck("warning", "No built-in pracht adapter dependency was found in package.json."),
|
|
499
|
-
);
|
|
500
|
-
} else {
|
|
501
|
-
checks.push(
|
|
502
|
-
createCheck(
|
|
503
|
-
"ok",
|
|
504
|
-
`Found adapter dependency ${adapterPackages.map((name) => JSON.stringify(name)).join(", ")}.`,
|
|
505
|
-
),
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function collectChangedFiles(root: string): { files: string[]; warning: string | null } {
|
|
511
|
-
try {
|
|
512
|
-
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
513
|
-
cwd: root,
|
|
514
|
-
encoding: "utf-8",
|
|
515
|
-
}).trim();
|
|
516
|
-
const prefix = execFileSync("git", ["rev-parse", "--show-prefix"], {
|
|
517
|
-
cwd: root,
|
|
518
|
-
encoding: "utf-8",
|
|
519
|
-
}).trim();
|
|
520
|
-
const output = execFileSync("git", ["status", "--porcelain", "--untracked-files=all"], {
|
|
521
|
-
cwd: repoRoot,
|
|
522
|
-
encoding: "utf-8",
|
|
523
|
-
});
|
|
524
|
-
const files = new Set<string>();
|
|
525
|
-
|
|
526
|
-
for (const line of output.split(/\r?\n/).filter(Boolean)) {
|
|
527
|
-
const record = line.slice(3);
|
|
528
|
-
if (!record) continue;
|
|
529
|
-
|
|
530
|
-
if (record.includes(" -> ")) {
|
|
531
|
-
const [from, to] = record.split(" -> ");
|
|
532
|
-
addChangedFile(files, repoRoot, prefix, from);
|
|
533
|
-
addChangedFile(files, repoRoot, prefix, to);
|
|
534
|
-
} else {
|
|
535
|
-
addChangedFile(files, repoRoot, prefix, record);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return {
|
|
540
|
-
files: [...files],
|
|
541
|
-
warning: null,
|
|
542
|
-
};
|
|
543
|
-
} catch {
|
|
544
|
-
return {
|
|
545
|
-
files: [],
|
|
546
|
-
warning: "Unable to determine changed files from git; ran full verification instead.",
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function addChangedFile(
|
|
552
|
-
files: Set<string>,
|
|
553
|
-
repoRoot: string,
|
|
554
|
-
prefix: string,
|
|
555
|
-
repoRelativePath: string,
|
|
556
|
-
): void {
|
|
557
|
-
if (prefix && !repoRelativePath.startsWith(prefix)) {
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const projectRelativePath = prefix ? repoRelativePath.slice(prefix.length) : repoRelativePath;
|
|
562
|
-
if (!projectRelativePath) {
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
files.add(resolve(repoRoot, projectRelativePath));
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function filterFrameworkFiles(
|
|
570
|
-
project: ProjectConfig,
|
|
571
|
-
files: string[],
|
|
572
|
-
packageJsonPath: string,
|
|
573
|
-
): string[] {
|
|
574
|
-
const appFile = resolveProjectPath(project.root, project.appFile);
|
|
575
|
-
const routesDir = resolveProjectPath(project.root, project.routesDir);
|
|
576
|
-
const shellsDir = resolveProjectPath(project.root, project.shellsDir);
|
|
577
|
-
const middlewareDir = resolveProjectPath(project.root, project.middlewareDir);
|
|
578
|
-
const serverDir = resolveProjectPath(project.root, project.serverDir);
|
|
579
|
-
const apiDir = resolveProjectPath(project.root, project.apiDir);
|
|
580
|
-
const pagesDir = project.pagesDir ? resolveProjectPath(project.root, project.pagesDir) : null;
|
|
581
|
-
|
|
582
|
-
return files.filter((file) => {
|
|
583
|
-
if (CONFIG_FILE_NAMES.has(basename(file))) return true;
|
|
584
|
-
if (normalizePath(file) === normalizePath(packageJsonPath)) return true;
|
|
585
|
-
if (project.mode === "manifest" && normalizePath(file) === normalizePath(appFile)) return true;
|
|
586
|
-
if (isWithinDirectory(file, routesDir) && MODULE_SOURCE_RE.test(file)) return true;
|
|
587
|
-
if (isWithinDirectory(file, shellsDir) && MODULE_SOURCE_RE.test(file)) return true;
|
|
588
|
-
if (isWithinDirectory(file, middlewareDir) && MODULE_SOURCE_RE.test(file)) return true;
|
|
589
|
-
if (isWithinDirectory(file, serverDir) && MODULE_SOURCE_RE.test(file)) return true;
|
|
590
|
-
if (isWithinDirectory(file, apiDir) && MODULE_SOURCE_RE.test(file)) return true;
|
|
591
|
-
if (pagesDir && isWithinDirectory(file, pagesDir) && PAGE_SOURCE_RE.test(file)) return true;
|
|
592
|
-
return false;
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function requiresFullVerification(project: ProjectConfig, changedFiles: string[]): boolean {
|
|
597
|
-
const packageJsonPath = resolve(project.root, "package.json");
|
|
598
|
-
const appFile = resolveProjectPath(project.root, project.appFile);
|
|
599
|
-
|
|
600
|
-
return changedFiles.some((file) => {
|
|
601
|
-
const normalized = normalizePath(file);
|
|
602
|
-
if (CONFIG_FILE_NAMES.has(basename(file))) return true;
|
|
603
|
-
if (normalized === normalizePath(packageJsonPath)) return true;
|
|
604
|
-
if (project.mode === "manifest" && normalized === normalizePath(appFile)) return true;
|
|
605
|
-
return false;
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
type PagesFile = { file: string; kind: "shell" } | { file: string; kind: "ignored" } | PagesRoute;
|
|
610
|
-
|
|
611
|
-
interface PagesRoute {
|
|
612
|
-
file: string;
|
|
613
|
-
kind: "route";
|
|
614
|
-
routePath: string;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function scanPagesDirectory(pagesDir: string): PagesFile[] {
|
|
618
|
-
return listFilesRecursively(pagesDir)
|
|
619
|
-
.filter((file) => PAGE_SOURCE_RE.test(file))
|
|
620
|
-
.map((file) => describePagesFile(pagesDir, file));
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function describePagesFile(pagesDir: string, file: string): PagesFile {
|
|
624
|
-
const relativePath = relative(pagesDir, file).replace(/\\/g, "/");
|
|
625
|
-
const routePath = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, "");
|
|
626
|
-
const name = basename(routePath);
|
|
627
|
-
|
|
628
|
-
if (hasPagesAppShell(file)) {
|
|
629
|
-
return { file, kind: "shell" };
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (name.startsWith("_")) {
|
|
633
|
-
return { file, kind: "ignored" };
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (routePath === "index") {
|
|
637
|
-
return { file, kind: "route", routePath: "/" };
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const withoutIndex = routePath.replace(/\/index$/, "");
|
|
641
|
-
const normalized = withoutIndex
|
|
642
|
-
.replace(/\[\.\.\.([^\]]+)\]/g, "*")
|
|
643
|
-
.replace(/\[([^\].]+)\]/g, ":$1");
|
|
644
|
-
|
|
645
|
-
return {
|
|
646
|
-
file,
|
|
647
|
-
kind: "route",
|
|
648
|
-
routePath: normalizeRoutePath(`/${normalized}`),
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function collectDuplicateRoutePaths(routes: PagesRoute[]): { files: string[]; path: string }[] {
|
|
653
|
-
const routeMap = new Map<string, string[]>();
|
|
654
|
-
|
|
655
|
-
for (const route of routes) {
|
|
656
|
-
const files = routeMap.get(route.routePath) ?? [];
|
|
657
|
-
files.push(route.file);
|
|
658
|
-
routeMap.set(route.routePath, files);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
return [...routeMap.entries()]
|
|
662
|
-
.filter(([, files]) => files.length > 1)
|
|
663
|
-
.map(([path, files]) => ({ files, path }));
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function resolveApiRoutePath(apiDir: string, file: string): string {
|
|
667
|
-
let relativePath = relative(apiDir, file).replace(/\\/g, "/");
|
|
668
|
-
relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
669
|
-
|
|
670
|
-
if (relativePath === "index") {
|
|
671
|
-
relativePath = "";
|
|
672
|
-
} else {
|
|
673
|
-
relativePath = relativePath.replace(/\/index$/, "");
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
relativePath = relativePath.replace(/\[([^\]]+)\]/g, ":$1");
|
|
677
|
-
|
|
678
|
-
return normalizeRoutePath(relativePath ? `/api/${relativePath}` : "/api");
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function toModuleSpecifier(fromDir: string, filePath: string): string {
|
|
682
|
-
const relativePath = relative(fromDir, filePath).replace(/\\/g, "/");
|
|
683
|
-
if (relativePath.startsWith(".")) {
|
|
684
|
-
return relativePath;
|
|
685
|
-
}
|
|
686
|
-
return `./${relativePath}`;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function normalizeRoutePath(path: string): string {
|
|
690
|
-
if (!path || path === "/") {
|
|
691
|
-
return "/";
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const withLeadingSlash = path.startsWith("/") ? path : `/${path}`;
|
|
695
|
-
const collapsed = withLeadingSlash.replace(/\/{2,}/g, "/");
|
|
696
|
-
return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function isWithinDirectory(filePath: string, directoryPath: string): boolean {
|
|
700
|
-
const relativePath = relative(directoryPath, filePath);
|
|
701
|
-
return relativePath === "" || (!relativePath.startsWith("..") && !relativePath.startsWith("../"));
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function normalizePath(value: string): string {
|
|
705
|
-
return value.replace(/\\/g, "/");
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function createCheck(status: Check["status"], message: string): Check {
|
|
709
|
-
return { message, status };
|
|
710
|
-
}
|