create-backlist 6.0.2 → 6.0.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "6.0.2",
3
+ "version": "6.0.4",
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
@@ -1,68 +1,149 @@
1
- const fs = require('fs-extra');
2
- const { glob } = require('glob');
3
- const parser = require('@babel/parser');
4
- const traverse = require('@babel/traverse').default;
5
-
6
- /**
7
- * Convert segment -> ControllerName (Users -> Users, user-orders -> UserOrders)
8
- */
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
+ // Small utils
13
+ // -------------------------
14
+ function normalizeSlashes(p) {
15
+ return String(p || "").replace(/\\/g, "/");
16
+ }
17
+
18
+ function readJSONSafe(p) {
19
+ try {
20
+ return fs.readJsonSync(p);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ // -------------------------
27
+ // AUTH detection (for addAuth)
28
+ // -------------------------
29
+ function findAuthUsageInRepo(rootDir) {
30
+ // 1) package.json quick check
31
+ const pkgPath = path.join(rootDir, "package.json");
32
+ const pkg = readJSONSafe(pkgPath) || {};
33
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
34
+
35
+ const authDeps = [
36
+ "next-auth",
37
+ "@auth/core",
38
+ "@clerk/nextjs",
39
+ "@supabase/auth-helpers-nextjs",
40
+ "@supabase/supabase-js",
41
+ "firebase",
42
+ "lucia",
43
+ ];
44
+
45
+ if (authDeps.some((d) => deps[d])) return true;
46
+
47
+ // 2) Source scan for common auth identifiers
48
+ const scanDirs = ["src", "app", "pages", "components", "lib", "utils"]
49
+ .map((d) => path.join(rootDir, d))
50
+ .filter((d) => fs.existsSync(d));
51
+
52
+ const patterns = [
53
+ "next-auth",
54
+ "getServerSession",
55
+ "useSession",
56
+ "SessionProvider",
57
+ "@clerk/nextjs",
58
+ "ClerkProvider",
59
+ "auth()",
60
+ "currentUser",
61
+ "createServerClient",
62
+ ];
63
+
64
+ for (const dir of scanDirs) {
65
+ const files = glob.sync(`${normalizeSlashes(dir)}/**/*.{js,ts,jsx,tsx}`, {
66
+ ignore: [
67
+ "**/node_modules/**",
68
+ "**/dist/**",
69
+ "**/build/**",
70
+ "**/.next/**",
71
+ "**/coverage/**",
72
+ ],
73
+ });
74
+
75
+ for (const f of files) {
76
+ try {
77
+ const content = fs.readFileSync(f, "utf8");
78
+ if (patterns.some((p) => content.includes(p))) return true;
79
+ } catch {
80
+ // ignore read errors
81
+ }
82
+ }
83
+ }
84
+
85
+ return false;
86
+ }
87
+
88
+ // -------------------------
89
+ // Frontend API call analyzer (your code)
90
+ // -------------------------
9
91
  function toTitleCase(str) {
10
- if (!str) return 'Default';
92
+ if (!str) return "Default";
11
93
  return String(str)
12
- .replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase()) // kebab/snake to camel
13
- .replace(/^\w/, c => c.toUpperCase())
14
- .replace(/[^a-zA-Z0-9]/g, '');
94
+ .replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
95
+ .replace(/^\w/, (c) => c.toUpperCase())
96
+ .replace(/[^a-zA-Z0-9]/g, "");
15
97
  }
16
98
 
17
99
  function normalizeRouteForBackend(urlValue) {
18
- // Convert template placeholders {id} -> :id
19
- return urlValue.replace(/\{(\w+)\}/g, ':$1');
100
+ return urlValue.replace(/\{(\w+)\}/g, ":$1");
20
101
  }
21
102
 
22
103
  function inferTypeFromNode(node) {
23
- if (!node) return 'String';
104
+ if (!node) return "String";
24
105
  switch (node.type) {
25
- case 'StringLiteral': return 'String';
26
- case 'NumericLiteral': return 'Number';
27
- case 'BooleanLiteral': return 'Boolean';
28
- case 'NullLiteral': return 'String';
29
- default: return 'String';
106
+ case "StringLiteral":
107
+ return "String";
108
+ case "NumericLiteral":
109
+ return "Number";
110
+ case "BooleanLiteral":
111
+ return "Boolean";
112
+ case "NullLiteral":
113
+ return "String";
114
+ default:
115
+ return "String";
30
116
  }
31
117
  }
32
118
 
33
119
  function extractObjectSchema(objExpr) {
34
120
  const schemaFields = {};
35
- if (!objExpr || objExpr.type !== 'ObjectExpression') return null;
121
+ if (!objExpr || objExpr.type !== "ObjectExpression") return null;
36
122
 
37
123
  for (const prop of objExpr.properties) {
38
- if (prop.type !== 'ObjectProperty') continue;
124
+ if (prop.type !== "ObjectProperty") continue;
39
125
 
40
126
  const key =
41
- prop.key.type === 'Identifier' ? prop.key.name :
42
- prop.key.type === 'StringLiteral' ? prop.key.value :
43
- null;
127
+ prop.key.type === "Identifier"
128
+ ? prop.key.name
129
+ : prop.key.type === "StringLiteral"
130
+ ? prop.key.value
131
+ : null;
44
132
 
45
133
  if (!key) continue;
46
-
47
134
  schemaFields[key] = inferTypeFromNode(prop.value);
48
135
  }
49
136
  return schemaFields;
50
137
  }
51
138
 
52
- /**
53
- * Try to resolve Identifier -> its init value if it's const payload = {...}
54
- */
55
- function resolveIdentifierToInit(path, identifierName) {
139
+ function resolveIdentifierToInit(pathObj, identifierName) {
56
140
  try {
57
- const binding = path.scope.getBinding(identifierName);
141
+ const binding = pathObj.scope.getBinding(identifierName);
58
142
  if (!binding) return null;
59
- const declPath = binding.path; // VariableDeclarator path usually
143
+ const declPath = binding.path;
60
144
  if (!declPath || !declPath.node) return null;
61
145
 
62
- // VariableDeclarator: id = init
63
- if (declPath.node.type === 'VariableDeclarator') {
64
- return declPath.node.init || null;
65
- }
146
+ if (declPath.node.type === "VariableDeclarator") return declPath.node.init || null;
66
147
  return null;
67
148
  } catch {
68
149
  return null;
@@ -72,18 +153,17 @@ function resolveIdentifierToInit(path, identifierName) {
72
153
  function getUrlValue(urlNode) {
73
154
  if (!urlNode) return null;
74
155
 
75
- if (urlNode.type === 'StringLiteral') return urlNode.value;
156
+ if (urlNode.type === "StringLiteral") return urlNode.value;
76
157
 
77
- if (urlNode.type === 'TemplateLiteral') {
78
- // `/api/users/${id}` -> `/api/users/{id}`
158
+ if (urlNode.type === "TemplateLiteral") {
79
159
  const quasis = urlNode.quasis || [];
80
160
  const exprs = urlNode.expressions || [];
81
- let out = '';
161
+ let out = "";
82
162
  for (let i = 0; i < quasis.length; i++) {
83
163
  out += quasis[i].value.raw;
84
164
  if (exprs[i]) {
85
- if (exprs[i].type === 'Identifier') out += `{${exprs[i].name}}`;
86
- else out += `{param}`;
165
+ if (exprs[i].type === "Identifier") out += `{${exprs[i].name}}`;
166
+ else out += `{param${i + 1}}`;
87
167
  }
88
168
  }
89
169
  return out;
@@ -92,58 +172,95 @@ function getUrlValue(urlNode) {
92
172
  return null;
93
173
  }
94
174
 
175
+ function extractApiPath(urlValue) {
176
+ if (!urlValue) return null;
177
+ const idx = urlValue.indexOf("/api/");
178
+ if (idx === -1) return null;
179
+ return urlValue.slice(idx);
180
+ }
181
+
95
182
  function deriveControllerNameFromUrl(urlValue) {
96
- // supports: /api/users, /api/v1/users, /api/admin/users
97
- const parts = urlValue.split('/').filter(Boolean); // ["api","v1","users"]
98
- const apiIndex = parts.indexOf('api');
99
- const seg = (apiIndex >= 0 && parts.length > apiIndex + 1)
100
- ? parts[apiIndex + 1]
101
- : parts[0];
183
+ const apiPath = extractApiPath(urlValue) || urlValue;
184
+ const parts = String(apiPath).split("/").filter(Boolean);
185
+ const apiIndex = parts.indexOf("api");
186
+
187
+ let seg = null;
188
+
189
+ if (apiIndex >= 0) {
190
+ seg = parts[apiIndex + 1] || null;
191
+ if (seg && /^v\d+$/i.test(seg)) {
192
+ seg = parts[apiIndex + 2] || seg;
193
+ }
194
+ } else {
195
+ seg = parts[0] || null;
196
+ }
102
197
 
103
198
  return toTitleCase(seg);
104
199
  }
105
200
 
106
201
  function deriveActionName(method, route) {
107
- // method + last segment heuristic
108
- const cleaned = route.replace(/^\/api\//, '/').replace(/[/:{}-]/g, ' ');
109
- const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || 'Action';
110
- return `${method.toLowerCase()}${toTitleCase(last)}`;
202
+ const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
203
+ const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
204
+ return `${String(method).toLowerCase()}${toTitleCase(last)}`;
111
205
  }
112
206
 
113
207
  function extractPathParams(route) {
114
208
  const params = [];
115
- const re = /[:{]([a-zA-Z0-9_]+)[}]/g; // matches :id or {id}
209
+ const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
116
210
  let m;
117
211
  while ((m = re.exec(route))) params.push(m[1]);
118
212
  return Array.from(new Set(params));
119
213
  }
120
214
 
121
215
  function extractQueryParamsFromUrl(urlValue) {
122
- // if url has ?a=b&c=d as string literal
123
216
  try {
124
- const qIndex = urlValue.indexOf('?');
217
+ const qIndex = urlValue.indexOf("?");
125
218
  if (qIndex === -1) return [];
126
219
  const qs = urlValue.slice(qIndex + 1);
127
- return qs.split('&').map(p => p.split('=')[0]).filter(Boolean);
220
+ return qs
221
+ .split("&")
222
+ .map((p) => p.split("=")[0])
223
+ .filter(Boolean);
128
224
  } catch {
129
225
  return [];
130
226
  }
131
227
  }
132
228
 
229
+ function detectAxiosLikeMethod(node) {
230
+ if (!node.callee || node.callee.type !== "MemberExpression") return null;
231
+
232
+ const prop = node.callee.property;
233
+ if (!prop || prop.type !== "Identifier") return null;
234
+
235
+ const name = prop.name.toLowerCase();
236
+ if (!HTTP_METHODS.has(name)) return null;
237
+
238
+ return name.toUpperCase();
239
+ }
240
+
133
241
  async function analyzeFrontend(srcPath) {
242
+ if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
134
243
  if (!fs.existsSync(srcPath)) {
135
244
  throw new Error(`The source directory '${srcPath}' does not exist.`);
136
245
  }
137
246
 
138
- const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, { ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] });
247
+ const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
248
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
249
+ });
250
+
139
251
  const endpoints = new Map();
140
252
 
141
253
  for (const file of files) {
142
- const code = await fs.readFile(file, 'utf-8');
254
+ let code;
255
+ try {
256
+ code = await fs.readFile(file, "utf-8");
257
+ } catch {
258
+ continue;
259
+ }
143
260
 
144
261
  let ast;
145
262
  try {
146
- ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
263
+ ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
147
264
  } catch {
148
265
  continue;
149
266
  }
@@ -152,88 +269,98 @@ async function analyzeFrontend(srcPath) {
152
269
  CallExpression(callPath) {
153
270
  const node = callPath.node;
154
271
 
155
- // --- Detect fetch(url, options) ---
156
- const isFetch = node.callee.type === 'Identifier' && node.callee.name === 'fetch';
157
-
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';
272
+ const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
273
+ const axiosMethod = detectAxiosLikeMethod(node);
164
274
 
165
- if (!isFetch && !isAxiosMethod) return;
275
+ if (!isFetch && !axiosMethod) return;
166
276
 
167
277
  let urlValue = null;
168
- let method = 'GET';
278
+ let method = "GET";
169
279
  let schemaFields = null;
170
280
 
171
281
  if (isFetch) {
172
282
  urlValue = getUrlValue(node.arguments[0]);
173
283
  const optionsNode = node.arguments[1];
174
284
 
175
- 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];
285
+ if (optionsNode && optionsNode.type === "ObjectExpression") {
286
+ const methodProp = optionsNode.properties.find(
287
+ (p) =>
288
+ p.type === "ObjectProperty" &&
289
+ p.key.type === "Identifier" &&
290
+ p.key.name === "method"
291
+ );
292
+ if (methodProp && methodProp.value.type === "StringLiteral") {
293
+ method = methodProp.value.value.toUpperCase();
294
+ }
190
295
 
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);
296
+ if (["POST", "PUT", "PATCH"].includes(method)) {
297
+ const bodyProp = optionsNode.properties.find(
298
+ (p) =>
299
+ p.type === "ObjectProperty" &&
300
+ p.key.type === "Identifier" &&
301
+ p.key.name === "body"
302
+ );
303
+
304
+ if (bodyProp) {
305
+ const v = bodyProp.value;
306
+
307
+ if (
308
+ v.type === "CallExpression" &&
309
+ v.callee.type === "MemberExpression" &&
310
+ v.callee.object.type === "Identifier" &&
311
+ v.callee.object.name === "JSON" &&
312
+ v.callee.property.type === "Identifier" &&
313
+ v.callee.property.name === "stringify"
314
+ ) {
315
+ const arg0 = v.arguments[0];
316
+
317
+ if (arg0?.type === "ObjectExpression") {
318
+ schemaFields = extractObjectSchema(arg0);
319
+ } else if (arg0?.type === "Identifier") {
320
+ const init = resolveIdentifierToInit(callPath, arg0.name);
321
+ if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
322
+ }
196
323
  }
197
324
  }
198
325
  }
199
326
  }
200
327
  }
201
328
 
202
- if (isAxiosMethod) {
203
- method = node.callee.property.name.toUpperCase();
329
+ if (axiosMethod) {
330
+ method = axiosMethod;
204
331
  urlValue = getUrlValue(node.arguments[0]);
205
332
 
206
- // axios.post(url, data)
207
- if (['POST', 'PUT', 'PATCH'].includes(method)) {
333
+ if (["POST", "PUT", "PATCH"].includes(method)) {
208
334
  const dataArg = node.arguments[1];
209
- if (dataArg?.type === 'ObjectExpression') {
335
+ if (dataArg?.type === "ObjectExpression") {
210
336
  schemaFields = extractObjectSchema(dataArg);
211
- } else if (dataArg?.type === 'Identifier') {
337
+ } else if (dataArg?.type === "Identifier") {
212
338
  const init = resolveIdentifierToInit(callPath, dataArg.name);
213
- if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
339
+ if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
214
340
  }
215
341
  }
216
342
  }
217
343
 
218
- if (!urlValue || !urlValue.startsWith('/api/')) return;
344
+ const apiPath = extractApiPath(urlValue);
345
+ if (!apiPath) return;
219
346
 
220
- const route = normalizeRouteForBackend(urlValue.split('?')[0]); // drop query string
221
- const controllerName = deriveControllerNameFromUrl(urlValue);
347
+ const route = normalizeRouteForBackend(apiPath.split("?")[0]);
348
+ const controllerName = deriveControllerNameFromUrl(apiPath);
222
349
  const actionName = deriveActionName(method, route);
223
350
 
224
351
  const key = `${method}:${route}`;
225
352
  if (!endpoints.has(key)) {
226
353
  endpoints.set(key, {
227
- path: urlValue,
228
- route, // normalized for backend: /api/users/:id
354
+ path: apiPath,
355
+ route,
229
356
  method,
230
357
  controllerName,
231
358
  actionName,
232
359
  pathParams: extractPathParams(route),
233
- queryParams: extractQueryParamsFromUrl(urlValue),
234
- schemaFields, // backward compat (your generators use this)
360
+ queryParams: extractQueryParamsFromUrl(apiPath),
361
+ schemaFields,
235
362
  requestBody: schemaFields ? { fields: schemaFields } : null,
236
- sourceFile: file
363
+ sourceFile: normalizeSlashes(file),
237
364
  });
238
365
  }
239
366
  },
@@ -243,4 +370,21 @@ async function analyzeFrontend(srcPath) {
243
370
  return Array.from(endpoints.values());
244
371
  }
245
372
 
246
- module.exports = { analyzeFrontend };
373
+ // -------------------------
374
+ // Main analyze() exported for CLI
375
+ // -------------------------
376
+ function analyze(projectRoot = process.cwd()) {
377
+ const rootDir = path.resolve(projectRoot);
378
+
379
+ return {
380
+ rootDir: normalizeSlashes(rootDir),
381
+ hasAuth: findAuthUsageInRepo(rootDir),
382
+ // If CLI expects addAuth directly, keep both:
383
+ addAuth: findAuthUsageInRepo(rootDir),
384
+ };
385
+ }
386
+
387
+ module.exports = {
388
+ analyze,
389
+ analyzeFrontend,
390
+ };
@@ -2,7 +2,8 @@
2
2
  import request from 'supertest';
3
3
  import express from 'express';
4
4
 
5
- import apiRoutes from '../routes';
5
+ import apiRoutes from '../routes';
6
+
6
7
  <% if (addAuth) { -%>
7
8
  import authRoutes from '../routes/Auth.routes';
8
9
  <% } -%>
@@ -13,6 +14,7 @@ app.use(express.json());
13
14
  <% if (addAuth) { -%>
14
15
  app.use('/api/auth', authRoutes);
15
16
  <% } -%>
17
+
16
18
  app.use('/api', apiRoutes);
17
19
 
18
20
  describe('API Endpoints (Generated)', () => {
@@ -22,14 +24,35 @@ describe('API Endpoints (Generated)', () => {
22
24
 
23
25
  <% endpoints
24
26
  .filter(ep => ep && ep.route && ep.method)
25
- .forEach(ep => {
26
- const method = ep.method.toLowerCase();
27
- const url = (ep.route.startsWith('/api/') ? ep.route.replace(/^\/api/, '') : ep.route)
28
- .replace(/:\w+/g, '1'); // replace path params with dummy value
27
+ .forEach(ep => {
28
+ const method = String(ep.method).toLowerCase();
29
+
30
+ // ep.route might be "/api/..." or "/..."
31
+ const url = (String(ep.route).startsWith('/api/')
32
+ ? String(ep.route).replace(/^\/api/, '')
33
+ : String(ep.route)
34
+ )
35
+ // replace ":id" style params with dummy value
36
+ .replace(/:\w+/g, '1');
29
37
  -%>
30
38
  it('<%= ep.method %> <%= url %> should respond', async () => {
31
- const res = await request(app).<%= method %>('<%= '/api' + url %>')<% if (['post','put','patch'].includes(method) && ep.schemaFields) { %>
32
- .send(<%- JSON.stringify(Object.fromEntries(Object.entries(ep.schemaFields).map(([k,t]) => [k, t === 'Number' ? 1 : (t === 'Boolean' ? true : 'test') ]))) %>)<% } %>;
39
+ const req = request(app).<%= method %>('<%= '/api' + url %>');
40
+
41
+ <% if (['post','put','patch'].includes(method) && ep.schemaFields) { -%>
42
+ req.send(
43
+ <%- JSON.stringify(
44
+ Object.fromEntries(
45
+ Object.entries(ep.schemaFields).map(([k, t]) => [
46
+ k,
47
+ t === 'Number' ? 1 : (t === 'Boolean' ? true : 'test')
48
+ ])
49
+ )
50
+ ) %>
51
+ );
52
+ <% } -%>
53
+
54
+ const res = await req;
55
+
33
56
  // We only assert "not 404" because generated handlers may be TODO stubs,
34
57
  // and auth/validation may affect exact status codes.
35
58
  expect(res.statusCode).not.toBe(404);