@pracht/cli 1.1.5 → 1.2.1
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 +22 -0
- package/bin/pracht.js +1 -40
- package/dist/build-DlABFPbG.mjs +214 -0
- package/dist/build-metadata-BOChHO8g.mjs +58 -0
- package/dist/dev-BCFe3g38.mjs +25 -0
- package/dist/doctor-CQq64tfW.mjs +25 -0
- package/dist/generate-sHtRHRAH.mjs +400 -0
- package/dist/index.mjs +41 -0
- package/dist/inspect-DP-h3vLw.mjs +123 -0
- package/dist/manifest-DGq1n5LT.mjs +99 -0
- package/dist/project-DUyH_7eJ.mjs +148 -0
- package/dist/verification-CId4crSF.mjs +372 -0
- package/dist/verify-D7Bg1Su6.mjs +31 -0
- package/package.json +6 -2
- package/src/build-metadata.ts +101 -0
- package/{lib/build-shared.js → src/build-shared.ts} +30 -20
- package/src/commands/build.ts +137 -0
- package/src/commands/dev.ts +27 -0
- package/src/commands/doctor.ts +33 -0
- package/{lib/commands/generate.js → src/commands/generate.ts} +193 -149
- package/{lib/commands/inspect.js → src/commands/inspect.ts} +83 -34
- package/src/commands/verify.ts +37 -0
- package/src/constants.ts +26 -0
- package/src/index.ts +21 -0
- package/{lib/manifest.js → src/manifest.ts} +32 -13
- package/{lib/project.js → src/project.ts} +51 -34
- package/src/utils.ts +72 -0
- package/{lib/verification.js → src/verification.ts} +103 -34
- package/tsdown.config.ts +8 -0
- package/lib/build-metadata.js +0 -73
- 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
- package/lib/constants.js +0 -22
|
@@ -1,40 +1,76 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { createServer, type ViteDevServer } from "vite";
|
|
5
6
|
|
|
6
|
-
import { handleCliError
|
|
7
|
+
import { handleCliError } from "../utils.js";
|
|
7
8
|
import { readClientBuildAssets } from "../build-metadata.js";
|
|
8
|
-
import { HTTP_METHODS } from "../constants.js";
|
|
9
|
+
import { HTTP_METHODS, type HttpMethod } from "../constants.js";
|
|
9
10
|
import { readProjectConfig, resolveProjectPath } from "../project.js";
|
|
10
11
|
|
|
11
12
|
const INSPECT_TARGETS = new Set(["routes", "api", "build", "all"]);
|
|
12
|
-
const METHOD_ORDER = [...HTTP_METHODS];
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
const METHOD_ORDER: HttpMethod[] = [...HTTP_METHODS];
|
|
14
|
+
|
|
15
|
+
export default defineCommand({
|
|
16
|
+
meta: {
|
|
17
|
+
name: "inspect",
|
|
18
|
+
description: "Inspect resolved app graph",
|
|
19
|
+
},
|
|
20
|
+
args: {
|
|
21
|
+
target: {
|
|
22
|
+
type: "positional",
|
|
23
|
+
description: "Inspect target: routes, api, build, or all",
|
|
24
|
+
required: false,
|
|
25
|
+
},
|
|
26
|
+
json: {
|
|
27
|
+
type: "boolean",
|
|
28
|
+
description: "Output as JSON",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
async run({ args }) {
|
|
32
|
+
const target = args.target || "all";
|
|
33
|
+
|
|
34
|
+
if (!INSPECT_TARGETS.has(target)) {
|
|
35
|
+
handleCliError(new Error(`Unknown inspect target: ${target}`), {
|
|
36
|
+
json: Boolean(args.json),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
const report = await runInspect(process.cwd(), { target });
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
if (args.json) {
|
|
43
|
+
console.log(JSON.stringify(report, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
printInspectReport(report);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
interface InspectReport {
|
|
52
|
+
api?: { file: string; methods: string[]; path: string }[];
|
|
53
|
+
build?: {
|
|
54
|
+
adapterTarget: string;
|
|
55
|
+
clientEntryUrl: string | null;
|
|
56
|
+
cssManifest: Record<string, string[]>;
|
|
57
|
+
jsManifest: Record<string, string[]>;
|
|
58
|
+
};
|
|
59
|
+
mode: string;
|
|
60
|
+
routes?: {
|
|
61
|
+
file: string;
|
|
62
|
+
id: string;
|
|
63
|
+
loaderFile: string | null;
|
|
64
|
+
middleware: string[];
|
|
65
|
+
path: string;
|
|
66
|
+
render: string | null;
|
|
67
|
+
revalidate: unknown;
|
|
68
|
+
shell: string | null;
|
|
69
|
+
shellFile: string | null;
|
|
70
|
+
}[];
|
|
35
71
|
}
|
|
36
72
|
|
|
37
|
-
|
|
73
|
+
async function runInspect(root: string, { target = "all" } = {}): Promise<InspectReport> {
|
|
38
74
|
const project = readProjectConfig(root);
|
|
39
75
|
|
|
40
76
|
if (!project.configFile) {
|
|
@@ -49,9 +85,7 @@ export async function runInspect(root, { target = "all" } = {}) {
|
|
|
49
85
|
|
|
50
86
|
if (project.mode === "manifest") {
|
|
51
87
|
const manifestPath = resolveProjectPath(project.root, project.appFile);
|
|
52
|
-
|
|
53
|
-
readFileSync(manifestPath, "utf-8");
|
|
54
|
-
} catch {
|
|
88
|
+
if (!existsSync(manifestPath)) {
|
|
55
89
|
throw new Error(`App manifest is missing at ${project.appFile}.`);
|
|
56
90
|
}
|
|
57
91
|
}
|
|
@@ -66,7 +100,7 @@ export async function runInspect(root, { target = "all" } = {}) {
|
|
|
66
100
|
|
|
67
101
|
try {
|
|
68
102
|
const serverModule = await server.ssrLoadModule("virtual:pracht/server");
|
|
69
|
-
const report = {
|
|
103
|
+
const report: InspectReport = {
|
|
70
104
|
mode: project.mode,
|
|
71
105
|
};
|
|
72
106
|
|
|
@@ -76,7 +110,7 @@ export async function runInspect(root, { target = "all" } = {}) {
|
|
|
76
110
|
|
|
77
111
|
if (target === "api" || target === "all") {
|
|
78
112
|
report.api = await Promise.all(
|
|
79
|
-
serverModule.apiRoutes.map(async (route) => ({
|
|
113
|
+
serverModule.apiRoutes.map(async (route: { file: string; path: string }) => ({
|
|
80
114
|
file: route.file,
|
|
81
115
|
methods: await detectApiMethods(server, root, route.file),
|
|
82
116
|
path: route.path,
|
|
@@ -100,7 +134,19 @@ export async function runInspect(root, { target = "all" } = {}) {
|
|
|
100
134
|
}
|
|
101
135
|
}
|
|
102
136
|
|
|
103
|
-
|
|
137
|
+
interface RouteEntry {
|
|
138
|
+
file: string;
|
|
139
|
+
id: string;
|
|
140
|
+
loaderFile?: string;
|
|
141
|
+
middleware: string[];
|
|
142
|
+
path: string;
|
|
143
|
+
render?: string;
|
|
144
|
+
revalidate?: unknown;
|
|
145
|
+
shell?: string;
|
|
146
|
+
shellFile?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function serializeRoutes(routes: RouteEntry[]) {
|
|
104
150
|
return routes.map((route) => ({
|
|
105
151
|
file: route.file,
|
|
106
152
|
id: route.id,
|
|
@@ -114,11 +160,14 @@ function serializeRoutes(routes) {
|
|
|
114
160
|
}));
|
|
115
161
|
}
|
|
116
162
|
|
|
117
|
-
async function detectApiMethods(
|
|
163
|
+
async function detectApiMethods(
|
|
164
|
+
server: ViteDevServer,
|
|
165
|
+
root: string,
|
|
166
|
+
file: string,
|
|
167
|
+
): Promise<string[]> {
|
|
118
168
|
const resolvedFile = resolve(root, `.${file}`);
|
|
119
169
|
const source = readFileSync(resolvedFile, "utf-8");
|
|
120
170
|
|
|
121
|
-
// Use module evaluation first so re-exported handlers are reflected too.
|
|
122
171
|
try {
|
|
123
172
|
const module = await server.ssrLoadModule(file);
|
|
124
173
|
return METHOD_ORDER.filter((method) => typeof module[method] === "function");
|
|
@@ -129,7 +178,7 @@ async function detectApiMethods(server, root, file) {
|
|
|
129
178
|
}
|
|
130
179
|
}
|
|
131
180
|
|
|
132
|
-
function printInspectReport(report) {
|
|
181
|
+
function printInspectReport(report: InspectReport): void {
|
|
133
182
|
console.log(`Pracht inspect (${report.mode} mode)`);
|
|
134
183
|
|
|
135
184
|
if (report.routes) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
|
|
3
|
+
import { runVerification } from "../verification.js";
|
|
4
|
+
|
|
5
|
+
export default defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "verify",
|
|
8
|
+
description: "Fast framework-aware verification",
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
changed: {
|
|
12
|
+
type: "boolean",
|
|
13
|
+
description: "Only check changed files",
|
|
14
|
+
},
|
|
15
|
+
json: {
|
|
16
|
+
type: "boolean",
|
|
17
|
+
description: "Output as JSON",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
async run({ args }) {
|
|
21
|
+
const report = runVerification(process.cwd(), { changed: Boolean(args.changed) });
|
|
22
|
+
|
|
23
|
+
if (args.json) {
|
|
24
|
+
console.log(JSON.stringify(report, null, 2));
|
|
25
|
+
} else {
|
|
26
|
+
console.log(`Pracht verify (${report.mode} mode, ${report.scope} scope)`);
|
|
27
|
+
for (const check of report.checks) {
|
|
28
|
+
console.log(`${check.status.toUpperCase().padEnd(7)} ${check.message}`);
|
|
29
|
+
}
|
|
30
|
+
console.log(report.ok ? "\nNo blocking issues found." : "\nBlocking issues found.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!report.ok) {
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HttpMethod } from "@pracht/core";
|
|
2
|
+
|
|
3
|
+
export type { HttpMethod };
|
|
4
|
+
|
|
5
|
+
export const VERSION = "0.0.0";
|
|
6
|
+
|
|
7
|
+
export const PROJECT_DEFAULTS = {
|
|
8
|
+
apiDir: "/src/api",
|
|
9
|
+
appFile: "/src/routes.ts",
|
|
10
|
+
middlewareDir: "/src/middleware",
|
|
11
|
+
pagesDefaultRender: "ssr",
|
|
12
|
+
pagesDir: "",
|
|
13
|
+
routesDir: "/src/routes",
|
|
14
|
+
serverDir: "/src/server",
|
|
15
|
+
shellsDir: "/src/shells",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const HTTP_METHODS = new Set<HttpMethod>([
|
|
19
|
+
"GET",
|
|
20
|
+
"POST",
|
|
21
|
+
"PUT",
|
|
22
|
+
"PATCH",
|
|
23
|
+
"DELETE",
|
|
24
|
+
"HEAD",
|
|
25
|
+
"OPTIONS",
|
|
26
|
+
]);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineCommand, runMain } from "citty";
|
|
2
|
+
|
|
3
|
+
import { VERSION } from "./constants.js";
|
|
4
|
+
|
|
5
|
+
const main = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "pracht",
|
|
8
|
+
version: VERSION,
|
|
9
|
+
description: "The pracht CLI",
|
|
10
|
+
},
|
|
11
|
+
subCommands: {
|
|
12
|
+
build: () => import("./commands/build.js").then((m) => m.default),
|
|
13
|
+
dev: () => import("./commands/dev.js").then((m) => m.default),
|
|
14
|
+
doctor: () => import("./commands/doctor.js").then((m) => m.default),
|
|
15
|
+
generate: () => import("./commands/generate.js").then((m) => m.default),
|
|
16
|
+
inspect: () => import("./commands/inspect.js").then((m) => m.default),
|
|
17
|
+
verify: () => import("./commands/verify.js").then((m) => m.default),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
runMain(main);
|
|
@@ -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,45 +40,56 @@ 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(
|
|
38
|
-
|
|
54
|
+
export function resolveRouteModulePath(
|
|
55
|
+
project: ProjectConfig,
|
|
56
|
+
routePath: string,
|
|
57
|
+
extension: string,
|
|
58
|
+
): { absolutePath: string; relativePath: string } {
|
|
59
|
+
const segments = segmentsFromPath(routePath);
|
|
39
60
|
const relativePath =
|
|
40
61
|
segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
|
|
41
62
|
const absolutePath = resolve(resolveProjectPath(project.root, project.routesDir), relativePath);
|
|
42
63
|
return { absolutePath, relativePath };
|
|
43
64
|
}
|
|
44
65
|
|
|
45
|
-
export function resolvePagesRouteModulePath(
|
|
46
|
-
|
|
66
|
+
export function resolvePagesRouteModulePath(
|
|
67
|
+
project: ProjectConfig,
|
|
68
|
+
routePath: string,
|
|
69
|
+
extension: string,
|
|
70
|
+
): { absolutePath: string; relativePath: string } {
|
|
71
|
+
const segments = segmentsFromPath(routePath);
|
|
47
72
|
const relativePath =
|
|
48
73
|
segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
|
|
49
74
|
const absolutePath = resolve(resolveProjectPath(project.root, project.pagesDir), relativePath);
|
|
50
75
|
return { absolutePath, relativePath };
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
export function resolveApiModulePath(
|
|
54
|
-
|
|
78
|
+
export function resolveApiModulePath(
|
|
79
|
+
project: ProjectConfig,
|
|
80
|
+
endpointPath: string,
|
|
81
|
+
): { absolutePath: string; relativePath: string } {
|
|
82
|
+
const segments = segmentsFromPath(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,31 +136,20 @@ 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
|
|
124
|
-
return
|
|
125
|
-
.split("/")
|
|
126
|
-
.filter(Boolean)
|
|
127
|
-
.map((segment) => {
|
|
128
|
-
if (segment.startsWith(":")) return `[${segment.slice(1)}]`;
|
|
129
|
-
if (segment === "*") return "[...slug]";
|
|
130
|
-
return segment;
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function segmentsFromApiPath(endpointPath) {
|
|
135
|
-
return endpointPath
|
|
151
|
+
function segmentsFromPath(path: string): string[] {
|
|
152
|
+
return path
|
|
136
153
|
.split("/")
|
|
137
154
|
.filter(Boolean)
|
|
138
155
|
.map((segment) => {
|
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
|
+
}
|