express-launcher 1.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/README.md +55 -0
- package/bin/index.js +115 -0
- package/lib/generator.js +444 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Express Launcher
|
|
2
|
+
|
|
3
|
+
Use this detailed CLI tool to scaffold your Express.js applications with modern best practices, TypeScript support, and various integrations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Language Support:** JavaScript or TypeScript
|
|
8
|
+
- **Database Integration:** MongoDB, PostgreSQL, MySQL (with Prisma)
|
|
9
|
+
- **Middleware:** CORS, Helmet, Morgan, Rate Limiter
|
|
10
|
+
- **Error Handling:** Advanced global error handler, `AppError` class, and `asyncHandler` wrapper
|
|
11
|
+
- **Linting:** ESLint configuration (Flat Config, TypeScript support)
|
|
12
|
+
- **Structure:** MVC pattern or Modular structure (routes, controllers, utils, middlewares)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g express-launcher
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Run the following command to start the interactive generator:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
express-launcher [project-name]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or just:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
express-launcher
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Follow the prompts to configure your project.
|
|
35
|
+
|
|
36
|
+
## Generated Project Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
my-app/
|
|
40
|
+
├── src/
|
|
41
|
+
│ ├── controllers/
|
|
42
|
+
│ ├── lib/ # Database connection / Prisma client
|
|
43
|
+
│ ├── middlewares/ # Error handling, rate limiting
|
|
44
|
+
│ ├── routes/
|
|
45
|
+
│ ├── utils/ # AppError, asyncHandler
|
|
46
|
+
│ └── server.js (or index.ts)
|
|
47
|
+
├── prisma/ # If SQL DB selected
|
|
48
|
+
├── .env
|
|
49
|
+
├── package.json
|
|
50
|
+
└── README.md
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT © Narasimha
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
const inquirer = require('inquirer');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const { generateProject } = require('../lib/generator');
|
|
8
|
+
const packageJson = require('../package.json');
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.version(packageJson.version)
|
|
14
|
+
.description('A CLI to generate Express.js projects')
|
|
15
|
+
.argument('[project-name]', 'Name of the project directory')
|
|
16
|
+
.action(async (projectName) => {
|
|
17
|
+
let answers = { name: projectName };
|
|
18
|
+
|
|
19
|
+
if (!projectName) {
|
|
20
|
+
const nameAnswer = await inquirer.prompt([
|
|
21
|
+
{
|
|
22
|
+
type: 'input',
|
|
23
|
+
name: 'name',
|
|
24
|
+
message: 'What is the name of your project?',
|
|
25
|
+
default: 'my-express-app',
|
|
26
|
+
validate: (input) => input.trim() !== '' || 'Project name cannot be empty.'
|
|
27
|
+
}
|
|
28
|
+
]);
|
|
29
|
+
answers.name = nameAnswer.name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const config = await inquirer.prompt([
|
|
33
|
+
{
|
|
34
|
+
type: 'confirm',
|
|
35
|
+
name: 'useSrc',
|
|
36
|
+
message: 'Would you like to organize your project inside a src directory?',
|
|
37
|
+
default: true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: 'list',
|
|
41
|
+
name: 'language',
|
|
42
|
+
message: 'Which language would you like to use?',
|
|
43
|
+
choices: ['JavaScript', 'TypeScript'],
|
|
44
|
+
default: 'JavaScript'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'confirm',
|
|
48
|
+
name: 'useAlias',
|
|
49
|
+
message: 'Would you like to configure "@/” as an alias for the src directory?',
|
|
50
|
+
default: true,
|
|
51
|
+
when: (answers) => answers.useSrc
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'list',
|
|
55
|
+
name: 'database',
|
|
56
|
+
message: 'Which database would you like to set up?',
|
|
57
|
+
choices: ['None', 'MongoDB', 'PostgreSQL', 'MySQL'],
|
|
58
|
+
default: 'None'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'confirm',
|
|
62
|
+
name: 'cors',
|
|
63
|
+
message: 'Would you like to enable CORS support?',
|
|
64
|
+
default: true
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'confirm',
|
|
68
|
+
name: 'helmet',
|
|
69
|
+
message: 'Would you like to add Helmet for security best practices?',
|
|
70
|
+
default: true
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: 'confirm',
|
|
74
|
+
name: 'morgan',
|
|
75
|
+
message: 'Would you like to enable Morgan for request logging?',
|
|
76
|
+
default: true
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'confirm',
|
|
80
|
+
name: 'errorHandler',
|
|
81
|
+
message: 'Would you like to include a centralized error handler?',
|
|
82
|
+
default: true
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: 'confirm',
|
|
86
|
+
name: 'rateLimiter',
|
|
87
|
+
message: 'Would you like to add rate limiter?',
|
|
88
|
+
default: true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'confirm',
|
|
92
|
+
name: 'eslint',
|
|
93
|
+
message: 'Would you like to set up ESLint for linting?',
|
|
94
|
+
default: true
|
|
95
|
+
}
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
const finalConfig = { ...answers, ...config };
|
|
100
|
+
|
|
101
|
+
console.log(chalk.blue(`\nGenerating project in ./${finalConfig.name}...\n`));
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await generateProject(finalConfig);
|
|
105
|
+
console.log(chalk.green('\nSuccess! Your project is ready.'));
|
|
106
|
+
console.log(chalk.white(`\ncd ${finalConfig.name}`));
|
|
107
|
+
console.log(chalk.white('npm install'));
|
|
108
|
+
console.log(chalk.white(finalConfig.language === 'TypeScript' ? 'npm run dev' : 'npm start\n'));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(chalk.red('Error generating project:'), error.message);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
program.parse(process.argv);
|
package/lib/generator.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
async function generateProject(config) {
|
|
6
|
+
const { name, useSrc, language, useAlias, database, cors, helmet, morgan, errorHandler, rateLimiter, eslint } = config;
|
|
7
|
+
const isTs = language === 'TypeScript';
|
|
8
|
+
const rootDir = path.resolve(process.cwd(), name);
|
|
9
|
+
const srcDir = useSrc ? path.join(rootDir, 'src') : rootDir;
|
|
10
|
+
|
|
11
|
+
// Folders to create
|
|
12
|
+
const folders = ['routes', 'controllers', 'utils', 'lib', 'middlewares', '../tests'];
|
|
13
|
+
if (database === 'PostgreSQL' || database === 'MySQL') {
|
|
14
|
+
folders.push('../prisma');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (database !== 'PostgreSQL' || database !== 'MySQL') {
|
|
18
|
+
folders.push('models');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(rootDir)) {
|
|
22
|
+
throw new Error(`Directory ${name} already exists.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`Creating directory ${name}...`);
|
|
26
|
+
await fs.ensureDir(rootDir);
|
|
27
|
+
if (useSrc) await fs.ensureDir(srcDir);
|
|
28
|
+
|
|
29
|
+
for (const folder of folders) {
|
|
30
|
+
if (folder.startsWith('../')) {
|
|
31
|
+
await fs.ensureDir(path.join(rootDir, folder.replace('../', '')));
|
|
32
|
+
} else {
|
|
33
|
+
await fs.ensureDir(path.join(srcDir, folder));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 1. Generate package.json
|
|
38
|
+
const packageJson = {
|
|
39
|
+
name: name,
|
|
40
|
+
version: '1.0.0',
|
|
41
|
+
description: '',
|
|
42
|
+
main: isTs ? (useSrc ? 'dist/index.js' : 'dist/server.js') : (useSrc ? 'src/server.js' : 'server.js'),
|
|
43
|
+
type: "module",
|
|
44
|
+
scripts: {
|
|
45
|
+
start: isTs ? 'node dist/index.js' : 'node src/server.js',
|
|
46
|
+
dev: isTs ? 'tsx watch src/index.ts' : 'node --watch src/server.js',
|
|
47
|
+
build: isTs ? 'tsc' : undefined,
|
|
48
|
+
...(eslint ? { "lint": "eslint src", "lint:fix": "eslint src --fix" } : {}),
|
|
49
|
+
...(isTs ? { "watch": "tsc -w" } : {})
|
|
50
|
+
},
|
|
51
|
+
dependencies: {
|
|
52
|
+
express: '^4.19.2',
|
|
53
|
+
dotenv: '^16.4.5'
|
|
54
|
+
},
|
|
55
|
+
devDependencies: {
|
|
56
|
+
// nodemon replaced by native watch or tsx
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (cors) packageJson.dependencies.cors = '^2.8.5';
|
|
61
|
+
if (helmet) packageJson.dependencies.helmet = '^7.1.0';
|
|
62
|
+
if (morgan) packageJson.dependencies.morgan = '^1.10.0';
|
|
63
|
+
if (rateLimiter) packageJson.dependencies['express-rate-limit'] = '^7.2.0';
|
|
64
|
+
|
|
65
|
+
if (database === 'MongoDB') {
|
|
66
|
+
packageJson.dependencies.mongoose = '^8.2.0';
|
|
67
|
+
} else if (database === 'PostgreSQL' || database === 'MySQL') {
|
|
68
|
+
packageJson.dependencies['@prisma/client'] = '^7.3.0';
|
|
69
|
+
packageJson.devDependencies.prisma = '^7.3.0';
|
|
70
|
+
|
|
71
|
+
// Add driver adapters based on database choice
|
|
72
|
+
if (database === 'PostgreSQL') {
|
|
73
|
+
packageJson.dependencies['@prisma/adapter-pg'] = '^7.3.0';
|
|
74
|
+
packageJson.dependencies['pg'] = '^8.11.3';
|
|
75
|
+
if (isTs) packageJson.devDependencies['@types/pg'] = '^8.11.0';
|
|
76
|
+
} else if (database === 'MySQL') {
|
|
77
|
+
packageJson.dependencies['@prisma/adapter-mariadb'] = '^7.3.0';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isTs) {
|
|
82
|
+
packageJson.devDependencies.typescript = '^5.3.3';
|
|
83
|
+
packageJson.devDependencies.tsx = '^4.7.1';
|
|
84
|
+
packageJson.devDependencies['@types/node'] = '^20.11.19';
|
|
85
|
+
packageJson.devDependencies['@types/express'] = '^4.17.21';
|
|
86
|
+
if (cors) packageJson.devDependencies['@types/cors'] = '^2.8.17';
|
|
87
|
+
if (morgan) packageJson.devDependencies['@types/morgan'] = '^1.9.9';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (eslint) {
|
|
91
|
+
if (isTs) {
|
|
92
|
+
packageJson.devDependencies = {
|
|
93
|
+
...packageJson.devDependencies,
|
|
94
|
+
"eslint": "9.39.2",
|
|
95
|
+
"globals": "17.1.0",
|
|
96
|
+
"typescript-eslint": "8.53.1",
|
|
97
|
+
"eslint-plugin-import": "2.32.0"
|
|
98
|
+
};
|
|
99
|
+
} else {
|
|
100
|
+
packageJson.devDependencies = {
|
|
101
|
+
...packageJson.devDependencies,
|
|
102
|
+
"eslint": "9.39.2",
|
|
103
|
+
"@eslint/js": "9.39.2",
|
|
104
|
+
"globals": "17.1.0"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await fs.writeJson(path.join(rootDir, 'package.json'), packageJson, { spaces: 2 });
|
|
110
|
+
|
|
111
|
+
// 2. Generate tsconfig.json (if TS)
|
|
112
|
+
if (isTs) {
|
|
113
|
+
const tsConfig = {
|
|
114
|
+
compilerOptions: {
|
|
115
|
+
target: 'ES2022',
|
|
116
|
+
module: 'NodeNext',
|
|
117
|
+
moduleResolution: 'NodeNext',
|
|
118
|
+
outDir: './dist',
|
|
119
|
+
strict: true,
|
|
120
|
+
esModuleInterop: true,
|
|
121
|
+
skipLibCheck: true,
|
|
122
|
+
forceConsistentCasingInFileNames: true
|
|
123
|
+
},
|
|
124
|
+
exclude: ["prisma.config.ts"] // Exclude config file from build to satisfy rootDir: ./src
|
|
125
|
+
};
|
|
126
|
+
if (useSrc) tsConfig.compilerOptions.rootDir = './src';
|
|
127
|
+
// if (useSrc) tsConfig.include = ["src/**/*"]; // Optional: be explicit
|
|
128
|
+
if (useAlias) {
|
|
129
|
+
tsConfig.compilerOptions.baseUrl = '.';
|
|
130
|
+
tsConfig.compilerOptions.paths = { "@/*": ["src/*"] };
|
|
131
|
+
}
|
|
132
|
+
await fs.writeJson(path.join(rootDir, 'tsconfig.json'), tsConfig, { spaces: 2 });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Generate ESLint config
|
|
136
|
+
if (eslint) {
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
const eslintConfigContent = isTs
|
|
140
|
+
? `import globals from "globals";
|
|
141
|
+
import pluginJs from "@eslint/js";
|
|
142
|
+
import tseslint from "typescript-eslint";
|
|
143
|
+
|
|
144
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
145
|
+
export default [
|
|
146
|
+
{files: ["**/*.{js,mjs,cjs,ts}"]},
|
|
147
|
+
{languageOptions: { globals: globals.node }},
|
|
148
|
+
pluginJs.configs.recommended,
|
|
149
|
+
...tseslint.configs.recommended,
|
|
150
|
+
];`
|
|
151
|
+
: `import globals from "globals";
|
|
152
|
+
import pluginJs from "@eslint/js";
|
|
153
|
+
|
|
154
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
155
|
+
export default [
|
|
156
|
+
{files: ["**/*.{js,mjs,cjs}"]},
|
|
157
|
+
{languageOptions: { globals: globals.node }},
|
|
158
|
+
pluginJs.configs.recommended,
|
|
159
|
+
];`;
|
|
160
|
+
|
|
161
|
+
await fs.writeFile(path.join(rootDir, 'eslint.config.mjs'), eslintConfigContent);
|
|
162
|
+
|
|
163
|
+
// Remove old logic if any (the logic above replaced the old devDependencies block part,
|
|
164
|
+
// but we need to ensure we don't duplicate or leave old file writes steps if they were separate.
|
|
165
|
+
// The original code had separate steps for package.json deps (lines 90-98) and config write (127-139).
|
|
166
|
+
// I am replacing the config write block here (127-139) but I also need to handle the deps which were at 90-98.
|
|
167
|
+
// ideally I should have targeted the deps block too.
|
|
168
|
+
// Let's do a better replace plan.
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 4. Generate Database Config
|
|
172
|
+
if (database === 'MongoDB') {
|
|
173
|
+
const dbFile = isTs ? 'db.ts' : 'db.js';
|
|
174
|
+
let dbContent = '';
|
|
175
|
+
if (isTs) {
|
|
176
|
+
dbContent = `import mongoose from 'mongoose';\n\nconst connectDB = async () => {\n try {\n await mongoose.connect(process.env.MONGO_URI as string);\n console.log('MongoDB Connected');\n } catch (err) {\n console.error(err);\n process.exit(1);\n }\n};\n\nexport default connectDB;`;
|
|
177
|
+
} else {
|
|
178
|
+
dbContent = `import mongoose from 'mongoose';\n\nconst connectDB = async () => {\n try {\n await mongoose.connect(process.env.MONGO_URI);\n console.log('MongoDB Connected');\n } catch (err) {\n console.error(err);\n process.exit(1);\n }\n};\n\nexport default connectDB;`;
|
|
179
|
+
}
|
|
180
|
+
await fs.writeFile(path.join(srcDir, 'lib', dbFile), dbContent);
|
|
181
|
+
} else if (database === 'PostgreSQL' || database === 'MySQL') {
|
|
182
|
+
// Prisma client initialization with driver adapters
|
|
183
|
+
const prismaClientFile = isTs ? 'prisma.ts' : 'prisma.js';
|
|
184
|
+
let prismaContent = '';
|
|
185
|
+
|
|
186
|
+
if (database === 'PostgreSQL') {
|
|
187
|
+
if (isTs) {
|
|
188
|
+
prismaContent = `import 'dotenv/config';\nimport { Pool } from 'pg';\nimport { PrismaPg } from '@prisma/adapter-pg';\nimport { PrismaClient } from '@prisma/client';\n\nconst connectionString = process.env.DATABASE_URL;\n\n// Using Pool is standard for @prisma/adapter-pg to allow connection management\nconst pool = new Pool({ connectionString });\nconst adapter = new PrismaPg(pool);\nconst prisma = new PrismaClient({ adapter });\n\nexport default prisma;`;
|
|
189
|
+
} else {
|
|
190
|
+
prismaContent = `import 'dotenv/config';\nimport pg from 'pg';\nimport { PrismaPg } from '@prisma/adapter-pg';\nimport { PrismaClient } from '@prisma/client';\n\nconst { Pool } = pg;\nconst connectionString = process.env.DATABASE_URL;\n\nconst pool = new Pool({ connectionString });\nconst adapter = new PrismaPg(pool);\nconst prisma = new PrismaClient({ adapter });\n\nexport default prisma;`;
|
|
191
|
+
}
|
|
192
|
+
} else if (database === 'MySQL') {
|
|
193
|
+
if (isTs) {
|
|
194
|
+
prismaContent = `import 'dotenv/config';\nimport { PrismaMariaDb } from '@prisma/adapter-mariadb';\nimport { PrismaClient } from '@prisma/client';\n\nconst adapter = new PrismaMariaDb({\n host: process.env.DATABASE_HOST || 'localhost',\n user: process.env.DATABASE_USER || 'root',\n password: process.env.DATABASE_PASSWORD || '',\n database: process.env.DATABASE_NAME || '${name}',\n connectionLimit: 5\n});\n\nconst prisma = new PrismaClient({ adapter });\n\nexport default prisma;`;
|
|
195
|
+
} else {
|
|
196
|
+
prismaContent = `import 'dotenv/config';\nimport { PrismaMariaDb } from '@prisma/adapter-mariadb';\nimport { PrismaClient } from '@prisma/client';\n\nconst adapter = new PrismaMariaDb({\n host: process.env.DATABASE_HOST || 'localhost',\n user: process.env.DATABASE_USER || 'root',\n password: process.env.DATABASE_PASSWORD || '',\n database: process.env.DATABASE_NAME || '${name}',\n connectionLimit: 5\n});\n\nconst prisma = new PrismaClient({ adapter });\n\nexport default prisma;`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await fs.writeFile(path.join(srcDir, 'lib', prismaClientFile), prismaContent);
|
|
201
|
+
|
|
202
|
+
// Generate prisma.config.ts/js
|
|
203
|
+
if (isTs) {
|
|
204
|
+
const prismaConfig = `import 'dotenv/config'\nimport { defineConfig, env } from 'prisma/config'\n\nexport default defineConfig({\n schema: 'prisma/schema.prisma',\n migrations: {\n path: 'prisma/migrations',\n },\n datasource: {\n url: env('DATABASE_URL'),\n },\n})\n`;
|
|
205
|
+
await fs.writeFile(path.join(rootDir, 'prisma.config.ts'), prismaConfig);
|
|
206
|
+
} else {
|
|
207
|
+
const prismaConfig = `import 'dotenv/config';\nimport { defineConfig, env } from 'prisma/config';\n\nexport default defineConfig({\n schema: 'prisma/schema.prisma',\n migrations: {\n path: 'prisma/migrations',\n },\n datasource: {\n url: env('DATABASE_URL'),\n },\n});\n`;
|
|
208
|
+
await fs.writeFile(path.join(rootDir, 'prisma.config.js'), prismaConfig);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Generate schema.prisma (no url)
|
|
212
|
+
const provider = database === 'PostgreSQL' ? 'postgresql' : 'mysql';
|
|
213
|
+
const schema = `datasource db {\n provider = "${provider}"\n}\n\ngenerator client {\n provider = "prisma-client-js"\n }\n\n// Add your models here\n// Example:\n// model User {\n// id Int @id @default(autoincrement())\n// email String @unique\n// name String?\n// }\n`;
|
|
214
|
+
await fs.writeFile(path.join(rootDir, 'prisma', 'schema.prisma'), schema);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 4.5 Generate Error Handling Files
|
|
218
|
+
if (errorHandler) {
|
|
219
|
+
// AppError
|
|
220
|
+
const appErrorFile = isTs ? 'AppError.ts' : 'AppError.js';
|
|
221
|
+
const appErrorContent = isTs
|
|
222
|
+
? `export class AppError extends Error {
|
|
223
|
+
public statusCode: number;
|
|
224
|
+
public status: string;
|
|
225
|
+
public isOperational: boolean;
|
|
226
|
+
|
|
227
|
+
constructor(message: string, statusCode: number) {
|
|
228
|
+
super(message);
|
|
229
|
+
this.statusCode = statusCode;
|
|
230
|
+
this.status = \`\${statusCode}\`.startsWith('4') ? 'fail' : 'error';
|
|
231
|
+
this.isOperational = true;
|
|
232
|
+
|
|
233
|
+
Error.captureStackTrace(this, this.constructor);
|
|
234
|
+
}
|
|
235
|
+
}`
|
|
236
|
+
: `export class AppError extends Error {
|
|
237
|
+
constructor(message, statusCode) {
|
|
238
|
+
super(message);
|
|
239
|
+
this.statusCode = statusCode;
|
|
240
|
+
this.status = \`\${statusCode}\`.startsWith('4') ? 'fail' : 'error';
|
|
241
|
+
this.isOperational = true;
|
|
242
|
+
|
|
243
|
+
Error.captureStackTrace(this, this.constructor);
|
|
244
|
+
}
|
|
245
|
+
}`;
|
|
246
|
+
await fs.writeFile(path.join(srcDir, 'utils', appErrorFile), appErrorContent);
|
|
247
|
+
|
|
248
|
+
// AsyncHandler
|
|
249
|
+
const asyncHandlerFile = isTs ? 'asyncHandler.ts' : 'asyncHandler.js';
|
|
250
|
+
const asyncHandlerContent = isTs
|
|
251
|
+
? `import { Request, Response, NextFunction } from 'express';
|
|
252
|
+
|
|
253
|
+
export const asyncHandler = (fn: Function) => {
|
|
254
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
255
|
+
fn(req, res, next).catch(next);
|
|
256
|
+
};
|
|
257
|
+
};`
|
|
258
|
+
: `export const asyncHandler = (fn) => {
|
|
259
|
+
return (req, res, next) => {
|
|
260
|
+
fn(req, res, next).catch(next);
|
|
261
|
+
};
|
|
262
|
+
};`;
|
|
263
|
+
await fs.writeFile(path.join(srcDir, 'utils', asyncHandlerFile), asyncHandlerContent);
|
|
264
|
+
|
|
265
|
+
// Global Error Middleware
|
|
266
|
+
const errorMiddlewareFile = isTs ? 'error.ts' : 'error.js';
|
|
267
|
+
const errorMiddlewareContent = isTs
|
|
268
|
+
? `import { Request, Response, NextFunction } from 'express';
|
|
269
|
+
import { AppError } from '../utils/AppError${useAlias ? '' : '.js'}';
|
|
270
|
+
|
|
271
|
+
export const globalErrorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
|
|
272
|
+
err.statusCode = err.statusCode || 500;
|
|
273
|
+
err.status = err.status || 'error';
|
|
274
|
+
|
|
275
|
+
if (process.env.NODE_ENV === 'development') {
|
|
276
|
+
res.status(err.statusCode).json({
|
|
277
|
+
status: err.status,
|
|
278
|
+
error: err,
|
|
279
|
+
message: err.message,
|
|
280
|
+
stack: err.stack,
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
// Production
|
|
284
|
+
if (err.isOperational) {
|
|
285
|
+
res.status(err.statusCode).json({
|
|
286
|
+
status: err.status,
|
|
287
|
+
message: err.message,
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
// Programming or other unknown error
|
|
291
|
+
console.error('ERROR 💥', err);
|
|
292
|
+
res.status(500).json({
|
|
293
|
+
status: 'error',
|
|
294
|
+
message: 'Something went very wrong!',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};`
|
|
299
|
+
: `import { AppError } from '../utils/AppError.js';
|
|
300
|
+
|
|
301
|
+
export const globalErrorHandler = (err, req, res, next) => {
|
|
302
|
+
err.statusCode = err.statusCode || 500;
|
|
303
|
+
err.status = err.status || 'error';
|
|
304
|
+
|
|
305
|
+
if (process.env.NODE_ENV === 'development') {
|
|
306
|
+
res.status(err.statusCode).json({
|
|
307
|
+
status: err.status,
|
|
308
|
+
error: err,
|
|
309
|
+
message: err.message,
|
|
310
|
+
stack: err.stack,
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
// Production
|
|
314
|
+
if (err.isOperational) {
|
|
315
|
+
res.status(err.statusCode).json({
|
|
316
|
+
status: err.status,
|
|
317
|
+
message: err.message,
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
// Programming or other unknown error
|
|
321
|
+
console.error('ERROR 💥', err);
|
|
322
|
+
res.status(500).json({
|
|
323
|
+
status: 'error',
|
|
324
|
+
message: 'Something went very wrong!',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};`;
|
|
329
|
+
await fs.writeFile(path.join(srcDir, 'middlewares', errorMiddlewareFile), errorMiddlewareContent);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 5. Generate App Entry
|
|
333
|
+
let appContent = ``;
|
|
334
|
+
|
|
335
|
+
if (isTs) {
|
|
336
|
+
appContent += `import express, { Express, Request, Response, NextFunction } from 'express';\n`;
|
|
337
|
+
appContent += `import dotenv from 'dotenv';\n`;
|
|
338
|
+
if (cors) appContent += `import cors from 'cors';\n`;
|
|
339
|
+
if (helmet) appContent += `import helmet from 'helmet';\n`;
|
|
340
|
+
if (morgan) appContent += `import morgan from 'morgan';\n`;
|
|
341
|
+
if (rateLimiter) appContent += `import rateLimit from 'express-rate-limit';\n`;
|
|
342
|
+
if (database === 'MongoDB') appContent += `import connectDB from './lib/db.js';\n`;
|
|
343
|
+
if (errorHandler) {
|
|
344
|
+
appContent += `import { globalErrorHandler } from './middlewares/error${useAlias ? '' : '.js'}';\n`;
|
|
345
|
+
appContent += `import { AppError } from './utils/AppError${useAlias ? '' : '.js'}';\n`;
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
appContent += `import express from 'express';\n`;
|
|
349
|
+
appContent += `import dotenv from 'dotenv';\n`;
|
|
350
|
+
if (cors) appContent += `import cors from 'cors';\n`;
|
|
351
|
+
if (helmet) appContent += `import helmet from 'helmet';\n`;
|
|
352
|
+
if (morgan) appContent += `import morgan from 'morgan';\n`;
|
|
353
|
+
if (rateLimiter) appContent += `import rateLimit from 'express-rate-limit';\n`;
|
|
354
|
+
if (database === 'MongoDB') appContent += `import connectDB from './lib/db.js';\n`;
|
|
355
|
+
if (errorHandler) {
|
|
356
|
+
appContent += `import { globalErrorHandler } from './middlewares/error.js';\n`;
|
|
357
|
+
appContent += `import { AppError } from './utils/AppError.js';\n`;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
appContent += `\ndotenv.config();\n\n`;
|
|
362
|
+
if (database === 'MongoDB') appContent += `connectDB();\n\n`;
|
|
363
|
+
|
|
364
|
+
appContent += isTs ? `const app: Express = express();\n` : `const app = express();\n`;
|
|
365
|
+
appContent += `const port = process.env.PORT || 3000;\n\n`;
|
|
366
|
+
|
|
367
|
+
appContent += `app.use(express.json());\n`;
|
|
368
|
+
appContent += `app.use(express.urlencoded({ extended: true }));\n`;
|
|
369
|
+
if (cors) appContent += `app.use(cors());\n`;
|
|
370
|
+
if (helmet) appContent += `app.use(helmet());\n`;
|
|
371
|
+
if (morgan) appContent += `app.use(morgan('dev'));\n`;
|
|
372
|
+
|
|
373
|
+
if (rateLimiter) {
|
|
374
|
+
appContent += `\nconst limiter = rateLimit({\n windowMs: 15 * 60 * 1000,\n max: 100\n});\n`;
|
|
375
|
+
appContent += `app.use(limiter);\n`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
appContent += `\n// Your routes will go here\n`;
|
|
379
|
+
appContent += `// Example: app.use('/api', routes);\n\n`;
|
|
380
|
+
|
|
381
|
+
appContent += `app.get('/', (req${isTs ? ': Request' : ''}, res${isTs ? ': Response' : ''}) => {\n`;
|
|
382
|
+
appContent += ` res.json({ message: 'Welcome to ${name} API' });\n`;
|
|
383
|
+
appContent += `});\n`;
|
|
384
|
+
|
|
385
|
+
appContent += `\napp.all('*', (req, res, next) => {\n`;
|
|
386
|
+
if (errorHandler) {
|
|
387
|
+
appContent += ` throw new AppError(\`Can't find \${req.originalUrl} on this server!\`, 404);\n`;
|
|
388
|
+
} else {
|
|
389
|
+
appContent += ` res.status(404).json({ status: 'fail', message: \`Can't find \${req.originalUrl} on this server!\` });\n`;
|
|
390
|
+
}
|
|
391
|
+
appContent += `});\n`;
|
|
392
|
+
|
|
393
|
+
if (errorHandler) {
|
|
394
|
+
appContent += `\napp.use(globalErrorHandler);\n`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
appContent += `\napp.listen(port, () => {\n console.log(\`[server]: Server is running at http://localhost:\${port}\`);\n});\n`;
|
|
398
|
+
|
|
399
|
+
const entryFile = isTs ? 'index.ts' : 'server.js';
|
|
400
|
+
await fs.writeFile(path.join(srcDir, entryFile), appContent);
|
|
401
|
+
|
|
402
|
+
// 6. Create .env
|
|
403
|
+
let envContent = `PORT=3000\nNODE_ENV=development\n`;
|
|
404
|
+
if (database === 'MongoDB') {
|
|
405
|
+
envContent += `MONGO_URI=mongodb://localhost:27017/${name}\n`;
|
|
406
|
+
} else if (database === 'PostgreSQL') {
|
|
407
|
+
envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${name}?schema=public"\n`;
|
|
408
|
+
} else if (database === 'MySQL') {
|
|
409
|
+
envContent += `DATABASE_URL="mysql://user:password@localhost:3306/${name}"\n`;
|
|
410
|
+
envContent += `DATABASE_HOST="localhost"\n`;
|
|
411
|
+
envContent += `DATABASE_USER="root"\n`;
|
|
412
|
+
envContent += `DATABASE_PASSWORD=""\n`;
|
|
413
|
+
envContent += `DATABASE_NAME="${name}"\n`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await fs.writeFile(path.join(rootDir, '.env'), envContent);
|
|
417
|
+
|
|
418
|
+
// 7. Create .gitignore
|
|
419
|
+
const gitIgnore = `node_modules\ndist\n.env\n`;
|
|
420
|
+
await fs.writeFile(path.join(rootDir, '.gitignore'), gitIgnore);
|
|
421
|
+
|
|
422
|
+
// 8. Create README
|
|
423
|
+
let readme = `# ${name}\n\n`;
|
|
424
|
+
readme += `## Setup\n\n\`\`\`bash\nnpm install\n\`\`\`\n\n`;
|
|
425
|
+
|
|
426
|
+
if (database === 'PostgreSQL' || database === 'MySQL') {
|
|
427
|
+
readme += `## Database Setup\n\n`;
|
|
428
|
+
readme += `1. Update the \`.env\` file with your database credentials\n`;
|
|
429
|
+
readme += `2. Run Prisma migrations:\n\n\`\`\`bash\nnpx prisma migrate dev --name init\n\`\`\`\n\n`;
|
|
430
|
+
readme += `3. Generate Prisma Client:\n\n\`\`\`bash\nnpx prisma generate\n\`\`\`\n\n`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
readme += `## Development\n\n\`\`\`bash\nnpm run dev\n\`\`\`\n\n`;
|
|
434
|
+
|
|
435
|
+
if (isTs) {
|
|
436
|
+
readme += `## Build\n\n\`\`\`bash\nnpm run build\n\`\`\`\n\n`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
readme += `## Start\n\n\`\`\`bash\nnpm start\n\`\`\`\n`;
|
|
440
|
+
|
|
441
|
+
await fs.writeFile(path.join(rootDir, 'README.md'), readme);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = { generateProject };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "express-launcher",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Simple CLI to generate Express.js projects",
|
|
5
|
+
"main": "bin/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"express-launcher": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"express",
|
|
15
|
+
"generator",
|
|
16
|
+
"scaffold",
|
|
17
|
+
"boilerplate",
|
|
18
|
+
"starter",
|
|
19
|
+
"typescript",
|
|
20
|
+
"javascript",
|
|
21
|
+
"prisma",
|
|
22
|
+
"mongodb",
|
|
23
|
+
"postgresql",
|
|
24
|
+
"mysql",
|
|
25
|
+
"express-generator",
|
|
26
|
+
"create-express-app",
|
|
27
|
+
"backend"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/NarasimhaVaddala/express-launcher.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/NarasimhaVaddala/express-launcher/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/NarasimhaVaddala/express-launcher#readme",
|
|
37
|
+
"author": "Narasimha Vaddala",
|
|
38
|
+
"license": "ISC",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"chalk": "^4.1.2",
|
|
41
|
+
"commander": "^14.0.2",
|
|
42
|
+
"fs-extra": "^11.3.3",
|
|
43
|
+
"inquirer": "^8.2.7"
|
|
44
|
+
}
|
|
45
|
+
}
|