express-ignite-cli
Version:
Powerful Express.js CLI to generate boilerplate projects with auth, swagger, testing, and more!
419 lines (357 loc) ⢠11.1 kB
JavaScript
import inquirer from "inquirer";
import fs from "fs";
import path from "path";
import chalk from "chalk";
import { execSync } from "child_process";
import { fileURLToPath } from "url";
import { dirname } from "path";
import { generateAuthModule } from "./generateAuthModule.js";
import { imp, exp } from './helper.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const basePackages = [
"express",
"morgan",
"multer",
"cors",
"bcrypt",
"jsonwebtoken",
"dotenv",
"express-rate-limit",
"nodemon",
"swagger-ui-express",
"swagger-jsdoc",
"express-validator",
"jest",
"supertest"
];
function write(filePath, content) {
fs.writeFileSync(filePath, content.trim());
console.log(chalk.green("ā Created:"), filePath);
}
function generateSwaggerFile(projectPath, ext, isESM) {
const filePath = path.join(projectPath, `configs/swagger.${ext}`);
const content = `
${imp("swaggerJsdoc", "swagger-jsdoc", isESM)}
${imp("swaggerUi", "swagger-ui-express", isESM)}
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Express API with Swagger',
version: '1.0.0',
},
},
apis: ['./routes/*.${ext}'],
};
const specs = swaggerJsdoc(options);
const setupSwagger = (app) => {
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
};
${exp("setupSwagger", isESM)}
`;
write(filePath, content);
}
async function main() {
console.log(chalk.cyan("\nš Welcome to Express App Generator\n"));
const { projectName, language, moduleSystem, db, orm } = await inquirer.prompt([
{
name: "projectName",
message: "š Project name:",
validate: (val) => !!val || "Project name is required",
},
{
type: "list",
name: "language",
// message: "š¦ Choose language:",
message: "š§ Currently we have an issue with TypeScript, so we are working on it. Please choose JavaScript for now.",
choices: ["JavaScript", "TypeScript"],
},
{
type: "list",
name: "moduleSystem",
message: "š ļø Choose module system:",
choices: [
{ name: "ECMAScript Modules (import/export)", value: "esm" },
{ name: "CommonJS (require/module.exports)", value: "cjs" },
],
},
{
type: "list",
name: "db",
message: "šļø Choose a database:",
choices: ["MongoDB", "MySQL", "PostgreSQL"],
},
{
type: "list",
name: "orm",
message: "š§ Choose ORM for SQL DB:",
when: (answers) => answers.db !== "MongoDB",
choices: ["Sequelize", "Prisma", "TypeORM"],
},
]);
const isESM = moduleSystem === "esm";
const isTS = language === "TypeScript";
const ext = isTS ? "ts" : "js";
const projectPath = path.join(process.cwd(), projectName);
console.log(chalk.green(`\nš Creating project: ${projectName}\n`));
fs.mkdirSync(projectPath);
process.chdir(projectPath);
execSync("npm init -y", { stdio: "inherit" });
// Base packages
execSync(`npm install ${basePackages.join(" ")}`, { stdio: "inherit" });
// DB Packages
if (db === "MongoDB") {
execSync("npm install mongoose", { stdio: "inherit" });
} else {
if (orm === "Sequelize") {
const driver = db === "MySQL" ? "mysql2" : "pg pg-hstore";
execSync(`npm install sequelize ${driver}`, { stdio: "inherit" });
} else if (orm === "Prisma") {
execSync("npm install prisma @prisma/client", { stdio: "inherit" });
execSync("npx prisma init", { stdio: "inherit" });
} else if (orm === "TypeORM") {
const driver = db === "MySQL" ? "mysql2" : "pg";
execSync(`npm install typeorm reflect-metadata ${driver}`, {
stdio: "inherit",
});
}
}
if (isTS) {
// execSync(
// "npm install -D typescript @types/express @types/node ts-node-dev",
// {
// stdio: "inherit",
// }
// );
execSync(
"npm install -D typescript ts-node-dev @types/node @types/express @types/morgan @types/multer @types/cors @types/bcrypt @types/jsonwebtoken @types/express-rate-limit @types/express-validator @types/jest @types/supertest",
{ stdio: "inherit" }
);
execSync("npx tsc --init", { stdio: "inherit" });
const tsConfigPath = path.join(projectPath, "tsconfig.json");
fs.writeFileSync(
tsConfigPath,
JSON.stringify(
{
compilerOptions: {
target: "ES2020",
module: isESM ? "ESNext" : "CommonJS",
moduleResolution: isESM ? "NodeNext" : "Node",
esModuleInterop: true,
forceConsistentCasingInFileNames: true,
strict: true,
skipLibCheck: true,
outDir: "dist",
resolveJsonModule: true,
},
include: ["**/*.ts"],
},
null,
2
)
);
}
// Folders
const folders = [
"controllers",
"services",
"routes",
"middlewares",
"utils",
"validations",
"configs",
"constants",
"models",
];
folders.forEach((folder) => fs.mkdirSync(path.join(projectPath, folder)));
// .env
let dbUri = "DB_URI=your_db_uri";
if (db === "MongoDB") {
dbUri = "DB_URI=mongodb://localhost:27017/your_db_name";
} else if (db === "MySQL") {
dbUri = "DB_URI=mysql://username:password@localhost:3306/your_db_name";
} else if (db === "PostgreSQL") {
dbUri = "DB_URI=postgresql://username:password@localhost:5432/your_db_name";
}
fs.writeFileSync(path.join(projectPath, ".env"), `PORT=5000\nJWT_SECRET=your_secret\n${dbUri}`);
// app.(js|ts)
fs.writeFileSync(
path.join(projectPath, `app.${ext}`),
`
${imp("express", "express", isESM)}
${imp("dotenv", "dotenv", isESM)}
${imp("cors", "cors", isESM)}
${imp("rateLimit", "express-rate-limit", isESM)}
${imp("errorHandler", `./middlewares/errorHandler.${ext}`, isESM)}
${imp("setupSwagger", `./configs/swagger.${ext}`, isESM, true)}
${imp("authRoutes", `./routes/authRoutes.${ext}`, isESM)}
dotenv.config();
const app = express();
app.use(express.json());
app.use(cors());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
// uncomment this line if you want to use db after adding the key in .env
// connectDb();
setupSwagger(app);
app.get('/', (req, res) => res.send('API is running...'));
app.use("/api/auth", authRoutes);
app.use(errorHandler);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(\`š Server running on port \${PORT}\`));
`.trim()
);
// authMiddleware
fs.writeFileSync(
path.join(projectPath, `middlewares/authMiddleware.${ext}`),
`
const protect = (req, res, next) => {
// Add your JWT auth logic here
next();
};
${exp("protect", isESM)}
`.trim()
);
// generateToken
fs.writeFileSync(
path.join(projectPath, `utils/generateToken.${ext}`),
`
${imp("jwt", "jsonwebtoken", isESM)}
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: '30d',
});
};
${exp("generateToken", isESM)}
`.trim()
);
// passwords
fs.writeFileSync(
path.join(projectPath, `utils/passwords.${ext}`),
`
${imp("bcrypt", "bcrypt", isESM)}
const hashPassword = async (password) => await bcrypt.hash(password, 10);
const comparePasswords = async (plain, hashed) => await bcrypt.compare(plain, hashed);
${exp(["hashPassword","comparePasswords"], isESM)}
`.trim()
);
// connectDb
let connectDbCode = "";
if (db === "MongoDB") {
connectDbCode = `
${imp("mongoose", "mongoose", isESM)}
const connectDb = async () => {
try {
await mongoose.connect(process.env.DB_URI);
console.log('MongoDB Connected');
} catch (err) {
console.error(err);
process.exit(1);
}
};
${exp("connectDb", isESM)}
`.trim();
} else if (orm === "Sequelize") {
connectDbCode = `
${imp("Sequelize", "sequelize", isESM)}
${imp("dotenv", "dotenv", isESM)}
dotenv.config();
const sequelize = new Sequelize(process.env.DB_URI);
${exp("sequelize", isESM)}
`.trim();
} else if (orm === "Prisma") {
connectDbCode = `
${imp("PrismaClient", "@prisma/client", isESM, true)}
const prisma = new PrismaClient();
${exp("prisma", isESM)}
`.trim();
} else if (orm === "TypeORM") {
connectDbCode = `
${imp("DataSource", "typeorm", isESM)}
${imp("dotenv", "dotenv", isESM)}
dotenv.config();
const AppDataSource = new DataSource({
type: '${db.toLowerCase()}',
url: process.env.DB_URI,
entities: [],
synchronize: true,
});
${exp("AppDataSource", isESM)}
`.trim();
}
fs.writeFileSync(
path.join(projectPath, `configs/connectDb.${ext}`),
connectDbCode
);
// ā
Generate Swagger File
generateSwaggerFile(projectPath, ext, isESM);
console.log(chalk.green("\nā
Express app created successfully!\n"));
console.log(chalk.blue(`š cd ${projectName} && npm run dev (or start)`));
console.log(chalk.yellow("\nš§ Initializing Git..."));
execSync("git init", { stdio: "inherit" });
console.log(chalk.yellow("š Creating .gitignore..."));
fs.writeFileSync(
path.join(projectPath, ".gitignore"),
`
node_modules
.env
dist
build
.DS_Store`.trim()
);
console.log(chalk.yellow("š Creating README.md..."));
fs.writeFileSync(
path.join(projectPath, "README.md"),
`
# ${projectName}
Generated with \`create-express-cli\` š
## š¦ Stack
- Language: ${language}
- Database: ${db}
- ${db !== "MongoDB" ? `ORM: ${orm}` : "ODM: Mongoose"}
## š Project Structure
\`\`\`
.
āāā app.${ext}
āāā controllers/
āāā services/
āāā routes/
āāā middlewares/
āāā utils/
āāā configs/
āāā validations/
āāā constants/
āāā models/
āāā .env
\`\`\`
## š Scripts
\`\`\`bash
npm run dev # Start dev server
npm run build # TypeScript build (TS only)
npm start # Start production server
\`\`\`
## š Docs
Visit: http://localhost:5000/api-docs
`.trim()
);
console.log(chalk.yellow("š§ Adding scripts to package.json..."));
const pkgPath = path.join(projectPath, "package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
pkg.scripts = {
dev: isTS
? isESM
? "ts-node-dev --respawn --loader ts-node/esm app.ts"
: "ts-node-dev --respawn app.ts"
: "nodemon app.js",
...(isTS ? { build: "tsc" } : {}),
start: isTS ? "node dist/app.js" : "node app.js",
test: "jest",
};
pkg.type = isESM ? "module" : "commonjs";
// if (isESM) pkg.type = "module";
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
console.log(chalk.yellow("\nš Generating Auth Module..."));
generateAuthModule(projectPath, isTS, db, orm, isESM);
}
main();