create-backlist 7.0.1 → 7.3.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.
Files changed (54) hide show
  1. package/README.md +1 -10
  2. package/bin/index.js +242 -275
  3. package/package.json +3 -2
  4. package/src/ai-agent.js +171 -171
  5. package/src/analyzer.js +750 -495
  6. package/src/env-resolver.js +70 -0
  7. package/src/generators/dotnet.js +134 -133
  8. package/src/generators/java.js +248 -233
  9. package/src/generators/js.js +346 -0
  10. package/src/generators/nestjs.js +278 -0
  11. package/src/generators/node.js +404 -404
  12. package/src/generators/python.js +86 -104
  13. package/src/generators/template.js +22 -22
  14. package/src/project-detector.js +131 -0
  15. package/src/templates/dotnet/partials/Dockerfile.ejs +27 -0
  16. package/src/templates/dotnet/partials/docker-compose.yml.ejs +33 -0
  17. package/src/templates/java-spring/partials/Controller.java.ejs +3 -3
  18. package/src/templates/js-express/base/server.js +59 -0
  19. package/src/templates/js-express/partials/Dockerfile.ejs +12 -0
  20. package/src/templates/js-express/partials/auth.controller.js.ejs +66 -0
  21. package/src/templates/js-express/partials/auth.middleware.js.ejs +19 -0
  22. package/src/templates/js-express/partials/auth.routes.js.ejs +9 -0
  23. package/src/templates/js-express/partials/controller.js.ejs +53 -0
  24. package/src/templates/js-express/partials/db.js.ejs +19 -0
  25. package/src/templates/js-express/partials/docker-compose.yml.ejs +46 -0
  26. package/src/templates/js-express/partials/model.js.ejs +18 -0
  27. package/src/templates/js-express/partials/package.json.ejs +17 -0
  28. package/src/templates/js-express/partials/prisma.schema.ejs +21 -0
  29. package/src/templates/js-express/partials/routes.js.ejs +19 -0
  30. package/src/templates/js-express/partials/seeder.js.ejs +103 -0
  31. package/src/templates/js-express/partials/service.js.ejs +51 -0
  32. package/src/templates/js-express/partials/swagger.js.ejs +30 -0
  33. package/src/templates/js-express/partials/test.js.ejs +46 -0
  34. package/src/templates/nestjs/base/app.module.ts +9 -0
  35. package/src/templates/nestjs/base/main.ts +23 -0
  36. package/src/templates/nestjs/base/tsconfig.json +21 -0
  37. package/src/templates/nestjs/partials/auth.controller.ts.ejs +17 -0
  38. package/src/templates/nestjs/partials/auth.module.ts.ejs +17 -0
  39. package/src/templates/nestjs/partials/auth.service.ts.ejs +70 -0
  40. package/src/templates/nestjs/partials/controller.ts.ejs +34 -0
  41. package/src/templates/nestjs/partials/create-dto.ts.ejs +22 -0
  42. package/src/templates/nestjs/partials/jwt-guard.ts.ejs +24 -0
  43. package/src/templates/nestjs/partials/module.ts.ejs +10 -0
  44. package/src/templates/nestjs/partials/package.json.ejs +27 -0
  45. package/src/templates/nestjs/partials/prisma.service.ts.ejs +13 -0
  46. package/src/templates/nestjs/partials/schema.ts.ejs +19 -0
  47. package/src/templates/nestjs/partials/service.ts.ejs +67 -0
  48. package/src/templates/nestjs/partials/update-dto.ts.ejs +4 -0
  49. package/src/templates/node-ts-express/partials/HexController.ts.ejs +56 -56
  50. package/src/templates/node-ts-express/partials/HexRepository.ts.ejs +26 -26
  51. package/src/templates/node-ts-express/partials/HexService.ts.ejs +27 -27
  52. package/src/utils.js +11 -11
  53. /package/src/templates/{node-ts-express → dotnet}/partials/DbContext.cs.ejs +0 -0
  54. /package/src/templates/{node-ts-express → dotnet}/partials/Model.cs.ejs +0 -0
package/src/analyzer.js CHANGED
@@ -1,496 +1,751 @@
1
- /* eslint-disable @typescript-eslint/no-var-requires */
2
- import fs from "fs-extra";
3
- import path from "node:path";
4
- import { glob } from "glob";
5
-
6
- import parser from "@babel/parser";
7
- import _traverse from "@babel/traverse";
8
- const traverse = _traverse.default || _traverse;
9
-
10
- const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete"]);
11
-
12
- // -------------------------
13
- // Utils
14
- // -------------------------
15
- function normalizeSlashes(p) {
16
- return String(p || "").replace(/\\/g, "/");
17
- }
18
-
19
- function toTitleCase(str) {
20
- if (!str) return "Default";
21
- return String(str)
22
- .replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
23
- .replace(/^\w/, (c) => c.toUpperCase())
24
- .replace(/[^a-zA-Z0-9]/g, "");
25
- }
26
-
27
- // Convert `/api/users/{id}` -> `/api/users/:id`
28
- function normalizeRouteForBackend(urlValue) {
29
- return String(urlValue || "").replace(/\{(\w+)\}/g, ":$1");
30
- }
31
-
32
- function extractApiPath(urlValue) {
33
- // supports:
34
- // - /api/...
35
- // - http://localhost:5000/api/...
36
- if (!urlValue) return null;
37
- const idx = urlValue.indexOf("/api/");
38
- if (idx === -1) return null;
39
- return urlValue.slice(idx); // => /api/...
40
- }
41
-
42
- function extractPathParams(route) {
43
- const params = [];
44
- const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
45
- let m;
46
- while ((m = re.exec(route))) params.push(m[1]);
47
- return Array.from(new Set(params));
48
- }
49
-
50
- function extractQueryParamsFromUrl(urlValue) {
51
- try {
52
- const qIndex = urlValue.indexOf("?");
53
- if (qIndex === -1) return [];
54
- const qs = urlValue.slice(qIndex + 1);
55
- return qs
56
- .split("&")
57
- .map((p) => p.split("=")[0])
58
- .filter(Boolean);
59
- } catch {
60
- return [];
61
- }
62
- }
63
-
64
- function deriveControllerNameFromUrl(urlValue) {
65
- const apiPath = extractApiPath(urlValue) || urlValue;
66
- const parts = String(apiPath).split("/").filter(Boolean); // ["api","v1","products"]
67
- const apiIndex = parts.indexOf("api");
68
-
69
- let seg = null;
70
- if (apiIndex >= 0) {
71
- seg = parts[apiIndex + 1] || null;
72
-
73
- // skip version segment (v1, v2, v10...)
74
- if (seg && /^v\d+$/i.test(seg)) {
75
- seg = parts[apiIndex + 2] || seg;
76
- }
77
- } else {
78
- seg = parts[0] || null;
79
- }
80
-
81
- return toTitleCase(seg);
82
- }
83
-
84
- function deriveActionName(method, route) {
85
- const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
86
- const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
87
- return `${String(method).toLowerCase()}${toTitleCase(last)}`;
88
- }
89
-
90
- // -------------------------
91
- // URL extraction
92
- // -------------------------
93
- function getUrlValue(urlNode) {
94
- if (!urlNode) return null;
95
-
96
- if (urlNode.type === "StringLiteral") return urlNode.value;
97
-
98
- if (urlNode.type === "TemplateLiteral") {
99
- // `/api/users/${id}` -> `/api/users/{id}` or `{param1}`
100
- const quasis = urlNode.quasis || [];
101
- const exprs = urlNode.expressions || [];
102
- let out = "";
103
- for (let i = 0; i < quasis.length; i++) {
104
- out += quasis[i].value.raw;
105
- if (exprs[i]) {
106
- if (exprs[i].type === "Identifier") out += `{${exprs[i].name}}`;
107
- else out += `{param${i + 1}}`;
108
- }
109
- }
110
- return out;
111
- }
112
-
113
- return null;
114
- }
115
-
116
- // -------------------------
117
- // axios-like detection
118
- // -------------------------
119
- function detectAxiosLikeMethod(node) {
120
- // axios.get(...) / api.get(...) / httpClient.post(...) etc
121
- if (!node.callee || node.callee.type !== "MemberExpression") return null;
122
-
123
- const prop = node.callee.property;
124
- if (!prop || prop.type !== "Identifier") return null;
125
-
126
- const name = prop.name.toLowerCase();
127
- if (!HTTP_METHODS.has(name)) return null;
128
-
129
- return name.toUpperCase();
130
- }
131
-
132
- // -------------------------
133
- // Request body schema (simple + identifier tracing)
134
- // -------------------------
135
- function inferTypeFromNode(node) {
136
- if (!node) return "String";
137
- switch (node.type) {
138
- case "StringLiteral":
139
- return "String";
140
- case "NumericLiteral":
141
- return "Number";
142
- case "BooleanLiteral":
143
- return "Boolean";
144
- case "NullLiteral":
145
- return "String";
146
- default:
147
- return "String";
148
- }
149
- }
150
-
151
- function extractObjectSchema(objExpr) {
152
- const schemaFields = {};
153
- if (!objExpr || objExpr.type !== "ObjectExpression") return null;
154
-
155
- for (const prop of objExpr.properties) {
156
- if (prop.type !== "ObjectProperty") continue;
157
-
158
- const key =
159
- prop.key.type === "Identifier"
160
- ? prop.key.name
161
- : prop.key.type === "StringLiteral"
162
- ? prop.key.value
163
- : null;
164
-
165
- if (!key) continue;
166
- schemaFields[key] = inferTypeFromNode(prop.value);
167
- }
168
- return schemaFields;
169
- }
170
-
171
- function resolveIdentifierToInit(callPath, identifierName) {
172
- try {
173
- const binding = callPath.scope.getBinding(identifierName);
174
- if (!binding) return null;
175
- const declPath = binding.path;
176
- if (!declPath || !declPath.node) return null;
177
-
178
- if (declPath.node.type === "VariableDeclarator") return declPath.node.init || null;
179
- return null;
180
- } catch {
181
- return null;
182
- }
183
- }
184
-
185
- function isJSONStringifyCall(node) {
186
- // JSON.stringify(x)
187
- return (
188
- node &&
189
- node.type === "CallExpression" &&
190
- node.callee &&
191
- node.callee.type === "MemberExpression" &&
192
- node.callee.object &&
193
- node.callee.object.type === "Identifier" &&
194
- node.callee.object.name === "JSON" &&
195
- node.callee.property &&
196
- node.callee.property.type === "Identifier" &&
197
- node.callee.property.name === "stringify"
198
- );
199
- }
200
-
201
- // -------------------------
202
- // DB insights: guess db + infer models + seeds
203
- // -------------------------
204
- function guessDbTypeFromRepo(rootDir, endpoints = []) {
205
- try {
206
- const pkgPath = path.join(rootDir, "package.json");
207
- if (!fs.existsSync(pkgPath)) return heuristicallyGuessDB(endpoints);
208
-
209
- const pkg = fs.readJsonSync(pkgPath);
210
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
211
-
212
- if (deps.mongoose || deps.mongodb) return "mongodb-mongoose";
213
- if (deps.prisma || deps["@prisma/client"]) return "sql-prisma";
214
- if (deps.sequelize) return "sql-sequelize";
215
- if (deps.typeorm) return "sql-typeorm";
216
-
217
- return heuristicallyGuessDB(endpoints);
218
- } catch {
219
- return heuristicallyGuessDB(endpoints);
220
- }
221
- }
222
-
223
- function heuristicallyGuessDB(endpoints) {
224
- // Free Tier / Default Intelligence:
225
- // Analyze data complexity. If highly nested schemas are prominent, default NoSQL.
226
- // If many flat, relational-looking fields exist, default SQL.
227
- let maxNesting = 0;
228
- for (const ep of endpoints) {
229
- if (ep.schemaFields && Object.keys(ep.schemaFields).length > 6) {
230
- maxNesting++;
231
- }
232
- }
233
- return maxNesting > 3 ? "mongodb-mongoose" : "sql-prisma";
234
- }
235
-
236
- function inferModelsFromEndpoints(endpoints) {
237
- const models = new Map();
238
-
239
- for (const ep of endpoints) {
240
- const modelName = ep.controllerName || "Default";
241
-
242
- if (!models.has(modelName)) {
243
- models.set(modelName, {
244
- name: modelName,
245
- fields: {}, // merged fields from bodies
246
- sources: new Set(),
247
- endpoints: [],
248
- });
249
- }
250
-
251
- const m = models.get(modelName);
252
- m.endpoints.push({ method: ep.method, route: ep.route });
253
- if (ep.sourceFile) m.sources.add(ep.sourceFile);
254
-
255
- const fields = ep.schemaFields || (ep.requestBody && ep.requestBody.fields) || null;
256
- if (fields) {
257
- for (const [k, t] of Object.entries(fields)) {
258
- if (!m.fields[k]) m.fields[k] = t || "String";
259
- }
260
- }
261
- }
262
-
263
- return Array.from(models.values()).map((m) => ({
264
- name: m.name,
265
- fields: m.fields,
266
- sources: Array.from(m.sources),
267
- endpoints: m.endpoints,
268
- }));
269
- }
270
-
271
- function seedValueForType(t) {
272
- if (t === "Number") return 1;
273
- if (t === "Boolean") return true;
274
- return "test"; // String default
275
- }
276
-
277
- function generateSeedsFromModels(models, perModel = 3) {
278
- return models.map((m) => {
279
- const rows = [];
280
- for (let i = 0; i < perModel; i++) {
281
- const obj = {};
282
- for (const [k, t] of Object.entries(m.fields || {})) {
283
- obj[k] = seedValueForType(t);
284
- }
285
- rows.push(obj);
286
- }
287
- return { model: m.name, rows };
288
- });
289
- }
290
-
291
- // -------------------------
292
- // MAIN frontend analyzer
293
- // -------------------------
294
- export async function analyzeFrontend(srcPath) {
295
- if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
296
- if (!fs.existsSync(srcPath)) {
297
- throw new Error(`The source directory '${srcPath}' does not exist.`);
298
- }
299
-
300
- const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
301
- ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
302
- });
303
-
304
- const endpoints = new Map();
305
-
306
- for (const file of files) {
307
- let code;
308
- try {
309
- code = await fs.readFile(file, "utf-8");
310
- } catch {
311
- continue;
312
- }
313
-
314
- let ast;
315
- try {
316
- ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
317
- } catch {
318
- continue;
319
- }
320
-
321
- traverse(ast, {
322
- CallExpression(callPath) {
323
- const node = callPath.node;
324
-
325
- const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
326
- const axiosMethod = detectAxiosLikeMethod(node);
327
-
328
- if (!isFetch && !axiosMethod) return;
329
-
330
- let urlValue = null;
331
- let method = "GET";
332
- let schemaFields = null;
333
-
334
- // ---- fetch(url, options) ----
335
- if (isFetch) {
336
- urlValue = getUrlValue(node.arguments[0]);
337
- const optionsNode = node.arguments[1];
338
-
339
- if (optionsNode && optionsNode.type === "ObjectExpression") {
340
- const methodProp = optionsNode.properties.find(
341
- (p) =>
342
- p.type === "ObjectProperty" &&
343
- p.key.type === "Identifier" &&
344
- p.key.name === "method"
345
- );
346
- if (methodProp && methodProp.value.type === "StringLiteral") {
347
- method = methodProp.value.value.toUpperCase();
348
- }
349
-
350
- if (["POST", "PUT", "PATCH"].includes(method)) {
351
- const bodyProp = optionsNode.properties.find(
352
- (p) =>
353
- p.type === "ObjectProperty" &&
354
- p.key.type === "Identifier" &&
355
- p.key.name === "body"
356
- );
357
-
358
- if (bodyProp) {
359
- const v = bodyProp.value;
360
-
361
- if (isJSONStringifyCall(v)) {
362
- const arg0 = v.arguments[0];
363
-
364
- if (arg0?.type === "ObjectExpression") {
365
- schemaFields = extractObjectSchema(arg0);
366
- } else if (arg0?.type === "Identifier") {
367
- const init = resolveIdentifierToInit(callPath, arg0.name);
368
- if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
369
- }
370
- }
371
- }
372
- }
373
- }
374
- }
375
-
376
- // ---- axios-like client ----
377
- if (axiosMethod) {
378
- method = axiosMethod;
379
- urlValue = getUrlValue(node.arguments[0]);
380
-
381
- if (["POST", "PUT", "PATCH"].includes(method)) {
382
- const dataArg = node.arguments[1];
383
- if (dataArg?.type === "ObjectExpression") {
384
- schemaFields = extractObjectSchema(dataArg);
385
- } else if (dataArg?.type === "Identifier") {
386
- const init = resolveIdentifierToInit(callPath, dataArg.name);
387
- if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
388
- }
389
- }
390
- }
391
-
392
- // accept only URLs that contain /api/ anywhere
393
- const apiPath = extractApiPath(urlValue);
394
- if (!apiPath) return;
395
-
396
- const route = normalizeRouteForBackend(apiPath.split("?")[0]);
397
- const controllerName = deriveControllerNameFromUrl(apiPath);
398
- const actionName = deriveActionName(method, route);
399
-
400
- const key = `${method}:${route}`;
401
- if (!endpoints.has(key)) {
402
- endpoints.set(key, {
403
- path: apiPath,
404
- route,
405
- method,
406
- controllerName,
407
- actionName,
408
- pathParams: extractPathParams(route),
409
- queryParams: extractQueryParamsFromUrl(apiPath),
410
- schemaFields,
411
- requestBody: schemaFields ? { fields: schemaFields } : null,
412
- sourceFile: normalizeSlashes(file),
413
- });
414
- }
415
- },
416
- });
417
- }
418
-
419
- return Array.from(endpoints.values());
420
- }
421
-
422
- // -------------------------
423
- // Optional: full project analyze (endpoints + db insights)
424
- // -------------------------
425
- export async function analyze(projectRoot = process.cwd()) {
426
- const rootDir = path.resolve(projectRoot);
427
-
428
- const frontendSrc = ["src", "app", "pages"]
429
- .map((d) => path.join(rootDir, d))
430
- .find((d) => fs.existsSync(d));
431
-
432
- const endpoints = frontendSrc ? await analyzeFrontend(frontendSrc) : [];
433
-
434
- const models = inferModelsFromEndpoints(endpoints);
435
- const seeds = generateSeedsFromModels(models, 3);
436
- const guessedDb = guessDbTypeFromRepo(rootDir, endpoints);
437
-
438
- return {
439
- rootDir: normalizeSlashes(rootDir),
440
- endpoints,
441
- dbInsights: {
442
- guessedDb, // mongodb-mongoose | sql-prisma
443
- models, // inferred entities + fields
444
- seeds, // dummy seed rows
445
- },
446
- };
447
- }
448
-
449
- // -------------------------
450
- // NEW v7.0: Low-Cost Path Scanner (Standard Tier)
451
- // -------------------------
452
- export async function performLowCostPathScan(frontendSrcDir, endpoints) {
453
- // Ensures routes match frontend expectations
454
- const inconsistencies = [];
455
- endpoints.forEach(ep => {
456
- if (!ep.sourceFile || !ep.route) return;
457
- const fileBase = path.basename(ep.sourceFile).split('.')[0].toLowerCase();
458
- const routeBase = ep.controllerName ? ep.controllerName.toLowerCase() : '';
459
-
460
- // If the file containing the fetch is named 'AdminPanel' but route points to 'Products', note it.
461
- if (fileBase !== 'index' && fileBase !== 'api' && routeBase && !fileBase.includes(routeBase) && !routeBase.includes(fileBase)) {
462
- inconsistencies.push({
463
- file: ep.sourceFile,
464
- routeCalled: ep.route,
465
- warning: `Path Scanner: File '${fileBase}' calls unrelated route '${routeBase}'. Potential naming drift.`
466
- });
467
- }
468
- });
469
- return inconsistencies;
470
- }
471
-
472
- // -------------------------
473
- // NEW v7.0: Component Component Tree Extractor (DOM Sync Level 2)
474
- // -------------------------
475
- export async function extractComponentTreeTypes(frontendSrcDir) {
476
- // A heuristic simulation of live DOM cross-checking:
477
- // Parses JSX tags (<input type="date">) to build forced validations.
478
- if (!fs.existsSync(frontendSrcDir)) return [];
479
-
480
- const files = await glob(`${normalizeSlashes(frontendSrcDir)}/**/*.{jsx,tsx}`, {
481
- ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
482
- });
483
-
484
- const extractedTypes = [];
485
-
486
- for (const file of files) {
487
- try {
488
- const code = await fs.readFile(file, "utf-8");
489
- if (code.includes('type="date"')) extractedTypes.push({ file, fieldType: 'Date', rawHTMLType: 'date' });
490
- if (code.includes('type="number"')) extractedTypes.push({ file, fieldType: 'Number', rawHTMLType: 'number' });
491
- if (code.includes('type="email"')) extractedTypes.push({ file, fieldType: 'Email', rawHTMLType: 'email' });
492
- } catch {}
493
- }
494
-
495
- return extractedTypes;
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { glob } from "glob";
4
+
5
+ import parser from "@babel/parser";
6
+ import _traverse from "@babel/traverse";
7
+ const traverse = _traverse.default || _traverse;
8
+
9
+ const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete"]);
10
+
11
+ const STATIC_ASSET_EXTENSIONS = new Set([
12
+ ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico",
13
+ ".css", ".scss", ".less", ".woff", ".woff2", ".ttf", ".eot",
14
+ ".mp3", ".mp4", ".webm", ".pdf",
15
+ ]);
16
+
17
+ // -------------------------
18
+ // Utils
19
+ // -------------------------
20
+ function normalizeSlashes(p) {
21
+ return String(p || "").replace(/\\/g, "/");
22
+ }
23
+
24
+ function toTitleCase(str) {
25
+ if (!str) return "Default";
26
+ return String(str)
27
+ .replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
28
+ .replace(/^\w/, (c) => c.toUpperCase())
29
+ .replace(/[^a-zA-Z0-9]/g, "");
30
+ }
31
+
32
+ // Convert `/api/users/{id}` -> `/api/users/:id`
33
+ function normalizeRouteForBackend(urlValue) {
34
+ return String(urlValue || "").replace(/\{(\w+)\}/g, ":$1");
35
+ }
36
+
37
+ function extractApiPath(urlValue, envMap = new Map()) {
38
+ if (!urlValue) return null;
39
+
40
+ // 1. Original behavior: if URL contains /api/, extract from there
41
+ const idx = urlValue.indexOf("/api/");
42
+ if (idx !== -1) return urlValue.slice(idx);
43
+
44
+ // 2. Resolve env variable placeholders left by getUrlValue
45
+ let resolved = urlValue;
46
+ const envPattern = /\{(NEXT_PUBLIC_|REACT_APP_|VITE_)[^}]+\}/g;
47
+ resolved = resolved.replace(envPattern, (match) => {
48
+ const varName = match.slice(1, -1);
49
+ return envMap.get(varName) || "";
50
+ });
51
+
52
+ // Re-check after resolution
53
+ const idx2 = resolved.indexOf("/api/");
54
+ if (idx2 !== -1) return resolved.slice(idx2);
55
+
56
+ // 3. If resolved URL starts with http(s), extract the path portion
57
+ if (/^https?:\/\//.test(resolved)) {
58
+ try {
59
+ const url = new URL(resolved);
60
+ const pathname = url.pathname;
61
+ if (pathname && pathname !== "/") return pathname;
62
+ } catch {}
63
+ }
64
+
65
+ // 4. If it's a relative path starting with /, accept it
66
+ if (resolved.startsWith("/") && resolved.length > 1) {
67
+ // Filter out static assets
68
+ const ext = path.extname(resolved.split("?")[0]).toLowerCase();
69
+ if (STATIC_ASSET_EXTENSIONS.has(ext)) return null;
70
+ return resolved;
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ function extractPathParams(route) {
77
+ const params = [];
78
+ const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
79
+ let m;
80
+ while ((m = re.exec(route))) params.push(m[1]);
81
+ return Array.from(new Set(params));
82
+ }
83
+
84
+ function extractQueryParamsFromUrl(urlValue) {
85
+ try {
86
+ const qIndex = urlValue.indexOf("?");
87
+ if (qIndex === -1) return [];
88
+ const qs = urlValue.slice(qIndex + 1);
89
+ return qs
90
+ .split("&")
91
+ .map((p) => p.split("=")[0])
92
+ .filter(Boolean);
93
+ } catch {
94
+ return [];
95
+ }
96
+ }
97
+
98
+ function deriveControllerNameFromUrl(urlValue) {
99
+ const apiPath = extractApiPath(urlValue) || urlValue;
100
+ const parts = String(apiPath).split("/").filter(Boolean); // ["api","v1","products"]
101
+ const apiIndex = parts.indexOf("api");
102
+
103
+ let seg = null;
104
+ if (apiIndex >= 0) {
105
+ seg = parts[apiIndex + 1] || null;
106
+
107
+ // skip version segment (v1, v2, v10...)
108
+ if (seg && /^v\d+$/i.test(seg)) {
109
+ seg = parts[apiIndex + 2] || seg;
110
+ }
111
+ } else {
112
+ seg = parts[0] || null;
113
+ }
114
+
115
+ return toTitleCase(seg);
116
+ }
117
+
118
+ function deriveActionName(method, route) {
119
+ const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
120
+ const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
121
+ return `${String(method).toLowerCase()}${toTitleCase(last)}`;
122
+ }
123
+
124
+ // -------------------------
125
+ // URL extraction
126
+ // -------------------------
127
+ function extractEnvVarName(node) {
128
+ // process.env.NEXT_PUBLIC_API_URL
129
+ if (
130
+ node.type === "MemberExpression" &&
131
+ node.object?.type === "MemberExpression" &&
132
+ node.object.object?.type === "Identifier" &&
133
+ node.object.object.name === "process" &&
134
+ node.object.property?.name === "env" &&
135
+ node.property?.type === "Identifier"
136
+ ) {
137
+ return node.property.name;
138
+ }
139
+ // import.meta.env.VITE_API_URL
140
+ if (
141
+ node.type === "MemberExpression" &&
142
+ node.object?.type === "MemberExpression" &&
143
+ node.object.object?.type === "MetaProperty" &&
144
+ node.object.property?.name === "env" &&
145
+ node.property?.type === "Identifier"
146
+ ) {
147
+ return node.property.name;
148
+ }
149
+ return null;
150
+ }
151
+
152
+ function getUrlValue(urlNode, envMap = new Map()) {
153
+ if (!urlNode) return null;
154
+
155
+ if (urlNode.type === "StringLiteral") return urlNode.value;
156
+
157
+ if (urlNode.type === "TemplateLiteral") {
158
+ const quasis = urlNode.quasis || [];
159
+ const exprs = urlNode.expressions || [];
160
+ let out = "";
161
+ for (let i = 0; i < quasis.length; i++) {
162
+ out += quasis[i].value.raw;
163
+ if (exprs[i]) {
164
+ if (exprs[i].type === "Identifier") {
165
+ out += `{${exprs[i].name}}`;
166
+ } else if (exprs[i].type === "MemberExpression") {
167
+ const envName = extractEnvVarName(exprs[i]);
168
+ if (envName && envMap.has(envName)) {
169
+ out += envMap.get(envName);
170
+ } else if (envName) {
171
+ out += `{${envName}}`;
172
+ } else {
173
+ out += `{param${i + 1}}`;
174
+ }
175
+ } else {
176
+ out += `{param${i + 1}}`;
177
+ }
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ // Handle string concatenation: baseUrl + "/users"
184
+ if (urlNode.type === "BinaryExpression" && urlNode.operator === "+") {
185
+ const left = getUrlValue(urlNode.left, envMap);
186
+ const right = getUrlValue(urlNode.right, envMap);
187
+ if (left || right) return (left || "") + (right || "");
188
+ }
189
+
190
+ // Handle process.env.X / import.meta.env.X directly as URL
191
+ if (urlNode.type === "MemberExpression") {
192
+ const envName = extractEnvVarName(urlNode);
193
+ if (envName && envMap.has(envName)) {
194
+ return envMap.get(envName);
195
+ }
196
+ if (envName) {
197
+ return `{${envName}}`;
198
+ }
199
+ }
200
+
201
+ return null;
202
+ }
203
+
204
+ // -------------------------
205
+ // axios-like detection
206
+ // -------------------------
207
+ function detectAxiosLikeMethod(node) {
208
+ // axios.get(...) / api.get(...) / httpClient.post(...) etc
209
+ if (!node.callee || node.callee.type !== "MemberExpression") return null;
210
+
211
+ const prop = node.callee.property;
212
+ if (!prop || prop.type !== "Identifier") return null;
213
+
214
+ const name = prop.name.toLowerCase();
215
+ if (!HTTP_METHODS.has(name)) return null;
216
+
217
+ return name.toUpperCase();
218
+ }
219
+
220
+ // -------------------------
221
+ // Request body schema (simple + identifier tracing)
222
+ // -------------------------
223
+ function inferTypeFromNode(node) {
224
+ if (!node) return "String";
225
+ switch (node.type) {
226
+ case "StringLiteral":
227
+ return "String";
228
+ case "NumericLiteral":
229
+ return "Number";
230
+ case "BooleanLiteral":
231
+ return "Boolean";
232
+ case "NullLiteral":
233
+ return "String";
234
+ default:
235
+ return "String";
236
+ }
237
+ }
238
+
239
+ function extractObjectSchema(objExpr) {
240
+ const schemaFields = {};
241
+ if (!objExpr || objExpr.type !== "ObjectExpression") return null;
242
+
243
+ for (const prop of objExpr.properties) {
244
+ if (prop.type !== "ObjectProperty") continue;
245
+
246
+ const key =
247
+ prop.key.type === "Identifier"
248
+ ? prop.key.name
249
+ : prop.key.type === "StringLiteral"
250
+ ? prop.key.value
251
+ : null;
252
+
253
+ if (!key) continue;
254
+ schemaFields[key] = inferTypeFromNode(prop.value);
255
+ }
256
+ return schemaFields;
257
+ }
258
+
259
+ function resolveIdentifierToInit(callPath, identifierName) {
260
+ try {
261
+ const binding = callPath.scope.getBinding(identifierName);
262
+ if (!binding) return null;
263
+ const declPath = binding.path;
264
+ if (!declPath || !declPath.node) return null;
265
+
266
+ if (declPath.node.type === "VariableDeclarator") return declPath.node.init || null;
267
+ return null;
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+
273
+ function isJSONStringifyCall(node) {
274
+ // JSON.stringify(x)
275
+ return (
276
+ node &&
277
+ node.type === "CallExpression" &&
278
+ node.callee &&
279
+ node.callee.type === "MemberExpression" &&
280
+ node.callee.object &&
281
+ node.callee.object.type === "Identifier" &&
282
+ node.callee.object.name === "JSON" &&
283
+ node.callee.property &&
284
+ node.callee.property.type === "Identifier" &&
285
+ node.callee.property.name === "stringify"
286
+ );
287
+ }
288
+
289
+ // -------------------------
290
+ // DB insights: guess db + infer models + seeds
291
+ // -------------------------
292
+ function guessDbTypeFromRepo(rootDir, endpoints = []) {
293
+ try {
294
+ const pkgPath = path.join(rootDir, "package.json");
295
+ if (!fs.existsSync(pkgPath)) return heuristicallyGuessDB(endpoints);
296
+
297
+ const pkg = fs.readJsonSync(pkgPath);
298
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
299
+
300
+ if (deps.mongoose || deps.mongodb) return "mongodb-mongoose";
301
+ if (deps.prisma || deps["@prisma/client"]) return "sql-prisma";
302
+ if (deps.sequelize) return "sql-sequelize";
303
+ if (deps.typeorm) return "sql-typeorm";
304
+
305
+ return heuristicallyGuessDB(endpoints);
306
+ } catch {
307
+ return heuristicallyGuessDB(endpoints);
308
+ }
309
+ }
310
+
311
+ function heuristicallyGuessDB(endpoints) {
312
+ // Free Tier / Default Intelligence:
313
+ // Analyze data complexity. If highly nested schemas are prominent, default NoSQL.
314
+ // If many flat, relational-looking fields exist, default SQL.
315
+ let maxNesting = 0;
316
+ for (const ep of endpoints) {
317
+ if (ep.schemaFields && Object.keys(ep.schemaFields).length > 6) {
318
+ maxNesting++;
319
+ }
320
+ }
321
+ return maxNesting > 3 ? "mongodb-mongoose" : "sql-prisma";
322
+ }
323
+
324
+ function inferModelsFromEndpoints(endpoints) {
325
+ const models = new Map();
326
+
327
+ for (const ep of endpoints) {
328
+ const modelName = ep.controllerName || "Default";
329
+
330
+ if (!models.has(modelName)) {
331
+ models.set(modelName, {
332
+ name: modelName,
333
+ fields: {}, // merged fields from bodies
334
+ sources: new Set(),
335
+ endpoints: [],
336
+ });
337
+ }
338
+
339
+ const m = models.get(modelName);
340
+ m.endpoints.push({ method: ep.method, route: ep.route });
341
+ if (ep.sourceFile) m.sources.add(ep.sourceFile);
342
+
343
+ const fields = ep.schemaFields || (ep.requestBody && ep.requestBody.fields) || null;
344
+ if (fields) {
345
+ for (const [k, t] of Object.entries(fields)) {
346
+ if (!m.fields[k]) m.fields[k] = t || "String";
347
+ }
348
+ }
349
+ }
350
+
351
+ return Array.from(models.values()).map((m) => ({
352
+ name: m.name,
353
+ fields: m.fields,
354
+ sources: Array.from(m.sources),
355
+ endpoints: m.endpoints,
356
+ }));
357
+ }
358
+
359
+ // -------------------------
360
+ // Relationship detection from nested routes
361
+ // -------------------------
362
+ function singularize(word) {
363
+ if (!word) return word;
364
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
365
+ if (word.endsWith("ses") || word.endsWith("xes") || word.endsWith("zes")) return word.slice(0, -2);
366
+ if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
367
+ return word;
368
+ }
369
+
370
+ export function detectRelationships(endpoints) {
371
+ const seen = new Set();
372
+ const relationships = [];
373
+
374
+ for (const ep of endpoints) {
375
+ const route = ep.route || ep.path || "";
376
+ const segments = route.split("/").filter(Boolean);
377
+
378
+ for (let i = 0; i < segments.length - 2; i++) {
379
+ const parentSegment = segments[i];
380
+ const paramSegment = segments[i + 1];
381
+ const childSegment = segments[i + 2];
382
+
383
+ if (
384
+ parentSegment === "api" ||
385
+ /^v\d+$/i.test(parentSegment) ||
386
+ !paramSegment.startsWith(":")
387
+ ) continue;
388
+
389
+ const parentName = toTitleCase(singularize(parentSegment));
390
+ const childName = toTitleCase(singularize(childSegment));
391
+
392
+ if (!parentName || !childName || parentName === childName) continue;
393
+
394
+ const key = `${parentName}:${childName}`;
395
+ if (seen.has(key)) continue;
396
+ seen.add(key);
397
+
398
+ const foreignKey = parentName.charAt(0).toLowerCase() + parentName.slice(1) + "Id";
399
+
400
+ relationships.push({
401
+ parent: parentName,
402
+ child: childName,
403
+ type: "oneToMany",
404
+ foreignKey,
405
+ });
406
+ }
407
+ }
408
+
409
+ return relationships;
410
+ }
411
+
412
+ function seedValueForType(t) {
413
+ if (t === "Number") return 1;
414
+ if (t === "Boolean") return true;
415
+ return "test"; // String default
416
+ }
417
+
418
+ function generateSeedsFromModels(models, perModel = 3) {
419
+ return models.map((m) => {
420
+ const rows = [];
421
+ for (let i = 0; i < perModel; i++) {
422
+ const obj = {};
423
+ for (const [k, t] of Object.entries(m.fields || {})) {
424
+ obj[k] = seedValueForType(t);
425
+ }
426
+ rows.push(obj);
427
+ }
428
+ return { model: m.name, rows };
429
+ });
430
+ }
431
+
432
+ // -------------------------
433
+ // MAIN frontend analyzer
434
+ // -------------------------
435
+ export async function analyzeFrontend(srcPath, options = {}) {
436
+ if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
437
+ if (!fs.existsSync(srcPath)) {
438
+ throw new Error(`The source directory '${srcPath}' does not exist.`);
439
+ }
440
+ return analyzeFrontendMulti([srcPath], options);
441
+ }
442
+
443
+ export async function analyzeFrontendMulti(scanDirs, options = {}) {
444
+ const { envMap = new Map() } = options;
445
+
446
+ const allFiles = new Set();
447
+ for (const dir of scanDirs) {
448
+ if (!fs.existsSync(dir)) continue;
449
+ const files = await glob(`${normalizeSlashes(dir)}/**/*.{js,ts,jsx,tsx}`, {
450
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
451
+ });
452
+ files.forEach((f) => allFiles.add(path.resolve(f)));
453
+ }
454
+
455
+ const endpoints = new Map();
456
+
457
+ for (const file of allFiles) {
458
+ let code;
459
+ try {
460
+ code = await fs.readFile(file, "utf-8");
461
+ } catch {
462
+ continue;
463
+ }
464
+
465
+ let ast;
466
+ try {
467
+ ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
468
+ } catch {
469
+ continue;
470
+ }
471
+
472
+ traverse(ast, {
473
+ CallExpression(callPath) {
474
+ const node = callPath.node;
475
+
476
+ const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
477
+ const axiosMethod = detectAxiosLikeMethod(node);
478
+
479
+ if (!isFetch && !axiosMethod) return;
480
+
481
+ let urlValue = null;
482
+ let method = "GET";
483
+ let schemaFields = null;
484
+
485
+ if (isFetch) {
486
+ urlValue = getUrlValue(node.arguments[0], envMap);
487
+ const optionsNode = node.arguments[1];
488
+
489
+ if (optionsNode && optionsNode.type === "ObjectExpression") {
490
+ const methodProp = optionsNode.properties.find(
491
+ (p) =>
492
+ p.type === "ObjectProperty" &&
493
+ p.key.type === "Identifier" &&
494
+ p.key.name === "method"
495
+ );
496
+ if (methodProp && methodProp.value.type === "StringLiteral") {
497
+ method = methodProp.value.value.toUpperCase();
498
+ }
499
+
500
+ if (["POST", "PUT", "PATCH"].includes(method)) {
501
+ const bodyProp = optionsNode.properties.find(
502
+ (p) =>
503
+ p.type === "ObjectProperty" &&
504
+ p.key.type === "Identifier" &&
505
+ p.key.name === "body"
506
+ );
507
+
508
+ if (bodyProp) {
509
+ const v = bodyProp.value;
510
+
511
+ if (isJSONStringifyCall(v)) {
512
+ const arg0 = v.arguments[0];
513
+
514
+ if (arg0?.type === "ObjectExpression") {
515
+ schemaFields = extractObjectSchema(arg0);
516
+ } else if (arg0?.type === "Identifier") {
517
+ const init = resolveIdentifierToInit(callPath, arg0.name);
518
+ if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
519
+ }
520
+ }
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ if (axiosMethod) {
527
+ method = axiosMethod;
528
+ urlValue = getUrlValue(node.arguments[0], envMap);
529
+
530
+ if (["POST", "PUT", "PATCH"].includes(method)) {
531
+ const dataArg = node.arguments[1];
532
+ if (dataArg?.type === "ObjectExpression") {
533
+ schemaFields = extractObjectSchema(dataArg);
534
+ } else if (dataArg?.type === "Identifier") {
535
+ const init = resolveIdentifierToInit(callPath, dataArg.name);
536
+ if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
537
+ }
538
+ }
539
+ }
540
+
541
+ const apiPath = extractApiPath(urlValue, envMap);
542
+ if (!apiPath) return;
543
+
544
+ const route = normalizeRouteForBackend(apiPath.split("?")[0]);
545
+ const normalizedRoute = route.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
546
+ const controllerName = deriveControllerNameFromUrl(apiPath);
547
+ const actionName = deriveActionName(method, normalizedRoute);
548
+
549
+ const key = `${method}:${normalizedRoute}`;
550
+ if (!endpoints.has(key)) {
551
+ endpoints.set(key, {
552
+ path: apiPath,
553
+ route: normalizedRoute,
554
+ method,
555
+ controllerName,
556
+ actionName,
557
+ pathParams: extractPathParams(normalizedRoute),
558
+ queryParams: extractQueryParamsFromUrl(apiPath),
559
+ schemaFields,
560
+ requestBody: schemaFields ? { fields: schemaFields } : null,
561
+ sourceFile: normalizeSlashes(file),
562
+ });
563
+ }
564
+ },
565
+ });
566
+ }
567
+
568
+ return Array.from(endpoints.values());
569
+ }
570
+
571
+ // -------------------------
572
+ // Optional: full project analyze (endpoints + db insights)
573
+ // -------------------------
574
+ export async function analyze(projectRoot = process.cwd()) {
575
+ const rootDir = path.resolve(projectRoot);
576
+
577
+ const frontendSrc = ["src", "app", "pages"]
578
+ .map((d) => path.join(rootDir, d))
579
+ .find((d) => fs.existsSync(d));
580
+
581
+ const endpoints = frontendSrc ? await analyzeFrontend(frontendSrc) : [];
582
+
583
+ const models = inferModelsFromEndpoints(endpoints);
584
+ const seeds = generateSeedsFromModels(models, 3);
585
+ const guessedDb = guessDbTypeFromRepo(rootDir, endpoints);
586
+ const relationships = detectRelationships(endpoints);
587
+
588
+ return {
589
+ rootDir: normalizeSlashes(rootDir),
590
+ endpoints,
591
+ relationships,
592
+ dbInsights: {
593
+ guessedDb,
594
+ models,
595
+ seeds,
596
+ },
597
+ };
598
+ }
599
+
600
+ // -------------------------
601
+ // Next.js API Route Detection
602
+ // -------------------------
603
+ const NEXTJS_HTTP_EXPORTS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
604
+
605
+ export async function detectNextjsApiRoutes(apiRouteDirs) {
606
+ const routes = [];
607
+
608
+ for (const dir of apiRouteDirs) {
609
+ if (!fs.existsSync(dir)) continue;
610
+
611
+ // App Router: app/**/route.{ts,js}
612
+ const appRouterFiles = await glob(
613
+ `${normalizeSlashes(dir)}/**/route.{ts,js,tsx,jsx}`,
614
+ { ignore: ["**/node_modules/**"] }
615
+ );
616
+
617
+ for (const file of appRouterFiles) {
618
+ const relativePath = path.relative(dir, path.dirname(file));
619
+ let routePath = "/" + relativePath
620
+ .replace(/\\/g, "/")
621
+ .replace(/\(([^)]+)\)\//g, "")
622
+ .replace(/\[\.\.\.([^\]]+)\]/g, ":$1")
623
+ .replace(/\[([^\]]+)\]/g, ":$1");
624
+
625
+ if (routePath === "/.") routePath = "/";
626
+
627
+ const methods = await extractExportedHttpMethods(file);
628
+ for (const method of methods) {
629
+ routes.push({
630
+ route: routePath,
631
+ method,
632
+ controllerName: deriveControllerNameFromUrl(routePath),
633
+ sourceFile: normalizeSlashes(file),
634
+ isServerRoute: true,
635
+ });
636
+ }
637
+ }
638
+
639
+ // Pages Router: pages/api/**/*.{ts,js}
640
+ const pagesApiDir = path.join(dir, "api");
641
+ if (fs.existsSync(pagesApiDir)) {
642
+ const pagesApiFiles = await glob(
643
+ `${normalizeSlashes(pagesApiDir)}/**/*.{ts,js,tsx,jsx}`,
644
+ { ignore: ["**/node_modules/**"] }
645
+ );
646
+
647
+ for (const file of pagesApiFiles) {
648
+ const relativePath = path.relative(pagesApiDir, file);
649
+ let routePath = "/api/" + relativePath
650
+ .replace(/\\/g, "/")
651
+ .replace(/\.(ts|js|tsx|jsx)$/, "")
652
+ .replace(/\/index$/, "")
653
+ .replace(/\[\.\.\.([^\]]+)\]/g, ":$1")
654
+ .replace(/\[([^\]]+)\]/g, ":$1");
655
+
656
+ routes.push({
657
+ route: routePath,
658
+ method: "ALL",
659
+ controllerName: deriveControllerNameFromUrl(routePath),
660
+ sourceFile: normalizeSlashes(file),
661
+ isServerRoute: true,
662
+ });
663
+ }
664
+ }
665
+ }
666
+
667
+ return routes;
668
+ }
669
+
670
+ async function extractExportedHttpMethods(file) {
671
+ const methods = [];
672
+ try {
673
+ const code = await fs.readFile(file, "utf-8");
674
+ const ast = parser.parse(code, {
675
+ sourceType: "module",
676
+ plugins: ["jsx", "typescript"],
677
+ });
678
+
679
+ traverse(ast, {
680
+ ExportNamedDeclaration(nodePath) {
681
+ const decl = nodePath.node.declaration;
682
+ if (!decl) return;
683
+
684
+ if (decl.type === "FunctionDeclaration" && decl.id) {
685
+ const name = decl.id.name;
686
+ if (NEXTJS_HTTP_EXPORTS.has(name)) methods.push(name);
687
+ }
688
+
689
+ if (decl.type === "VariableDeclaration") {
690
+ for (const declarator of decl.declarations) {
691
+ if (declarator.id?.type === "Identifier") {
692
+ const name = declarator.id.name;
693
+ if (NEXTJS_HTTP_EXPORTS.has(name)) methods.push(name);
694
+ }
695
+ }
696
+ }
697
+ },
698
+ });
699
+ } catch {}
700
+
701
+ return methods.length > 0 ? methods : ["GET"];
702
+ }
703
+
704
+ // -------------------------
705
+ // NEW v7.0: Low-Cost Path Scanner (Standard Tier)
706
+ // -------------------------
707
+ export async function performLowCostPathScan(frontendSrcDir, endpoints) {
708
+ // Ensures routes match frontend expectations
709
+ const inconsistencies = [];
710
+ endpoints.forEach(ep => {
711
+ if (!ep.sourceFile || !ep.route) return;
712
+ const fileBase = path.basename(ep.sourceFile).split('.')[0].toLowerCase();
713
+ const routeBase = ep.controllerName ? ep.controllerName.toLowerCase() : '';
714
+
715
+ // If the file containing the fetch is named 'AdminPanel' but route points to 'Products', note it.
716
+ if (fileBase !== 'index' && fileBase !== 'api' && routeBase && !fileBase.includes(routeBase) && !routeBase.includes(fileBase)) {
717
+ inconsistencies.push({
718
+ file: ep.sourceFile,
719
+ routeCalled: ep.route,
720
+ warning: `Path Scanner: File '${fileBase}' calls unrelated route '${routeBase}'. Potential naming drift.`
721
+ });
722
+ }
723
+ });
724
+ return inconsistencies;
725
+ }
726
+
727
+ // -------------------------
728
+ // NEW v7.0: Component Component Tree Extractor (DOM Sync Level 2)
729
+ // -------------------------
730
+ export async function extractComponentTreeTypes(frontendSrcDir) {
731
+ // A heuristic simulation of live DOM cross-checking:
732
+ // Parses JSX tags (<input type="date">) to build forced validations.
733
+ if (!fs.existsSync(frontendSrcDir)) return [];
734
+
735
+ const files = await glob(`${normalizeSlashes(frontendSrcDir)}/**/*.{jsx,tsx}`, {
736
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
737
+ });
738
+
739
+ const extractedTypes = [];
740
+
741
+ for (const file of files) {
742
+ try {
743
+ const code = await fs.readFile(file, "utf-8");
744
+ if (code.includes('type="date"')) extractedTypes.push({ file, fieldType: 'Date', rawHTMLType: 'date' });
745
+ if (code.includes('type="number"')) extractedTypes.push({ file, fieldType: 'Number', rawHTMLType: 'number' });
746
+ if (code.includes('type="email"')) extractedTypes.push({ file, fieldType: 'Email', rawHTMLType: 'email' });
747
+ } catch {}
748
+ }
749
+
750
+ return extractedTypes;
496
751
  }