create-backlist 6.2.3 → 7.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.
package/src/analyzer.js CHANGED
@@ -1,436 +1,496 @@
1
- /* eslint-disable @typescript-eslint/no-var-requires */
2
- const fs = require("fs-extra");
3
- const path = require("path");
4
- const { glob } = require("glob");
5
-
6
- const parser = require("@babel/parser");
7
- const traverse = require("@babel/traverse").default;
8
-
9
- const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete"]);
10
-
11
- // -------------------------
12
- // Utils
13
- // -------------------------
14
- function normalizeSlashes(p) {
15
- return String(p || "").replace(/\\/g, "/");
16
- }
17
-
18
- function toTitleCase(str) {
19
- if (!str) return "Default";
20
- return String(str)
21
- .replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
22
- .replace(/^\w/, (c) => c.toUpperCase())
23
- .replace(/[^a-zA-Z0-9]/g, "");
24
- }
25
-
26
- // Convert `/api/users/{id}` -> `/api/users/:id`
27
- function normalizeRouteForBackend(urlValue) {
28
- return String(urlValue || "").replace(/\{(\w+)\}/g, ":$1");
29
- }
30
-
31
- function extractApiPath(urlValue) {
32
- // supports:
33
- // - /api/...
34
- // - http://localhost:5000/api/...
35
- if (!urlValue) return null;
36
- const idx = urlValue.indexOf("/api/");
37
- if (idx === -1) return null;
38
- return urlValue.slice(idx); // => /api/...
39
- }
40
-
41
- function extractPathParams(route) {
42
- const params = [];
43
- const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
44
- let m;
45
- while ((m = re.exec(route))) params.push(m[1]);
46
- return Array.from(new Set(params));
47
- }
48
-
49
- function extractQueryParamsFromUrl(urlValue) {
50
- try {
51
- const qIndex = urlValue.indexOf("?");
52
- if (qIndex === -1) return [];
53
- const qs = urlValue.slice(qIndex + 1);
54
- return qs
55
- .split("&")
56
- .map((p) => p.split("=")[0])
57
- .filter(Boolean);
58
- } catch {
59
- return [];
60
- }
61
- }
62
-
63
- function deriveControllerNameFromUrl(urlValue) {
64
- const apiPath = extractApiPath(urlValue) || urlValue;
65
- const parts = String(apiPath).split("/").filter(Boolean); // ["api","v1","products"]
66
- const apiIndex = parts.indexOf("api");
67
-
68
- let seg = null;
69
- if (apiIndex >= 0) {
70
- seg = parts[apiIndex + 1] || null;
71
-
72
- // skip version segment (v1, v2, v10...)
73
- if (seg && /^v\d+$/i.test(seg)) {
74
- seg = parts[apiIndex + 2] || seg;
75
- }
76
- } else {
77
- seg = parts[0] || null;
78
- }
79
-
80
- return toTitleCase(seg);
81
- }
82
-
83
- function deriveActionName(method, route) {
84
- const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
85
- const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
86
- return `${String(method).toLowerCase()}${toTitleCase(last)}`;
87
- }
88
-
89
- // -------------------------
90
- // URL extraction
91
- // -------------------------
92
- function getUrlValue(urlNode) {
93
- if (!urlNode) return null;
94
-
95
- if (urlNode.type === "StringLiteral") return urlNode.value;
96
-
97
- if (urlNode.type === "TemplateLiteral") {
98
- // `/api/users/${id}` -> `/api/users/{id}` or `{param1}`
99
- const quasis = urlNode.quasis || [];
100
- const exprs = urlNode.expressions || [];
101
- let out = "";
102
- for (let i = 0; i < quasis.length; i++) {
103
- out += quasis[i].value.raw;
104
- if (exprs[i]) {
105
- if (exprs[i].type === "Identifier") out += `{${exprs[i].name}}`;
106
- else out += `{param${i + 1}}`;
107
- }
108
- }
109
- return out;
110
- }
111
-
112
- return null;
113
- }
114
-
115
- // -------------------------
116
- // axios-like detection
117
- // -------------------------
118
- function detectAxiosLikeMethod(node) {
119
- // axios.get(...) / api.get(...) / httpClient.post(...) etc
120
- if (!node.callee || node.callee.type !== "MemberExpression") return null;
121
-
122
- const prop = node.callee.property;
123
- if (!prop || prop.type !== "Identifier") return null;
124
-
125
- const name = prop.name.toLowerCase();
126
- if (!HTTP_METHODS.has(name)) return null;
127
-
128
- return name.toUpperCase();
129
- }
130
-
131
- // -------------------------
132
- // Request body schema (simple + identifier tracing)
133
- // -------------------------
134
- function inferTypeFromNode(node) {
135
- if (!node) return "String";
136
- switch (node.type) {
137
- case "StringLiteral":
138
- return "String";
139
- case "NumericLiteral":
140
- return "Number";
141
- case "BooleanLiteral":
142
- return "Boolean";
143
- case "NullLiteral":
144
- return "String";
145
- default:
146
- return "String";
147
- }
148
- }
149
-
150
- function extractObjectSchema(objExpr) {
151
- const schemaFields = {};
152
- if (!objExpr || objExpr.type !== "ObjectExpression") return null;
153
-
154
- for (const prop of objExpr.properties) {
155
- if (prop.type !== "ObjectProperty") continue;
156
-
157
- const key =
158
- prop.key.type === "Identifier"
159
- ? prop.key.name
160
- : prop.key.type === "StringLiteral"
161
- ? prop.key.value
162
- : null;
163
-
164
- if (!key) continue;
165
- schemaFields[key] = inferTypeFromNode(prop.value);
166
- }
167
- return schemaFields;
168
- }
169
-
170
- function resolveIdentifierToInit(callPath, identifierName) {
171
- try {
172
- const binding = callPath.scope.getBinding(identifierName);
173
- if (!binding) return null;
174
- const declPath = binding.path;
175
- if (!declPath || !declPath.node) return null;
176
-
177
- if (declPath.node.type === "VariableDeclarator") return declPath.node.init || null;
178
- return null;
179
- } catch {
180
- return null;
181
- }
182
- }
183
-
184
- function isJSONStringifyCall(node) {
185
- // JSON.stringify(x)
186
- return (
187
- node &&
188
- node.type === "CallExpression" &&
189
- node.callee &&
190
- node.callee.type === "MemberExpression" &&
191
- node.callee.object &&
192
- node.callee.object.type === "Identifier" &&
193
- node.callee.object.name === "JSON" &&
194
- node.callee.property &&
195
- node.callee.property.type === "Identifier" &&
196
- node.callee.property.name === "stringify"
197
- );
198
- }
199
-
200
- // -------------------------
201
- // DB insights: guess db + infer models + seeds
202
- // -------------------------
203
- function guessDbTypeFromRepo(rootDir) {
204
- // Best-effort; if it's only frontend repo, usually null.
205
- try {
206
- const pkgPath = path.join(rootDir, "package.json");
207
- if (!fs.existsSync(pkgPath)) return null;
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 null;
218
- } catch {
219
- return null;
220
- }
221
- }
222
-
223
- function inferModelsFromEndpoints(endpoints) {
224
- const models = new Map();
225
-
226
- for (const ep of endpoints) {
227
- const modelName = ep.controllerName || "Default";
228
-
229
- if (!models.has(modelName)) {
230
- models.set(modelName, {
231
- name: modelName,
232
- fields: {}, // merged fields from bodies
233
- sources: new Set(),
234
- endpoints: [],
235
- });
236
- }
237
-
238
- const m = models.get(modelName);
239
- m.endpoints.push({ method: ep.method, route: ep.route });
240
- if (ep.sourceFile) m.sources.add(ep.sourceFile);
241
-
242
- const fields = ep.schemaFields || (ep.requestBody && ep.requestBody.fields) || null;
243
- if (fields) {
244
- for (const [k, t] of Object.entries(fields)) {
245
- if (!m.fields[k]) m.fields[k] = t || "String";
246
- }
247
- }
248
- }
249
-
250
- return Array.from(models.values()).map((m) => ({
251
- name: m.name,
252
- fields: m.fields,
253
- sources: Array.from(m.sources),
254
- endpoints: m.endpoints,
255
- }));
256
- }
257
-
258
- function seedValueForType(t) {
259
- if (t === "Number") return 1;
260
- if (t === "Boolean") return true;
261
- return "test"; // String default
262
- }
263
-
264
- function generateSeedsFromModels(models, perModel = 3) {
265
- return models.map((m) => {
266
- const rows = [];
267
- for (let i = 0; i < perModel; i++) {
268
- const obj = {};
269
- for (const [k, t] of Object.entries(m.fields || {})) {
270
- obj[k] = seedValueForType(t);
271
- }
272
- rows.push(obj);
273
- }
274
- return { model: m.name, rows };
275
- });
276
- }
277
-
278
- // -------------------------
279
- // MAIN frontend analyzer
280
- // -------------------------
281
- async function analyzeFrontend(srcPath) {
282
- if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
283
- if (!fs.existsSync(srcPath)) {
284
- throw new Error(`The source directory '${srcPath}' does not exist.`);
285
- }
286
-
287
- const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
288
- ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
289
- });
290
-
291
- const endpoints = new Map();
292
-
293
- for (const file of files) {
294
- let code;
295
- try {
296
- code = await fs.readFile(file, "utf-8");
297
- } catch {
298
- continue;
299
- }
300
-
301
- let ast;
302
- try {
303
- ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
304
- } catch {
305
- continue;
306
- }
307
-
308
- traverse(ast, {
309
- CallExpression(callPath) {
310
- const node = callPath.node;
311
-
312
- const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
313
- const axiosMethod = detectAxiosLikeMethod(node);
314
-
315
- if (!isFetch && !axiosMethod) return;
316
-
317
- let urlValue = null;
318
- let method = "GET";
319
- let schemaFields = null;
320
-
321
- // ---- fetch(url, options) ----
322
- if (isFetch) {
323
- urlValue = getUrlValue(node.arguments[0]);
324
- const optionsNode = node.arguments[1];
325
-
326
- if (optionsNode && optionsNode.type === "ObjectExpression") {
327
- const methodProp = optionsNode.properties.find(
328
- (p) =>
329
- p.type === "ObjectProperty" &&
330
- p.key.type === "Identifier" &&
331
- p.key.name === "method"
332
- );
333
- if (methodProp && methodProp.value.type === "StringLiteral") {
334
- method = methodProp.value.value.toUpperCase();
335
- }
336
-
337
- if (["POST", "PUT", "PATCH"].includes(method)) {
338
- const bodyProp = optionsNode.properties.find(
339
- (p) =>
340
- p.type === "ObjectProperty" &&
341
- p.key.type === "Identifier" &&
342
- p.key.name === "body"
343
- );
344
-
345
- if (bodyProp) {
346
- const v = bodyProp.value;
347
-
348
- if (isJSONStringifyCall(v)) {
349
- const arg0 = v.arguments[0];
350
-
351
- if (arg0?.type === "ObjectExpression") {
352
- schemaFields = extractObjectSchema(arg0);
353
- } else if (arg0?.type === "Identifier") {
354
- const init = resolveIdentifierToInit(callPath, arg0.name);
355
- if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
356
- }
357
- }
358
- }
359
- }
360
- }
361
- }
362
-
363
- // ---- axios-like client ----
364
- if (axiosMethod) {
365
- method = axiosMethod;
366
- urlValue = getUrlValue(node.arguments[0]);
367
-
368
- if (["POST", "PUT", "PATCH"].includes(method)) {
369
- const dataArg = node.arguments[1];
370
- if (dataArg?.type === "ObjectExpression") {
371
- schemaFields = extractObjectSchema(dataArg);
372
- } else if (dataArg?.type === "Identifier") {
373
- const init = resolveIdentifierToInit(callPath, dataArg.name);
374
- if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
375
- }
376
- }
377
- }
378
-
379
- // accept only URLs that contain /api/ anywhere
380
- const apiPath = extractApiPath(urlValue);
381
- if (!apiPath) return;
382
-
383
- const route = normalizeRouteForBackend(apiPath.split("?")[0]);
384
- const controllerName = deriveControllerNameFromUrl(apiPath);
385
- const actionName = deriveActionName(method, route);
386
-
387
- const key = `${method}:${route}`;
388
- if (!endpoints.has(key)) {
389
- endpoints.set(key, {
390
- path: apiPath,
391
- route,
392
- method,
393
- controllerName,
394
- actionName,
395
- pathParams: extractPathParams(route),
396
- queryParams: extractQueryParamsFromUrl(apiPath),
397
- schemaFields,
398
- requestBody: schemaFields ? { fields: schemaFields } : null,
399
- sourceFile: normalizeSlashes(file),
400
- });
401
- }
402
- },
403
- });
404
- }
405
-
406
- return Array.from(endpoints.values());
407
- }
408
-
409
- // -------------------------
410
- // Optional: full project analyze (endpoints + db insights)
411
- // -------------------------
412
- async function analyze(projectRoot = process.cwd()) {
413
- const rootDir = path.resolve(projectRoot);
414
-
415
- const frontendSrc = ["src", "app", "pages"]
416
- .map((d) => path.join(rootDir, d))
417
- .find((d) => fs.existsSync(d));
418
-
419
- const endpoints = frontendSrc ? await analyzeFrontend(frontendSrc) : [];
420
-
421
- const models = inferModelsFromEndpoints(endpoints);
422
- const seeds = generateSeedsFromModels(models, 3);
423
- const guessedDb = guessDbTypeFromRepo(rootDir);
424
-
425
- return {
426
- rootDir: normalizeSlashes(rootDir),
427
- endpoints,
428
- dbInsights: {
429
- guessedDb, // null | mongodb-mongoose | sql-prisma | ...
430
- models, // inferred entities + fields
431
- seeds, // dummy seed rows
432
- },
433
- };
434
- }
435
-
436
- module.exports = { analyzeFrontend, analyze };
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;
496
+ }