create-backlist 6.0.2 → 6.0.3
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/package.json +1 -1
- package/src/analyzer.js +101 -68
package/package.json
CHANGED
package/src/analyzer.js
CHANGED
|
@@ -3,19 +3,17 @@ const { glob } = require('glob');
|
|
|
3
3
|
const parser = require('@babel/parser');
|
|
4
4
|
const traverse = require('@babel/traverse').default;
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
6
|
+
const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
|
|
7
|
+
|
|
9
8
|
function toTitleCase(str) {
|
|
10
9
|
if (!str) return 'Default';
|
|
11
10
|
return String(str)
|
|
12
|
-
.replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
|
|
11
|
+
.replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
|
|
13
12
|
.replace(/^\w/, c => c.toUpperCase())
|
|
14
13
|
.replace(/[^a-zA-Z0-9]/g, '');
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
function normalizeRouteForBackend(urlValue) {
|
|
18
|
-
// Convert template placeholders {id} -> :id
|
|
19
17
|
return urlValue.replace(/\{(\w+)\}/g, ':$1');
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -43,26 +41,19 @@ function extractObjectSchema(objExpr) {
|
|
|
43
41
|
null;
|
|
44
42
|
|
|
45
43
|
if (!key) continue;
|
|
46
|
-
|
|
47
44
|
schemaFields[key] = inferTypeFromNode(prop.value);
|
|
48
45
|
}
|
|
49
46
|
return schemaFields;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
/**
|
|
53
|
-
* Try to resolve Identifier -> its init value if it's const payload = {...}
|
|
54
|
-
*/
|
|
55
49
|
function resolveIdentifierToInit(path, identifierName) {
|
|
56
50
|
try {
|
|
57
51
|
const binding = path.scope.getBinding(identifierName);
|
|
58
52
|
if (!binding) return null;
|
|
59
|
-
const declPath = binding.path;
|
|
53
|
+
const declPath = binding.path;
|
|
60
54
|
if (!declPath || !declPath.node) return null;
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
if (declPath.node.type === 'VariableDeclarator') {
|
|
64
|
-
return declPath.node.init || null;
|
|
65
|
-
}
|
|
56
|
+
if (declPath.node.type === 'VariableDeclarator') return declPath.node.init || null;
|
|
66
57
|
return null;
|
|
67
58
|
} catch {
|
|
68
59
|
return null;
|
|
@@ -75,7 +66,7 @@ function getUrlValue(urlNode) {
|
|
|
75
66
|
if (urlNode.type === 'StringLiteral') return urlNode.value;
|
|
76
67
|
|
|
77
68
|
if (urlNode.type === 'TemplateLiteral') {
|
|
78
|
-
// `/api/users/${id}` -> `/api/users/{id}`
|
|
69
|
+
// `/api/users/${id}` -> `/api/users/{id}` or `{param1}`
|
|
79
70
|
const quasis = urlNode.quasis || [];
|
|
80
71
|
const exprs = urlNode.expressions || [];
|
|
81
72
|
let out = '';
|
|
@@ -83,7 +74,7 @@ function getUrlValue(urlNode) {
|
|
|
83
74
|
out += quasis[i].value.raw;
|
|
84
75
|
if (exprs[i]) {
|
|
85
76
|
if (exprs[i].type === 'Identifier') out += `{${exprs[i].name}}`;
|
|
86
|
-
else out += `{param}`;
|
|
77
|
+
else out += `{param${i + 1}}`;
|
|
87
78
|
}
|
|
88
79
|
}
|
|
89
80
|
return out;
|
|
@@ -92,34 +83,52 @@ function getUrlValue(urlNode) {
|
|
|
92
83
|
return null;
|
|
93
84
|
}
|
|
94
85
|
|
|
86
|
+
function extractApiPath(urlValue) {
|
|
87
|
+
// supports:
|
|
88
|
+
// - /api/...
|
|
89
|
+
// - http://localhost:5000/api/...
|
|
90
|
+
if (!urlValue) return null;
|
|
91
|
+
const idx = urlValue.indexOf('/api/');
|
|
92
|
+
if (idx === -1) return null;
|
|
93
|
+
return urlValue.slice(idx); // => /api/...
|
|
94
|
+
}
|
|
95
|
+
|
|
95
96
|
function deriveControllerNameFromUrl(urlValue) {
|
|
96
|
-
|
|
97
|
-
const parts =
|
|
97
|
+
const apiPath = extractApiPath(urlValue) || urlValue;
|
|
98
|
+
const parts = String(apiPath).split('/').filter(Boolean); // ["api","v1","products"]
|
|
98
99
|
const apiIndex = parts.indexOf('api');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
|
|
101
|
+
let seg = null;
|
|
102
|
+
|
|
103
|
+
if (apiIndex >= 0) {
|
|
104
|
+
seg = parts[apiIndex + 1] || null;
|
|
105
|
+
|
|
106
|
+
// skip version segment (v1, v2, v10...)
|
|
107
|
+
if (seg && /^v\d+$/i.test(seg)) {
|
|
108
|
+
seg = parts[apiIndex + 2] || seg;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
seg = parts[0] || null;
|
|
112
|
+
}
|
|
102
113
|
|
|
103
114
|
return toTitleCase(seg);
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
function deriveActionName(method, route) {
|
|
107
|
-
|
|
108
|
-
const cleaned = route.replace(/^\/api\//, '/').replace(/[/:{}-]/g, ' ');
|
|
118
|
+
const cleaned = String(route).replace(/^\/api\//, '/').replace(/[/:{}-]/g, ' ');
|
|
109
119
|
const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || 'Action';
|
|
110
|
-
return `${method.toLowerCase()}${toTitleCase(last)}`;
|
|
120
|
+
return `${String(method).toLowerCase()}${toTitleCase(last)}`;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
function extractPathParams(route) {
|
|
114
124
|
const params = [];
|
|
115
|
-
const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
|
|
125
|
+
const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
|
|
116
126
|
let m;
|
|
117
127
|
while ((m = re.exec(route))) params.push(m[1]);
|
|
118
128
|
return Array.from(new Set(params));
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
function extractQueryParamsFromUrl(urlValue) {
|
|
122
|
-
// if url has ?a=b&c=d as string literal
|
|
123
132
|
try {
|
|
124
133
|
const qIndex = urlValue.indexOf('?');
|
|
125
134
|
if (qIndex === -1) return [];
|
|
@@ -130,12 +139,28 @@ function extractQueryParamsFromUrl(urlValue) {
|
|
|
130
139
|
}
|
|
131
140
|
}
|
|
132
141
|
|
|
142
|
+
function detectAxiosLikeMethod(node) {
|
|
143
|
+
// axios.get(...) / api.get(...) / httpClient.post(...) etc
|
|
144
|
+
if (!node.callee || node.callee.type !== 'MemberExpression') return null;
|
|
145
|
+
|
|
146
|
+
const prop = node.callee.property;
|
|
147
|
+
if (!prop || prop.type !== 'Identifier') return null;
|
|
148
|
+
|
|
149
|
+
const name = prop.name.toLowerCase();
|
|
150
|
+
if (!HTTP_METHODS.has(name)) return null;
|
|
151
|
+
|
|
152
|
+
return name.toUpperCase();
|
|
153
|
+
}
|
|
154
|
+
|
|
133
155
|
async function analyzeFrontend(srcPath) {
|
|
134
156
|
if (!fs.existsSync(srcPath)) {
|
|
135
157
|
throw new Error(`The source directory '${srcPath}' does not exist.`);
|
|
136
158
|
}
|
|
137
159
|
|
|
138
|
-
const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, {
|
|
160
|
+
const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, {
|
|
161
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**']
|
|
162
|
+
});
|
|
163
|
+
|
|
139
164
|
const endpoints = new Map();
|
|
140
165
|
|
|
141
166
|
for (const file of files) {
|
|
@@ -152,58 +177,64 @@ async function analyzeFrontend(srcPath) {
|
|
|
152
177
|
CallExpression(callPath) {
|
|
153
178
|
const node = callPath.node;
|
|
154
179
|
|
|
155
|
-
// --- Detect fetch(url, options) ---
|
|
156
180
|
const isFetch = node.callee.type === 'Identifier' && node.callee.name === 'fetch';
|
|
181
|
+
const axiosMethod = detectAxiosLikeMethod(node);
|
|
157
182
|
|
|
158
|
-
|
|
159
|
-
const isAxiosMethod =
|
|
160
|
-
node.callee.type === 'MemberExpression' &&
|
|
161
|
-
node.callee.object.type === 'Identifier' &&
|
|
162
|
-
node.callee.object.name === 'axios' &&
|
|
163
|
-
node.callee.property.type === 'Identifier';
|
|
164
|
-
|
|
165
|
-
if (!isFetch && !isAxiosMethod) return;
|
|
183
|
+
if (!isFetch && !axiosMethod) return;
|
|
166
184
|
|
|
167
185
|
let urlValue = null;
|
|
168
186
|
let method = 'GET';
|
|
169
187
|
let schemaFields = null;
|
|
170
188
|
|
|
189
|
+
// ---- fetch() ----
|
|
171
190
|
if (isFetch) {
|
|
172
191
|
urlValue = getUrlValue(node.arguments[0]);
|
|
173
192
|
const optionsNode = node.arguments[1];
|
|
174
193
|
|
|
175
194
|
if (optionsNode && optionsNode.type === 'ObjectExpression') {
|
|
176
|
-
const methodProp = optionsNode.properties.find(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
const methodProp = optionsNode.properties.find(
|
|
196
|
+
p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'method'
|
|
197
|
+
);
|
|
198
|
+
if (methodProp && methodProp.value.type === 'StringLiteral') {
|
|
199
|
+
method = methodProp.value.value.toUpperCase();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// body schema for POST/PUT/PATCH
|
|
203
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
204
|
+
const bodyProp = optionsNode.properties.find(
|
|
205
|
+
p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'body'
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (bodyProp) {
|
|
209
|
+
const v = bodyProp.value;
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
v.type === 'CallExpression' &&
|
|
213
|
+
v.callee.type === 'MemberExpression' &&
|
|
214
|
+
v.callee.object.type === 'Identifier' &&
|
|
215
|
+
v.callee.object.name === 'JSON' &&
|
|
216
|
+
v.callee.property.type === 'Identifier' &&
|
|
217
|
+
v.callee.property.name === 'stringify'
|
|
218
|
+
) {
|
|
219
|
+
const arg0 = v.arguments[0];
|
|
220
|
+
|
|
221
|
+
if (arg0?.type === 'ObjectExpression') {
|
|
222
|
+
schemaFields = extractObjectSchema(arg0);
|
|
223
|
+
} else if (arg0?.type === 'Identifier') {
|
|
224
|
+
const init = resolveIdentifierToInit(callPath, arg0.name);
|
|
225
|
+
if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
|
|
226
|
+
}
|
|
196
227
|
}
|
|
197
228
|
}
|
|
198
229
|
}
|
|
199
230
|
}
|
|
200
231
|
}
|
|
201
232
|
|
|
202
|
-
|
|
203
|
-
|
|
233
|
+
// ---- axios-like client ----
|
|
234
|
+
if (axiosMethod) {
|
|
235
|
+
method = axiosMethod;
|
|
204
236
|
urlValue = getUrlValue(node.arguments[0]);
|
|
205
237
|
|
|
206
|
-
// axios.post(url, data)
|
|
207
238
|
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
208
239
|
const dataArg = node.arguments[1];
|
|
209
240
|
if (dataArg?.type === 'ObjectExpression') {
|
|
@@ -215,32 +246,34 @@ async function analyzeFrontend(srcPath) {
|
|
|
215
246
|
}
|
|
216
247
|
}
|
|
217
248
|
|
|
218
|
-
|
|
249
|
+
// accept only URLs that contain /api/
|
|
250
|
+
const apiPath = extractApiPath(urlValue);
|
|
251
|
+
if (!apiPath) return;
|
|
219
252
|
|
|
220
|
-
const route = normalizeRouteForBackend(
|
|
221
|
-
const controllerName = deriveControllerNameFromUrl(
|
|
253
|
+
const route = normalizeRouteForBackend(apiPath.split('?')[0]);
|
|
254
|
+
const controllerName = deriveControllerNameFromUrl(apiPath);
|
|
222
255
|
const actionName = deriveActionName(method, route);
|
|
223
256
|
|
|
224
257
|
const key = `${method}:${route}`;
|
|
225
258
|
if (!endpoints.has(key)) {
|
|
226
259
|
endpoints.set(key, {
|
|
227
|
-
path:
|
|
228
|
-
route,
|
|
260
|
+
path: apiPath,
|
|
261
|
+
route,
|
|
229
262
|
method,
|
|
230
263
|
controllerName,
|
|
231
264
|
actionName,
|
|
232
265
|
pathParams: extractPathParams(route),
|
|
233
|
-
queryParams: extractQueryParamsFromUrl(
|
|
234
|
-
schemaFields,
|
|
266
|
+
queryParams: extractQueryParamsFromUrl(apiPath),
|
|
267
|
+
schemaFields,
|
|
235
268
|
requestBody: schemaFields ? { fields: schemaFields } : null,
|
|
236
269
|
sourceFile: file
|
|
237
270
|
});
|
|
238
271
|
}
|
|
239
|
-
}
|
|
272
|
+
}
|
|
240
273
|
});
|
|
241
274
|
}
|
|
242
275
|
|
|
243
276
|
return Array.from(endpoints.values());
|
|
244
277
|
}
|
|
245
278
|
|
|
246
|
-
module.exports = { analyzeFrontend };
|
|
279
|
+
module.exports = { analyzeFrontend };
|