create-backlist 1.3.2 → 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,11 +1,15 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "1.3.2",
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": {
7
7
  "create-backlist": "bin/index.js"
8
8
  },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
9
13
  "scripts": {
10
14
  "start": "node bin/index.js"
11
15
  },
@@ -21,4 +25,4 @@
21
25
  "glob": "^10.3.3",
22
26
  "inquirer": "^8.2.4"
23
27
  }
24
- }
28
+ }
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,58 +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
- // --- Step 1: Analyze Frontend ---
12
- console.log(chalk.blue(' -> Analyzing frontend for API endpoints...'));
13
- const endpoints = await analyzeFrontend(frontendSrcDir);
14
- if (endpoints.length > 0) {
15
- console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
16
- } else {
17
- console.log(chalk.yellow(' -> No API endpoints found. A basic project will be created.'));
18
- }
14
+ try {
15
+ // --- Step 1: Analyze Frontend ---
16
+ console.log(chalk.blue(' -> Analyzing frontend for API endpoints...'));
17
+ const endpoints = await analyzeFrontend(frontendSrcDir);
18
+
19
+ if (endpoints.length > 0) {
20
+ console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
21
+ } else {
22
+ console.log(
23
+ chalk.yellow(' -> No API endpoints found. A basic project will be created.')
24
+ );
25
+ }
26
+
27
+ // --- Step 2: Scaffold Base Project ---
28
+ console.log(chalk.blue(' -> Scaffolding Node.js (Express + TS) project...'));
29
+
30
+ const baseDir = getTemplatePath('node-ts-express/base');
31
+ const serverTemplatePath = path.join(baseDir, 'server.ts');
32
+ const tsconfigTemplatePath = path.join(baseDir, 'tsconfig.json');
33
+
34
+ const destSrcDir = path.join(projectDir, 'src');
35
+ const serverDestPath = path.join(destSrcDir, 'server.ts');
36
+ const tsconfigDestPath = path.join(projectDir, 'tsconfig.json');
37
+
38
+ await fs.ensureDir(destSrcDir);
39
+ await fs.copy(serverTemplatePath, serverDestPath);
40
+ await fs.copy(tsconfigTemplatePath, tsconfigDestPath);
41
+
42
+ console.log(chalk.gray(' -> Base server.ts and tsconfig.json copied.'));
43
+
44
+ // --- Step 3: Generate package.json and routes.ts ---
45
+ await renderAndWrite(
46
+ getTemplatePath('node-ts-express/partials/package.json.ejs'),
47
+ path.join(projectDir, 'package.json'),
48
+ { projectName }
49
+ );
50
+
51
+ await renderAndWrite(
52
+ getTemplatePath('node-ts-express/partials/routes.ts.ejs'),
53
+ path.join(destSrcDir, 'routes.ts'),
54
+ { endpoints }
55
+ );
56
+
57
+ console.log(chalk.gray(' -> package.json and routes.ts generated.'));
58
+
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'));
19
74
 
20
- // --- Step 2: Scaffold Base Project ---
21
- console.log(chalk.blue(' -> Scaffolding Node.js (Express + TS) project...'));
22
-
23
- // Copy the base template directory which includes `src/server.ts`
24
- const baseDir = getTemplatePath('node-ts-express/base');
25
- await fs.copy(baseDir, projectDir);
26
-
27
- // --- Step 3: Generate Dynamic Files from Templates ---
28
-
29
- // Generate package.json
30
- await renderAndWrite(
31
- getTemplatePath('node-ts-express/partials/package.json.ejs'),
32
- path.join(projectDir, 'package.json'),
33
- { projectName }
34
- );
35
-
36
- // Generate routes.ts based on analyzed endpoints
37
- await renderAndWrite(
38
- getTemplatePath('node-ts-express/partials/routes.ts.ejs'),
39
- path.join(projectDir, 'src', 'routes.ts'),
40
- { endpoints }
41
- );
42
-
43
- // --- Step 4: Modify the copied server.ts to inject routes ---
44
- // THIS IS THE FIX: We do this AFTER the base files are copied.
45
- const serverFilePath = path.join(projectDir, 'src', 'server.ts');
46
- let serverFileContent = await fs.readFile(serverFilePath, 'utf-8');
47
- serverFileContent = serverFileContent.replace('// INJECT:ROUTES', `import apiRoutes from './routes';\napp.use(apiRoutes);`);
48
- await fs.writeFile(serverFilePath, serverFileContent);
49
-
50
- // --- Step 5: Install Dependencies ---
51
- console.log(chalk.magenta(' -> Installing dependencies (npm install)...'));
52
- await execa('npm', ['install'], { cwd: projectDir });
53
-
54
- // --- Step 6: Generate README ---
55
- await renderAndWrite(
56
- getTemplatePath('node-ts-express/partials/README.md.ejs'),
57
- path.join(projectDir, 'README.md'),
58
- { projectName }
59
- );
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}.`);
97
+ }
98
+
99
+ let serverFileContent = await fs.readFile(serverDestPath, 'utf-8');
100
+ serverFileContent = serverFileContent.replace(
101
+ '// INJECT:ROUTES',
102
+ `import apiRoutes from './routes';\napp.use(apiRoutes);`
103
+ );
104
+ await fs.writeFile(serverDestPath, serverFileContent);
105
+
106
+ console.log(chalk.gray(' -> server.ts modified successfully.'));
107
+
108
+ // --- Step 7: Install dependencies ---
109
+ console.log(chalk.magenta(' -> Installing dependencies (npm install)...'));
110
+ await execa('npm', ['install'], { cwd: projectDir });
111
+
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 ---
128
+ await renderAndWrite(
129
+ getTemplatePath('node-ts-express/partials/README.md.ejs'),
130
+ path.join(projectDir, 'README.md'),
131
+ { projectName }
132
+ );
133
+
134
+ console.log(chalk.green('✅ Project generation completed successfully!'));
135
+ } catch (error) {
136
+ console.error(chalk.red('❌ Error generating Node project:'), error);
137
+ throw error; // Pass to main CLI handler
138
+ }
60
139
  }
61
140
 
62
- 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;