create-backlist 6.0.3 → 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.3",
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,44 +1,134 @@
1
- const fs = require('fs-extra');
2
- const { glob } = require('glob');
3
- const parser = require('@babel/parser');
4
- const traverse = require('@babel/traverse').default;
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
5
 
6
- const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
6
+ const parser = require("@babel/parser");
7
+ const traverse = require("@babel/traverse").default;
7
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
+ // -------------------------
8
91
  function toTitleCase(str) {
9
- if (!str) return 'Default';
92
+ if (!str) return "Default";
10
93
  return String(str)
11
94
  .replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
12
- .replace(/^\w/, c => c.toUpperCase())
13
- .replace(/[^a-zA-Z0-9]/g, '');
95
+ .replace(/^\w/, (c) => c.toUpperCase())
96
+ .replace(/[^a-zA-Z0-9]/g, "");
14
97
  }
15
98
 
16
99
  function normalizeRouteForBackend(urlValue) {
17
- return urlValue.replace(/\{(\w+)\}/g, ':$1');
100
+ return urlValue.replace(/\{(\w+)\}/g, ":$1");
18
101
  }
19
102
 
20
103
  function inferTypeFromNode(node) {
21
- if (!node) return 'String';
104
+ if (!node) return "String";
22
105
  switch (node.type) {
23
- case 'StringLiteral': return 'String';
24
- case 'NumericLiteral': return 'Number';
25
- case 'BooleanLiteral': return 'Boolean';
26
- case 'NullLiteral': return 'String';
27
- 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";
28
116
  }
29
117
  }
30
118
 
31
119
  function extractObjectSchema(objExpr) {
32
120
  const schemaFields = {};
33
- if (!objExpr || objExpr.type !== 'ObjectExpression') return null;
121
+ if (!objExpr || objExpr.type !== "ObjectExpression") return null;
34
122
 
35
123
  for (const prop of objExpr.properties) {
36
- if (prop.type !== 'ObjectProperty') continue;
124
+ if (prop.type !== "ObjectProperty") continue;
37
125
 
38
126
  const key =
39
- prop.key.type === 'Identifier' ? prop.key.name :
40
- prop.key.type === 'StringLiteral' ? prop.key.value :
41
- null;
127
+ prop.key.type === "Identifier"
128
+ ? prop.key.name
129
+ : prop.key.type === "StringLiteral"
130
+ ? prop.key.value
131
+ : null;
42
132
 
43
133
  if (!key) continue;
44
134
  schemaFields[key] = inferTypeFromNode(prop.value);
@@ -46,14 +136,14 @@ function extractObjectSchema(objExpr) {
46
136
  return schemaFields;
47
137
  }
48
138
 
49
- function resolveIdentifierToInit(path, identifierName) {
139
+ function resolveIdentifierToInit(pathObj, identifierName) {
50
140
  try {
51
- const binding = path.scope.getBinding(identifierName);
141
+ const binding = pathObj.scope.getBinding(identifierName);
52
142
  if (!binding) return null;
53
143
  const declPath = binding.path;
54
144
  if (!declPath || !declPath.node) return null;
55
145
 
56
- if (declPath.node.type === 'VariableDeclarator') return declPath.node.init || null;
146
+ if (declPath.node.type === "VariableDeclarator") return declPath.node.init || null;
57
147
  return null;
58
148
  } catch {
59
149
  return null;
@@ -63,17 +153,16 @@ function resolveIdentifierToInit(path, identifierName) {
63
153
  function getUrlValue(urlNode) {
64
154
  if (!urlNode) return null;
65
155
 
66
- if (urlNode.type === 'StringLiteral') return urlNode.value;
156
+ if (urlNode.type === "StringLiteral") return urlNode.value;
67
157
 
68
- if (urlNode.type === 'TemplateLiteral') {
69
- // `/api/users/${id}` -> `/api/users/{id}` or `{param1}`
158
+ if (urlNode.type === "TemplateLiteral") {
70
159
  const quasis = urlNode.quasis || [];
71
160
  const exprs = urlNode.expressions || [];
72
- let out = '';
161
+ let out = "";
73
162
  for (let i = 0; i < quasis.length; i++) {
74
163
  out += quasis[i].value.raw;
75
164
  if (exprs[i]) {
76
- if (exprs[i].type === 'Identifier') out += `{${exprs[i].name}}`;
165
+ if (exprs[i].type === "Identifier") out += `{${exprs[i].name}}`;
77
166
  else out += `{param${i + 1}}`;
78
167
  }
79
168
  }
@@ -84,26 +173,21 @@ function getUrlValue(urlNode) {
84
173
  }
85
174
 
86
175
  function extractApiPath(urlValue) {
87
- // supports:
88
- // - /api/...
89
- // - http://localhost:5000/api/...
90
176
  if (!urlValue) return null;
91
- const idx = urlValue.indexOf('/api/');
177
+ const idx = urlValue.indexOf("/api/");
92
178
  if (idx === -1) return null;
93
- return urlValue.slice(idx); // => /api/...
179
+ return urlValue.slice(idx);
94
180
  }
95
181
 
96
182
  function deriveControllerNameFromUrl(urlValue) {
97
183
  const apiPath = extractApiPath(urlValue) || urlValue;
98
- const parts = String(apiPath).split('/').filter(Boolean); // ["api","v1","products"]
99
- const apiIndex = parts.indexOf('api');
184
+ const parts = String(apiPath).split("/").filter(Boolean);
185
+ const apiIndex = parts.indexOf("api");
100
186
 
101
187
  let seg = null;
102
188
 
103
189
  if (apiIndex >= 0) {
104
190
  seg = parts[apiIndex + 1] || null;
105
-
106
- // skip version segment (v1, v2, v10...)
107
191
  if (seg && /^v\d+$/i.test(seg)) {
108
192
  seg = parts[apiIndex + 2] || seg;
109
193
  }
@@ -115,8 +199,8 @@ function deriveControllerNameFromUrl(urlValue) {
115
199
  }
116
200
 
117
201
  function deriveActionName(method, route) {
118
- const cleaned = String(route).replace(/^\/api\//, '/').replace(/[/:{}-]/g, ' ');
119
- const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || 'Action';
202
+ const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
203
+ const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
120
204
  return `${String(method).toLowerCase()}${toTitleCase(last)}`;
121
205
  }
122
206
 
@@ -130,21 +214,23 @@ function extractPathParams(route) {
130
214
 
131
215
  function extractQueryParamsFromUrl(urlValue) {
132
216
  try {
133
- const qIndex = urlValue.indexOf('?');
217
+ const qIndex = urlValue.indexOf("?");
134
218
  if (qIndex === -1) return [];
135
219
  const qs = urlValue.slice(qIndex + 1);
136
- return qs.split('&').map(p => p.split('=')[0]).filter(Boolean);
220
+ return qs
221
+ .split("&")
222
+ .map((p) => p.split("=")[0])
223
+ .filter(Boolean);
137
224
  } catch {
138
225
  return [];
139
226
  }
140
227
  }
141
228
 
142
229
  function detectAxiosLikeMethod(node) {
143
- // axios.get(...) / api.get(...) / httpClient.post(...) etc
144
- if (!node.callee || node.callee.type !== 'MemberExpression') return null;
230
+ if (!node.callee || node.callee.type !== "MemberExpression") return null;
145
231
 
146
232
  const prop = node.callee.property;
147
- if (!prop || prop.type !== 'Identifier') return null;
233
+ if (!prop || prop.type !== "Identifier") return null;
148
234
 
149
235
  const name = prop.name.toLowerCase();
150
236
  if (!HTTP_METHODS.has(name)) return null;
@@ -153,22 +239,28 @@ function detectAxiosLikeMethod(node) {
153
239
  }
154
240
 
155
241
  async function analyzeFrontend(srcPath) {
242
+ if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
156
243
  if (!fs.existsSync(srcPath)) {
157
244
  throw new Error(`The source directory '${srcPath}' does not exist.`);
158
245
  }
159
246
 
160
- const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, {
161
- ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**']
247
+ const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
248
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
162
249
  });
163
250
 
164
251
  const endpoints = new Map();
165
252
 
166
253
  for (const file of files) {
167
- 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
+ }
168
260
 
169
261
  let ast;
170
262
  try {
171
- ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
263
+ ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
172
264
  } catch {
173
265
  continue;
174
266
  }
@@ -177,52 +269,56 @@ async function analyzeFrontend(srcPath) {
177
269
  CallExpression(callPath) {
178
270
  const node = callPath.node;
179
271
 
180
- const isFetch = node.callee.type === 'Identifier' && node.callee.name === 'fetch';
272
+ const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
181
273
  const axiosMethod = detectAxiosLikeMethod(node);
182
274
 
183
275
  if (!isFetch && !axiosMethod) return;
184
276
 
185
277
  let urlValue = null;
186
- let method = 'GET';
278
+ let method = "GET";
187
279
  let schemaFields = null;
188
280
 
189
- // ---- fetch() ----
190
281
  if (isFetch) {
191
282
  urlValue = getUrlValue(node.arguments[0]);
192
283
  const optionsNode = node.arguments[1];
193
284
 
194
- if (optionsNode && optionsNode.type === 'ObjectExpression') {
285
+ if (optionsNode && optionsNode.type === "ObjectExpression") {
195
286
  const methodProp = optionsNode.properties.find(
196
- p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'method'
287
+ (p) =>
288
+ p.type === "ObjectProperty" &&
289
+ p.key.type === "Identifier" &&
290
+ p.key.name === "method"
197
291
  );
198
- if (methodProp && methodProp.value.type === 'StringLiteral') {
292
+ if (methodProp && methodProp.value.type === "StringLiteral") {
199
293
  method = methodProp.value.value.toUpperCase();
200
294
  }
201
295
 
202
- // body schema for POST/PUT/PATCH
203
- if (['POST', 'PUT', 'PATCH'].includes(method)) {
296
+ if (["POST", "PUT", "PATCH"].includes(method)) {
204
297
  const bodyProp = optionsNode.properties.find(
205
- p => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'body'
298
+ (p) =>
299
+ p.type === "ObjectProperty" &&
300
+ p.key.type === "Identifier" &&
301
+ p.key.name === "body"
206
302
  );
207
303
 
208
304
  if (bodyProp) {
209
305
  const v = bodyProp.value;
210
306
 
211
307
  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'
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"
218
314
  ) {
219
315
  const arg0 = v.arguments[0];
220
316
 
221
- if (arg0?.type === 'ObjectExpression') {
317
+ if (arg0?.type === "ObjectExpression") {
222
318
  schemaFields = extractObjectSchema(arg0);
223
- } else if (arg0?.type === 'Identifier') {
319
+ } else if (arg0?.type === "Identifier") {
224
320
  const init = resolveIdentifierToInit(callPath, arg0.name);
225
- if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
321
+ if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
226
322
  }
227
323
  }
228
324
  }
@@ -230,27 +326,25 @@ async function analyzeFrontend(srcPath) {
230
326
  }
231
327
  }
232
328
 
233
- // ---- axios-like client ----
234
329
  if (axiosMethod) {
235
330
  method = axiosMethod;
236
331
  urlValue = getUrlValue(node.arguments[0]);
237
332
 
238
- if (['POST', 'PUT', 'PATCH'].includes(method)) {
333
+ if (["POST", "PUT", "PATCH"].includes(method)) {
239
334
  const dataArg = node.arguments[1];
240
- if (dataArg?.type === 'ObjectExpression') {
335
+ if (dataArg?.type === "ObjectExpression") {
241
336
  schemaFields = extractObjectSchema(dataArg);
242
- } else if (dataArg?.type === 'Identifier') {
337
+ } else if (dataArg?.type === "Identifier") {
243
338
  const init = resolveIdentifierToInit(callPath, dataArg.name);
244
- if (init?.type === 'ObjectExpression') schemaFields = extractObjectSchema(init);
339
+ if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
245
340
  }
246
341
  }
247
342
  }
248
343
 
249
- // accept only URLs that contain /api/
250
344
  const apiPath = extractApiPath(urlValue);
251
345
  if (!apiPath) return;
252
346
 
253
- const route = normalizeRouteForBackend(apiPath.split('?')[0]);
347
+ const route = normalizeRouteForBackend(apiPath.split("?")[0]);
254
348
  const controllerName = deriveControllerNameFromUrl(apiPath);
255
349
  const actionName = deriveActionName(method, route);
256
350
 
@@ -266,14 +360,31 @@ async function analyzeFrontend(srcPath) {
266
360
  queryParams: extractQueryParamsFromUrl(apiPath),
267
361
  schemaFields,
268
362
  requestBody: schemaFields ? { fields: schemaFields } : null,
269
- sourceFile: file
363
+ sourceFile: normalizeSlashes(file),
270
364
  });
271
365
  }
272
- }
366
+ },
273
367
  });
274
368
  }
275
369
 
276
370
  return Array.from(endpoints.values());
277
371
  }
278
372
 
279
- 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);