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/bin/index.js +503 -140
- package/package.json +11 -8
- package/src/ai-agent.js +171 -0
- package/src/analyzer.js +496 -436
- package/src/generators/node.js +405 -374
- package/src/generators/template.js +23 -21
- package/src/templates/node-ts-express/partials/HexController.ts.ejs +56 -0
- package/src/templates/node-ts-express/partials/HexRepository.ts.ejs +26 -0
- package/src/templates/node-ts-express/partials/HexService.ts.ejs +27 -0
- package/src/utils.js +12 -14
package/src/analyzer.js
CHANGED
|
@@ -1,436 +1,496 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.replace(
|
|
23
|
-
.replace(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
// -
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return null;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
node
|
|
189
|
-
node.
|
|
190
|
-
node.callee
|
|
191
|
-
node.callee.
|
|
192
|
-
node.callee.object
|
|
193
|
-
node.callee.object.
|
|
194
|
-
node.callee.
|
|
195
|
-
node.callee.property
|
|
196
|
-
node.callee.property.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
const pkgPath = path.join(rootDir, "package.json");
|
|
207
|
-
if (!fs.existsSync(pkgPath)) return
|
|
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
|
|
218
|
-
} catch {
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
+
}
|