@pracht/cli 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ export function ensureCoreNamedImport(source, name) {
2
+ const match = source.match(/import\s*\{([^}]+)\}\s*from\s*["']@pracht\/core["'];?/);
3
+ if (!match) {
4
+ return `import { ${name} } from "@pracht/core";\n${source}`;
5
+ }
6
+
7
+ const names = match[1]
8
+ .split(",")
9
+ .map((item) => item.trim())
10
+ .filter(Boolean);
11
+ if (!names.includes(name)) {
12
+ names.push(name);
13
+ }
14
+
15
+ return source.replace(match[0], `import { ${names.join(", ")} } from "@pracht/core";`);
16
+ }
17
+
18
+ export function upsertObjectEntry(source, key, entry) {
19
+ const property = findNamedBlock(source, key, "{", "}");
20
+ if (!property) {
21
+ const routesMatch = source.match(/^(\s*)routes\s*:/m);
22
+ if (!routesMatch || routesMatch.index == null) {
23
+ throw new Error(`Could not find a "${key}" or "routes" block in the app manifest.`);
24
+ }
25
+
26
+ const indent = routesMatch[1];
27
+ const block = `${indent}${key}: {\n${indent} ${entry},\n${indent}},\n`;
28
+ return `${source.slice(0, routesMatch.index)}${block}${source.slice(routesMatch.index)}`;
29
+ }
30
+
31
+ return insertBlockEntry(source, property, entry);
32
+ }
33
+
34
+ export function insertArrayItem(source, key, item) {
35
+ const property = findNamedBlock(source, key, "[", "]");
36
+ if (!property) {
37
+ throw new Error(`Could not find "${key}" in the app manifest.`);
38
+ }
39
+
40
+ return insertBlockEntry(source, property, item);
41
+ }
42
+
43
+ export function toManifestModulePath(manifestPath, targetFilePath) {
44
+ const relativePath = targetFilePath
45
+ .replaceAll("\\", "/")
46
+ .replace(manifestPath.replaceAll("\\", "/").replace(/\/[^/]+$/, ""), "")
47
+ .replace(/^\//, "");
48
+
49
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
50
+ }
51
+
52
+ export function extractRegistryEntries(source, key) {
53
+ const block = findNamedBlock(source, key, "{", "}");
54
+ if (!block) return [];
55
+ const inner = source.slice(block.openIndex + 1, block.closeIndex);
56
+ const entries = [];
57
+ const pattern =
58
+ /([A-Za-z0-9_-]+)\s*:\s*(?:(["'`])([^"'`]+)\2|\(\)\s*=>\s*import\(\s*(["'`])([^"'`]+)\4\s*\))/g;
59
+
60
+ for (const match of inner.matchAll(pattern)) {
61
+ entries.push({ name: match[1], path: match[3] ?? match[5] });
62
+ }
63
+
64
+ return entries;
65
+ }
66
+
67
+ export function extractRelativeModulePaths(source) {
68
+ const results = new Set();
69
+ for (const match of source.matchAll(/["'`]((?:\.\.\/|\.\/)[^"'`]+)["'`]/g)) {
70
+ results.add(match[1]);
71
+ }
72
+ return results;
73
+ }
74
+
75
+ function insertBlockEntry(source, block, entry) {
76
+ const inner = source.slice(block.openIndex + 1, block.closeIndex);
77
+ const closingIndent = block.indent;
78
+ const childIndent = `${closingIndent} `;
79
+ const trimmed = inner.trim();
80
+
81
+ if (!trimmed) {
82
+ return `${source.slice(0, block.openIndex + 1)}\n${indentMultiline(entry, childIndent)}\n${closingIndent}${source.slice(block.closeIndex)}`;
83
+ }
84
+
85
+ const needsComma = !/[,[{(]\s*$/.test(inner) && !/,\s*$/.test(trimmed);
86
+ const insertPrefix = needsComma ? "," : "";
87
+ return `${source.slice(0, block.closeIndex)}${insertPrefix}\n${indentMultiline(entry, childIndent)}\n${closingIndent}${source.slice(block.closeIndex)}`;
88
+ }
89
+
90
+ function findNamedBlock(source, key, openChar, closeChar) {
91
+ const pattern = new RegExp(`^([ \\t]*)${key}\\s*:\\s*\\${openChar}`, "m");
92
+ const match = source.match(pattern);
93
+ if (!match || match.index == null) {
94
+ return null;
95
+ }
96
+
97
+ const openIndex = source.indexOf(openChar, match.index);
98
+ const closeIndex = findMatchingDelimiter(source, openIndex, openChar, closeChar);
99
+ return {
100
+ closeIndex,
101
+ indent: match[1],
102
+ openIndex,
103
+ };
104
+ }
105
+
106
+ function findMatchingDelimiter(source, openIndex, openChar, closeChar) {
107
+ let depth = 0;
108
+ let quoteChar = null;
109
+ let escaping = false;
110
+
111
+ for (let index = openIndex; index < source.length; index += 1) {
112
+ const current = source[index];
113
+ if (quoteChar) {
114
+ if (escaping) {
115
+ escaping = false;
116
+ continue;
117
+ }
118
+ if (current === "\\") {
119
+ escaping = true;
120
+ continue;
121
+ }
122
+ if (current === quoteChar) {
123
+ quoteChar = null;
124
+ }
125
+ continue;
126
+ }
127
+
128
+ if (current === '"' || current === "'" || current === "`") {
129
+ quoteChar = current;
130
+ continue;
131
+ }
132
+ if (current === openChar) depth += 1;
133
+ if (current === closeChar) {
134
+ depth -= 1;
135
+ if (depth === 0) return index;
136
+ }
137
+ }
138
+
139
+ throw new Error(`Could not find matching ${closeChar} for ${openChar}.`);
140
+ }
141
+
142
+ function indentMultiline(value, indent) {
143
+ return value
144
+ .split("\n")
145
+ .map((line) => `${indent}${line}`)
146
+ .join("\n");
147
+ }
package/lib/project.js ADDED
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, relative, resolve } from "node:path";
3
+
4
+ import { ensureTrailingNewline } from "./cli.js";
5
+ import { PROJECT_DEFAULTS } from "./constants.js";
6
+
7
+ export function readProjectConfig(root) {
8
+ const configFile = findConfigFile(root);
9
+ const rawConfig = configFile ? readFileSync(configFile, "utf-8") : "";
10
+ const config = {
11
+ ...PROJECT_DEFAULTS,
12
+ configFile,
13
+ hasPrachtPlugin: /\bpracht\s*\(/.test(rawConfig),
14
+ rawConfig,
15
+ root,
16
+ };
17
+
18
+ for (const key of Object.keys(PROJECT_DEFAULTS)) {
19
+ const value = readQuotedConfigValue(rawConfig, key);
20
+ if (typeof value === "string") {
21
+ config[key] = normalizeConfigPath(value);
22
+ }
23
+ }
24
+
25
+ config.mode = config.pagesDir ? "pages" : "manifest";
26
+ return config;
27
+ }
28
+
29
+ export function resolveProjectPath(root, configPath) {
30
+ return resolve(root, `.${configPath}`);
31
+ }
32
+
33
+ export function resolveScopedFile(root, configDir, fileName) {
34
+ return resolve(resolveProjectPath(root, configDir), fileName);
35
+ }
36
+
37
+ export function resolveRouteModulePath(project, routePath, extension) {
38
+ const segments = segmentsFromRoutePath(routePath);
39
+ const relativePath =
40
+ segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
41
+ const absolutePath = resolve(resolveProjectPath(project.root, project.routesDir), relativePath);
42
+ return { absolutePath, relativePath };
43
+ }
44
+
45
+ export function resolvePagesRouteModulePath(project, routePath, extension) {
46
+ const segments = segmentsFromRoutePath(routePath);
47
+ const relativePath =
48
+ segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
49
+ const absolutePath = resolve(resolveProjectPath(project.root, project.pagesDir), relativePath);
50
+ return { absolutePath, relativePath };
51
+ }
52
+
53
+ export function resolveApiModulePath(project, endpointPath) {
54
+ const segments = segmentsFromApiPath(endpointPath);
55
+ const relativePath = segments.length === 0 ? "index.ts" : `${segments.join("/")}.ts`;
56
+ const absolutePath = resolve(resolveProjectPath(project.root, project.apiDir), relativePath);
57
+ return { absolutePath, relativePath };
58
+ }
59
+
60
+ export function displayPath(root, filePath) {
61
+ return relative(root, filePath) || ".";
62
+ }
63
+
64
+ export function writeGeneratedFile(filePath, source) {
65
+ if (existsSync(filePath)) {
66
+ throw new Error(`Refusing to overwrite existing file ${filePath}.`);
67
+ }
68
+
69
+ mkdirSync(dirname(filePath), { recursive: true });
70
+ writeFileSync(filePath, ensureTrailingNewline(source), "utf-8");
71
+ }
72
+
73
+ export function assertFileExists(filePath, message) {
74
+ if (!existsSync(filePath)) {
75
+ throw new Error(message);
76
+ }
77
+ }
78
+
79
+ export function listFilesRecursively(dir) {
80
+ const files = [];
81
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
82
+ const fullPath = resolve(dir, entry.name);
83
+ if (entry.isDirectory()) {
84
+ files.push(...listFilesRecursively(fullPath));
85
+ } else {
86
+ files.push(fullPath);
87
+ }
88
+ }
89
+ return files;
90
+ }
91
+
92
+ export function hasPagesAppShell(filePath) {
93
+ return /^_app\.(ts|tsx|js|jsx)$/.test(basename(filePath));
94
+ }
95
+
96
+ function findConfigFile(root) {
97
+ for (const name of [
98
+ "vite.config.ts",
99
+ "vite.config.mts",
100
+ "vite.config.js",
101
+ "vite.config.mjs",
102
+ "vite.config.cjs",
103
+ "vite.config.cts",
104
+ ]) {
105
+ const file = resolve(root, name);
106
+ if (existsSync(file)) return file;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ function readQuotedConfigValue(source, key) {
112
+ if (!source) return null;
113
+ const pattern = new RegExp(`${key}\\s*:\\s*(["'\\\`])([^"'\\\`]+)\\1`);
114
+ const match = source.match(pattern);
115
+ return match ? match[2] : null;
116
+ }
117
+
118
+ function normalizeConfigPath(value) {
119
+ if (!value) return value;
120
+ return value.startsWith("/") ? value : `/${value}`;
121
+ }
122
+
123
+ function segmentsFromRoutePath(routePath) {
124
+ return routePath
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
136
+ .split("/")
137
+ .filter(Boolean)
138
+ .map((segment) => {
139
+ if (segment.startsWith(":")) return `[${segment.slice(1)}]`;
140
+ if (segment === "*") return "[...slug]";
141
+ return segment;
142
+ });
143
+ }