create-backlist 1.3.3 → 2.0.0

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": "1.3.3",
3
+ "version": "2.0.0",
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,12 +3,24 @@ const { glob } = require('glob');
3
3
  const parser = require('@babel/parser');
4
4
  const traverse = require('@babel/traverse').default;
5
5
 
6
+ /**
7
+ * Converts a string to TitleCase, which is suitable for model and controller names.
8
+ * e.g., 'user-orders' -> 'UserOrders'
9
+ * @param {string} str The input string.
10
+ * @returns {string} The TitleCased string.
11
+ */
6
12
  function toTitleCase(str) {
7
13
  if (!str) return 'Default';
8
- return str.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())
14
+ return str.replace(/-_(\w)/g, g => g[1].toUpperCase()) // handle snake_case and kebab-case
15
+ .replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())
9
16
  .replace(/[^a-zA-Z0-9]/g, '');
10
17
  }
11
18
 
19
+ /**
20
+ * Analyzes frontend source files to find API endpoints and their details.
21
+ * @param {string} srcPath The path to the frontend source directory.
22
+ * @returns {Promise<Array<object>>} A promise that resolves to an array of endpoint objects.
23
+ */
12
24
  async function analyzeFrontend(srcPath) {
13
25
  if (!fs.existsSync(srcPath)) {
14
26
  throw new Error(`The source directory '${srcPath}' does not exist.`);
@@ -21,39 +33,92 @@ async function analyzeFrontend(srcPath) {
21
33
  const code = await fs.readFile(file, 'utf-8');
22
34
  try {
23
35
  const ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
36
+
24
37
  traverse(ast, {
25
38
  CallExpression(path) {
39
+ // We are only interested in 'fetch' calls
26
40
  if (path.node.callee.name !== 'fetch') return;
41
+
27
42
  const urlNode = path.node.arguments[0];
28
43
 
29
44
  let urlValue;
30
45
  if (urlNode.type === 'StringLiteral') {
31
46
  urlValue = urlNode.value;
32
47
  } else if (urlNode.type === 'TemplateLiteral' && urlNode.quasis.length > 0) {
33
- urlValue = urlNode.quasis.map(q => q.value.raw).join('{id}');
48
+ // Reconstruct path for dynamic URLs like `/api/users/${id}` -> `/api/users/{id}`
49
+ urlValue = urlNode.quasis.map((q, i) => {
50
+ return q.value.raw + (urlNode.expressions[i] ? `{${urlNode.expressions[i].name || 'id'}}` : '');
51
+ }).join('');
34
52
  }
35
53
 
54
+ // Only process API calls that start with '/api/'
36
55
  if (!urlValue || !urlValue.startsWith('/api/')) return;
37
56
 
38
57
  let method = 'GET';
58
+ let schemaFields = null;
39
59
 
40
60
  const optionsNode = path.node.arguments[1];
41
61
  if (optionsNode && optionsNode.type === 'ObjectExpression') {
62
+ // Find the HTTP method
42
63
  const methodProp = optionsNode.properties.find(p => p.key.name === 'method');
43
64
  if (methodProp && methodProp.value.type === 'StringLiteral') {
44
65
  method = methodProp.value.value.toUpperCase();
45
66
  }
67
+
68
+ // --- NEW LOGIC: Analyze the 'body' for POST/PUT requests ---
69
+ if (method === 'POST' || method === 'PUT') {
70
+ const bodyProp = optionsNode.properties.find(p => p.key.name === 'body');
71
+
72
+ // Check if body is wrapped in JSON.stringify
73
+ if (bodyProp && bodyProp.value.callee && bodyProp.value.callee.name === 'JSON.stringify') {
74
+ const dataObjectNode = bodyProp.value.arguments[0];
75
+
76
+ // This is a simplified analysis assuming the object is defined inline.
77
+ // A more robust solution would trace variables back to their definition.
78
+ if (dataObjectNode.type === 'ObjectExpression') {
79
+ schemaFields = {};
80
+ dataObjectNode.properties.forEach(prop => {
81
+ const key = prop.key.name;
82
+ const valueNode = prop.value;
83
+
84
+ // Infer Mongoose schema type based on the value's literal type
85
+ if (valueNode.type === 'StringLiteral') {
86
+ schemaFields[key] = 'String';
87
+ } else if (valueNode.type === 'NumericLiteral') {
88
+ schemaFields[key] = 'Number';
89
+ } else if (valueNode.type === 'BooleanLiteral') {
90
+ schemaFields[key] = 'Boolean';
91
+ } else {
92
+ // Default to String if the type is complex or a variable
93
+ schemaFields[key] = 'String';
94
+ }
95
+ });
96
+ }
97
+ }
98
+ }
46
99
  }
47
100
 
101
+ // Generate a clean controller name (e.g., /api/user-orders -> UserOrders)
48
102
  const controllerName = toTitleCase(urlValue.split('/')[2]);
49
103
  const key = `${method}:${urlValue}`;
104
+
105
+ // Avoid adding duplicate endpoints
50
106
  if (!endpoints.has(key)) {
51
- endpoints.set(key, { path: urlValue, method, controllerName });
107
+ endpoints.set(key, {
108
+ path: urlValue,
109
+ method,
110
+ controllerName,
111
+ schemaFields // This will be null for GET/DELETE, and an object for POST/PUT
112
+ });
52
113
  }
53
114
  },
54
115
  });
55
- } catch (e) { /* Ignore parsing errors */ }
116
+ } catch (e) {
117
+ // Ignore files that babel can't parse (e.g., CSS-in-JS files)
118
+ }
56
119
  }
120
+
121
+ // Return all found endpoints as an array
57
122
  return Array.from(endpoints.values());
58
123
  }
59
124
 
@@ -5,81 +5,137 @@ const path = require('path');
5
5
  const { analyzeFrontend } = require('../analyzer');
6
6
  const { renderAndWrite, getTemplatePath } = require('./template');
7
7
 
8
+ /**
9
+ * Generate a Node.js + TypeScript (Express) backend project automatically.
10
+ */
8
11
  async function generateNodeProject(options) {
9
12
  const { projectDir, projectName, frontendSrcDir } = options;
10
13
 
11
14
  try {
12
15
  // --- Step 1: Analyze Frontend ---
13
- console.log(chalk.blue(' -> Analyzing frontend for API endpoints...'));
16
+ console.log(chalk.blue(' -> Analyzing frontend for API endpoints...'));
14
17
  const endpoints = await analyzeFrontend(frontendSrcDir);
18
+
15
19
  if (endpoints.length > 0) {
16
- console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
20
+ console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
17
21
  } else {
18
- console.log(chalk.yellow(' -> No API endpoints found. A basic project will be created.'));
22
+ console.log(
23
+ chalk.yellow(' -> No API endpoints found. A basic project will be created.')
24
+ );
19
25
  }
20
26
 
21
27
  // --- Step 2: Scaffold Base Project ---
22
28
  console.log(chalk.blue(' -> Scaffolding Node.js (Express + TS) project...'));
23
-
24
- // Define paths clearly
29
+
25
30
  const baseDir = getTemplatePath('node-ts-express/base');
26
31
  const serverTemplatePath = path.join(baseDir, 'server.ts');
27
32
  const tsconfigTemplatePath = path.join(baseDir, 'tsconfig.json');
28
-
33
+
29
34
  const destSrcDir = path.join(projectDir, 'src');
30
35
  const serverDestPath = path.join(destSrcDir, 'server.ts');
31
36
  const tsconfigDestPath = path.join(projectDir, 'tsconfig.json');
32
37
 
33
- // Ensure destination directory exists
34
38
  await fs.ensureDir(destSrcDir);
35
-
36
- // Copy base files individually for clarity
37
39
  await fs.copy(serverTemplatePath, serverDestPath);
38
40
  await fs.copy(tsconfigTemplatePath, tsconfigDestPath);
39
-
40
- console.log(chalk.gray(' -> Base server.ts copied.'));
41
-
42
- // --- Step 3: Generate Dynamic Files ---
41
+
42
+ console.log(chalk.gray(' -> Base server.ts and tsconfig.json copied.'));
43
+
44
+ // --- Step 3: Generate package.json and routes.ts ---
43
45
  await renderAndWrite(
44
46
  getTemplatePath('node-ts-express/partials/package.json.ejs'),
45
47
  path.join(projectDir, 'package.json'),
46
48
  { projectName }
47
49
  );
50
+
48
51
  await renderAndWrite(
49
52
  getTemplatePath('node-ts-express/partials/routes.ts.ejs'),
50
53
  path.join(destSrcDir, 'routes.ts'),
51
54
  { endpoints }
52
55
  );
53
-
56
+
54
57
  console.log(chalk.gray(' -> package.json and routes.ts generated.'));
55
58
 
56
- // --- Step 4: Modify the copied server.ts ---
57
- // Check if the file exists before reading
58
- if (!await fs.pathExists(serverDestPath)) {
59
- throw new Error(`Critical error: server.ts was not found at ${serverDestPath} after copy.`);
59
+ // --- Step 4: Analyze endpoints for models/controllers ---
60
+ const modelsToGenerate = new Map();
61
+
62
+ endpoints.forEach((ep) => {
63
+ if (ep.schemaFields && !modelsToGenerate.has(ep.controllerName)) {
64
+ modelsToGenerate.set(ep.controllerName, ep.schemaFields);
65
+ }
66
+ });
67
+
68
+ // --- Step 5: Generate Models and Controllers if applicable ---
69
+ if (modelsToGenerate.size > 0) {
70
+ console.log(chalk.blue(' -> Generating database models and controllers...'));
71
+
72
+ await fs.ensureDir(path.join(projectDir, 'src', 'models'));
73
+ await fs.ensureDir(path.join(projectDir, 'src', 'controllers'));
74
+
75
+ for (const [modelName, schema] of modelsToGenerate.entries()) {
76
+ // Generate Model file
77
+ await renderAndWrite(
78
+ getTemplatePath('node-ts-express/partials/Model.ts.ejs'),
79
+ path.join(projectDir, 'src', 'models', `${modelName}.model.ts`),
80
+ { modelName, schema }
81
+ );
82
+
83
+ // Generate Controller file
84
+ await renderAndWrite(
85
+ getTemplatePath('node-ts-express/partials/Controller.ts.ejs'),
86
+ path.join(projectDir, 'src', 'controllers', `${modelName}.controller.ts`),
87
+ { modelName }
88
+ );
89
+ }
90
+
91
+ console.log(chalk.gray(' -> Models and controllers generated.'));
92
+ }
93
+
94
+ // --- Step 6: Modify server.ts ---
95
+ if (!(await fs.pathExists(serverDestPath))) {
96
+ throw new Error(`Critical error: server.ts was not found at ${serverDestPath}.`);
60
97
  }
61
-
98
+
62
99
  let serverFileContent = await fs.readFile(serverDestPath, 'utf-8');
63
- serverFileContent = serverFileContent.replace('// INJECT:ROUTES', `import apiRoutes from './routes';\napp.use(apiRoutes);`);
100
+ serverFileContent = serverFileContent.replace(
101
+ '// INJECT:ROUTES',
102
+ `import apiRoutes from './routes';\napp.use(apiRoutes);`
103
+ );
64
104
  await fs.writeFile(serverDestPath, serverFileContent);
65
105
 
66
106
  console.log(chalk.gray(' -> server.ts modified successfully.'));
67
107
 
68
- // --- Step 5: Install Dependencies ---
108
+ // --- Step 7: Install dependencies ---
69
109
  console.log(chalk.magenta(' -> Installing dependencies (npm install)...'));
70
110
  await execa('npm', ['install'], { cwd: projectDir });
71
111
 
72
- // --- Step 6: Generate README ---
112
+ // --- Step 8: Add mongoose if models were generated ---
113
+ if (modelsToGenerate.size > 0) {
114
+ console.log(chalk.gray(' -> Adding Mongoose to dependencies...'));
115
+
116
+ const packageJsonPath = path.join(projectDir, 'package.json');
117
+ const packageJson = await fs.readJson(packageJsonPath);
118
+ packageJson.dependencies = packageJson.dependencies || {};
119
+ packageJson.dependencies['mongoose'] = '^7.5.0';
120
+
121
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
122
+
123
+ console.log(chalk.magenta(' -> Installing new dependencies (mongoose)...'));
124
+ await execa('npm', ['install'], { cwd: projectDir });
125
+ }
126
+
127
+ // --- Step 9: Generate README ---
73
128
  await renderAndWrite(
74
129
  getTemplatePath('node-ts-express/partials/README.md.ejs'),
75
130
  path.join(projectDir, 'README.md'),
76
131
  { projectName }
77
132
  );
78
133
 
134
+ console.log(chalk.green('✅ Project generation completed successfully!'));
79
135
  } catch (error) {
80
- // Re-throw the error to be caught by the main CLI handler
81
- throw error;
136
+ console.error(chalk.red('❌ Error generating Node project:'), error);
137
+ throw error; // Pass to main CLI handler
82
138
  }
83
139
  }
84
140
 
85
- module.exports = { generateNodeProject };
141
+ module.exports = { generateNodeProject };
@@ -0,0 +1,57 @@
1
+ // Auto-generated by create-backlist on <%= new Date().toISOString() %>
2
+ import { Request, Response } from 'express';
3
+ import <%= modelName %>, { I<%= modelName %> } from '../models/<%= modelName %>.model';
4
+
5
+ // @desc Create a new <%= modelName %>
6
+ export const create<%= modelName %> = async (req: Request, res: Response) => {
7
+ try {
8
+ const newDoc = new <%= modelName %>(req.body);
9
+ await newDoc.save();
10
+ res.status(201).json(newDoc);
11
+ } catch (error) {
12
+ res.status(500).json({ message: 'Error creating document', error });
13
+ }
14
+ };
15
+
16
+ // @desc Get all <%= modelName %>s
17
+ export const getAll<%= modelName %>s = async (req: Request, res: Response) => {
18
+ try {
19
+ const docs = await <%= modelName %>.find();
20
+ res.status(200).json(docs);
21
+ } catch (error) {
22
+ res.status(500).json({ message: 'Error fetching documents', error });
23
+ }
24
+ };
25
+
26
+ // @desc Get a single <%= modelName %> by ID
27
+ export const get<%= modelName %>ById = async (req: Request, res: Response) => {
28
+ try {
29
+ const doc = await <%= modelName %>.findById(req.params.id);
30
+ if (!doc) return res.status(404).json({ message: 'Document not found' });
31
+ res.status(200).json(doc);
32
+ } catch (error) {
33
+ res.status(500).json({ message: 'Error fetching document', error });
34
+ }
35
+ };
36
+
37
+ // @desc Update a <%= modelName %> by ID
38
+ export const update<%= modelName %>ById = async (req: Request, res: Response) => {
39
+ try {
40
+ const doc = await <%= modelName %>.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
41
+ if (!doc) return res.status(404).json({ message: 'Document not found' });
42
+ res.status(200).json(doc);
43
+ } catch (error) {
44
+ res.status(500).json({ message: 'Error updating document', error });
45
+ }
46
+ };
47
+
48
+ // @desc Delete a <%= modelName %> by ID
49
+ export const delete<%= modelName %>ById = async (req: Request, res: Response) => {
50
+ try {
51
+ const doc = await <%= modelName %>.findByIdAndDelete(req.params.id);
52
+ if (!doc) return res.status(404).json({ message: 'Document not found' });
53
+ res.status(200).json({ message: 'Document deleted successfully' });
54
+ } catch (error) {
55
+ res.status(500).json({ message: 'Error deleting document', error });
56
+ }
57
+ };
@@ -0,0 +1,22 @@
1
+ // Auto-generated by create-backlist on <%= new Date().toISOString() %>
2
+ import mongoose, { Schema, Document } from 'mongoose';
3
+
4
+ // Define the interface for the Document
5
+ export interface I<%= modelName %> extends Document {
6
+ <% Object.keys(schema).forEach(key => { %>
7
+ <%= key %>: <%= schema[key].toLowerCase() %>;
8
+ <% }); %>
9
+ }
10
+
11
+ // Define the Mongoose Schema
12
+ const <%= modelName %>Schema: Schema = new Schema({
13
+ <% Object.keys(schema).forEach(key => { %>
14
+ <%= key %>: {
15
+ type: <%= schema[key] %>,
16
+ // TODO: Add 'required', 'unique', etc. here
17
+ },
18
+ <% }); %>
19
+ }, { timestamps: true });
20
+
21
+ // Create and export the Model
22
+ export default mongoose.model<I<%= modelName %>>('<%= modelName %>', <%= modelName %>Schema);
@@ -1,23 +1,60 @@
1
- // Auto-generated by Backlist on <%= new Date().toISOString() %>
1
+ // Auto-generated by create-backlist on <%= new Date().toISOString() %>
2
2
  import { Router, Request, Response } from 'express';
3
+ <%# Create a unique set of controller names from the endpoints array %>
4
+ <% const controllersToImport = new Set(endpoints.map(ep => ep.controllerName).filter(name => name !== 'Default')); %>
5
+
6
+ // Import all the generated controllers
7
+ <% for (const controller of controllersToImport) { %>
8
+ import * as <%= controller %>Controller from '../controllers/<%= controller %>.controller';
9
+ <% } %>
3
10
 
4
11
  const router = Router();
5
12
 
6
- <% if (endpoints.length === 0) { %>
7
- // No API endpoints were detected in the frontend code. Add your routes here.
8
- <% } else { %>
13
+ <%# Loop through each endpoint found by the analyzer %>
9
14
  <% endpoints.forEach(endpoint => { %>
10
- <%# Convert /api/users/{id} to /users/:id %>
11
- <% const expressPath = endpoint.path.replace('/api', '').replace(/{(\w+)}/g, ':$1'); %>
15
+ <%
16
+ // Convert URL path for Express router (e.g., /api/users/{id} -> /users/:id)
17
+ const expressPath = endpoint.path.replace('/api', '').replace(/{(\w+)}/g, ':$1');
18
+ const controllerName = endpoint.controllerName;
19
+ let handlerFunction;
20
+
21
+ // --- LOGIC TO MAP ENDPOINT TO A CRUD CONTROLLER FUNCTION ---
22
+ // This logic assumes a standard RESTful API structure.
23
+
24
+ if (controllerName !== 'Default') {
25
+ if (endpoint.method === 'POST' && !expressPath.includes(':')) {
26
+ // e.g., POST /users -> create a new user
27
+ handlerFunction = `${controllerName}Controller.create${controllerName}`;
28
+ } else if (endpoint.method === 'GET' && !expressPath.includes(':')) {
29
+ // e.g., GET /users -> get all users
30
+ handlerFunction = `${controllerName}Controller.getAll${controllerName}s`;
31
+ } else if (endpoint.method === 'GET' && expressPath.includes(':')) {
32
+ // e.g., GET /users/:id -> get a single user by ID
33
+ handlerFunction = `${controllerName}Controller.get${controllerName}ById`;
34
+ } else if (endpoint.method === 'PUT' && expressPath.includes(':')) {
35
+ // e.g., PUT /users/:id -> update a user by ID
36
+ handlerFunction = `${controllerName}Controller.update${controllerName}ById`;
37
+ } else if (endpoint.method === 'DELETE' && expressPath.includes(':')) {
38
+ // e.g., DELETE /users/:id -> delete a user by ID
39
+ handlerFunction = `${controllerName}Controller.delete${controllerName}ById`;
40
+ }
41
+ }
42
+
43
+ // If no specific CRUD function matches, or if it's a default/unhandled route,
44
+ // create a simple placeholder function.
45
+ if (!handlerFunction) {
46
+ handlerFunction = `(req: Request, res: Response) => {
47
+ // TODO: Implement logic for this custom endpoint
48
+ res.status(501).json({ message: 'Handler not implemented for <%= endpoint.method %> <%= expressPath %>' });
49
+ }`;
50
+ }
51
+ %>
12
52
  /**
13
- * Handler for <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>
53
+ * Route for <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>
54
+ * Mapped to: <%- handlerFunction.includes('=>') ? 'Inline Handler' : handlerFunction %>
14
55
  */
15
- router.<%= endpoint.method.toLowerCase() %>('<%- expressPath %>', (req: Request, res: Response) => {
16
- console.log(`Request received for: ${req.method} ${req.path}`);
17
- // TODO: Implement your logic here.
18
- res.status(200).json({ message: 'Auto-generated response for <%= endpoint.path %>' });
19
- });
56
+ router.<%= endpoint.method.toLowerCase() %>('<%- expressPath %>', <%- handlerFunction %>);
57
+
20
58
  <% }); %>
21
- <% } %>
22
59
 
23
60
  export default router;