create-caspian-app 0.0.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.
@@ -0,0 +1,381 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import Parser from "tree-sitter";
4
+ import Python from "tree-sitter-python";
5
+ import { getFileMeta } from "./utils";
6
+
7
+ const { __dirname } = getFileMeta();
8
+
9
+ /**
10
+ * ---------------------------------------------------------
11
+ * Configuration & Interfaces
12
+ * ---------------------------------------------------------
13
+ */
14
+
15
+ interface CaspianConfig {
16
+ projectName: string;
17
+ projectRootPath: string;
18
+ componentScanDirs: string[];
19
+ excludeFiles?: string[];
20
+ }
21
+
22
+ interface ComponentProp {
23
+ name: string;
24
+ type: string;
25
+ hasDefault: boolean;
26
+ defaultValue?: string;
27
+ options?: string[];
28
+ }
29
+
30
+ interface ComponentMetadata {
31
+ componentName: string;
32
+ filePath: string;
33
+ relativePath: string;
34
+ importRoute: string;
35
+ acceptsArbitraryProps: boolean;
36
+ props: ComponentProp[];
37
+ }
38
+
39
+ const CONFIG_FILENAME = "caspian.config.json";
40
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
41
+ const CONFIG_PATH = path.join(PROJECT_ROOT, CONFIG_FILENAME);
42
+
43
+ /**
44
+ * ---------------------------------------------------------
45
+ * AST Helpers
46
+ * ---------------------------------------------------------
47
+ */
48
+
49
+ const parser = new Parser();
50
+ parser.setLanguage(Python as any);
51
+
52
+ /**
53
+ * Extracts string values from a Dictionary node
54
+ */
55
+ function extractDictKeysFromNode(node: Parser.SyntaxNode): string[] {
56
+ const keys: string[] = [];
57
+ for (const child of node.children) {
58
+ if (child.type === "dictionary_splat") continue;
59
+
60
+ if (child.type === "pair") {
61
+ const keyNode = child.childForFieldName("key");
62
+ if (
63
+ keyNode &&
64
+ (keyNode.type === "string" || keyNode.type === "identifier")
65
+ ) {
66
+ keys.push(keyNode.text.replace(/^['"]|['"]$/g, ""));
67
+ }
68
+ }
69
+ }
70
+ return keys;
71
+ }
72
+
73
+ /**
74
+ * Extracts values from a Literal[...] type node
75
+ */
76
+ function extractLiteralValues(node: Parser.SyntaxNode): string[] {
77
+ const values: string[] = [];
78
+
79
+ if (node.type !== "subscript" && node.type !== "generic_type") return values;
80
+
81
+ let typeName: string | undefined;
82
+ if (node.type === "subscript") {
83
+ typeName = node.childForFieldName("value")?.text;
84
+ } else if (node.type === "generic_type") {
85
+ typeName = node.children.find((c) => c.type === "identifier")?.text;
86
+ }
87
+
88
+ if (typeName !== "Literal") return values;
89
+
90
+ const findStrings = (n: Parser.SyntaxNode) => {
91
+ if (n.type === "string") {
92
+ values.push(n.text.replace(/^['"]|['"]$/g, ""));
93
+ return;
94
+ }
95
+ for (const child of n.children) {
96
+ findStrings(child);
97
+ }
98
+ };
99
+
100
+ findStrings(node);
101
+ return values;
102
+ }
103
+
104
+ /**
105
+ * Collects module-level type aliases
106
+ */
107
+ function collectTypeAliases(
108
+ rootNode: Parser.SyntaxNode
109
+ ): Map<string, string[]> {
110
+ const aliases = new Map<string, string[]>();
111
+
112
+ for (const child of rootNode.children) {
113
+ if (child.type === "expression_statement") {
114
+ const assignment = child.firstChild;
115
+ if (assignment?.type === "assignment") {
116
+ const left = assignment.childForFieldName("left");
117
+ const right = assignment.childForFieldName("right");
118
+
119
+ if (left?.type === "identifier" && right) {
120
+ const values = extractLiteralValues(right);
121
+ if (values.length > 0) {
122
+ aliases.set(left.text, values);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return aliases;
129
+ }
130
+
131
+ /**
132
+ * ---------------------------------------------------------
133
+ * Core Analysis Logic (AST Based)
134
+ * ---------------------------------------------------------
135
+ */
136
+
137
+ function analyzeFile(filePath: string, rootDir: string): ComponentMetadata[] {
138
+ const fileContent = fs.readFileSync(filePath, "utf-8");
139
+ const tree = parser.parse(fileContent);
140
+ const components: ComponentMetadata[] = [];
141
+ const typeAliases = collectTypeAliases(tree.rootNode);
142
+
143
+ const query = new Parser.Query(
144
+ Python as any,
145
+ `(decorated_definition
146
+ (decorator) @dec
147
+ (function_definition) @func
148
+ )`
149
+ );
150
+
151
+ const matches = query.matches(tree.rootNode);
152
+
153
+ for (const match of matches) {
154
+ const decoratorNode = match.captures.find((c) => c.name === "dec")?.node;
155
+ if (decoratorNode?.text.trim() !== "@component") continue;
156
+
157
+ const funcNode = match.captures.find((c) => c.name === "func")?.node;
158
+ if (!funcNode) continue;
159
+
160
+ const nameNode = funcNode.childForFieldName("name");
161
+ const componentName = nameNode?.text || "Unknown";
162
+ const paramsNode = funcNode.childForFieldName("parameters");
163
+ const bodyNode = funcNode.childForFieldName("body");
164
+
165
+ // 1. Parse Props (Parameters)
166
+ const props: ComponentProp[] = [];
167
+ const propMap = new Map<string, ComponentProp>();
168
+ let acceptsArbitraryProps = false;
169
+
170
+ if (paramsNode) {
171
+ for (const param of paramsNode.children) {
172
+ // --- Detect **props (dictionary_splat_pattern) ---
173
+ if (param.type === "dictionary_splat_pattern") {
174
+ acceptsArbitraryProps = true;
175
+ continue;
176
+ }
177
+
178
+ // Skip punctuation and list splats (*args)
179
+ if (["(", ")", ",", "list_splat_pattern"].includes(param.type))
180
+ continue;
181
+
182
+ let name = "";
183
+ let defaultValue: string | undefined = undefined;
184
+ let typeNode: Parser.SyntaxNode | null = null;
185
+
186
+ if (param.type === "identifier") {
187
+ name = param.text;
188
+ } else if (param.type === "default_parameter") {
189
+ name = param.childForFieldName("name")?.text || "";
190
+ defaultValue = param.childForFieldName("value")?.text;
191
+ } else if (param.type === "typed_parameter") {
192
+ name = param.childForFieldName("name")?.text || "";
193
+ typeNode = param.childForFieldName("type");
194
+ } else if (param.type === "typed_default_parameter") {
195
+ name = param.childForFieldName("name")?.text || "";
196
+ defaultValue = param.childForFieldName("value")?.text;
197
+ typeNode = param.childForFieldName("type");
198
+ }
199
+
200
+ // Clean up quotes from default value
201
+ if (defaultValue)
202
+ defaultValue = defaultValue.replace(/^['"]|['"]$/g, "");
203
+
204
+ // Exclude standard python args
205
+ if (name === "self" || name === "cls") continue;
206
+
207
+ // --- Extract Type String ---
208
+ let propType = "Any";
209
+ if (typeNode) {
210
+ propType = typeNode.text;
211
+ }
212
+
213
+ // --- Extract Options ---
214
+ let options: string[] = [];
215
+ if (typeNode) {
216
+ let actualType: Parser.SyntaxNode | null = typeNode;
217
+ if (typeNode.type === "type") {
218
+ actualType = typeNode.firstChild;
219
+ }
220
+
221
+ if (actualType?.type === "identifier") {
222
+ options = typeAliases.get(actualType.text) || [];
223
+ } else if (
224
+ actualType?.type === "subscript" ||
225
+ actualType?.type === "generic_type"
226
+ ) {
227
+ options = extractLiteralValues(actualType);
228
+ } else if (actualType) {
229
+ const findSubscript = (
230
+ n: Parser.SyntaxNode
231
+ ): Parser.SyntaxNode | null => {
232
+ if (n.type === "subscript") return n;
233
+ for (const c of n.children) {
234
+ const found = findSubscript(c);
235
+ if (found) return found;
236
+ }
237
+ return null;
238
+ };
239
+ const subscript = findSubscript(actualType);
240
+ if (subscript) {
241
+ options = extractLiteralValues(subscript);
242
+ }
243
+ }
244
+ }
245
+
246
+ const propObj: ComponentProp = {
247
+ name,
248
+ type: propType,
249
+ hasDefault: defaultValue !== undefined,
250
+ defaultValue,
251
+ options: options.length > 0 ? options : undefined,
252
+ };
253
+
254
+ props.push(propObj);
255
+ propMap.set(name, propObj);
256
+ }
257
+ }
258
+
259
+ // 2. Parse Body for Dictionaries
260
+ if (bodyNode) {
261
+ for (const statement of bodyNode.children) {
262
+ if (statement.type === "expression_statement") {
263
+ const assignment = statement.firstChild;
264
+ if (assignment?.type === "assignment") {
265
+ const left = assignment.childForFieldName("left");
266
+ const right = assignment.childForFieldName("right");
267
+
268
+ if (left?.type === "identifier" && right?.type === "dictionary") {
269
+ const varName = left.text;
270
+ if (varName.endsWith("s")) {
271
+ const propName = varName.slice(0, -1);
272
+ const relatedProp = propMap.get(propName);
273
+
274
+ if (relatedProp) {
275
+ if (!relatedProp.options) {
276
+ relatedProp.options = extractDictKeysFromNode(right);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ // Generate Metadata Paths
287
+ const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
288
+ const pathNoExt = relativePath.replace(/\.py$/, "");
289
+ const importRoute = pathNoExt.replace(/\//g, ".");
290
+
291
+ components.push({
292
+ componentName,
293
+ filePath,
294
+ relativePath,
295
+ importRoute,
296
+ acceptsArbitraryProps,
297
+ props,
298
+ });
299
+ }
300
+
301
+ return components;
302
+ }
303
+
304
+ /**
305
+ * ---------------------------------------------------------
306
+ * File System Helpers
307
+ * ---------------------------------------------------------
308
+ */
309
+ function loadConfig(): CaspianConfig {
310
+ if (!fs.existsSync(CONFIG_PATH)) {
311
+ console.error(`❌ Configuration file not found at: ${CONFIG_PATH}`);
312
+ process.exit(1);
313
+ }
314
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
315
+ }
316
+
317
+ function walkDirectory(
318
+ dir: string,
319
+ fileList: string[] = [],
320
+ excludeList: string[] = []
321
+ ): string[] {
322
+ if (!fs.existsSync(dir)) return fileList;
323
+ const files = fs.readdirSync(dir);
324
+ files.forEach((file) => {
325
+ const fullPath = path.join(dir, file);
326
+ const stat = fs.statSync(fullPath);
327
+ const relativeCheck = path
328
+ .relative(PROJECT_ROOT, fullPath)
329
+ .replace(/\\/g, "/");
330
+ const isExcluded = excludeList.some((ex) =>
331
+ relativeCheck.includes(ex.replace(/^\.\//, ""))
332
+ );
333
+ if (isExcluded) return;
334
+
335
+ if (stat.isDirectory()) {
336
+ walkDirectory(fullPath, fileList, excludeList);
337
+ } else if (path.extname(file) === ".py") {
338
+ fileList.push(fullPath);
339
+ }
340
+ });
341
+ return fileList;
342
+ }
343
+
344
+ /**
345
+ * ---------------------------------------------------------
346
+ * Execution Entry Point
347
+ * ---------------------------------------------------------
348
+ */
349
+ export async function componentMap() {
350
+ console.log(`🔍 Starting Component Analysis (AST Powered)...`);
351
+ const config = loadConfig();
352
+ let allFiles: string[] = [];
353
+
354
+ config.componentScanDirs.forEach((scanDir) => {
355
+ allFiles = walkDirectory(
356
+ path.join(PROJECT_ROOT, scanDir),
357
+ allFiles,
358
+ config.excludeFiles || []
359
+ );
360
+ });
361
+
362
+ console.log(`📂 Found ${allFiles.length} Python files.`);
363
+ const componentRegistry: ComponentMetadata[] = [];
364
+
365
+ allFiles.forEach((file) => {
366
+ try {
367
+ const foundComponents = analyzeFile(file, PROJECT_ROOT);
368
+ componentRegistry.push(...foundComponents);
369
+ } catch (e) {
370
+ console.warn(`⚠️ Failed to parse ${file}:`, e);
371
+ }
372
+ });
373
+
374
+ console.log(`✅ Discovered ${componentRegistry.length} Components.`);
375
+
376
+ const outputPath = path.join(__dirname, "component-map.json");
377
+ if (componentRegistry.length > 0 || !fs.existsSync(outputPath)) {
378
+ fs.writeFileSync(outputPath, JSON.stringify(componentRegistry, null, 2));
379
+ console.log(`📝 Component map written to: ${outputPath}`);
380
+ }
381
+ }
@@ -0,0 +1,36 @@
1
+ [
2
+ "./src/app/docs/auth/index.html",
3
+ "./src/app/docs/cli/index.html",
4
+ "./src/app/docs/components/index.html",
5
+ "./src/app/docs/database/index.html",
6
+ "./src/app/docs/files/error/index.html",
7
+ "./src/app/docs/files/index/index.html",
8
+ "./src/app/docs/files/layout/index.html",
9
+ "./src/app/docs/files/loading/index.html",
10
+ "./src/app/docs/files/not-found/index.html",
11
+ "./src/app/docs/index.html",
12
+ "./src/app/docs/index.py",
13
+ "./src/app/docs/installation/index.html",
14
+ "./src/app/docs/layout.html",
15
+ "./src/app/docs/layout.py",
16
+ "./src/app/docs/loading.html",
17
+ "./src/app/docs/reactivity/index.html",
18
+ "./src/app/docs/routing/index.html",
19
+ "./src/app/docs/rpc/index.html",
20
+ "./src/app/docs/structure/index.html",
21
+ "./src/app/docs/__pycache__/index.cpython-314.pyc",
22
+ "./src/app/docs/__pycache__/layout.cpython-314.pyc",
23
+ "./src/app/error.html",
24
+ "./src/app/globals.css",
25
+ "./src/app/index.html",
26
+ "./src/app/index.py",
27
+ "./src/app/layout.html",
28
+ "./src/app/not-found.html",
29
+ "./src/app/__pycache__/index.cpython-314.pyc",
30
+ "./src/app/__pycache__/layout.cpython-314.pyc",
31
+ "./public/css/styles.css",
32
+ "./public/favicon.ico",
33
+ "./public/js/global-functions.js",
34
+ "./public/js/main.js",
35
+ "./public/js/pp-reactive-v1.js"
36
+ ]
@@ -0,0 +1,49 @@
1
+ import { existsSync, readdirSync, statSync, writeFileSync } from "fs";
2
+ import { join, sep, relative } from "path";
3
+ import { getFileMeta } from "./utils.js";
4
+ import { PUBLIC_DIR, APP_DIR } from "../settings/utils.js";
5
+
6
+ const { __dirname } = getFileMeta();
7
+
8
+ const jsonFilePath = "settings/files-list.json";
9
+
10
+ const getAllFiles = (dirPath: string): string[] => {
11
+ const files: string[] = [];
12
+
13
+ if (!existsSync(dirPath)) {
14
+ console.error(`Directory not found: ${dirPath}`);
15
+ return files;
16
+ }
17
+
18
+ const items = readdirSync(dirPath);
19
+ items.forEach((item) => {
20
+ const fullPath = join(dirPath, item);
21
+ if (statSync(fullPath).isDirectory()) {
22
+ files.push(...getAllFiles(fullPath));
23
+ } else {
24
+ const relativePath = `.${sep}${relative(
25
+ join(__dirname, ".."),
26
+ fullPath
27
+ )}`;
28
+ files.push(relativePath.replace(/\\/g, "/").replace(/^\.\.\//, ""));
29
+ }
30
+ });
31
+
32
+ return files;
33
+ };
34
+
35
+ export const generateFileListJson = async (): Promise<void> => {
36
+ const appFiles = getAllFiles(APP_DIR);
37
+ const publicFiles = getAllFiles(PUBLIC_DIR);
38
+
39
+ const allFiles = [...appFiles, ...publicFiles];
40
+
41
+ if (allFiles.length > 0) {
42
+ writeFileSync(jsonFilePath, JSON.stringify(allFiles, null, 2));
43
+ console.log(
44
+ `File list generated: ${appFiles.length} app files, ${publicFiles.length} public files`
45
+ );
46
+ } else {
47
+ console.error("No files found to save in the JSON file.");
48
+ }
49
+ };
@@ -0,0 +1,119 @@
1
+ import { writeFile } from "fs";
2
+ import { join, basename, normalize, relative, sep } from "path";
3
+ import caspianConfigJson from "../caspian.config.json";
4
+ import { promises as fsPromises } from "fs";
5
+ import { generateFileListJson } from "./files-list";
6
+ import { componentMap } from "./component-map";
7
+
8
+ const currentProjectRoot = process.cwd();
9
+ const newProjectName = basename(currentProjectRoot);
10
+ const configFilePath = join(currentProjectRoot, "caspian.config.json");
11
+
12
+ function updateProjectNameInConfig(
13
+ filePath: string,
14
+ newProjectName: string,
15
+ currentRoot: string
16
+ ): void {
17
+ const newWebPath = calculateDynamicWebPath(
18
+ currentRoot,
19
+ caspianConfigJson.projectRootPath,
20
+ caspianConfigJson.bsPathRewrite?.["^/"]
21
+ );
22
+
23
+ caspianConfigJson.projectName = newProjectName;
24
+ caspianConfigJson.projectRootPath = currentRoot;
25
+ caspianConfigJson.bsTarget = `http://localhost${newWebPath}`;
26
+
27
+ if (!caspianConfigJson.bsPathRewrite) {
28
+ (caspianConfigJson as any).bsPathRewrite = {};
29
+ }
30
+ caspianConfigJson.bsPathRewrite["^/"] = newWebPath;
31
+
32
+ writeFile(
33
+ filePath,
34
+ JSON.stringify(caspianConfigJson, null, 2),
35
+ "utf8",
36
+ (err) => {
37
+ if (err) {
38
+ console.error("Error writing the updated JSON file:", err);
39
+ return;
40
+ }
41
+ console.log(
42
+ `Configuration updated.\nProject: ${newProjectName}\nURL: http://localhost${newWebPath}`
43
+ );
44
+ }
45
+ );
46
+ }
47
+
48
+ function calculateDynamicWebPath(
49
+ currentPath: string,
50
+ oldPath?: string,
51
+ oldUrl?: string
52
+ ): string {
53
+ let webRoot: string | null = null;
54
+
55
+ if (oldPath && oldUrl) {
56
+ try {
57
+ const normOldPath = normalize(oldPath);
58
+ const normOldUrl = normalize(oldUrl).replace(/^[\\\/]|[\\\/]$/g, "");
59
+
60
+ if (normOldPath.endsWith(normOldUrl)) {
61
+ webRoot = normOldPath.slice(0, -normOldUrl.length);
62
+ if (webRoot.endsWith(sep)) webRoot = webRoot.slice(0, -1);
63
+ }
64
+ } catch (e) {}
65
+ }
66
+
67
+ if (webRoot) {
68
+ const relPath = relative(webRoot, currentPath);
69
+ const urlPath = relPath.split(sep).join("/");
70
+ return `/${urlPath}/`.replace(/\/+/g, "/");
71
+ }
72
+
73
+ return "/";
74
+ }
75
+
76
+ updateProjectNameInConfig(configFilePath, newProjectName, currentProjectRoot);
77
+
78
+ export const deleteFilesIfExist = async (
79
+ filePaths: string[]
80
+ ): Promise<void> => {
81
+ for (const filePath of filePaths) {
82
+ try {
83
+ await fsPromises.unlink(filePath);
84
+ } catch (error) {
85
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
86
+ console.error(`Error deleting ${filePath}:`, error);
87
+ }
88
+ }
89
+ }
90
+ };
91
+
92
+ export async function deleteDirectoriesIfExist(
93
+ dirPaths: string[]
94
+ ): Promise<void> {
95
+ for (const dirPath of dirPaths) {
96
+ try {
97
+ await fsPromises.rm(dirPath, { recursive: true, force: true });
98
+ console.log(`Deleted directory: ${dirPath}`);
99
+ } catch (error) {
100
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
101
+ console.error(`Error deleting directory (${dirPath}):`, error);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ export const filesToDelete = [
108
+ join(currentProjectRoot, "settings", "component-map.json"),
109
+ ];
110
+
111
+ export const dirsToDelete = [
112
+ join(currentProjectRoot, "caches"),
113
+ join(currentProjectRoot, ".casp"),
114
+ ];
115
+
116
+ await deleteFilesIfExist(filesToDelete);
117
+ await deleteDirectoriesIfExist(dirsToDelete);
118
+ await generateFileListJson();
119
+ await componentMap();