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.
- package/README.md +1 -10
- package/bin/index.js +242 -275
- package/package.json +3 -2
- package/src/ai-agent.js +171 -171
- package/src/analyzer.js +750 -495
- package/src/env-resolver.js +70 -0
- package/src/generators/dotnet.js +134 -133
- package/src/generators/java.js +248 -233
- package/src/generators/js.js +346 -0
- package/src/generators/nestjs.js +278 -0
- package/src/generators/node.js +404 -404
- package/src/generators/python.js +86 -104
- package/src/generators/template.js +22 -22
- package/src/project-detector.js +131 -0
- package/src/templates/dotnet/partials/Dockerfile.ejs +27 -0
- package/src/templates/dotnet/partials/docker-compose.yml.ejs +33 -0
- package/src/templates/java-spring/partials/Controller.java.ejs +3 -3
- package/src/templates/js-express/base/server.js +59 -0
- package/src/templates/js-express/partials/Dockerfile.ejs +12 -0
- package/src/templates/js-express/partials/auth.controller.js.ejs +66 -0
- package/src/templates/js-express/partials/auth.middleware.js.ejs +19 -0
- package/src/templates/js-express/partials/auth.routes.js.ejs +9 -0
- package/src/templates/js-express/partials/controller.js.ejs +53 -0
- package/src/templates/js-express/partials/db.js.ejs +19 -0
- package/src/templates/js-express/partials/docker-compose.yml.ejs +46 -0
- package/src/templates/js-express/partials/model.js.ejs +18 -0
- package/src/templates/js-express/partials/package.json.ejs +17 -0
- package/src/templates/js-express/partials/prisma.schema.ejs +21 -0
- package/src/templates/js-express/partials/routes.js.ejs +19 -0
- package/src/templates/js-express/partials/seeder.js.ejs +103 -0
- package/src/templates/js-express/partials/service.js.ejs +51 -0
- package/src/templates/js-express/partials/swagger.js.ejs +30 -0
- package/src/templates/js-express/partials/test.js.ejs +46 -0
- package/src/templates/nestjs/base/app.module.ts +9 -0
- package/src/templates/nestjs/base/main.ts +23 -0
- package/src/templates/nestjs/base/tsconfig.json +21 -0
- package/src/templates/nestjs/partials/auth.controller.ts.ejs +17 -0
- package/src/templates/nestjs/partials/auth.module.ts.ejs +17 -0
- package/src/templates/nestjs/partials/auth.service.ts.ejs +70 -0
- package/src/templates/nestjs/partials/controller.ts.ejs +34 -0
- package/src/templates/nestjs/partials/create-dto.ts.ejs +22 -0
- package/src/templates/nestjs/partials/jwt-guard.ts.ejs +24 -0
- package/src/templates/nestjs/partials/module.ts.ejs +10 -0
- package/src/templates/nestjs/partials/package.json.ejs +27 -0
- package/src/templates/nestjs/partials/prisma.service.ts.ejs +13 -0
- package/src/templates/nestjs/partials/schema.ts.ejs +19 -0
- package/src/templates/nestjs/partials/service.ts.ejs +67 -0
- package/src/templates/nestjs/partials/update-dto.ts.ejs +4 -0
- package/src/templates/node-ts-express/partials/HexController.ts.ejs +56 -56
- package/src/templates/node-ts-express/partials/HexRepository.ts.ejs +26 -26
- package/src/templates/node-ts-express/partials/HexService.ts.ejs +27 -27
- package/src/utils.js +11 -11
- /package/src/templates/{node-ts-express → dotnet}/partials/DbContext.cs.ejs +0 -0
- /package/src/templates/{node-ts-express → dotnet}/partials/Model.cs.ejs +0 -0
package/src/analyzer.js
CHANGED
|
@@ -1,496 +1,751 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return String(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
if (!
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
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
|
-
return
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
endpoints
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
}
|