@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.
@@ -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(source, key) {
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
- function insertBlockEntry(source, block, entry) {
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(source, key, openChar, closeChar) {
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(source, openIndex, openChar, closeChar) {
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 "./cli.js";
4
+ import { ensureTrailingNewline } from "./utils.js";
5
5
  import { PROJECT_DEFAULTS } from "./constants.js";
6
6
 
7
- export function readProjectConfig(root) {
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(project, routePath, extension) {
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(project, routePath, extension) {
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(project, endpointPath) {
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 function runDoctor(root) {
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(root, options = {}) {
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(project, checks, configDisplayPath) {
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(project, checks, { changedFiles, scope }) {
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(project, checks, { changedFiles, scope }) {
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(project, checks, pagesDir, changedFiles) {
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(project, checks, { changedFiles, scope }) {
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(project, checks, packageJsonPath) {
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(files, repoRoot, prefix, repoRelativePath) {
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(project, files, packageJsonPath) {
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
- function scanPagesDirectory(pagesDir) {
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
  }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ clean: true,
5
+ entry: ["src/index.ts"],
6
+ format: "esm",
7
+ external: [/^@pracht\//, /^node:/, "vite", "citty"],
8
+ });