boilerbase 1.0.1
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 +111 -0
- package/generators/createAuthFiles.js +13 -0
- package/generators/createEnvFile.js +3 -0
- package/generators/createFiles.js +71 -0
- package/generators/createFolders.js +20 -0
- package/generators/createMongoConfig.js +3 -0
- package/generators/createPackageJson.js +3 -0
- package/generators/createProject.js +35 -0
- package/generators/createPublicFiles.js +9 -0
- package/generators/createReadme.js +3 -0
- package/generators/createServerFile.js +3 -0
- package/generators/createViews.js +11 -0
- package/generators/installDependencies.js +22 -0
- package/generators/openVSCode.js +18 -0
- package/generators/templates.js +260 -0
- package/package.json +28 -0
- package/templates/authTemplate.js +11 -0
- package/templates/cssTemplate.js +5 -0
- package/templates/dbTemplate.js +3 -0
- package/templates/envTemplate.js +3 -0
- package/templates/modelTemplate.js +3 -0
- package/templates/packageTemplate.js +3 -0
- package/templates/readmeTemplate.js +3 -0
- package/templates/routeTemplate.js +9 -0
- package/templates/serverTemplate.js +3 -0
- package/templates/viewTemplate.js +11 -0
- package/utils/constants.js +23 -0
- package/utils/helper.js +29 -0
- package/utils/pathUtils.js +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# BoilerBase
|
|
2
|
+
|
|
3
|
+
A modern beginner-friendly backend scaffolding CLI for Node.js developers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm link
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx boilerbase
|
|
16
|
+
# or
|
|
17
|
+
boilerbase
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
- Prompts for project settings in the terminal
|
|
23
|
+
- Generates folders and starter files dynamically
|
|
24
|
+
- Installs dependencies automatically
|
|
25
|
+
- Opens the generated project in VS Code
|
|
26
|
+
|
|
27
|
+
## Stack
|
|
28
|
+
|
|
29
|
+
- Node.js
|
|
30
|
+
- inquirer
|
|
31
|
+
- chalk
|
|
32
|
+
- ora
|
|
33
|
+
- fs-extra
|
|
34
|
+
|
|
35
|
+
## Generated project
|
|
36
|
+
|
|
37
|
+
The generated backend starter includes:
|
|
38
|
+
|
|
39
|
+
- Express server scaffold
|
|
40
|
+
- Optional MongoDB config
|
|
41
|
+
- JWT auth routes and middleware
|
|
42
|
+
- EJS or HTML views
|
|
43
|
+
- CSS starter file and public assets
|
|
44
|
+
|
|
45
|
+
## Publish
|
|
46
|
+
|
|
47
|
+
This package is ready for `npm link`, `npm publish`, and `npx boilerbase`.
|
|
48
|
+
|
|
49
|
+
If your npm account has 2FA enabled, publish with an OTP:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm publish --otp=123456
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or create a granular access token with `bypass 2fa` enabled and use that token for publishing.
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const inquirer = require("inquirer");
|
|
5
|
+
const chalk = require("chalk");
|
|
6
|
+
const ora = require("ora");
|
|
7
|
+
|
|
8
|
+
const createProject = require("../generators/createProject");
|
|
9
|
+
const installDependencies = require("../generators/installDependencies");
|
|
10
|
+
const openVSCode = require("../generators/openVSCode");
|
|
11
|
+
const { sanitizeProjectName } = require("../utils/pathUtils");
|
|
12
|
+
|
|
13
|
+
const prompts = [
|
|
14
|
+
{
|
|
15
|
+
type: "input",
|
|
16
|
+
name: "projectName",
|
|
17
|
+
message: "Project name",
|
|
18
|
+
default: "my-backend-app",
|
|
19
|
+
validate: (value) => (value.trim() ? true : "Project name is required."),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: "list",
|
|
23
|
+
name: "databaseChoice",
|
|
24
|
+
message: "Select database",
|
|
25
|
+
choices: ["MongoDB", "None"],
|
|
26
|
+
default: "MongoDB",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "list",
|
|
30
|
+
name: "authenticationType",
|
|
31
|
+
message: "Select authentication",
|
|
32
|
+
choices: ["JWT", "None"],
|
|
33
|
+
default: "JWT",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "list",
|
|
37
|
+
name: "templateEngine",
|
|
38
|
+
message: "Select template engine",
|
|
39
|
+
choices: ["EJS", "HTML"],
|
|
40
|
+
default: "EJS",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: "list",
|
|
44
|
+
name: "cssFramework",
|
|
45
|
+
message: "Select CSS framework",
|
|
46
|
+
choices: ["Vanilla CSS", "Bootstrap"],
|
|
47
|
+
default: "Vanilla CSS",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: "confirm",
|
|
51
|
+
name: "openInVSCode",
|
|
52
|
+
message: "Open the generated folder in another VS Code Tab?",
|
|
53
|
+
default: true,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const run = async () => {
|
|
58
|
+
console.log(chalk.green("\n✔ Welcome to BoilerBase\n"));
|
|
59
|
+
|
|
60
|
+
const answers = await inquirer.prompt(prompts);
|
|
61
|
+
const projectName = sanitizeProjectName(answers.projectName);
|
|
62
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
63
|
+
|
|
64
|
+
const generationSpinner = ora("Generating folders and files...").start();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await createProject({
|
|
68
|
+
projectName: answers.projectName,
|
|
69
|
+
displayProjectName: answers.projectName.trim(),
|
|
70
|
+
databaseChoice: answers.databaseChoice,
|
|
71
|
+
authenticationType: answers.authenticationType,
|
|
72
|
+
templateEngine: answers.templateEngine,
|
|
73
|
+
cssFramework: answers.cssFramework,
|
|
74
|
+
includeMVCStructure: true,
|
|
75
|
+
includeAuthSystem: answers.authenticationType === "JWT",
|
|
76
|
+
includeREADME: true,
|
|
77
|
+
targetDirectory: process.cwd(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
generationSpinner.succeed("Generating folders and files...");
|
|
81
|
+
|
|
82
|
+
const installSpinner = ora("Installing dependencies...").start();
|
|
83
|
+
await installDependencies(projectPath);
|
|
84
|
+
installSpinner.succeed("Installing dependencies...");
|
|
85
|
+
|
|
86
|
+
if (answers.openInVSCode) {
|
|
87
|
+
const openSpinner = ora("Opening VS Code...").start();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await openVSCode(projectPath);
|
|
91
|
+
openSpinner.succeed("Opening VS Code...");
|
|
92
|
+
} catch (error) {
|
|
93
|
+
openSpinner.warn("Opening VS Code skipped");
|
|
94
|
+
console.log(chalk.yellow(`\n! ${error.message}`));
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
console.log(chalk.yellow("\n! Skipped opening VS Code."));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(chalk.green("\n✔ Project ready successfully"));
|
|
101
|
+
console.log(chalk.cyan("\nNext steps:"));
|
|
102
|
+
console.log(chalk.white(`cd ${projectName}`));
|
|
103
|
+
console.log(chalk.white("npm run dev\n"));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
generationSpinner.fail("Generation failed");
|
|
106
|
+
console.error(chalk.red(`\n✖ ${error.message}`));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
run();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const {
|
|
2
|
+
buildAuthMiddlewareTemplate,
|
|
3
|
+
buildAuthControllerTemplate,
|
|
4
|
+
buildAuthRoutesTemplate,
|
|
5
|
+
buildUserModelTemplate,
|
|
6
|
+
} = require("./templates");
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
buildAuthMiddlewareTemplate,
|
|
10
|
+
buildAuthControllerTemplate,
|
|
11
|
+
buildAuthRoutesTemplate,
|
|
12
|
+
buildUserModelTemplate,
|
|
13
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
buildPackageTemplate,
|
|
6
|
+
buildEnvTemplate,
|
|
7
|
+
buildDbTemplate,
|
|
8
|
+
buildHomeControllerTemplate,
|
|
9
|
+
buildIndexRouteTemplate,
|
|
10
|
+
buildServerTemplate,
|
|
11
|
+
buildAuthMiddlewareTemplate,
|
|
12
|
+
buildUserModelTemplate,
|
|
13
|
+
buildAuthControllerTemplate,
|
|
14
|
+
buildAuthRoutesTemplate,
|
|
15
|
+
buildIndexViewTemplate,
|
|
16
|
+
buildLoginViewTemplate,
|
|
17
|
+
buildRegisterViewTemplate,
|
|
18
|
+
buildReadmeTemplate,
|
|
19
|
+
buildPublicCssTemplate,
|
|
20
|
+
buildPublicJsTemplate,
|
|
21
|
+
getViewExtension,
|
|
22
|
+
isAuthEnabled,
|
|
23
|
+
isMongoDB,
|
|
24
|
+
isMVC,
|
|
25
|
+
} = require("./templates");
|
|
26
|
+
|
|
27
|
+
const writeFile = async (filePath, contents) => {
|
|
28
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
29
|
+
await fs.writeFile(filePath, contents, "utf8");
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const createFiles = async (projectPath, options) => {
|
|
33
|
+
const viewExtension = getViewExtension(options);
|
|
34
|
+
|
|
35
|
+
await writeFile(path.join(projectPath, "server.js"), buildServerTemplate(options));
|
|
36
|
+
await writeFile(path.join(projectPath, "package.json"), buildPackageTemplate(options));
|
|
37
|
+
await writeFile(path.join(projectPath, ".env"), buildEnvTemplate(options));
|
|
38
|
+
await writeFile(path.join(projectPath, "config", "db.js"), buildDbTemplate(options));
|
|
39
|
+
await writeFile(path.join(projectPath, "public", "css", "style.css"), buildPublicCssTemplate(options));
|
|
40
|
+
await writeFile(path.join(projectPath, "public", "js", "main.js"), buildPublicJsTemplate(options));
|
|
41
|
+
|
|
42
|
+
if (isMVC(options)) {
|
|
43
|
+
await writeFile(path.join(projectPath, "controllers", "homeController.js"), buildHomeControllerTemplate(options));
|
|
44
|
+
await writeFile(path.join(projectPath, "routes", "index.js"), buildIndexRouteTemplate(options));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isAuthEnabled(options)) {
|
|
48
|
+
await writeFile(path.join(projectPath, "middleware", "authMiddleware.js"), buildAuthMiddlewareTemplate(options));
|
|
49
|
+
await writeFile(path.join(projectPath, "controllers", "authController.js"), buildAuthControllerTemplate(options));
|
|
50
|
+
await writeFile(path.join(projectPath, "routes", "authRoutes.js"), buildAuthRoutesTemplate(options));
|
|
51
|
+
|
|
52
|
+
if (isMongoDB(options)) {
|
|
53
|
+
await writeFile(path.join(projectPath, "models", "User.js"), buildUserModelTemplate(options));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await writeFile(path.join(projectPath, "views", `index.${viewExtension}`), buildIndexViewTemplate(options));
|
|
58
|
+
|
|
59
|
+
if (isAuthEnabled(options)) {
|
|
60
|
+
await writeFile(path.join(projectPath, "views", `login.${viewExtension}`), buildLoginViewTemplate(options));
|
|
61
|
+
await writeFile(path.join(projectPath, "views", `register.${viewExtension}`), buildRegisterViewTemplate(options));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.includeREADME) {
|
|
65
|
+
await writeFile(path.join(projectPath, "README.md"), buildReadmeTemplate(options));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return projectPath;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
module.exports = createFiles;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const { PROJECT_ROOT_FOLDERS } = require("../utils/constants");
|
|
5
|
+
|
|
6
|
+
const createFolders = async (projectPath, options = {}) => {
|
|
7
|
+
const folders = [...PROJECT_ROOT_FOLDERS];
|
|
8
|
+
|
|
9
|
+
for (const folder of folders) {
|
|
10
|
+
await fs.ensureDir(path.join(projectPath, folder));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (options.includeMVCStructure) {
|
|
14
|
+
await fs.ensureDir(path.join(projectPath, "config"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return projectPath;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = createFolders;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
|
|
3
|
+
const createFolders = require("./createFolders");
|
|
4
|
+
const createFiles = require("./createFiles");
|
|
5
|
+
const { getProjectPath, sanitizeProjectName } = require("../utils/pathUtils");
|
|
6
|
+
|
|
7
|
+
const createProject = async (options) => {
|
|
8
|
+
const displayProjectName = options.projectName.trim();
|
|
9
|
+
const projectName = sanitizeProjectName(displayProjectName);
|
|
10
|
+
const targetDirectory = options.targetDirectory || process.cwd();
|
|
11
|
+
const projectPath = getProjectPath(targetDirectory, projectName);
|
|
12
|
+
|
|
13
|
+
if (await fs.pathExists(projectPath)) {
|
|
14
|
+
const contents = await fs.readdir(projectPath);
|
|
15
|
+
|
|
16
|
+
if (contents.length > 0) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`The folder "${projectName}" already exists and is not empty.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await fs.ensureDir(projectPath);
|
|
24
|
+
|
|
25
|
+
await createFolders(projectPath, options);
|
|
26
|
+
await createFiles(projectPath, {
|
|
27
|
+
...options,
|
|
28
|
+
projectName,
|
|
29
|
+
displayProjectName,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return projectPath;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
module.exports = createProject;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
|
|
3
|
+
const installDependencies = (projectPath) =>
|
|
4
|
+
new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn("npm", ["install"], {
|
|
6
|
+
cwd: projectPath,
|
|
7
|
+
stdio: "inherit",
|
|
8
|
+
shell: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
child.on("error", reject);
|
|
12
|
+
child.on("close", (code) => {
|
|
13
|
+
if (code === 0) {
|
|
14
|
+
resolve();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
reject(new Error(`npm install failed with exit code ${code}`));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
module.exports = installDependencies;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
|
|
3
|
+
const openVSCode = (projectPath) =>
|
|
4
|
+
new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn("code", [projectPath], {
|
|
6
|
+
stdio: "ignore",
|
|
7
|
+
shell: true,
|
|
8
|
+
detached: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
child.once("error", reject);
|
|
12
|
+
child.once("spawn", () => {
|
|
13
|
+
child.unref();
|
|
14
|
+
resolve();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
module.exports = openVSCode;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const { sanitizeProjectName, toPackageName } = require("../utils/pathUtils");
|
|
2
|
+
|
|
3
|
+
const isMongoDB = (options) => options.databaseChoice === "MongoDB";
|
|
4
|
+
const isAuthEnabled = (options) =>
|
|
5
|
+
options.includeAuthSystem && options.authenticationType === "JWT";
|
|
6
|
+
const isEJS = (options) => options.templateEngine === "EJS";
|
|
7
|
+
const isBootstrap = (options) => options.cssFramework === "Bootstrap";
|
|
8
|
+
const isMVC = (options) => options.includeMVCStructure;
|
|
9
|
+
|
|
10
|
+
const getViewExtension = (options) => (isEJS(options) ? "ejs" : "html");
|
|
11
|
+
|
|
12
|
+
const buildPackageTemplate = (options) => {
|
|
13
|
+
const dependencies = {
|
|
14
|
+
express: "^5.1.0",
|
|
15
|
+
dotenv: "^16.5.0",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (isEJS(options)) dependencies.ejs = "^3.1.10";
|
|
19
|
+
if (isMongoDB(options)) dependencies.mongoose = "^8.15.0";
|
|
20
|
+
if (isAuthEnabled(options)) {
|
|
21
|
+
dependencies.jsonwebtoken = "^9.0.2";
|
|
22
|
+
dependencies.bcryptjs = "^3.0.2";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return JSON.stringify(
|
|
26
|
+
{
|
|
27
|
+
name: toPackageName(options.projectName),
|
|
28
|
+
version: "1.0.0",
|
|
29
|
+
description: `Starter project generated for ${options.displayProjectName || options.projectName}`,
|
|
30
|
+
main: "server.js",
|
|
31
|
+
scripts: {
|
|
32
|
+
start: "node server.js",
|
|
33
|
+
dev: "nodemon server.js",
|
|
34
|
+
},
|
|
35
|
+
dependencies,
|
|
36
|
+
devDependencies: {
|
|
37
|
+
nodemon: "^3.1.10",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
null,
|
|
41
|
+
2,
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const buildEnvTemplate = (options) => {
|
|
46
|
+
const lines = ["PORT=3000", "NODE_ENV=development"];
|
|
47
|
+
|
|
48
|
+
if (isMongoDB(options)) {
|
|
49
|
+
lines.push(
|
|
50
|
+
`MONGO_URI=mongodb://127.0.0.1:27017/${toPackageName(options.projectName)}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isAuthEnabled(options)) {
|
|
55
|
+
lines.push("JWT_SECRET=replace-with-a-strong-secret");
|
|
56
|
+
lines.push("JWT_EXPIRES_IN=7d");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return `${lines.join("\n")}\n`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const buildDbTemplate = (options) => {
|
|
63
|
+
if (!isMongoDB(options)) {
|
|
64
|
+
return `module.exports = async function connectDB() {\n console.log('Database not configured. Set up your own data layer when ready.');\n};\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return `const mongoose = require('mongoose');\n\nconst connectDB = async () => {\n try {\n await mongoose.connect(process.env.MONGO_URI);\n console.log('MongoDB connected successfully');\n } catch (error) {\n console.error('MongoDB connection error:', error.message);\n process.exit(1);\n }\n};\n\nmodule.exports = connectDB;\n`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const buildHomeControllerTemplate = (options) => {
|
|
71
|
+
if (isEJS(options)) {
|
|
72
|
+
return `const renderHome = (req, res) => {\n res.render('index', {\n pageTitle: 'BoilerBase',\n featureLabel: 'Developer-ready starter project',\n });\n};\n\nmodule.exports = {\n renderHome,\n};\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `const path = require('path');\n\nconst renderHome = (req, res) => {\n res.sendFile(path.join(__dirname, '../views/index.html'));\n};\n\nmodule.exports = {\n renderHome,\n};\n`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const buildIndexRouteTemplate = () => {
|
|
79
|
+
return `const express = require('express');\nconst { renderHome } = require('../controllers/homeController');\n\nconst router = express.Router();\n\nrouter.get('/', renderHome);\n\nmodule.exports = router;\n`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildServerTemplate = (options) => {
|
|
83
|
+
const lines = [
|
|
84
|
+
"require('dotenv').config();",
|
|
85
|
+
"const path = require('path');",
|
|
86
|
+
"const express = require('express');",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
if (isMongoDB(options)) {
|
|
90
|
+
lines.push("const connectDB = require('./config/db');");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isMVC(options)) {
|
|
94
|
+
lines.push("const homeRoutes = require('./routes/index');");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isAuthEnabled(options)) {
|
|
98
|
+
lines.push("const authRoutes = require('./routes/authRoutes');");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push(
|
|
102
|
+
"",
|
|
103
|
+
"const app = express();",
|
|
104
|
+
"const PORT = process.env.PORT || 3000;",
|
|
105
|
+
"",
|
|
106
|
+
"app.use(express.urlencoded({ extended: true }));",
|
|
107
|
+
"app.use(express.json());",
|
|
108
|
+
"app.use(express.static(path.join(__dirname, 'public')));",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (isEJS(options)) {
|
|
112
|
+
lines.push(
|
|
113
|
+
"app.set('view engine', 'ejs');",
|
|
114
|
+
"app.set('views', path.join(__dirname, 'views'));",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isMongoDB(options)) {
|
|
119
|
+
lines.push("", "connectDB();");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isMVC(options)) {
|
|
123
|
+
lines.push("", "app.use('/', homeRoutes);");
|
|
124
|
+
} else if (isEJS(options)) {
|
|
125
|
+
lines.push(
|
|
126
|
+
"",
|
|
127
|
+
"app.get('/', (req, res) => {",
|
|
128
|
+
" res.render('index', {",
|
|
129
|
+
" pageTitle: 'BoilerBase',",
|
|
130
|
+
" featureLabel: 'Developer-ready starter project',",
|
|
131
|
+
" });",
|
|
132
|
+
"});",
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
lines.push(
|
|
136
|
+
"",
|
|
137
|
+
"app.get('/', (req, res) => {",
|
|
138
|
+
" res.sendFile(path.join(__dirname, 'views', 'index.html'));",
|
|
139
|
+
"});",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isAuthEnabled(options)) {
|
|
144
|
+
lines.push("app.use('/auth', authRoutes);");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lines.push(
|
|
148
|
+
"",
|
|
149
|
+
"app.get('/health', (req, res) => {",
|
|
150
|
+
" res.json({",
|
|
151
|
+
" status: 'ok',",
|
|
152
|
+
` project: '${sanitizeProjectName(options.projectName)}',`,
|
|
153
|
+
" });",
|
|
154
|
+
"});",
|
|
155
|
+
"",
|
|
156
|
+
"app.listen(PORT, () => {",
|
|
157
|
+
" console.log('Server running on port ' + PORT);",
|
|
158
|
+
"});",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return `${lines.join("\n")}\n`;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const buildAuthMiddlewareTemplate = () => {
|
|
165
|
+
return `const jwt = require('jsonwebtoken');\n\nconst protectRoute = (req, res, next) => {\n const authHeader = req.headers.authorization || '';\n const token = authHeader.startsWith('Bearer ') ? authHeader.split(' ')[1] : null;\n\n if (!token) {\n return res.status(401).json({ message: 'Authentication required.' });\n }\n\n try {\n req.user = jwt.verify(token, process.env.JWT_SECRET);\n return next();\n } catch (error) {\n return res.status(401).json({ message: 'Invalid or expired token.' });\n }\n};\n\nmodule.exports = {\n protectRoute,\n};\n`;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const buildUserModelTemplate = () => {
|
|
169
|
+
return `const mongoose = require('mongoose');\n\nconst userSchema = new mongoose.Schema(\n {\n name: { type: String, required: true },\n email: { type: String, required: true, unique: true },\n password: { type: String, required: true },\n },\n { timestamps: true }\n);\n\nmodule.exports = mongoose.model('User', userSchema);\n`;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const buildAuthControllerTemplate = (options) => {
|
|
173
|
+
if (isMongoDB(options)) {
|
|
174
|
+
return `const bcrypt = require('bcryptjs');\nconst jwt = require('jsonwebtoken');\nconst User = require('../models/User');\n\nconst registerUser = async (req, res) => {\n try {\n const { name, email, password } = req.body;\n const existingUser = await User.findOne({ email });\n\n if (existingUser) {\n return res.status(400).json({ message: 'User already exists.' });\n }\n\n const hashedPassword = await bcrypt.hash(password, 10);\n const user = await User.create({ name, email, password: hashedPassword });\n\n return res.status(201).json({\n message: 'User registered successfully.',\n user: {\n id: user._id,\n name: user.name,\n email: user.email,\n },\n });\n } catch (error) {\n return res.status(500).json({ message: error.message });\n }\n};\n\nconst loginUser = async (req, res) => {\n try {\n const { email, password } = req.body;\n const user = await User.findOne({ email });\n\n if (!user) {\n return res.status(404).json({ message: 'User not found.' });\n }\n\n const passwordMatches = await bcrypt.compare(password, user.password);\n\n if (!passwordMatches) {\n return res.status(401).json({ message: 'Invalid credentials.' });\n }\n\n const token = jwt.sign(\n { id: user._id, email: user.email },\n process.env.JWT_SECRET,\n { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }\n );\n\n return res.json({ token });\n } catch (error) {\n return res.status(500).json({ message: error.message });\n }\n};\n\nmodule.exports = {\n registerUser,\n loginUser,\n};\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return `const bcrypt = require('bcryptjs');\nconst jwt = require('jsonwebtoken');\n\nconst demoUsers = [];\n\nconst registerUser = async (req, res) => {\n try {\n const { name, email, password } = req.body;\n const existingUser = demoUsers.find((user) => user.email === email);\n\n if (existingUser) {\n return res.status(400).json({ message: 'User already exists.' });\n }\n\n const hashedPassword = await bcrypt.hash(password, 10);\n const user = {\n id: Date.now().toString(),\n name,\n email,\n password: hashedPassword,\n };\n\n demoUsers.push(user);\n\n return res.status(201).json({\n message: 'User registered successfully.',\n user: {\n id: user.id,\n name: user.name,\n email: user.email,\n },\n });\n } catch (error) {\n return res.status(500).json({ message: error.message });\n }\n};\n\nconst loginUser = async (req, res) => {\n try {\n const { email, password } = req.body;\n const user = demoUsers.find((entry) => entry.email === email);\n\n if (!user) {\n return res.status(404).json({ message: 'User not found.' });\n }\n\n const passwordMatches = await bcrypt.compare(password, user.password);\n\n if (!passwordMatches) {\n return res.status(401).json({ message: 'Invalid credentials.' });\n }\n\n const token = jwt.sign(\n { id: user.id, email: user.email },\n process.env.JWT_SECRET,\n { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }\n );\n\n return res.json({ token });\n } catch (error) {\n return res.status(500).json({ message: error.message });\n }\n};\n\nmodule.exports = {\n registerUser,\n loginUser,\n};\n`;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const buildAuthRoutesTemplate = () => {
|
|
181
|
+
return `const express = require('express');\nconst { registerUser, loginUser } = require('../controllers/authController');\nconst { protectRoute } = require('../middleware/authMiddleware');\n\nconst router = express.Router();\n\nrouter.post('/register', registerUser);\nrouter.post('/login', loginUser);\nrouter.get('/profile', protectRoute, (req, res) => {\n res.json({\n message: 'Protected route reached successfully.',\n user: req.user,\n });\n});\n\nmodule.exports = router;\n`;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const buildIndexViewTemplate = (options) => {
|
|
185
|
+
const bootstrapLink = isBootstrap(options)
|
|
186
|
+
? ` <link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css' rel='stylesheet'>\n`
|
|
187
|
+
: "";
|
|
188
|
+
const actionCopy = isEJS(options)
|
|
189
|
+
? `<span><%= featureLabel %></span>`
|
|
190
|
+
: `<span>Developer-ready starter project</span>`;
|
|
191
|
+
|
|
192
|
+
if (isEJS(options)) {
|
|
193
|
+
return `<!DOCTYPE html>\n<html lang='en'>\n<head>\n <meta charset='UTF-8'>\n <meta name='viewport' content='width=device-width, initial-scale=1.0'>\n <title><%= pageTitle %></title>\n${bootstrapLink} <link rel='stylesheet' href='/css/style.css'>\n</head>\n<body>\n <main class='page-shell'>\n <section class='hero-card'>\n <div class='hero-badge'>BoilerBase Starter</div>\n <h1>Project scaffold ready in seconds.</h1>\n <p>${actionCopy}</p>\n <div class='hero-grid'>\n <article>\n <strong>MongoDB-ready</strong>\n <span>Config, models, and environment setup included.</span>\n </article>\n <article>\n <strong>Auth-friendly</strong>\n <span>JWT routes and middleware are scaffolded when enabled.</span>\n </article>\n <article>\n <strong>Responsive UI</strong>\n <span>Built with a polished, mobile-ready starter layout.</span>\n </article>\n </div>\n </section>\n </main>\n <script src='/js/main.js'></script>\n</body>\n</html>\n`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return `<!DOCTYPE html>\n<html lang='en'>\n<head>\n <meta charset='UTF-8'>\n <meta name='viewport' content='width=device-width, initial-scale=1.0'>\n <title>BoilerBase</title>\n${bootstrapLink} <link rel='stylesheet' href='/css/style.css'>\n</head>\n<body>\n <main class='page-shell'>\n <section class='hero-card'>\n <div class='hero-badge'>BoilerBase Starter</div>\n <h1>Project scaffold ready in seconds.</h1>\n <p>Developer-ready starter project</p>\n <div class='hero-grid'>\n <article>\n <strong>MongoDB-ready</strong>\n <span>Config, models, and environment setup included.</span>\n </article>\n <article>\n <strong>Auth-friendly</strong>\n <span>JWT routes and middleware are scaffolded when enabled.</span>\n </article>\n <article>\n <strong>Responsive UI</strong>\n <span>Built with a polished, mobile-ready starter layout.</span>\n </article>\n </div>\n </section>\n </main>\n <script src='/js/main.js'></script>\n</body>\n</html>\n`;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const buildLoginViewTemplate = (options) => {
|
|
200
|
+
if (!isEJS(options)) {
|
|
201
|
+
return `<!DOCTYPE html>\n<html lang='en'>\n<head>\n <meta charset='UTF-8'>\n <meta name='viewport' content='width=device-width, initial-scale=1.0'>\n <title>Login</title>\n <link rel='stylesheet' href='/css/style.css'>\n</head>\n<body>\n <section class='auth-card'>\n <h1>Login</h1>\n <p>Connect this form to your auth API.</p>\n </section>\n</body>\n</html>\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return `<section class='auth-card'>\n <h1>Login</h1>\n <p>Connect this form to your auth API.</p>\n</section>\n`;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const buildRegisterViewTemplate = (options) => {
|
|
208
|
+
if (!isEJS(options)) {
|
|
209
|
+
return `<!DOCTYPE html>\n<html lang='en'>\n<head>\n <meta charset='UTF-8'>\n <meta name='viewport' content='width=device-width, initial-scale=1.0'>\n <title>Register</title>\n <link rel='stylesheet' href='/css/style.css'>\n</head>\n<body>\n <section class='auth-card'>\n <h1>Register</h1>\n <p>Connect this form to your auth API.</p>\n </section>\n</body>\n</html>\n`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return `<section class='auth-card'>\n <h1>Register</h1>\n <p>Connect this form to your auth API.</p>\n</section>\n`;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const buildReadmeTemplate = (options) => {
|
|
216
|
+
const features = [
|
|
217
|
+
"- Express server scaffold",
|
|
218
|
+
`- ${options.databaseChoice} data layer`,
|
|
219
|
+
`- ${options.authenticationType} authentication scaffold`,
|
|
220
|
+
`- ${options.templateEngine} templating`,
|
|
221
|
+
`- ${options.cssFramework} styling`,
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
return `# ${options.displayProjectName || options.projectName}\n\nGenerated by BoilerBase CLI.\n\n## Included\n${features.join("\n")}\n\n## Run\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n`;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const buildPublicCssTemplate = (options) => {
|
|
228
|
+
const frameworkAccent = isBootstrap(options) ? "#7dd3fc" : "#8b5cf6";
|
|
229
|
+
|
|
230
|
+
return `:root {\n color-scheme: dark;\n --bg: #07111f;\n --panel: rgba(12, 18, 33, 0.88);\n --panel-border: rgba(148, 163, 184, 0.18);\n --text: #e2e8f0;\n --muted: #94a3b8;\n --accent: ${frameworkAccent};\n --accent-2: #22d3ee;\n --shadow: 0 30px 80px rgba(0, 0, 0, 0.35);\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n min-height: 100vh;\n font-family: Inter, Segoe UI, Arial, sans-serif;\n color: var(--text);\n background:\n radial-gradient(circle at top left, rgba(34, 211, 238, 0.16), transparent 28%),\n radial-gradient(circle at top right, rgba(139, 92, 246, 0.18), transparent 24%),\n linear-gradient(180deg, #040812 0%, #07111f 100%);\n}\n\nbody::before {\n content: '';\n position: fixed;\n inset: 0;\n background-image: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);\n background-size: 36px 36px;\n pointer-events: none;\n opacity: 0.35;\n}\n\n.page-shell {\n position: relative;\n z-index: 1;\n min-height: 100vh;\n display: grid;\n place-items: center;\n padding: 40px 20px;\n}\n\n.hero-card,\n.auth-card {\n width: min(1100px, 100%);\n background: var(--panel);\n border: 1px solid var(--panel-border);\n box-shadow: var(--shadow);\n backdrop-filter: blur(20px);\n border-radius: 28px;\n padding: clamp(28px, 5vw, 56px);\n}\n\n.hero-badge {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 10px 14px;\n border-radius: 999px;\n background: rgba(34, 211, 238, 0.12);\n color: #cffafe;\n border: 1px solid rgba(34, 211, 238, 0.22);\n font-size: 0.85rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\nh1 {\n margin: 20px 0 12px;\n font-size: clamp(2.4rem, 5vw, 4.8rem);\n line-height: 0.95;\n}\n\np {\n margin: 0;\n max-width: 60ch;\n color: var(--muted);\n font-size: 1.05rem;\n}\n\n.hero-grid {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 18px;\n margin-top: 32px;\n}\n\n.hero-grid article {\n padding: 20px;\n border-radius: 20px;\n background: rgba(15, 23, 42, 0.66);\n border: 1px solid rgba(148, 163, 184, 0.14);\n}\n\n.hero-grid strong {\n display: block;\n margin-bottom: 8px;\n color: white;\n}\n\n.hero-grid span {\n color: var(--muted);\n line-height: 1.6;\n}\n\n@media (max-width: 900px) {\n .hero-grid {\n grid-template-columns: 1fr;\n }\n}\n`;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const buildPublicJsTemplate = () => {
|
|
234
|
+
return `document.addEventListener('DOMContentLoaded', () => {\n document.body.classList.add('is-ready');\n\n const cards = document.querySelectorAll('.hero-grid article, .form-card, .stats-card');\n cards.forEach((card, index) => {\n card.style.animationDelay = String(index * 90) + 'ms';\n card.classList.add('reveal');\n });\n});\n`;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
buildPackageTemplate,
|
|
239
|
+
buildEnvTemplate,
|
|
240
|
+
buildDbTemplate,
|
|
241
|
+
buildHomeControllerTemplate,
|
|
242
|
+
buildIndexRouteTemplate,
|
|
243
|
+
buildServerTemplate,
|
|
244
|
+
buildAuthMiddlewareTemplate,
|
|
245
|
+
buildUserModelTemplate,
|
|
246
|
+
buildAuthControllerTemplate,
|
|
247
|
+
buildAuthRoutesTemplate,
|
|
248
|
+
buildIndexViewTemplate,
|
|
249
|
+
buildLoginViewTemplate,
|
|
250
|
+
buildRegisterViewTemplate,
|
|
251
|
+
buildReadmeTemplate,
|
|
252
|
+
buildPublicCssTemplate,
|
|
253
|
+
buildPublicJsTemplate,
|
|
254
|
+
getViewExtension,
|
|
255
|
+
isMongoDB,
|
|
256
|
+
isAuthEnabled,
|
|
257
|
+
isEJS,
|
|
258
|
+
isBootstrap,
|
|
259
|
+
isMVC,
|
|
260
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "boilerbase",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A minimal developer CLI for generating backend starter projects",
|
|
5
|
+
"main": "./bin/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"boilerbase": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"generators",
|
|
15
|
+
"templates",
|
|
16
|
+
"utils",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node ./bin/index.js"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chalk": "^4.1.2",
|
|
24
|
+
"fs-extra": "^11.3.0",
|
|
25
|
+
"inquirer": "^8.2.7",
|
|
26
|
+
"ora": "^5.4.1"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const PROJECT_ROOT_FOLDERS = [
|
|
2
|
+
"config",
|
|
3
|
+
"controllers",
|
|
4
|
+
"middleware",
|
|
5
|
+
"models",
|
|
6
|
+
"routes",
|
|
7
|
+
"views",
|
|
8
|
+
"public",
|
|
9
|
+
"public/css",
|
|
10
|
+
"public/js",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const FORM_DEFAULTS = {
|
|
14
|
+
databaseChoice: "MongoDB",
|
|
15
|
+
authenticationType: "JWT",
|
|
16
|
+
templateEngine: "EJS",
|
|
17
|
+
cssFramework: "Vanilla CSS",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
PROJECT_ROOT_FOLDERS,
|
|
22
|
+
FORM_DEFAULTS,
|
|
23
|
+
};
|
package/utils/helper.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { FORM_DEFAULTS } = require("./constants");
|
|
2
|
+
|
|
3
|
+
const normalizeOptions = (body = {}) => {
|
|
4
|
+
const projectName = (body.projectName || "").trim();
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
projectName,
|
|
8
|
+
databaseChoice: body.databaseChoice || FORM_DEFAULTS.databaseChoice,
|
|
9
|
+
authenticationType: body.authenticationType || FORM_DEFAULTS.authenticationType,
|
|
10
|
+
templateEngine: body.templateEngine || FORM_DEFAULTS.templateEngine,
|
|
11
|
+
cssFramework: body.cssFramework || FORM_DEFAULTS.cssFramework,
|
|
12
|
+
includeMVCStructure: body.includeMVCStructure === "on" || body.includeMVCStructure === "true",
|
|
13
|
+
includeAuthSystem: body.includeAuthSystem === "on" || body.includeAuthSystem === "true",
|
|
14
|
+
includeREADME: body.includeREADME === "on" || body.includeREADME === "true",
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const validateOptions = (options) => {
|
|
19
|
+
if (!options.projectName) {
|
|
20
|
+
return "Project name is required.";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
normalizeOptions,
|
|
28
|
+
validateOptions,
|
|
29
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const sanitizeProjectName = (value = "") => {
|
|
4
|
+
const cleaned = value
|
|
5
|
+
.trim()
|
|
6
|
+
.replace(/[^a-zA-Z0-9\s-_]/g, "")
|
|
7
|
+
.replace(/\s+/g, "-")
|
|
8
|
+
.replace(/-+/g, "-")
|
|
9
|
+
.replace(/^-|-$/g, "");
|
|
10
|
+
|
|
11
|
+
return cleaned || "my-app";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const toPackageName = (value = "") => sanitizeProjectName(value).toLowerCase();
|
|
15
|
+
|
|
16
|
+
const getProjectPath = (baseDir, projectName) =>
|
|
17
|
+
path.join(baseDir, sanitizeProjectName(projectName));
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
sanitizeProjectName,
|
|
21
|
+
toPackageName,
|
|
22
|
+
getProjectPath,
|
|
23
|
+
};
|