@pracht/cli 1.1.5 → 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.
- package/CHANGELOG.md +11 -0
- package/bin/pracht.js +1 -40
- package/dist/build-BzQAQjuy.mjs +212 -0
- package/dist/build-metadata-IABPFFKk.mjs +49 -0
- package/dist/dev-BCFe3g38.mjs +25 -0
- package/dist/doctor-BcoqyBk-.mjs +25 -0
- package/dist/generate-BXkePCIx.mjs +404 -0
- package/dist/index.mjs +41 -0
- package/dist/inspect-DIjjCfs3.mjs +128 -0
- package/dist/manifest-DGq1n5LT.mjs +99 -0
- package/dist/project-DydjqBoS.mjs +155 -0
- package/dist/verification-Dfl3X4Zo.mjs +372 -0
- package/dist/verify-CObGxWtW.mjs +31 -0
- package/package.json +6 -2
- package/{lib/build-metadata.js → src/build-metadata.ts} +22 -9
- package/{lib/build-shared.js → src/build-shared.ts} +39 -13
- package/src/commands/build.ts +132 -0
- package/src/commands/dev.ts +27 -0
- package/src/commands/doctor.ts +33 -0
- package/{lib/commands/generate.js → src/commands/generate.ts} +178 -72
- package/{lib/commands/inspect.js → src/commands/inspect.ts} +81 -30
- package/src/commands/verify.ts +37 -0
- package/{lib/constants.js → src/constants.ts} +12 -2
- package/src/index.ts +21 -0
- package/{lib/manifest.js → src/manifest.ts} +32 -13
- package/{lib/project.js → src/project.ts} +48 -20
- package/src/utils.ts +72 -0
- package/{lib/verification.js → src/verification.ts} +103 -34
- package/tsdown.config.ts +8 -0
- package/lib/cli.js +0 -164
- package/lib/commands/build.js +0 -120
- package/lib/commands/dev.js +0 -16
- package/lib/commands/doctor.js +0 -21
- package/lib/commands/verify.js +0 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function ensureCoreNamedImport(source, name) {
|
|
1
|
+
export function ensureCoreNamedImport(source: string, name: string): string {
|
|
2
2
|
const match = source.match(/import\s*\{([^}]+)\}\s*from\s*["']@pracht\/core["'];?/);
|
|
3
3
|
if (!match) {
|
|
4
4
|
return `import { ${name} } from "@pracht/core";\n${source}`;
|
|
@@ -15,7 +15,7 @@ export function ensureCoreNamedImport(source, name) {
|
|
|
15
15
|
return source.replace(match[0], `import { ${names.join(", ")} } from "@pracht/core";`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function upsertObjectEntry(source, key, entry) {
|
|
18
|
+
export function upsertObjectEntry(source: string, key: string, entry: string): string {
|
|
19
19
|
const property = findNamedBlock(source, key, "{", "}");
|
|
20
20
|
if (!property) {
|
|
21
21
|
const routesMatch = source.match(/^(\s*)routes\s*:/m);
|
|
@@ -31,7 +31,7 @@ export function upsertObjectEntry(source, key, entry) {
|
|
|
31
31
|
return insertBlockEntry(source, property, entry);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function insertArrayItem(source, key, item) {
|
|
34
|
+
export function insertArrayItem(source: string, key: string, item: string): string {
|
|
35
35
|
const property = findNamedBlock(source, key, "[", "]");
|
|
36
36
|
if (!property) {
|
|
37
37
|
throw new Error(`Could not find "${key}" in the app manifest.`);
|
|
@@ -40,7 +40,7 @@ export function insertArrayItem(source, key, item) {
|
|
|
40
40
|
return insertBlockEntry(source, property, item);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function toManifestModulePath(manifestPath, targetFilePath) {
|
|
43
|
+
export function toManifestModulePath(manifestPath: string, targetFilePath: string): string {
|
|
44
44
|
const relativePath = targetFilePath
|
|
45
45
|
.replaceAll("\\", "/")
|
|
46
46
|
.replace(manifestPath.replaceAll("\\", "/").replace(/\/[^/]+$/, ""), "")
|
|
@@ -49,11 +49,14 @@ export function toManifestModulePath(manifestPath, targetFilePath) {
|
|
|
49
49
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function extractRegistryEntries(
|
|
52
|
+
export function extractRegistryEntries(
|
|
53
|
+
source: string,
|
|
54
|
+
key: string,
|
|
55
|
+
): { name: string; path: string }[] {
|
|
53
56
|
const block = findNamedBlock(source, key, "{", "}");
|
|
54
57
|
if (!block) return [];
|
|
55
58
|
const inner = source.slice(block.openIndex + 1, block.closeIndex);
|
|
56
|
-
const entries = [];
|
|
59
|
+
const entries: { name: string; path: string }[] = [];
|
|
57
60
|
const pattern =
|
|
58
61
|
/([A-Za-z0-9_-]+)\s*:\s*(?:(["'`])([^"'`]+)\2|\(\)\s*=>\s*import\(\s*(["'`])([^"'`]+)\4\s*\))/g;
|
|
59
62
|
|
|
@@ -64,15 +67,21 @@ export function extractRegistryEntries(source, key) {
|
|
|
64
67
|
return entries;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
export function extractRelativeModulePaths(source) {
|
|
68
|
-
const results = new Set();
|
|
70
|
+
export function extractRelativeModulePaths(source: string): Set<string> {
|
|
71
|
+
const results = new Set<string>();
|
|
69
72
|
for (const match of source.matchAll(/["'`]((?:\.\.\/|\.\/)[^"'`]+)["'`]/g)) {
|
|
70
73
|
results.add(match[1]);
|
|
71
74
|
}
|
|
72
75
|
return results;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
interface BlockLocation {
|
|
79
|
+
closeIndex: number;
|
|
80
|
+
indent: string;
|
|
81
|
+
openIndex: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function insertBlockEntry(source: string, block: BlockLocation, entry: string): string {
|
|
76
85
|
const inner = source.slice(block.openIndex + 1, block.closeIndex);
|
|
77
86
|
const closingIndent = block.indent;
|
|
78
87
|
const childIndent = `${closingIndent} `;
|
|
@@ -87,7 +96,12 @@ function insertBlockEntry(source, block, entry) {
|
|
|
87
96
|
return `${source.slice(0, block.closeIndex)}${insertPrefix}\n${indentMultiline(entry, childIndent)}\n${closingIndent}${source.slice(block.closeIndex)}`;
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
function findNamedBlock(
|
|
99
|
+
function findNamedBlock(
|
|
100
|
+
source: string,
|
|
101
|
+
key: string,
|
|
102
|
+
openChar: string,
|
|
103
|
+
closeChar: string,
|
|
104
|
+
): BlockLocation | null {
|
|
91
105
|
const pattern = new RegExp(`^([ \\t]*)${key}\\s*:\\s*\\${openChar}`, "m");
|
|
92
106
|
const match = source.match(pattern);
|
|
93
107
|
if (!match || match.index == null) {
|
|
@@ -103,9 +117,14 @@ function findNamedBlock(source, key, openChar, closeChar) {
|
|
|
103
117
|
};
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
function findMatchingDelimiter(
|
|
120
|
+
function findMatchingDelimiter(
|
|
121
|
+
source: string,
|
|
122
|
+
openIndex: number,
|
|
123
|
+
openChar: string,
|
|
124
|
+
closeChar: string,
|
|
125
|
+
): number {
|
|
107
126
|
let depth = 0;
|
|
108
|
-
let quoteChar = null;
|
|
127
|
+
let quoteChar: string | null = null;
|
|
109
128
|
let escaping = false;
|
|
110
129
|
|
|
111
130
|
for (let index = openIndex; index < source.length; index += 1) {
|
|
@@ -139,7 +158,7 @@ function findMatchingDelimiter(source, openIndex, openChar, closeChar) {
|
|
|
139
158
|
throw new Error(`Could not find matching ${closeChar} for ${openChar}.`);
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
function indentMultiline(value, indent) {
|
|
161
|
+
function indentMultiline(value: string, indent: string): string {
|
|
143
162
|
return value
|
|
144
163
|
.split("\n")
|
|
145
164
|
.map((line) => `${indent}${line}`)
|
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, relative, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { ensureTrailingNewline } from "./
|
|
4
|
+
import { ensureTrailingNewline } from "./utils.js";
|
|
5
5
|
import { PROJECT_DEFAULTS } from "./constants.js";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export interface ProjectConfig {
|
|
8
|
+
apiDir: string;
|
|
9
|
+
appFile: string;
|
|
10
|
+
configFile: string | null;
|
|
11
|
+
hasPrachtPlugin: boolean;
|
|
12
|
+
middlewareDir: string;
|
|
13
|
+
mode: "manifest" | "pages";
|
|
14
|
+
pagesDefaultRender: string;
|
|
15
|
+
pagesDir: string;
|
|
16
|
+
rawConfig: string;
|
|
17
|
+
root: string;
|
|
18
|
+
routesDir: string;
|
|
19
|
+
serverDir: string;
|
|
20
|
+
shellsDir: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readProjectConfig(root: string): ProjectConfig {
|
|
8
24
|
const configFile = findConfigFile(root);
|
|
9
25
|
const rawConfig = configFile ? readFileSync(configFile, "utf-8") : "";
|
|
10
|
-
const config = {
|
|
26
|
+
const config: Record<string, unknown> = {
|
|
11
27
|
...PROJECT_DEFAULTS,
|
|
12
28
|
configFile,
|
|
13
29
|
hasPrachtPlugin: /\bpracht\s*\(/.test(rawConfig),
|
|
30
|
+
mode: "manifest" as const,
|
|
14
31
|
rawConfig,
|
|
15
32
|
root,
|
|
16
33
|
};
|
|
@@ -23,18 +40,22 @@ export function readProjectConfig(root) {
|
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
config.mode = config.pagesDir ? "pages" : "manifest";
|
|
26
|
-
return config;
|
|
43
|
+
return config as unknown as ProjectConfig;
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
export function resolveProjectPath(root, configPath) {
|
|
46
|
+
export function resolveProjectPath(root: string, configPath: string): string {
|
|
30
47
|
return resolve(root, `.${configPath}`);
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
export function resolveScopedFile(root, configDir, fileName) {
|
|
50
|
+
export function resolveScopedFile(root: string, configDir: string, fileName: string): string {
|
|
34
51
|
return resolve(resolveProjectPath(root, configDir), fileName);
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
export function resolveRouteModulePath(
|
|
54
|
+
export function resolveRouteModulePath(
|
|
55
|
+
project: ProjectConfig,
|
|
56
|
+
routePath: string,
|
|
57
|
+
extension: string,
|
|
58
|
+
): { absolutePath: string; relativePath: string } {
|
|
38
59
|
const segments = segmentsFromRoutePath(routePath);
|
|
39
60
|
const relativePath =
|
|
40
61
|
segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
|
|
@@ -42,7 +63,11 @@ export function resolveRouteModulePath(project, routePath, extension) {
|
|
|
42
63
|
return { absolutePath, relativePath };
|
|
43
64
|
}
|
|
44
65
|
|
|
45
|
-
export function resolvePagesRouteModulePath(
|
|
66
|
+
export function resolvePagesRouteModulePath(
|
|
67
|
+
project: ProjectConfig,
|
|
68
|
+
routePath: string,
|
|
69
|
+
extension: string,
|
|
70
|
+
): { absolutePath: string; relativePath: string } {
|
|
46
71
|
const segments = segmentsFromRoutePath(routePath);
|
|
47
72
|
const relativePath =
|
|
48
73
|
segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
|
|
@@ -50,18 +75,21 @@ export function resolvePagesRouteModulePath(project, routePath, extension) {
|
|
|
50
75
|
return { absolutePath, relativePath };
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
export function resolveApiModulePath(
|
|
78
|
+
export function resolveApiModulePath(
|
|
79
|
+
project: ProjectConfig,
|
|
80
|
+
endpointPath: string,
|
|
81
|
+
): { absolutePath: string; relativePath: string } {
|
|
54
82
|
const segments = segmentsFromApiPath(endpointPath);
|
|
55
83
|
const relativePath = segments.length === 0 ? "index.ts" : `${segments.join("/")}.ts`;
|
|
56
84
|
const absolutePath = resolve(resolveProjectPath(project.root, project.apiDir), relativePath);
|
|
57
85
|
return { absolutePath, relativePath };
|
|
58
86
|
}
|
|
59
87
|
|
|
60
|
-
export function displayPath(root, filePath) {
|
|
88
|
+
export function displayPath(root: string, filePath: string): string {
|
|
61
89
|
return relative(root, filePath) || ".";
|
|
62
90
|
}
|
|
63
91
|
|
|
64
|
-
export function writeGeneratedFile(filePath, source) {
|
|
92
|
+
export function writeGeneratedFile(filePath: string, source: string): void {
|
|
65
93
|
if (existsSync(filePath)) {
|
|
66
94
|
throw new Error(`Refusing to overwrite existing file ${filePath}.`);
|
|
67
95
|
}
|
|
@@ -70,14 +98,14 @@ export function writeGeneratedFile(filePath, source) {
|
|
|
70
98
|
writeFileSync(filePath, ensureTrailingNewline(source), "utf-8");
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
export function assertFileExists(filePath, message) {
|
|
101
|
+
export function assertFileExists(filePath: string, message: string): void {
|
|
74
102
|
if (!existsSync(filePath)) {
|
|
75
103
|
throw new Error(message);
|
|
76
104
|
}
|
|
77
105
|
}
|
|
78
106
|
|
|
79
|
-
export function listFilesRecursively(dir) {
|
|
80
|
-
const files = [];
|
|
107
|
+
export function listFilesRecursively(dir: string): string[] {
|
|
108
|
+
const files: string[] = [];
|
|
81
109
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
82
110
|
const fullPath = resolve(dir, entry.name);
|
|
83
111
|
if (entry.isDirectory()) {
|
|
@@ -89,11 +117,11 @@ export function listFilesRecursively(dir) {
|
|
|
89
117
|
return files;
|
|
90
118
|
}
|
|
91
119
|
|
|
92
|
-
export function hasPagesAppShell(filePath) {
|
|
120
|
+
export function hasPagesAppShell(filePath: string): boolean {
|
|
93
121
|
return /^_app\.(ts|tsx|js|jsx)$/.test(basename(filePath));
|
|
94
122
|
}
|
|
95
123
|
|
|
96
|
-
function findConfigFile(root) {
|
|
124
|
+
function findConfigFile(root: string): string | null {
|
|
97
125
|
for (const name of [
|
|
98
126
|
"vite.config.ts",
|
|
99
127
|
"vite.config.mts",
|
|
@@ -108,19 +136,19 @@ function findConfigFile(root) {
|
|
|
108
136
|
return null;
|
|
109
137
|
}
|
|
110
138
|
|
|
111
|
-
function readQuotedConfigValue(source, key) {
|
|
139
|
+
function readQuotedConfigValue(source: string, key: string): string | null {
|
|
112
140
|
if (!source) return null;
|
|
113
141
|
const pattern = new RegExp(`${key}\\s*:\\s*(["'\\\`])([^"'\\\`]+)\\1`);
|
|
114
142
|
const match = source.match(pattern);
|
|
115
143
|
return match ? match[2] : null;
|
|
116
144
|
}
|
|
117
145
|
|
|
118
|
-
function normalizeConfigPath(value) {
|
|
146
|
+
function normalizeConfigPath(value: string): string {
|
|
119
147
|
if (!value) return value;
|
|
120
148
|
return value.startsWith("/") ? value : `/${value}`;
|
|
121
149
|
}
|
|
122
150
|
|
|
123
|
-
function segmentsFromRoutePath(routePath) {
|
|
151
|
+
function segmentsFromRoutePath(routePath: string): string[] {
|
|
124
152
|
return routePath
|
|
125
153
|
.split("/")
|
|
126
154
|
.filter(Boolean)
|
|
@@ -131,7 +159,7 @@ function segmentsFromRoutePath(routePath) {
|
|
|
131
159
|
});
|
|
132
160
|
}
|
|
133
161
|
|
|
134
|
-
function segmentsFromApiPath(endpointPath) {
|
|
162
|
+
function segmentsFromApiPath(endpointPath: string): string[] {
|
|
135
163
|
return endpointPath
|
|
136
164
|
.split("/")
|
|
137
165
|
.filter(Boolean)
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { HTTP_METHODS, type HttpMethod } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
export function quote(value: string): string {
|
|
4
|
+
return JSON.stringify(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ensureTrailingNewline(value: string): string {
|
|
8
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseCommaList(value: string | string[] | boolean | undefined): string[] {
|
|
12
|
+
if (!value || typeof value === "boolean") return [];
|
|
13
|
+
const values = Array.isArray(value) ? value : [value];
|
|
14
|
+
return values
|
|
15
|
+
.flatMap((entry) => String(entry).split(","))
|
|
16
|
+
.map((entry) => entry.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseApiMethods(value: string | string[] | boolean | undefined): HttpMethod[] {
|
|
21
|
+
const methods = parseCommaList(value);
|
|
22
|
+
const normalized =
|
|
23
|
+
methods.length === 0
|
|
24
|
+
? (["GET"] as HttpMethod[])
|
|
25
|
+
: methods.map((entry) => entry.toUpperCase() as HttpMethod);
|
|
26
|
+
|
|
27
|
+
for (const method of normalized) {
|
|
28
|
+
if (!HTTP_METHODS.has(method)) {
|
|
29
|
+
throw new Error(`Unsupported HTTP method "${method}".`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [...new Set(normalized)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function requireEnum(
|
|
37
|
+
value: string | undefined,
|
|
38
|
+
key: string,
|
|
39
|
+
allowed: string[],
|
|
40
|
+
fallback: string,
|
|
41
|
+
): string {
|
|
42
|
+
const val = value ?? fallback;
|
|
43
|
+
if (!allowed.includes(val)) {
|
|
44
|
+
throw new Error(`Invalid value for --${key}. Expected one of ${allowed.join(", ")}.`);
|
|
45
|
+
}
|
|
46
|
+
return val;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function requirePositiveInteger(
|
|
50
|
+
value: string | undefined,
|
|
51
|
+
key: string,
|
|
52
|
+
fallback: number,
|
|
53
|
+
): number {
|
|
54
|
+
const parsed = value == null ? fallback : Number.parseInt(value, 10);
|
|
55
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
56
|
+
throw new Error(`--${key} must be a positive integer.`);
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function handleCliError(error: unknown, { json }: { json: boolean }): never {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
if (json) {
|
|
64
|
+
console.error(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
65
|
+
} else {
|
|
66
|
+
console.error(message);
|
|
67
|
+
if (error instanceof Error && error.stack && process.env.DEBUG) {
|
|
68
|
+
console.error(error.stack);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
listFilesRecursively,
|
|
10
10
|
readProjectConfig,
|
|
11
11
|
resolveProjectPath,
|
|
12
|
+
type ProjectConfig,
|
|
12
13
|
} from "./project.js";
|
|
13
14
|
|
|
14
15
|
const CONFIG_FILE_NAMES = new Set([
|
|
@@ -23,7 +24,30 @@ const CONFIG_FILE_NAMES = new Set([
|
|
|
23
24
|
const MODULE_SOURCE_RE = /\.(ts|tsx|js|jsx)$/;
|
|
24
25
|
const PAGE_SOURCE_RE = /\.(ts|tsx|js|jsx|md|mdx)$/;
|
|
25
26
|
|
|
26
|
-
export
|
|
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 {
|
|
27
51
|
const report = runVerification(root);
|
|
28
52
|
|
|
29
53
|
return {
|
|
@@ -34,9 +58,12 @@ export function runDoctor(root) {
|
|
|
34
58
|
};
|
|
35
59
|
}
|
|
36
60
|
|
|
37
|
-
export function runVerification(
|
|
61
|
+
export function runVerification(
|
|
62
|
+
root: string,
|
|
63
|
+
options: { changed?: boolean } = {},
|
|
64
|
+
): VerificationReport {
|
|
38
65
|
const project = readProjectConfig(root);
|
|
39
|
-
const checks = [];
|
|
66
|
+
const checks: Check[] = [];
|
|
40
67
|
const packageJsonPath = resolve(project.root, "package.json");
|
|
41
68
|
const configDisplayPath = project.configFile
|
|
42
69
|
? displayPath(root, project.configFile)
|
|
@@ -45,7 +72,7 @@ export function runVerification(root, options = {}) {
|
|
|
45
72
|
|
|
46
73
|
collectConfigChecks(project, checks, configDisplayPath);
|
|
47
74
|
|
|
48
|
-
let changedInfo = {
|
|
75
|
+
let changedInfo: { files: string[]; warning: string | null } = {
|
|
49
76
|
files: [],
|
|
50
77
|
warning: null,
|
|
51
78
|
};
|
|
@@ -92,7 +119,11 @@ export function runVerification(root, options = {}) {
|
|
|
92
119
|
};
|
|
93
120
|
}
|
|
94
121
|
|
|
95
|
-
function collectConfigChecks(
|
|
122
|
+
function collectConfigChecks(
|
|
123
|
+
project: ProjectConfig,
|
|
124
|
+
checks: Check[],
|
|
125
|
+
configDisplayPath: string,
|
|
126
|
+
): void {
|
|
96
127
|
if (!project.configFile) {
|
|
97
128
|
checks.push(createCheck("error", "Missing vite config."));
|
|
98
129
|
} else {
|
|
@@ -106,7 +137,11 @@ function collectConfigChecks(project, checks, configDisplayPath) {
|
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
|
|
109
|
-
function collectManifestVerification(
|
|
140
|
+
function collectManifestVerification(
|
|
141
|
+
project: ProjectConfig,
|
|
142
|
+
checks: Check[],
|
|
143
|
+
{ changedFiles, scope }: { changedFiles: string[]; scope: string },
|
|
144
|
+
): void {
|
|
110
145
|
const manifestPath = resolveProjectPath(project.root, project.appFile);
|
|
111
146
|
if (!existsSync(manifestPath)) {
|
|
112
147
|
checks.push(createCheck("error", `App manifest is missing at ${project.appFile}.`));
|
|
@@ -187,12 +222,12 @@ function collectManifestVerification(project, checks, { changedFiles, scope }) {
|
|
|
187
222
|
}
|
|
188
223
|
|
|
189
224
|
function collectChangedManifestModuleChecks(
|
|
190
|
-
project,
|
|
191
|
-
checks,
|
|
192
|
-
manifestPath,
|
|
193
|
-
relativeModules,
|
|
194
|
-
changedFiles,
|
|
195
|
-
) {
|
|
225
|
+
project: ProjectConfig,
|
|
226
|
+
checks: Check[],
|
|
227
|
+
manifestPath: string,
|
|
228
|
+
relativeModules: string[],
|
|
229
|
+
changedFiles: string[],
|
|
230
|
+
): void {
|
|
196
231
|
const manifestDir = dirname(manifestPath);
|
|
197
232
|
const referencedModules = new Set(relativeModules.map(normalizePath));
|
|
198
233
|
const moduleDirectories = [
|
|
@@ -241,7 +276,11 @@ function collectChangedManifestModuleChecks(
|
|
|
241
276
|
}
|
|
242
277
|
}
|
|
243
278
|
|
|
244
|
-
function collectPagesVerification(
|
|
279
|
+
function collectPagesVerification(
|
|
280
|
+
project: ProjectConfig,
|
|
281
|
+
checks: Check[],
|
|
282
|
+
{ changedFiles, scope }: { changedFiles: string[]; scope: string },
|
|
283
|
+
): void {
|
|
245
284
|
const pagesDir = resolveProjectPath(project.root, project.pagesDir);
|
|
246
285
|
if (!existsSync(pagesDir)) {
|
|
247
286
|
checks.push(createCheck("error", `Pages directory is missing at ${project.pagesDir}.`));
|
|
@@ -250,7 +289,7 @@ function collectPagesVerification(project, checks, { changedFiles, scope }) {
|
|
|
250
289
|
|
|
251
290
|
const pages = scanPagesDirectory(pagesDir);
|
|
252
291
|
const routes = pages.filter((page) => page.kind === "route");
|
|
253
|
-
const duplicates = collectDuplicateRoutePaths(routes).map((entry) => ({
|
|
292
|
+
const duplicates = collectDuplicateRoutePaths(routes as PagesRoute[]).map((entry) => ({
|
|
254
293
|
...entry,
|
|
255
294
|
files: entry.files.map((file) => displayPath(project.root, file)),
|
|
256
295
|
}));
|
|
@@ -298,7 +337,12 @@ function collectPagesVerification(project, checks, { changedFiles, scope }) {
|
|
|
298
337
|
}
|
|
299
338
|
}
|
|
300
339
|
|
|
301
|
-
function collectChangedPagesChecks(
|
|
340
|
+
function collectChangedPagesChecks(
|
|
341
|
+
project: ProjectConfig,
|
|
342
|
+
checks: Check[],
|
|
343
|
+
pagesDir: string,
|
|
344
|
+
changedFiles: string[],
|
|
345
|
+
): void {
|
|
302
346
|
for (const file of changedFiles) {
|
|
303
347
|
if (!isWithinDirectory(file, pagesDir)) continue;
|
|
304
348
|
if (!PAGE_SOURCE_RE.test(file)) continue;
|
|
@@ -344,7 +388,11 @@ function collectChangedPagesChecks(project, checks, pagesDir, changedFiles) {
|
|
|
344
388
|
}
|
|
345
389
|
}
|
|
346
390
|
|
|
347
|
-
function collectApiVerification(
|
|
391
|
+
function collectApiVerification(
|
|
392
|
+
project: ProjectConfig,
|
|
393
|
+
checks: Check[],
|
|
394
|
+
{ changedFiles, scope }: { changedFiles: string[]; scope: string },
|
|
395
|
+
): void {
|
|
348
396
|
const apiDir = resolveProjectPath(project.root, project.apiDir);
|
|
349
397
|
const changedApiFiles = changedFiles.filter((file) => isWithinDirectory(file, apiDir));
|
|
350
398
|
if (scope === "changed" && changedApiFiles.length === 0) {
|
|
@@ -364,7 +412,7 @@ function collectApiVerification(project, checks, { changedFiles, scope }) {
|
|
|
364
412
|
}
|
|
365
413
|
|
|
366
414
|
const apiFiles = listFilesRecursively(apiDir).filter((file) => MODULE_SOURCE_RE.test(file));
|
|
367
|
-
const routeMap = new Map();
|
|
415
|
+
const routeMap = new Map<string, string[]>();
|
|
368
416
|
|
|
369
417
|
for (const file of apiFiles) {
|
|
370
418
|
const routePath = resolveApiRoutePath(apiDir, file);
|
|
@@ -422,14 +470,18 @@ function collectApiVerification(project, checks, { changedFiles, scope }) {
|
|
|
422
470
|
}
|
|
423
471
|
}
|
|
424
472
|
|
|
425
|
-
function collectPackageChecks(
|
|
473
|
+
function collectPackageChecks(
|
|
474
|
+
project: ProjectConfig,
|
|
475
|
+
checks: Check[],
|
|
476
|
+
packageJsonPath: string,
|
|
477
|
+
): void {
|
|
426
478
|
if (!existsSync(packageJsonPath)) {
|
|
427
479
|
checks.push(createCheck("warning", "No package.json found in the current app root."));
|
|
428
480
|
return;
|
|
429
481
|
}
|
|
430
482
|
|
|
431
483
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
432
|
-
const deps = {
|
|
484
|
+
const deps: Record<string, string> = {
|
|
433
485
|
...packageJson.dependencies,
|
|
434
486
|
...packageJson.devDependencies,
|
|
435
487
|
};
|
|
@@ -455,7 +507,7 @@ function collectPackageChecks(project, checks, packageJsonPath) {
|
|
|
455
507
|
}
|
|
456
508
|
}
|
|
457
509
|
|
|
458
|
-
function collectChangedFiles(root) {
|
|
510
|
+
function collectChangedFiles(root: string): { files: string[]; warning: string | null } {
|
|
459
511
|
try {
|
|
460
512
|
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
461
513
|
cwd: root,
|
|
@@ -469,7 +521,7 @@ function collectChangedFiles(root) {
|
|
|
469
521
|
cwd: repoRoot,
|
|
470
522
|
encoding: "utf-8",
|
|
471
523
|
});
|
|
472
|
-
const files = new Set();
|
|
524
|
+
const files = new Set<string>();
|
|
473
525
|
|
|
474
526
|
for (const line of output.split(/\r?\n/).filter(Boolean)) {
|
|
475
527
|
const record = line.slice(3);
|
|
@@ -496,7 +548,12 @@ function collectChangedFiles(root) {
|
|
|
496
548
|
}
|
|
497
549
|
}
|
|
498
550
|
|
|
499
|
-
function addChangedFile(
|
|
551
|
+
function addChangedFile(
|
|
552
|
+
files: Set<string>,
|
|
553
|
+
repoRoot: string,
|
|
554
|
+
prefix: string,
|
|
555
|
+
repoRelativePath: string,
|
|
556
|
+
): void {
|
|
500
557
|
if (prefix && !repoRelativePath.startsWith(prefix)) {
|
|
501
558
|
return;
|
|
502
559
|
}
|
|
@@ -509,7 +566,11 @@ function addChangedFile(files, repoRoot, prefix, repoRelativePath) {
|
|
|
509
566
|
files.add(resolve(repoRoot, projectRelativePath));
|
|
510
567
|
}
|
|
511
568
|
|
|
512
|
-
function filterFrameworkFiles(
|
|
569
|
+
function filterFrameworkFiles(
|
|
570
|
+
project: ProjectConfig,
|
|
571
|
+
files: string[],
|
|
572
|
+
packageJsonPath: string,
|
|
573
|
+
): string[] {
|
|
513
574
|
const appFile = resolveProjectPath(project.root, project.appFile);
|
|
514
575
|
const routesDir = resolveProjectPath(project.root, project.routesDir);
|
|
515
576
|
const shellsDir = resolveProjectPath(project.root, project.shellsDir);
|
|
@@ -532,7 +593,7 @@ function filterFrameworkFiles(project, files, packageJsonPath) {
|
|
|
532
593
|
});
|
|
533
594
|
}
|
|
534
595
|
|
|
535
|
-
function requiresFullVerification(project, changedFiles) {
|
|
596
|
+
function requiresFullVerification(project: ProjectConfig, changedFiles: string[]): boolean {
|
|
536
597
|
const packageJsonPath = resolve(project.root, "package.json");
|
|
537
598
|
const appFile = resolveProjectPath(project.root, project.appFile);
|
|
538
599
|
|
|
@@ -545,13 +606,21 @@ function requiresFullVerification(project, changedFiles) {
|
|
|
545
606
|
});
|
|
546
607
|
}
|
|
547
608
|
|
|
548
|
-
|
|
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[] {
|
|
549
618
|
return listFilesRecursively(pagesDir)
|
|
550
619
|
.filter((file) => PAGE_SOURCE_RE.test(file))
|
|
551
620
|
.map((file) => describePagesFile(pagesDir, file));
|
|
552
621
|
}
|
|
553
622
|
|
|
554
|
-
function describePagesFile(pagesDir, file) {
|
|
623
|
+
function describePagesFile(pagesDir: string, file: string): PagesFile {
|
|
555
624
|
const relativePath = relative(pagesDir, file).replace(/\\/g, "/");
|
|
556
625
|
const routePath = relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, "");
|
|
557
626
|
const name = basename(routePath);
|
|
@@ -580,8 +649,8 @@ function describePagesFile(pagesDir, file) {
|
|
|
580
649
|
};
|
|
581
650
|
}
|
|
582
651
|
|
|
583
|
-
function collectDuplicateRoutePaths(routes) {
|
|
584
|
-
const routeMap = new Map();
|
|
652
|
+
function collectDuplicateRoutePaths(routes: PagesRoute[]): { files: string[]; path: string }[] {
|
|
653
|
+
const routeMap = new Map<string, string[]>();
|
|
585
654
|
|
|
586
655
|
for (const route of routes) {
|
|
587
656
|
const files = routeMap.get(route.routePath) ?? [];
|
|
@@ -594,7 +663,7 @@ function collectDuplicateRoutePaths(routes) {
|
|
|
594
663
|
.map(([path, files]) => ({ files, path }));
|
|
595
664
|
}
|
|
596
665
|
|
|
597
|
-
function resolveApiRoutePath(apiDir, file) {
|
|
666
|
+
function resolveApiRoutePath(apiDir: string, file: string): string {
|
|
598
667
|
let relativePath = relative(apiDir, file).replace(/\\/g, "/");
|
|
599
668
|
relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
600
669
|
|
|
@@ -609,7 +678,7 @@ function resolveApiRoutePath(apiDir, file) {
|
|
|
609
678
|
return normalizeRoutePath(relativePath ? `/api/${relativePath}` : "/api");
|
|
610
679
|
}
|
|
611
680
|
|
|
612
|
-
function toModuleSpecifier(fromDir, filePath) {
|
|
681
|
+
function toModuleSpecifier(fromDir: string, filePath: string): string {
|
|
613
682
|
const relativePath = relative(fromDir, filePath).replace(/\\/g, "/");
|
|
614
683
|
if (relativePath.startsWith(".")) {
|
|
615
684
|
return relativePath;
|
|
@@ -617,7 +686,7 @@ function toModuleSpecifier(fromDir, filePath) {
|
|
|
617
686
|
return `./${relativePath}`;
|
|
618
687
|
}
|
|
619
688
|
|
|
620
|
-
function normalizeRoutePath(path) {
|
|
689
|
+
function normalizeRoutePath(path: string): string {
|
|
621
690
|
if (!path || path === "/") {
|
|
622
691
|
return "/";
|
|
623
692
|
}
|
|
@@ -627,15 +696,15 @@ function normalizeRoutePath(path) {
|
|
|
627
696
|
return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
|
|
628
697
|
}
|
|
629
698
|
|
|
630
|
-
function isWithinDirectory(filePath, directoryPath) {
|
|
699
|
+
function isWithinDirectory(filePath: string, directoryPath: string): boolean {
|
|
631
700
|
const relativePath = relative(directoryPath, filePath);
|
|
632
701
|
return relativePath === "" || (!relativePath.startsWith("..") && !relativePath.startsWith("../"));
|
|
633
702
|
}
|
|
634
703
|
|
|
635
|
-
function normalizePath(value) {
|
|
704
|
+
function normalizePath(value: string): string {
|
|
636
705
|
return value.replace(/\\/g, "/");
|
|
637
706
|
}
|
|
638
707
|
|
|
639
|
-
function createCheck(status, message) {
|
|
708
|
+
function createCheck(status: Check["status"], message: string): Check {
|
|
640
709
|
return { message, status };
|
|
641
710
|
}
|