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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/analyzer.js +101 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "6.0.2",
3
+ "version": "6.0.3",
4
4
  "description": "An advanced, multi-language backend generator based on frontend analysis.",
5
5
  "type": "commonjs",
6
6
  "bin": {
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
- * Convert segment -> ControllerName (Users -> Users, user-orders -> UserOrders)
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()) // kebab/snake to camel
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; // VariableDeclarator path usually
53
+ const declPath = binding.path;
60
54
  if (!declPath || !declPath.node) return null;
61
55
 
62
- // VariableDeclarator: id = init
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
- // supports: /api/users, /api/v1/users, /api/admin/users
97
- const parts = urlValue.split('/').filter(Boolean); // ["api","v1","users"]
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
- const seg = (apiIndex >= 0 && parts.length > apiIndex + 1)
100
- ? parts[apiIndex + 1]
101
- : parts[0];
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
- // method + last segment heuristic
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; // matches :id or {id}
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}`, { ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] });
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
- // --- Detect axios.<method>(url, data?, config?) ---
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(p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'method');
177
- if (methodProp && methodProp.value.type === 'StringLiteral') method = methodProp.value.value.toUpperCase();
178
-
179
- const bodyProp = optionsNode.properties.find(p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'body');
180
-
181
- if (bodyProp) {
182
- // body: JSON.stringify(objOrVar)
183
- const v = bodyProp.value;
184
-
185
- if (v.type === 'CallExpression' && v.callee.type === 'MemberExpression' &&
186
- v.callee.object.type === 'Identifier' && v.callee.object.name === 'JSON' &&
187
- v.callee.property.type === 'Identifier' && v.callee.property.name === 'stringify'
188
- ) {
189
- const arg0 = v.arguments[0];
190
-
191
- if (arg0?.type === 'ObjectExpression') {
192
- schemaFields = extractObjectSchema(arg0);
193
- } else if (arg0?.type === 'Identifier') {
194
- const init = resolveIdentifierToInit(callPath, arg0.name);
195
- if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
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
- if (isAxiosMethod) {
203
- method = node.callee.property.name.toUpperCase();
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
- if (!urlValue || !urlValue.startsWith('/api/')) return;
249
+ // accept only URLs that contain /api/
250
+ const apiPath = extractApiPath(urlValue);
251
+ if (!apiPath) return;
219
252
 
220
- const route = normalizeRouteForBackend(urlValue.split('?')[0]); // drop query string
221
- const controllerName = deriveControllerNameFromUrl(urlValue);
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: urlValue,
228
- route, // normalized for backend: /api/users/:id
260
+ path: apiPath,
261
+ route,
229
262
  method,
230
263
  controllerName,
231
264
  actionName,
232
265
  pathParams: extractPathParams(route),
233
- queryParams: extractQueryParamsFromUrl(urlValue),
234
- schemaFields, // backward compat (your generators use this)
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 };