xpresso-cli 1.3.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/LICENSE +15 -0
- package/bin/cli.js +26 -0
- package/bin/commands/createService.js +147 -0
- package/bin/commands/init.js +33 -0
- package/bin/create-service-cli.js +12 -0
- package/bin/utils/database.js +81 -0
- package/bin/utils/helpers.js +96 -0
- package/package.json +52 -0
- package/readme.md +128 -0
- package/template/app.js +36 -0
- package/template/bin/www +93 -0
- package/template/config/config.js +6 -0
- package/template/package.json +21 -0
- package/template/public/css/style.css +8 -0
- package/template/utils/helper.utils.js +33 -0
- package/template/utils/removeFile.utils.js +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Krish Dhiman
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { createServiceCommand } from './commands/createService.js';
|
|
6
|
+
|
|
7
|
+
// ==========================================
|
|
8
|
+
// CLI Application
|
|
9
|
+
// ==========================================
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('create-node-app')
|
|
13
|
+
.description('Scaffold a new Node.js application');
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command('init [project-directory]', { isDefault: true })
|
|
17
|
+
.description('Initialize a new Node.js project (Default command)')
|
|
18
|
+
.action(initCommand);
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('create-service <serviceName>')
|
|
22
|
+
.alias('create')
|
|
23
|
+
.description('Scaffold a new service (model, controller, routes)')
|
|
24
|
+
.action(createServiceCommand);
|
|
25
|
+
|
|
26
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
|
|
6
|
+
export async function createServiceCommand(serviceName, targetDir = process.cwd()) {
|
|
7
|
+
if (!serviceName) {
|
|
8
|
+
console.error(chalk.red('Please provide a service name. Example: create-node-app create-service auth'));
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const currentDir = targetDir;
|
|
13
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
14
|
+
|
|
15
|
+
// Default to no database if not found
|
|
16
|
+
let dbType = 'none';
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
21
|
+
const deps = pkg.dependencies || {};
|
|
22
|
+
if (deps.mongoose) {
|
|
23
|
+
dbType = 'mongoose';
|
|
24
|
+
} else if (deps.sequelize) {
|
|
25
|
+
dbType = 'sequelize';
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn(chalk.yellow('Could not parse package.json. Defaulting to standard model generation.'));
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
console.warn(chalk.yellow('No package.json found in current directory. Generating service without database dependencies.'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const serviceDir = path.join(currentDir, 'services', serviceName);
|
|
35
|
+
const controllerDir = path.join(serviceDir, 'controller');
|
|
36
|
+
const modelDir = path.join(serviceDir, 'model');
|
|
37
|
+
const routesDir = path.join(serviceDir, 'routes');
|
|
38
|
+
|
|
39
|
+
const spinner = ora(`Generating ${serviceName} service...`).start();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Create directories
|
|
43
|
+
fs.mkdirSync(controllerDir, { recursive: true });
|
|
44
|
+
fs.mkdirSync(modelDir, { recursive: true });
|
|
45
|
+
fs.mkdirSync(routesDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
// Generate Controller
|
|
48
|
+
const controllerContent = `export default {
|
|
49
|
+
// Add your controller methods here
|
|
50
|
+
// exampleMethod: async (req, res, next) => { ... }
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
fs.writeFileSync(path.join(controllerDir, `${serviceName}.controller.js`), controllerContent);
|
|
54
|
+
|
|
55
|
+
// Generate Routes
|
|
56
|
+
const routesContent = `import express from 'express'
|
|
57
|
+
import ${serviceName}Controller from '../controller/${serviceName}.controller.js'
|
|
58
|
+
|
|
59
|
+
const router = express.Router({ caseSensitive: true })
|
|
60
|
+
|
|
61
|
+
// Add your routes here
|
|
62
|
+
// router.get('/', ${serviceName}Controller.exampleMethod)
|
|
63
|
+
|
|
64
|
+
export default router
|
|
65
|
+
`;
|
|
66
|
+
fs.writeFileSync(path.join(routesDir, `${serviceName}.routes.js`), routesContent);
|
|
67
|
+
|
|
68
|
+
// Generate Model
|
|
69
|
+
let modelContent = '';
|
|
70
|
+
if (dbType === 'sequelize') {
|
|
71
|
+
modelContent = `import { DataTypes } from 'sequelize'
|
|
72
|
+
import sequelize from '../../../config/db.config.js'
|
|
73
|
+
|
|
74
|
+
const ${serviceName.charAt(0).toUpperCase() + serviceName.slice(1)} = sequelize.define('${serviceName}', {
|
|
75
|
+
name: {
|
|
76
|
+
type: DataTypes.STRING,
|
|
77
|
+
allowNull: false
|
|
78
|
+
}
|
|
79
|
+
}, {
|
|
80
|
+
timestamps: true
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export default ${serviceName.charAt(0).toUpperCase() + serviceName.slice(1)}
|
|
84
|
+
`;
|
|
85
|
+
} else {
|
|
86
|
+
// Default to mongoose structure
|
|
87
|
+
modelContent = `import mongoose from "mongoose";
|
|
88
|
+
|
|
89
|
+
const ${serviceName}Schema = new mongoose.Schema({
|
|
90
|
+
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export default mongoose.model('${serviceName}', ${serviceName}Schema);
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fs.writeFileSync(path.join(modelDir, `${serviceName}.model.js`), modelContent);
|
|
98
|
+
|
|
99
|
+
// Try to update app.js automatically
|
|
100
|
+
const appJsPath = path.join(currentDir, 'app.js');
|
|
101
|
+
if (fs.existsSync(appJsPath)) {
|
|
102
|
+
let appJsContent = fs.readFileSync(appJsPath, 'utf8');
|
|
103
|
+
|
|
104
|
+
const importStatement = `import ${serviceName}Routes from './services/${serviceName}/routes/${serviceName}.routes.js'`;
|
|
105
|
+
const useStatement = `app.use('/api/${serviceName}', ${serviceName}Routes)`;
|
|
106
|
+
|
|
107
|
+
// Inject import statement after the last import
|
|
108
|
+
if (!appJsContent.includes(importStatement)) {
|
|
109
|
+
const lastImportIndex = appJsContent.lastIndexOf('import ');
|
|
110
|
+
if (lastImportIndex !== -1) {
|
|
111
|
+
const endOfLastImport = appJsContent.indexOf('\n', lastImportIndex);
|
|
112
|
+
appJsContent = appJsContent.slice(0, endOfLastImport + 1) + importStatement + '\n' + appJsContent.slice(endOfLastImport + 1);
|
|
113
|
+
} else {
|
|
114
|
+
appJsContent = importStatement + '\n' + appJsContent;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Inject app.use statement before app.get('/', or before export default
|
|
119
|
+
if (!appJsContent.includes(useStatement)) {
|
|
120
|
+
const appGetIndex = appJsContent.indexOf("app.get('/'");
|
|
121
|
+
if (appGetIndex !== -1) {
|
|
122
|
+
appJsContent = appJsContent.slice(0, appGetIndex) + useStatement + '\n\n' + appJsContent.slice(appGetIndex);
|
|
123
|
+
} else {
|
|
124
|
+
const exportIndex = appJsContent.lastIndexOf('export default app');
|
|
125
|
+
if (exportIndex !== -1) {
|
|
126
|
+
appJsContent = appJsContent.slice(0, exportIndex) + useStatement + '\n\n' + appJsContent.slice(exportIndex);
|
|
127
|
+
} else {
|
|
128
|
+
appJsContent += '\n' + useStatement + '\n';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fs.writeFileSync(appJsPath, appJsContent);
|
|
134
|
+
spinner.succeed(`Successfully created ${chalk.green(serviceName)} service and updated app.js`);
|
|
135
|
+
} else {
|
|
136
|
+
spinner.succeed(`Successfully created ${chalk.green(serviceName)} service at ./services/${serviceName}`);
|
|
137
|
+
console.log(`\nDon't forget to register your routes in ${chalk.cyan('app.js')}!`);
|
|
138
|
+
console.log(` import ${serviceName}Routes from './services/${serviceName}/routes/${serviceName}.routes.js'`);
|
|
139
|
+
console.log(` app.use('/api/${serviceName}', ${serviceName}Routes)\n`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
} catch (err) {
|
|
143
|
+
spinner.fail(`Failed to create ${serviceName} service.`);
|
|
144
|
+
console.error(err);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { promptUser, copyTemplateFiles, installDependencies, showSuccessMessage } from '../utils/helpers.js';
|
|
5
|
+
import { configureDatabase } from '../utils/database.js';
|
|
6
|
+
import { createServiceCommand } from './createService.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
export async function initCommand(projectDirectory) {
|
|
12
|
+
// 1. Prompt User
|
|
13
|
+
const { targetDir, packageManager, database } = await promptUser(projectDirectory);
|
|
14
|
+
const targetPath = path.resolve(process.cwd(), targetDir);
|
|
15
|
+
const templateDir = path.join(__dirname, '..', '..', 'template');
|
|
16
|
+
|
|
17
|
+
console.log(`\nCreating a new Node.js app in ${chalk.green(targetPath)}\n`);
|
|
18
|
+
|
|
19
|
+
// 2. Copy Template Files
|
|
20
|
+
copyTemplateFiles(templateDir, targetPath);
|
|
21
|
+
|
|
22
|
+
// 3. Configure Database
|
|
23
|
+
configureDatabase(targetPath, database);
|
|
24
|
+
|
|
25
|
+
// 4. Dynamically generate the 'auth' service so it uses the correct DB model
|
|
26
|
+
await createServiceCommand('auth', targetPath);
|
|
27
|
+
|
|
28
|
+
// 5. Install Dependencies
|
|
29
|
+
installDependencies(targetPath, packageManager);
|
|
30
|
+
|
|
31
|
+
// 6. Success Message
|
|
32
|
+
showSuccessMessage(targetDir, targetPath, packageManager);
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { createServiceCommand } from './commands/createService.js';
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name('create-service')
|
|
8
|
+
.description('Scaffold a new service (model, controller, routes)')
|
|
9
|
+
.argument('<serviceName>', 'Name of the service to create')
|
|
10
|
+
.action(createServiceCommand);
|
|
11
|
+
|
|
12
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function configureDatabase(targetPath, database) {
|
|
5
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
6
|
+
if (!fs.existsSync(packageJsonPath)) return;
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
9
|
+
pkg.name = path.basename(targetPath);
|
|
10
|
+
|
|
11
|
+
const dbConfigPath = path.join(targetPath, 'config', 'db.config.js');
|
|
12
|
+
const targetConfigDir = path.dirname(dbConfigPath);
|
|
13
|
+
if (!fs.existsSync(targetConfigDir)) {
|
|
14
|
+
fs.mkdirSync(targetConfigDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (database === 'MongoDB (Mongoose)') {
|
|
18
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
19
|
+
pkg.dependencies.mongoose = '^8.0.0';
|
|
20
|
+
|
|
21
|
+
const mongooseContent = `import mongoose from "mongoose"
|
|
22
|
+
|
|
23
|
+
const options = {
|
|
24
|
+
serverSelectionTimeoutMS: 10000,
|
|
25
|
+
dbName: '',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const connectDB = async () => {
|
|
29
|
+
try {
|
|
30
|
+
await mongoose.connect('', options)
|
|
31
|
+
console.log(\`✅ MongoDB connected!\`)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(\`❌ MongoDB connection error: \${error.message}\`)
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default connectDB`;
|
|
39
|
+
fs.writeFileSync(dbConfigPath, mongooseContent);
|
|
40
|
+
|
|
41
|
+
} else if (database === 'PostgreSQL (Sequelize)') {
|
|
42
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
43
|
+
pkg.dependencies.sequelize = '^6.35.0';
|
|
44
|
+
pkg.dependencies.pg = '^8.11.3';
|
|
45
|
+
pkg.dependencies['pg-hstore'] = '^2.3.4';
|
|
46
|
+
|
|
47
|
+
const sequelizeContent = `import { Sequelize } from "sequelize"
|
|
48
|
+
|
|
49
|
+
export const sequelize = new Sequelize(
|
|
50
|
+
'', // DB name
|
|
51
|
+
'', // User
|
|
52
|
+
'', // Password
|
|
53
|
+
{
|
|
54
|
+
host: '',
|
|
55
|
+
dialect: 'postgres',
|
|
56
|
+
logging: false,
|
|
57
|
+
port: ''
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// Test connection
|
|
62
|
+
// (async () => {
|
|
63
|
+
// try {
|
|
64
|
+
// await sequelize.authenticate();
|
|
65
|
+
// console.log("PostgreSQL connected with Sequelize");
|
|
66
|
+
// } catch (err) {
|
|
67
|
+
// console.error("Error:", err.message);
|
|
68
|
+
// }
|
|
69
|
+
// })()
|
|
70
|
+
|
|
71
|
+
export default sequelize
|
|
72
|
+
`;
|
|
73
|
+
fs.writeFileSync(dbConfigPath, sequelizeContent);
|
|
74
|
+
|
|
75
|
+
} else {
|
|
76
|
+
fs.writeFileSync(dbConfigPath, '// No database configured\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Save updated package.json
|
|
80
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
|
|
81
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
|
|
8
|
+
export async function promptUser(projectDirectory) {
|
|
9
|
+
let targetDir = projectDirectory;
|
|
10
|
+
|
|
11
|
+
if (!targetDir) {
|
|
12
|
+
const answers = await inquirer.prompt([
|
|
13
|
+
{
|
|
14
|
+
type: 'input',
|
|
15
|
+
name: 'projectDirectory',
|
|
16
|
+
message: 'What is your project named?',
|
|
17
|
+
default: 'my-app'
|
|
18
|
+
}
|
|
19
|
+
]);
|
|
20
|
+
targetDir = answers.projectDirectory;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { packageManager, database } = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: 'list',
|
|
26
|
+
name: 'packageManager',
|
|
27
|
+
message: 'Which package manager would you like to use?',
|
|
28
|
+
choices: ['npm', 'yarn', 'pnpm'],
|
|
29
|
+
default: 'npm'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'list',
|
|
33
|
+
name: 'database',
|
|
34
|
+
message: 'Which database would you like to use?',
|
|
35
|
+
choices: ['MongoDB (Mongoose)', 'PostgreSQL (Sequelize)', 'None'],
|
|
36
|
+
default: 'MongoDB (Mongoose)'
|
|
37
|
+
}
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
return { targetDir, packageManager, database };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function copyTemplateFiles(templateDir, targetPath) {
|
|
44
|
+
const spinner = ora('Copying template files...').start();
|
|
45
|
+
|
|
46
|
+
if (fs.existsSync(targetPath)) {
|
|
47
|
+
spinner.fail(`Directory ${path.basename(targetPath)} already exists.`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
52
|
+
|
|
53
|
+
const copyRecursiveSync = (src, dest) => {
|
|
54
|
+
const exists = fs.existsSync(src);
|
|
55
|
+
const stats = exists && fs.statSync(src);
|
|
56
|
+
if (exists && stats.isDirectory()) {
|
|
57
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest);
|
|
58
|
+
fs.readdirSync(src).forEach((child) => {
|
|
59
|
+
if (child === 'node_modules') return;
|
|
60
|
+
copyRecursiveSync(path.join(src, child), path.join(dest, child));
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
fs.copyFileSync(src, dest);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
copyRecursiveSync(templateDir, targetPath);
|
|
69
|
+
spinner.succeed('Template copied successfully.');
|
|
70
|
+
} catch (err) {
|
|
71
|
+
spinner.fail('Failed to copy template.');
|
|
72
|
+
console.error(err);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function installDependencies(targetPath, packageManager) {
|
|
78
|
+
const installSpinner = ora(`Installing dependencies using ${packageManager}...`).start();
|
|
79
|
+
try {
|
|
80
|
+
execSync(`${packageManager} install`, { cwd: targetPath, stdio: 'pipe' });
|
|
81
|
+
installSpinner.succeed('Dependencies installed successfully.');
|
|
82
|
+
} catch (err) {
|
|
83
|
+
installSpinner.fail('Failed to install dependencies.');
|
|
84
|
+
console.error(err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function showSuccessMessage(targetDir, targetPath, packageManager) {
|
|
89
|
+
console.log(`\n${chalk.green('Success!')} Created ${path.basename(targetPath)} at ${targetPath}`);
|
|
90
|
+
console.log('\nInside that directory, you can run several commands:\n');
|
|
91
|
+
console.log(` ${chalk.cyan(`${packageManager} run dev`)}`);
|
|
92
|
+
console.log(' Starts the development server with nodemon.\n');
|
|
93
|
+
console.log('We suggest that you begin by typing:\n');
|
|
94
|
+
console.log(` ${chalk.cyan('cd')} ${targetDir}`);
|
|
95
|
+
console.log(` ${chalk.cyan(`${packageManager} run dev`)}\n`);
|
|
96
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xpresso-cli",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "A CLI tool to scaffold Express.js applications.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"private": false,
|
|
8
|
+
"bin": {
|
|
9
|
+
"xpresso-cli": "bin/cli.js",
|
|
10
|
+
"create-service": "bin/create-service-cli.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node ./bin/cli.js",
|
|
14
|
+
"create-service": "node ./bin/cli.js create-service"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"chalk": "^5.3.0",
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
|
+
"inquirer": "^9.2.14",
|
|
20
|
+
"ora": "^8.0.1"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/KrishDhimanOfficial/create_node_app.git"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"express",
|
|
28
|
+
"express.js",
|
|
29
|
+
"create-express-app",
|
|
30
|
+
"node",
|
|
31
|
+
"nodejs",
|
|
32
|
+
"cli",
|
|
33
|
+
"scaffold",
|
|
34
|
+
"generator",
|
|
35
|
+
"express-generator",
|
|
36
|
+
"boilerplate",
|
|
37
|
+
"starter",
|
|
38
|
+
"mvc",
|
|
39
|
+
"modular",
|
|
40
|
+
"api",
|
|
41
|
+
"mongoose",
|
|
42
|
+
"mongodb",
|
|
43
|
+
"sequelize",
|
|
44
|
+
"postgresql"
|
|
45
|
+
],
|
|
46
|
+
"author": "Krish Dhiman",
|
|
47
|
+
"license": "ISC",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/KrishDhimanOfficial/create_node_app/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/KrishDhimanOfficial/create_node_app#readme"
|
|
52
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# 🚀 xpresso-cli
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/xpresso-cli)
|
|
4
|
+
[](https://opensource.org/licenses/ISC)
|
|
5
|
+
|
|
6
|
+
A powerful, incredibly fast CLI tool to instantly scaffold production-ready Node.js Express applications. It comes packed with built-in database configurations and an intuitive modular service generator, saving you hours of boilerplate setup.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ✨ Features
|
|
11
|
+
|
|
12
|
+
- **⚡ Instant Setup:** Scaffolds a complete, production-ready Express.js MVC/Modular server structure in seconds.
|
|
13
|
+
- **🗄️ Database Ready:** Automatic, hassle-free database setup for **MongoDB (Mongoose)** or **PostgreSQL (Sequelize)**.
|
|
14
|
+
- **🛠️ Service Generator:** Built-in CLI command to automatically generate new service modules (Controllers, Models, Routes) instantly.
|
|
15
|
+
- **🔒 Best Practices Built-in:** Pre-configured `app.js` with CORS, compression, and global error handlers.
|
|
16
|
+
- **📦 Package Manager Choice:** Support for `npm`, `yarn`, and `pnpm`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📖 Step-by-Step Guide
|
|
21
|
+
|
|
22
|
+
### Step 1: Scaffold a New Application
|
|
23
|
+
|
|
24
|
+
You can use `npx` to create a new application instantly without installing anything globally:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx xpresso-cli my-app
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Alternatively, you can install the CLI globally:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g xpresso-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then you can use it directly anywhere:
|
|
37
|
+
```bash
|
|
38
|
+
xpresso-cli my-app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
*(If you run the command without a folder name, the CLI will interactively ask you for it!)*
|
|
42
|
+
|
|
43
|
+
### Step 2: Configure Your Project
|
|
44
|
+
|
|
45
|
+
The interactive CLI will prompt you to make a few quick decisions to tailor your app:
|
|
46
|
+
|
|
47
|
+
1. **Package Manager:** Choose between `npm`, `yarn`, or `pnpm`.
|
|
48
|
+
2. **Database:** Select your preferred database:
|
|
49
|
+
- `MongoDB` (uses Mongoose)
|
|
50
|
+
- `PostgreSQL` (uses Sequelize)
|
|
51
|
+
- `None` (if you want to set it up yourself later)
|
|
52
|
+
|
|
53
|
+
Once selected, the CLI will automatically:
|
|
54
|
+
- Generate the folder structure.
|
|
55
|
+
- Configure your database connection file.
|
|
56
|
+
- Generate an initial `auth` service tailored to your database choice.
|
|
57
|
+
- Install all necessary dependencies automatically!
|
|
58
|
+
|
|
59
|
+
### Step 3: Run Your Application
|
|
60
|
+
|
|
61
|
+
Navigate into your newly created project and start the development server:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cd my-app
|
|
65
|
+
npm start
|
|
66
|
+
```
|
|
67
|
+
*(If you selected yarn or pnpm, use `yarn start` or `pnpm start` respectively).*
|
|
68
|
+
|
|
69
|
+
You now have a fully functional Node.js server running! 🎉
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 🏗️ Generating New Services (The Magic)
|
|
74
|
+
|
|
75
|
+
Once your application is created, `xpresso-cli` provides an amazing built-in code generator that saves you from writing repetitive boilerplate for new features.
|
|
76
|
+
|
|
77
|
+
To generate a new service (e.g., for `user` management), navigate into your project folder and run:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx create-service user
|
|
81
|
+
```
|
|
82
|
+
*(If you installed the CLI globally, you can simply type `create-service user`)*
|
|
83
|
+
|
|
84
|
+
### What happens behind the scenes?
|
|
85
|
+
When you run `create-service user`, the CLI automatically:
|
|
86
|
+
1. Creates a Controller (`services/user/controller/user.controller.js`).
|
|
87
|
+
2. Creates Routes (`services/user/routes/user.routes.js`).
|
|
88
|
+
3. Creates a Model (`services/user/model/user.model.js`) specifically tailored to the database you selected during initial setup!
|
|
89
|
+
4. **Auto-wires everything:** It automatically updates your main `app.js` file to import and register your new `user` routes. You don't have to touch a thing!
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 📂 Folder Structure
|
|
94
|
+
|
|
95
|
+
Here is the clean, modular structure generated for your app:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
my-app/
|
|
99
|
+
├── bin/
|
|
100
|
+
│ └── www # Server startup script
|
|
101
|
+
├── config/
|
|
102
|
+
│ └── db.config.js # Database connection setup
|
|
103
|
+
├── public/ # Static files
|
|
104
|
+
├── services/
|
|
105
|
+
│ ├── auth/ # Automatically generated on init
|
|
106
|
+
│ │ ├── controller/
|
|
107
|
+
│ │ ├── model/
|
|
108
|
+
│ │ └── routes/
|
|
109
|
+
│ └── <your-service>/ # Automatically generated via 'create-service'
|
|
110
|
+
├── utils/
|
|
111
|
+
│ └── helper.utils.js # Helper functions and utilities
|
|
112
|
+
├── app.js # Express app configuration (auto-updated with new routes)
|
|
113
|
+
└── package.json # Project dependencies and scripts
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🤝 Contributing
|
|
119
|
+
|
|
120
|
+
Contributions, issues, and feature requests are always welcome! Feel free to check the [issues page](https://github.com/KrishDhimanOfficial/create_node_app/issues).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 📄 License
|
|
125
|
+
|
|
126
|
+
This project is licensed under the [ISC License](LICENSE).
|
|
127
|
+
|
|
128
|
+
**Created with ❤️ by [Krish Dhiman](https://github.com/KrishDhimanOfficial)**
|
package/template/app.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import cookieParser from 'cookie-parser'
|
|
3
|
+
import logger from 'morgan'
|
|
4
|
+
import { globalErrorHandler } from './utils/helper.utils.js'
|
|
5
|
+
import compression from 'compression'
|
|
6
|
+
import cors from 'cors'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = path.dirname(__filename)
|
|
12
|
+
const app = express()
|
|
13
|
+
|
|
14
|
+
app.use(cors())
|
|
15
|
+
app.use(logger('dev'))
|
|
16
|
+
app.use(express.json({ limit: '10kb' }))
|
|
17
|
+
app.use(express.urlencoded({ extended: true }))
|
|
18
|
+
app.use(compression(
|
|
19
|
+
{
|
|
20
|
+
level: 4, // compression level
|
|
21
|
+
threshold: 0, // Compress all
|
|
22
|
+
memLevel: 9, // memory usuage
|
|
23
|
+
filter: (req, res) => compression.filter(req, res)
|
|
24
|
+
}
|
|
25
|
+
))
|
|
26
|
+
app.use(cookieParser())
|
|
27
|
+
app.use(express.static(path.join(__dirname, 'public')))
|
|
28
|
+
|
|
29
|
+
app.get('/', (req, res) => {
|
|
30
|
+
return res.send('Hello World!')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// error handler
|
|
34
|
+
app.use(globalErrorHandler)
|
|
35
|
+
|
|
36
|
+
export default app
|
package/template/bin/www
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Module dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import app from '../app.js';
|
|
8
|
+
import Debug from 'debug';
|
|
9
|
+
const debug = Debug('create-node:server');
|
|
10
|
+
import http from 'http';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get port from environment and store in Express.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const port = normalizePort(process.env.PORT || '3000');
|
|
17
|
+
app.set('port', port);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create HTTP server.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const server = http.createServer(app);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Listen on provided port, on all network interfaces.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
server.listen(port, () => {
|
|
30
|
+
console.log(`✅ Server is running on port ${port}`)
|
|
31
|
+
})
|
|
32
|
+
server.on('error', onError);
|
|
33
|
+
server.on('listening', onListening);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a port into a number, string, or false.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
function normalizePort(val) {
|
|
40
|
+
const port = parseInt(val, 10);
|
|
41
|
+
|
|
42
|
+
if (isNaN(port)) {
|
|
43
|
+
// named pipe
|
|
44
|
+
return val;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (port >= 0) {
|
|
48
|
+
// port number
|
|
49
|
+
return port;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Event listener for HTTP server "error" event.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
function onError(error) {
|
|
60
|
+
if (error.syscall !== 'listen') {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const bind = typeof port === 'string'
|
|
65
|
+
? 'Pipe ' + port
|
|
66
|
+
: 'Port ' + port;
|
|
67
|
+
|
|
68
|
+
// handle specific listen errors with friendly messages
|
|
69
|
+
switch (error.code) {
|
|
70
|
+
case 'EACCES':
|
|
71
|
+
console.error(bind + ' requires elevated privileges');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
break;
|
|
74
|
+
case 'EADDRINUSE':
|
|
75
|
+
console.error(bind + ' is already in use');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Event listener for HTTP server "listening" event.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
function onListening() {
|
|
88
|
+
const addr = server.address();
|
|
89
|
+
const bind = typeof addr === 'string'
|
|
90
|
+
? 'pipe ' + addr
|
|
91
|
+
: 'port ' + addr.port;
|
|
92
|
+
debug('Listening on ' + bind);
|
|
93
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node ./bin/www",
|
|
8
|
+
"dev": "nodemon ./bin/www"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"compression": "^1.8.1",
|
|
12
|
+
"cookie-parser": "~1.4.4",
|
|
13
|
+
"cors": "^2.8.6",
|
|
14
|
+
"debug": "~2.6.9",
|
|
15
|
+
"express": "^4.22.1",
|
|
16
|
+
"morgan": "^1.10.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"nodemon": "^3.1.14"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { deleteFile } from './removeFile.utils.js'
|
|
2
|
+
|
|
3
|
+
export const ApiError = (message, statusCode) => {
|
|
4
|
+
const error = new Error(message)
|
|
5
|
+
error.statusCode = statusCode
|
|
6
|
+
error.success = false
|
|
7
|
+
return error
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const asyncHandler = (fn, name = 'UnknownController') => {
|
|
11
|
+
|
|
12
|
+
return (req, res, next) => {
|
|
13
|
+
Promise.resolve(fn(req, res, next)).catch((err) => {
|
|
14
|
+
console.error(`🔥 Error in ${name} : ${err.message}`)
|
|
15
|
+
if (req.file?.filename) deleteFile(req.file?.path)
|
|
16
|
+
if (req.files && req.files?.length > 0) req.files?.forEach(file => deleteFile(file.path))
|
|
17
|
+
next(err)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const globalErrorHandler = (err, req, res, next) => {
|
|
23
|
+
const statusCode = err.statusCode || 500;
|
|
24
|
+
|
|
25
|
+
// For API requests (JSON response)
|
|
26
|
+
return res.status(statusCode).json(
|
|
27
|
+
{
|
|
28
|
+
success: false,
|
|
29
|
+
message: err.message || 'Something went wrong',
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = path.dirname(__filename)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const deleteFile = async (folderPath) => {
|
|
11
|
+
try {
|
|
12
|
+
const imagePath = path.join(__dirname, '..', folderPath)
|
|
13
|
+
if (!fs.existsSync(imagePath)) return
|
|
14
|
+
|
|
15
|
+
await fs.promises.rm(imagePath, { force: true })
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(chalk.red('deleteFile error:', error.message))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { deleteFile }
|