@dhruvinjs/appinit 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/LICENSE +21 -0
- package/dist/actions/setup_linting.js +1 -0
- package/dist/constants.js +71 -0
- package/dist/generator/engine.js +1 -0
- package/dist/generator/shell.js +204 -0
- package/dist/generator/template_engine.js +121 -0
- package/dist/index.js +315 -0
- package/dist/types.js +1 -0
- package/dist/utils/utils..js +84 -0
- package/dist/utils/utils.js +156 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dhruvin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const restApi_deps = [
|
|
2
|
+
"express",
|
|
3
|
+
"dotenv",
|
|
4
|
+
"cors",
|
|
5
|
+
"jsonwebtoken",
|
|
6
|
+
"helmet",
|
|
7
|
+
"pino",
|
|
8
|
+
"pino-http", //middleware for the http
|
|
9
|
+
"zod",
|
|
10
|
+
"pino-pretty",
|
|
11
|
+
];
|
|
12
|
+
export const restApi_dev_deps = [
|
|
13
|
+
"nodemon",
|
|
14
|
+
"eslint@^9.0.0", // Force version 9
|
|
15
|
+
"@eslint/js@^9.0.0", // Match with eslint
|
|
16
|
+
"prettier",
|
|
17
|
+
"globals@^15.0.0", // Helps ESLint understand Node/Browser variables
|
|
18
|
+
];
|
|
19
|
+
export const restApi_deps_typescript = [
|
|
20
|
+
"@types/express",
|
|
21
|
+
"@types/jsonwebtoken",
|
|
22
|
+
"@types/node",
|
|
23
|
+
"typescript",
|
|
24
|
+
"@types/cors",
|
|
25
|
+
"ts-node",
|
|
26
|
+
"typescript-eslint@^8.0.0", // Works perfectly with v9
|
|
27
|
+
];
|
|
28
|
+
export const websocket_project_deps = [
|
|
29
|
+
{
|
|
30
|
+
package: "ws",
|
|
31
|
+
deps: ["ws"],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
package: "socket.io",
|
|
35
|
+
deps: ["socket.io"],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
export const db_deps = [
|
|
39
|
+
{
|
|
40
|
+
db: "none",
|
|
41
|
+
deps: [],
|
|
42
|
+
},
|
|
43
|
+
{ db: "mongo", deps: ["mongoose"] },
|
|
44
|
+
{ db: "postgresql_prisma", deps: ["@prisma/client@^6.0.0"] },
|
|
45
|
+
];
|
|
46
|
+
export const db_deps_dev = [
|
|
47
|
+
{ db: "none", deps: [] },
|
|
48
|
+
{ db: "mongo", deps: [] },
|
|
49
|
+
{ db: "postgresql_prisma", deps: ["prisma@^6.0.0"] },
|
|
50
|
+
];
|
|
51
|
+
export const restApi_dirs = [
|
|
52
|
+
"controller",
|
|
53
|
+
"utils",
|
|
54
|
+
"models",
|
|
55
|
+
"routes",
|
|
56
|
+
"services",
|
|
57
|
+
"config",
|
|
58
|
+
];
|
|
59
|
+
export const package_manager_env = [
|
|
60
|
+
{ package_manager: "npm", deps: ["npm", "npx"] },
|
|
61
|
+
{ package_manager: "pnpm,", deps: ["pnpm"] },
|
|
62
|
+
{ package_manager: "yarn", deps: ["yarn"] },
|
|
63
|
+
];
|
|
64
|
+
export const template_flags = [
|
|
65
|
+
"--ts-ws",
|
|
66
|
+
"--ts-rest",
|
|
67
|
+
"--ts-io",
|
|
68
|
+
"--js-rest",
|
|
69
|
+
"--js-ws",
|
|
70
|
+
"--js-io",
|
|
71
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { oraPromise } from "ora";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { restApi_deps, restApi_deps_typescript, db_deps, db_deps_dev, restApi_dirs, restApi_dev_deps, websocket_project_deps, } from "../constants.js";
|
|
7
|
+
import { removeProjectDirectory, showSuccessfulMessage, updatePackageJson, updateTsconfig, isToolInstalled, } from "../utils/utils.js";
|
|
8
|
+
import { write_rest_templates } from "./template_engine.js";
|
|
9
|
+
let current_project_path = null;
|
|
10
|
+
export async function setup_restApi_project(project_config) {
|
|
11
|
+
const pm = project_config.packageManager || "npm";
|
|
12
|
+
const pmInstall = pm === "yarn" ? "add" : "install";
|
|
13
|
+
// Set the current project path for SIGINT handler
|
|
14
|
+
current_project_path = project_config.path;
|
|
15
|
+
try {
|
|
16
|
+
// Check if package manager is installed
|
|
17
|
+
if (!(await isToolInstalled(pm))) {
|
|
18
|
+
throw new Error(`Package manager "${pm}" is not installed or not in your PATH.`);
|
|
19
|
+
}
|
|
20
|
+
if (!fs.existsSync(project_config.path)) {
|
|
21
|
+
fs.mkdirSync(project_config.path, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
// Handle package manager init differently - pnpm and yarn don't support -y flag
|
|
24
|
+
const initArgs = pm === "npm" ? ["init", "-y"] : ["init"];
|
|
25
|
+
await oraPromise(execa(pm, initArgs, { cwd: project_config.path }), {
|
|
26
|
+
text: "Initializing project...",
|
|
27
|
+
successText: chalk.green(`${pm} initialized`),
|
|
28
|
+
});
|
|
29
|
+
// Step 2: Set package name
|
|
30
|
+
await oraPromise(execa(pm, ["pkg", "set", `name=${project_config.name}`], {
|
|
31
|
+
cwd: project_config.path,
|
|
32
|
+
}), {
|
|
33
|
+
text: "Setting project name...",
|
|
34
|
+
successText: chalk.green(`Project name set to "${project_config.name}"`),
|
|
35
|
+
});
|
|
36
|
+
// Step 3: Install dependencies
|
|
37
|
+
const deps = restApi_deps;
|
|
38
|
+
const dbDependenciesObj = db_deps.find((d) => d.db === project_config.db);
|
|
39
|
+
const dbDeps = dbDependenciesObj?.deps ?? [];
|
|
40
|
+
const allDeps = [...deps, ...dbDeps];
|
|
41
|
+
await oraPromise(execa(pm, [pmInstall, ...allDeps], { cwd: project_config.path }), {
|
|
42
|
+
text: `Installing dependencies (${allDeps.length} packages)...`,
|
|
43
|
+
successText: chalk.green("Dependencies installed"),
|
|
44
|
+
});
|
|
45
|
+
// Step 4: Create directories
|
|
46
|
+
await oraPromise(generate_restApis_folder(project_config.path), {
|
|
47
|
+
text: "Creating project structure...",
|
|
48
|
+
successText: chalk.green("Directories created"),
|
|
49
|
+
});
|
|
50
|
+
// Step 5: Install dev dependencies
|
|
51
|
+
if (project_config.language === "ts") {
|
|
52
|
+
// console.log("typescript project");
|
|
53
|
+
await oraPromise(setup_restApiforTypeScript_project(project_config.path, pm, pmInstall), {
|
|
54
|
+
text: "Setuping typescript compiler",
|
|
55
|
+
successText: chalk.green("Ts project enabled"),
|
|
56
|
+
});
|
|
57
|
+
await oraPromise(updateTsconfig(project_config.path), {
|
|
58
|
+
text: "Configuring tsconfig.json...",
|
|
59
|
+
successText: chalk.green("tsconfig.json updated"),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const devDeps = [];
|
|
63
|
+
const dbDevDependenciesObj = db_deps_dev.find((d) => d.db === project_config.db);
|
|
64
|
+
const dbDev_Deps = dbDevDependenciesObj?.deps ?? [];
|
|
65
|
+
devDeps.push(...dbDev_Deps);
|
|
66
|
+
await oraPromise(execa(pm, [pmInstall, "-D", ...devDeps, ...restApi_dev_deps], {
|
|
67
|
+
cwd: project_config.path,
|
|
68
|
+
}), {
|
|
69
|
+
text: `Installing dev dependencies (${devDeps.length + restApi_dev_deps.length} packages)...`,
|
|
70
|
+
successText: chalk.green("Dev dependencies installed"),
|
|
71
|
+
});
|
|
72
|
+
if (project_config.db === "postgresql_prisma") {
|
|
73
|
+
await oraPromise(execa("npx", ["prisma", "init"], {
|
|
74
|
+
cwd: project_config.path,
|
|
75
|
+
}), {
|
|
76
|
+
text: `Setting up prisma ...`,
|
|
77
|
+
successText: chalk.green("Prisma Setup done"),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Step 6: Setup websockets if template includes websocket
|
|
81
|
+
if (project_config.template.includes("websocket")
|
|
82
|
+
&& project_config.websocket_package) {
|
|
83
|
+
await oraPromise(setup_websockets_project(project_config.websocket_package, project_config.language, project_config.path, pm, pmInstall), {
|
|
84
|
+
text: `Setting up ${project_config.websocket_package}...`,
|
|
85
|
+
successText: chalk.green(`${project_config.websocket_package} configured`),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
await oraPromise(updatePackageJson(project_config.path, project_config.language), {
|
|
89
|
+
text: `Updating package.json with dev scripts...`,
|
|
90
|
+
successText: chalk.green("Updated Package.json"),
|
|
91
|
+
});
|
|
92
|
+
if (!project_config.skipGit) {
|
|
93
|
+
try {
|
|
94
|
+
await oraPromise(execa(`git`, ["init"], { cwd: project_config.path }), {
|
|
95
|
+
successText: chalk.green("Initialized git repositary"),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
console.warn(chalk.yellow(`Warning: git initialization failed. Continuing without git repo. Reason: ${message}`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(chalk.yellow("ā ļø Skipping git initialization (--no-git flag)"));
|
|
105
|
+
}
|
|
106
|
+
await oraPromise(write_rest_templates({
|
|
107
|
+
...project_config,
|
|
108
|
+
path: project_config.path,
|
|
109
|
+
}), {
|
|
110
|
+
successText: chalk.green("Boilerplate code wrote successfully"),
|
|
111
|
+
});
|
|
112
|
+
// Final success message
|
|
113
|
+
showSuccessfulMessage(project_config);
|
|
114
|
+
// Clear the current project path on success
|
|
115
|
+
current_project_path = null;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
console.error(chalk.red(`Fatal: ${message}`));
|
|
120
|
+
await cleanupProjectDirectoryOnFailure(project_config.path);
|
|
121
|
+
// Clear the current project path after cleanup
|
|
122
|
+
current_project_path = null;
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function cleanupProjectDirectoryOnFailure(projectDirPath) {
|
|
127
|
+
try {
|
|
128
|
+
await removeProjectDirectory(projectDirPath);
|
|
129
|
+
console.log(chalk.yellow(`Removed directory: ${projectDirPath}`));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
console.warn(chalk.yellow(`Warning: failed to clean up project directory. Directory kept at "${projectDirPath}". Reason: ${message}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function generate_restApis_folder(project_path) {
|
|
137
|
+
try {
|
|
138
|
+
const srcDir = path.join(project_path, "src");
|
|
139
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
140
|
+
const dirs = restApi_dirs.map((dir) => path.join("src", dir));
|
|
141
|
+
for (const dir of dirs) {
|
|
142
|
+
fs.mkdirSync(path.join(project_path, dir), { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new Error(`Failed to create directories: ${error}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export async function setup_restApiforTypeScript_project(project_path, pm = "npm", pmInstall = "install") {
|
|
150
|
+
try {
|
|
151
|
+
await execa(pm, [pmInstall, "-D", ...restApi_deps_typescript], {
|
|
152
|
+
cwd: project_path,
|
|
153
|
+
});
|
|
154
|
+
await execa("npx", ["tsc", "--init"], { cwd: project_path });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
158
|
+
throw new Error(`Failed to set up the TypeScript project for rest_api template: ${message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export async function setup_websockets_project(websocket_package, language, project_path, pm = "npm", pmInstall = "install") {
|
|
162
|
+
try {
|
|
163
|
+
const dependencies = [];
|
|
164
|
+
const wsDeps = websocket_project_deps.find((d) => d.package === websocket_package);
|
|
165
|
+
if (!wsDeps) {
|
|
166
|
+
throw new Error(`Unsupported database type: ${websocket_package}`);
|
|
167
|
+
}
|
|
168
|
+
dependencies.push(...wsDeps.deps);
|
|
169
|
+
if (language === "ts" && websocket_package === "ws") {
|
|
170
|
+
await execa(pm, [pmInstall, "-D", "@types/ws"], {
|
|
171
|
+
cwd: project_path,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
await execa(pm, [pmInstall, ...dependencies], { cwd: project_path });
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error instanceof Error) {
|
|
178
|
+
if (error.name === "ExitPromptError") {
|
|
179
|
+
console.log("\nā ļø Process Cancelled by user");
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
184
|
+
throw new Error(`Failed to set up websocket project: ${message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
process.on("SIGINT", async () => {
|
|
188
|
+
console.log(chalk.yellow("\nā ļø Process cancelled by user (Ctrl+C detected)"));
|
|
189
|
+
if (current_project_path) {
|
|
190
|
+
console.log(chalk.yellow("Cleaning up project directory..."));
|
|
191
|
+
await cleanupProjectDirectoryOnFailure(current_project_path);
|
|
192
|
+
current_project_path = null;
|
|
193
|
+
}
|
|
194
|
+
process.exit(0);
|
|
195
|
+
});
|
|
196
|
+
process.on("SIGTERM", async () => {
|
|
197
|
+
console.log(chalk.yellow("\nā ļø Process cancelled by user (Ctrl+D detected)"));
|
|
198
|
+
if (current_project_path) {
|
|
199
|
+
console.log(chalk.yellow("Cleaned Up Project Directory"));
|
|
200
|
+
await cleanupProjectDirectoryOnFailure(current_project_path);
|
|
201
|
+
current_project_path = null;
|
|
202
|
+
}
|
|
203
|
+
process.exit(0);
|
|
204
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import fsExtra from "fs-extra";
|
|
4
|
+
import ejs from "ejs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
export async function write_rest_templates(project_config) {
|
|
8
|
+
const isTs = project_config.language === "ts";
|
|
9
|
+
const template_base_path = give_template_file_path();
|
|
10
|
+
const languageDir = isTs ? "typescript" : "javascript";
|
|
11
|
+
//sourceRoot means it will go to the template/base then langDir=> js or ts
|
|
12
|
+
const sourceRoot = path.join(template_base_path, languageDir);
|
|
13
|
+
const projectRoot = project_config.path;
|
|
14
|
+
const files = await listTemplateFiles(sourceRoot);
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
if (!shouldRenderTemplate(file, isTs)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const relativePath = path.relative(sourceRoot, file);
|
|
20
|
+
const targetPath = mapTemplatePathToTarget(relativePath, projectRoot, isTs);
|
|
21
|
+
await renderTemplateFile(file, targetPath, project_config);
|
|
22
|
+
}
|
|
23
|
+
await writeCommonFiles(template_base_path, projectRoot, project_config);
|
|
24
|
+
if (project_config.Dockerfile) {
|
|
25
|
+
const docker_template_root = give_dockerfile_path();
|
|
26
|
+
const docker_template_path = path.join(docker_template_root, languageDir, "Dockerfile.ejs");
|
|
27
|
+
const docker_target_path = path.join(projectRoot, "Dockerfile");
|
|
28
|
+
await write_dockerfile_template(project_config, docker_template_path, docker_target_path);
|
|
29
|
+
}
|
|
30
|
+
if (project_config.template.includes("websocket")) {
|
|
31
|
+
await write_websocket_index(project_config, template_base_path, projectRoot, isTs);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// dockerfile logic
|
|
35
|
+
async function write_dockerfile_template(project_config, docker_template_path, docker_target_path) {
|
|
36
|
+
if (!project_config.Dockerfile) {
|
|
37
|
+
throw new Error("Dockerfile is not selected in project_config");
|
|
38
|
+
}
|
|
39
|
+
await renderTemplateFile(docker_template_path, docker_target_path, project_config);
|
|
40
|
+
}
|
|
41
|
+
//websocket file logic
|
|
42
|
+
async function write_websocket_index(project_config, templateBase, projectRoot, isTs) {
|
|
43
|
+
if (!project_config.websocket_package) {
|
|
44
|
+
throw new Error("websocket_package is required for websocket templates");
|
|
45
|
+
}
|
|
46
|
+
const languageDir = isTs ? "typescript" : "javascript";
|
|
47
|
+
const ext = isTs ? "ts" : "js";
|
|
48
|
+
const websocketIndex = path.join(templateBase, "websockets", project_config.websocket_package, languageDir, `index.${ext}.ejs`);
|
|
49
|
+
const targetIndex = isTs
|
|
50
|
+
? path.join(projectRoot, "src", `index.${ext}`)
|
|
51
|
+
: path.join(projectRoot, `index.${ext}`);
|
|
52
|
+
await renderTemplateFile(websocketIndex, targetIndex, project_config);
|
|
53
|
+
}
|
|
54
|
+
function get_template_package_root() {
|
|
55
|
+
const pkg_json_path = require.resolve("appinit-templates/package.json");
|
|
56
|
+
return path.dirname(pkg_json_path);
|
|
57
|
+
}
|
|
58
|
+
function give_template_file_path() {
|
|
59
|
+
return path.join(get_template_package_root(), "base");
|
|
60
|
+
}
|
|
61
|
+
function give_dockerfile_path() {
|
|
62
|
+
return path.join(get_template_package_root(), "features", "docker", "base");
|
|
63
|
+
}
|
|
64
|
+
async function listTemplateFiles(root) {
|
|
65
|
+
const entries = await fsExtra.readdir(root, { withFileTypes: true });
|
|
66
|
+
const files = [];
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const fullPath = path.join(root, entry.name);
|
|
69
|
+
// first full path of file will be taken and recursion has started in which basically
|
|
70
|
+
// it will go to a dir and there is a nested dir it will go into that and basically will not stop
|
|
71
|
+
// until it finds a file if file is found then push it into the file folder
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
files.push(...(await listTemplateFiles(fullPath)));
|
|
74
|
+
}
|
|
75
|
+
else if (entry.isFile() && entry.name.endsWith(".ejs")) {
|
|
76
|
+
files.push(fullPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return files;
|
|
80
|
+
}
|
|
81
|
+
function shouldRenderTemplate(filePath, isTs) {
|
|
82
|
+
const fileName = path.basename(filePath);
|
|
83
|
+
if (!isTs && fileName.endsWith(".ts.ejs")) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
//function that removes the ejs and gives files extension according to language selected
|
|
89
|
+
function mapTemplatePathToTarget(relativePath, projectRoot, isTs) {
|
|
90
|
+
const parts = relativePath.split(path.sep).map((part) => {
|
|
91
|
+
if (part.startsWith(",")) {
|
|
92
|
+
return `.${part.slice(1)}`;
|
|
93
|
+
}
|
|
94
|
+
if (part.endsWith(".ejs")) {
|
|
95
|
+
return part.replace(/\.ejs$/, "");
|
|
96
|
+
}
|
|
97
|
+
return part;
|
|
98
|
+
});
|
|
99
|
+
let mapped = path.join(projectRoot, ...parts);
|
|
100
|
+
if (!isTs && relativePath === path.join("src", "index.js.ejs")) {
|
|
101
|
+
mapped = path.join(projectRoot, "index.js");
|
|
102
|
+
}
|
|
103
|
+
if (!isTs && mapped.endsWith(".ts")) {
|
|
104
|
+
mapped = mapped.replace(/\.ts$/, ".js");
|
|
105
|
+
}
|
|
106
|
+
return mapped;
|
|
107
|
+
}
|
|
108
|
+
//actual code generation where the ejs will render the boilerplate file and write the logic using async await
|
|
109
|
+
async function renderTemplateFile(srcPath, destPath, data) {
|
|
110
|
+
const templateData = { ...data, database: data.db };
|
|
111
|
+
const rendered = await ejs.renderFile(srcPath, templateData, { async: true });
|
|
112
|
+
await fsExtra.ensureDir(path.dirname(destPath));
|
|
113
|
+
await fsExtra.writeFile(destPath, rendered, "utf8");
|
|
114
|
+
}
|
|
115
|
+
async function writeCommonFiles(templateBase, projectRoot, project_config) {
|
|
116
|
+
const commonRoot = path.join(templateBase, "common");
|
|
117
|
+
const gitignore = path.join(commonRoot, ",gitignore.ejs");
|
|
118
|
+
await renderTemplateFile(gitignore, path.join(projectRoot, ".gitignore"), project_config);
|
|
119
|
+
const envFile = path.join(commonRoot, "env.ejs");
|
|
120
|
+
await renderTemplateFile(envFile, path.join(projectRoot, ".env"), project_config);
|
|
121
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
const program = new Command();
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import { checkEnv, generateRandomNames, getSafeProjectPath, isToolInstalled, } from "./utils/utils.js";
|
|
6
|
+
import { setup_restApi_project } from "./generator/shell.js";
|
|
7
|
+
import { package_manager_env, db_deps_dev, template_flags, } from "./constants.js";
|
|
8
|
+
function validateRawFlags(args) {
|
|
9
|
+
if (args.length >= 300) {
|
|
10
|
+
throw new Error("Input is too long! Are You trying to hack me or what?");
|
|
11
|
+
}
|
|
12
|
+
const found_presets = args.filter((flags) => template_flags.includes(flags));
|
|
13
|
+
//if no flag provided then I can ask the user only that bro what template do you want
|
|
14
|
+
if (found_presets.length > 1) {
|
|
15
|
+
// console.log(template_flag);
|
|
16
|
+
throw new Error("Cannot have 2 template flags in one command");
|
|
17
|
+
}
|
|
18
|
+
const allowed_dbs = db_deps_dev.map((items) => items.db);
|
|
19
|
+
const found_dbs = args.filter((dbs) => allowed_dbs.includes(dbs));
|
|
20
|
+
if (found_dbs.length > 1) {
|
|
21
|
+
throw new Error(`Cannot have 2 database choices in one command. You Choose: ${found_dbs.join(",")}`);
|
|
22
|
+
}
|
|
23
|
+
const allowed_pms = package_manager_env.map((items) => items.package_manager);
|
|
24
|
+
const found_pms = args.filter((pms) => allowed_pms.includes(pms));
|
|
25
|
+
if (found_pms.length > 1) {
|
|
26
|
+
throw new Error(`Cannot have 2 package_manager choices in one command. You Choose: ${found_pms.join(",")}`);
|
|
27
|
+
}
|
|
28
|
+
if (args.includes("--docker") && args.includes("--no-docker")) {
|
|
29
|
+
throw new Error("Docker and No-Docker Flag cannot exist in same command");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
program
|
|
33
|
+
.name("appinit")
|
|
34
|
+
.description("Generate production-ready backend projects")
|
|
35
|
+
.version("1.0.0")
|
|
36
|
+
// Preset flags
|
|
37
|
+
.option("--ts", "TypeScript + REST API (default template)")
|
|
38
|
+
.option("--js", "JavaScript + REST API (default template)")
|
|
39
|
+
.option("--ts-rest", "TypeScript + REST API")
|
|
40
|
+
.option("--ts-ws", "TypeScript + WebSocket (ws package)")
|
|
41
|
+
.option("--ts-io", "TypeScript + Socket.io")
|
|
42
|
+
.option("--js-rest", "JavaScript + REST API")
|
|
43
|
+
.option("--js-ws", "JavaScript + WebSocket (ws package)")
|
|
44
|
+
.option("--js-io", "JavaScript + Socket.io")
|
|
45
|
+
// Granular configuration flags
|
|
46
|
+
.option("--template <type>", "Template type: rest_api or websocket+rest_api")
|
|
47
|
+
.option("--lang <language>", "Language: js or ts")
|
|
48
|
+
.option("--db <database>", "Database: none, mongo, or postgresql_prisma")
|
|
49
|
+
.option("--ws <package>", "WebSocket package: ws or socket.io")
|
|
50
|
+
.option("--docker", "Include Dockerfile")
|
|
51
|
+
.option("--no-docker", "Skip Dockerfile")
|
|
52
|
+
// Utility flags
|
|
53
|
+
.option("--no-git", "Skip git initialization")
|
|
54
|
+
.option("--pm <manager>", "Package manager: npm, pnpm, or yarn")
|
|
55
|
+
.addHelpText("after", `
|
|
56
|
+
Examples:
|
|
57
|
+
$ appinit my-app --ts-rest
|
|
58
|
+
$ appinit my-app --ts-io --db mongo --docker
|
|
59
|
+
$ appinit my-app --js-ws --db none --no-git
|
|
60
|
+
$ appinit my-app --template rest_api --lang ts --db postgresql_prisma
|
|
61
|
+
`)
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
try {
|
|
64
|
+
// validateRawCommand(process.argv.slice(2).join(" "));
|
|
65
|
+
//process.argv prints everythign you typed into the terminal like
|
|
66
|
+
// npm init -y, here slice means create a new array after 2nd argument
|
|
67
|
+
// npx appinit --ts-ws mongo => this will only create a new array
|
|
68
|
+
// of ['--ts-ws','mongo']
|
|
69
|
+
validateRawFlags(process.argv.slice(2));
|
|
70
|
+
const flagConfig = {};
|
|
71
|
+
// Handle preset flags
|
|
72
|
+
const presets = [
|
|
73
|
+
{ flag: options.ts, template: "rest_api", language: "ts" },
|
|
74
|
+
{ flag: options.js, template: "rest_api", language: "js" },
|
|
75
|
+
{ flag: options.tsRest, template: "rest_api", language: "ts" },
|
|
76
|
+
{
|
|
77
|
+
flag: options.tsWs,
|
|
78
|
+
template: "websocket+rest_api",
|
|
79
|
+
language: "ts",
|
|
80
|
+
websocketPackage: "ws",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
flag: options.tsIo,
|
|
84
|
+
template: "websocket+rest_api",
|
|
85
|
+
language: "ts",
|
|
86
|
+
websocketPackage: "socket.io",
|
|
87
|
+
},
|
|
88
|
+
{ flag: options.jsRest, template: "rest_api", language: "js" },
|
|
89
|
+
{
|
|
90
|
+
flag: options.jsWs,
|
|
91
|
+
template: "websocket+rest_api",
|
|
92
|
+
language: "js",
|
|
93
|
+
websocketPackage: "ws",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
flag: options.jsIo,
|
|
97
|
+
template: "websocket+rest_api",
|
|
98
|
+
language: "js",
|
|
99
|
+
websocketPackage: "socket.io",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
const activePresets = presets.filter((p) => p.flag);
|
|
103
|
+
if (activePresets.length === 1) {
|
|
104
|
+
const preset = activePresets[0];
|
|
105
|
+
if (preset) {
|
|
106
|
+
flagConfig.template = preset.template;
|
|
107
|
+
flagConfig.language = preset.language;
|
|
108
|
+
if (preset.websocketPackage) {
|
|
109
|
+
flagConfig.websocket_package = preset.websocketPackage;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Handle granular flags (they override presets)
|
|
114
|
+
if (options.template) {
|
|
115
|
+
const validTemplates = ["rest_api", "websocket+rest_api"];
|
|
116
|
+
if (!validTemplates.includes(options.template)) {
|
|
117
|
+
console.error(`ā Error: Invalid template "${options.template}". Use: rest_api or websocket+rest_api`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
flagConfig.template = options.template;
|
|
121
|
+
}
|
|
122
|
+
if (options.lang) {
|
|
123
|
+
const validLangs = ["js", "ts"];
|
|
124
|
+
if (!validLangs.includes(options.lang)) {
|
|
125
|
+
console.error(`ā Error: Invalid language "${options.lang}". Use: js or ts`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
flagConfig.language = options.lang;
|
|
129
|
+
}
|
|
130
|
+
if (options.db) {
|
|
131
|
+
const validDbs = ["none", "mongo", "postgresql_prisma"];
|
|
132
|
+
if (!validDbs.includes(options.db)) {
|
|
133
|
+
console.error(`ā Error: Invalid database "${options.db}". Use: none, mongo, or postgresql_prisma`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
flagConfig.database = options.db;
|
|
137
|
+
}
|
|
138
|
+
if (options.ws) {
|
|
139
|
+
const validWs = ["ws", "socket.io"];
|
|
140
|
+
if (!validWs.includes(options.ws)) {
|
|
141
|
+
console.error(`ā Error: Invalid websocket package "${options.ws}". Use: ws or socket.io`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
flagConfig.websocket_package = options.ws;
|
|
145
|
+
}
|
|
146
|
+
if (options.pm) {
|
|
147
|
+
const validPm = ["npm", "pnpm", "yarn"];
|
|
148
|
+
if (!validPm.includes(options.pm)) {
|
|
149
|
+
console.error(`ā Error: Invalid package manager "${options.pm}". Use: npm, pnpm, or yarn`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Commander negated options can set defaults implicitly.
|
|
154
|
+
// Only treat docker as explicitly configured when the user passed a docker flag.
|
|
155
|
+
const dockerFlagProvided = process.argv.includes("--docker")
|
|
156
|
+
|| process.argv.includes("--no-docker");
|
|
157
|
+
if (dockerFlagProvided) {
|
|
158
|
+
flagConfig.docker = options.docker;
|
|
159
|
+
}
|
|
160
|
+
// Validate: websocket package only makes sense with websocket template
|
|
161
|
+
if (flagConfig.websocket_package && flagConfig.template === "rest_api") {
|
|
162
|
+
console.error("ā Error: --ws flag can only be used with websocket+rest_api template");
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
// PostgreSQL_Prisma + JavaScript warning
|
|
166
|
+
if (flagConfig.database === "postgresql_prisma"
|
|
167
|
+
&& flagConfig.language === "js") {
|
|
168
|
+
console.warn("ā ļø Warning: JavaScript does not work properly with PostgreSQL (Prisma). Consider using TypeScript.");
|
|
169
|
+
}
|
|
170
|
+
const answer = await inquirer.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: "input",
|
|
173
|
+
name: "name",
|
|
174
|
+
message: "Project Name",
|
|
175
|
+
default: generateRandomNames(),
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
let project_path = getSafeProjectPath(answer.name);
|
|
179
|
+
// console.log(project_path);
|
|
180
|
+
// Only prompt for template if not provided via flags
|
|
181
|
+
const templateSelection = flagConfig.template
|
|
182
|
+
? { template: flagConfig.template }
|
|
183
|
+
: await inquirer.prompt([
|
|
184
|
+
{
|
|
185
|
+
type: "select",
|
|
186
|
+
name: "template",
|
|
187
|
+
message: "Template",
|
|
188
|
+
choices: [
|
|
189
|
+
{
|
|
190
|
+
name: "REST API [Express, CORS, dotenv, JWT, Helmet, Pino]",
|
|
191
|
+
value: "rest_api",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "WebSockets + REST API [Express, CORS, dotenv, JWT, ws or socket.io]",
|
|
195
|
+
value: "websocket+rest_api",
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
default: 0,
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
await checkEnv();
|
|
202
|
+
const config = await inquirer.prompt([
|
|
203
|
+
{
|
|
204
|
+
type: "select",
|
|
205
|
+
name: "websocket_package",
|
|
206
|
+
message: "WebSocket Engine",
|
|
207
|
+
choices: [
|
|
208
|
+
{
|
|
209
|
+
name: "Socket.io (Room support, auto-reconnect, broadcast helpers)",
|
|
210
|
+
value: "socket.io",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "ws (Lightweight, manual control)",
|
|
214
|
+
value: "ws",
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
default: 0,
|
|
218
|
+
when: () => !flagConfig.websocket_package
|
|
219
|
+
&& templateSelection.template === "websocket+rest_api",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: "select",
|
|
223
|
+
name: "language",
|
|
224
|
+
message: "Language",
|
|
225
|
+
choices: [
|
|
226
|
+
{
|
|
227
|
+
name: "Javascript",
|
|
228
|
+
value: "js",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "Typescript",
|
|
232
|
+
value: "ts",
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
when: () => !flagConfig.language,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
type: "select",
|
|
239
|
+
name: "database",
|
|
240
|
+
message: (answers) => (flagConfig.language || answers.language) === "js"
|
|
241
|
+
? "Database (Warning: JavaScript does not work properly with PostgreSQL (Prisma). Use TypeScript.)"
|
|
242
|
+
: "Database",
|
|
243
|
+
choices: [
|
|
244
|
+
{ name: "None", value: "none" },
|
|
245
|
+
{ name: "MongoDB (Mongoose)", value: "mongo" },
|
|
246
|
+
{
|
|
247
|
+
name: "PostgreSQL (Prisma)",
|
|
248
|
+
value: "postgresql_prisma",
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
default: 0,
|
|
252
|
+
when: () => !flagConfig.database,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
type: "confirm",
|
|
256
|
+
name: "docker",
|
|
257
|
+
message: "Would you like to generate a Dockerfile for this project?",
|
|
258
|
+
default: false,
|
|
259
|
+
when: () => flagConfig.docker === undefined,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: "select",
|
|
263
|
+
name: "packageManager",
|
|
264
|
+
message: "Package Manager",
|
|
265
|
+
choices: [
|
|
266
|
+
{
|
|
267
|
+
name: "npm (default)",
|
|
268
|
+
value: "npm",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "pnpm",
|
|
272
|
+
value: "pnpm",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "yarn",
|
|
276
|
+
value: "yarn",
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
default: 0,
|
|
280
|
+
when: () => !options.pm,
|
|
281
|
+
},
|
|
282
|
+
]);
|
|
283
|
+
const fullConfig = {
|
|
284
|
+
...templateSelection,
|
|
285
|
+
...flagConfig,
|
|
286
|
+
...config,
|
|
287
|
+
};
|
|
288
|
+
if (fullConfig.docker && !(await isToolInstalled("docker"))) {
|
|
289
|
+
console.warn('Warning: "docker" is not installed. Dockerfile will be generated, but you cannot build/run containers until Docker is installed.');
|
|
290
|
+
}
|
|
291
|
+
await setup_restApi_project({
|
|
292
|
+
db: fullConfig.database,
|
|
293
|
+
path: project_path,
|
|
294
|
+
name: answer.name,
|
|
295
|
+
language: fullConfig.language,
|
|
296
|
+
template: fullConfig.template,
|
|
297
|
+
websocket_package: fullConfig.websocket_package,
|
|
298
|
+
Dockerfile: fullConfig.docker,
|
|
299
|
+
skipGit: options.git === false,
|
|
300
|
+
packageManager: options.pm || fullConfig.packageManager || "npm",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error instanceof Error) {
|
|
305
|
+
if (error.name === "ExitPromptError") {
|
|
306
|
+
console.log("\nā ļø Process Cancelled by user");
|
|
307
|
+
process.exit(0);
|
|
308
|
+
}
|
|
309
|
+
// Handle other errors
|
|
310
|
+
console.error("ā Error:", error.message);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
program.parse();
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { uniqueNamesGenerator, adjectives, colors, } from "unique-names-generator";
|
|
2
|
+
const techTerms = [
|
|
3
|
+
"forge",
|
|
4
|
+
"vortex",
|
|
5
|
+
"backend",
|
|
6
|
+
"api",
|
|
7
|
+
"server",
|
|
8
|
+
"core",
|
|
9
|
+
"nexus",
|
|
10
|
+
"stack",
|
|
11
|
+
"engine",
|
|
12
|
+
"base",
|
|
13
|
+
"node",
|
|
14
|
+
"shuttle",
|
|
15
|
+
];
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import fse from "fs-extra";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
export function generateRandomNames() {
|
|
20
|
+
const config = {
|
|
21
|
+
dictionaries: [adjectives, colors, techTerms], // Combine all three
|
|
22
|
+
separator: "-", // 'swift-red-forge'
|
|
23
|
+
length: 2, // Use 3 words for uniqueness
|
|
24
|
+
};
|
|
25
|
+
return uniqueNamesGenerator(config);
|
|
26
|
+
}
|
|
27
|
+
export async function updatePackageJson(projectPath, language) {
|
|
28
|
+
// logic here...
|
|
29
|
+
const packagePath = path.join(projectPath, "package.json");
|
|
30
|
+
try {
|
|
31
|
+
const pkg = await fse.readJson(packagePath);
|
|
32
|
+
const scripts = language === "js"
|
|
33
|
+
? {
|
|
34
|
+
dev: "nodemon index.js",
|
|
35
|
+
}
|
|
36
|
+
: {
|
|
37
|
+
dev: "nodemon --exec ts-node src/index.ts",
|
|
38
|
+
};
|
|
39
|
+
const orderedPkg = {
|
|
40
|
+
...pkg, // start from existing package.json
|
|
41
|
+
name: pkg.name,
|
|
42
|
+
version: pkg.version,
|
|
43
|
+
description: "Generated by Stack Forge",
|
|
44
|
+
main: "index.js",
|
|
45
|
+
type: "module",
|
|
46
|
+
scripts: { ...pkg.scripts, ...scripts },
|
|
47
|
+
};
|
|
48
|
+
await fse.writeJson(packagePath, orderedPkg, { spaces: 2 });
|
|
49
|
+
// console.log("Package.json updated successfully!");
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.log("Error in updating the package.json file", error);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function showSuccessfulMessage(project_config) {
|
|
58
|
+
console.log(chalk.bold.green(`\nš Successfully forged "${project_config.name}"!\n`));
|
|
59
|
+
console.log(chalk.white("Next steps:"));
|
|
60
|
+
console.log(chalk.cyan(` cd ${project_config.name}`));
|
|
61
|
+
console.log(chalk.cyan(` npm install`)); // Just in case they skipped auto-install
|
|
62
|
+
console.log(chalk.cyan(` npm run dev\n`));
|
|
63
|
+
console.log(chalk.gray("Happy coding! š"));
|
|
64
|
+
}
|
|
65
|
+
export async function updateTsconfig(file_path) {
|
|
66
|
+
try {
|
|
67
|
+
const tsconfigPath = path.join(file_path, "tsconfig.json");
|
|
68
|
+
const config = await fse.readJson(tsconfigPath);
|
|
69
|
+
config.compilerOptions = {
|
|
70
|
+
...config.compilerOptions,
|
|
71
|
+
rootDir: "./src",
|
|
72
|
+
outDir: "./dist",
|
|
73
|
+
sourceMap: false,
|
|
74
|
+
declaration: false,
|
|
75
|
+
declarationMap: false,
|
|
76
|
+
// Ensure other output-related things are false as requested
|
|
77
|
+
};
|
|
78
|
+
await fse.writeJSON(tsconfigPath, config, { spaces: 2 });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.log("Error in updating the tsconfig.json file", error);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { uniqueNamesGenerator, adjectives, colors, } from "unique-names-generator";
|
|
2
|
+
const techTerms = [
|
|
3
|
+
"forge",
|
|
4
|
+
"vortex",
|
|
5
|
+
"backend",
|
|
6
|
+
"api",
|
|
7
|
+
"server",
|
|
8
|
+
"core",
|
|
9
|
+
"nexus",
|
|
10
|
+
"stack",
|
|
11
|
+
"engine",
|
|
12
|
+
"base",
|
|
13
|
+
"node",
|
|
14
|
+
"shuttle",
|
|
15
|
+
];
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import fse from "fs-extra";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { execa } from "execa";
|
|
20
|
+
export function generateRandomNames() {
|
|
21
|
+
const config = {
|
|
22
|
+
dictionaries: [adjectives, colors, techTerms], // Combine all three
|
|
23
|
+
separator: "-",
|
|
24
|
+
length: 2,
|
|
25
|
+
};
|
|
26
|
+
return uniqueNamesGenerator(config);
|
|
27
|
+
}
|
|
28
|
+
export async function updatePackageJson(projectPath, language) {
|
|
29
|
+
// logic here...
|
|
30
|
+
const packagePath = path.join(projectPath, "package.json");
|
|
31
|
+
try {
|
|
32
|
+
const pkg = await fse.readJson(packagePath);
|
|
33
|
+
const scripts = language === "js"
|
|
34
|
+
? {
|
|
35
|
+
dev: "nodemon index.js",
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
dev: "nodemon --exec ts-node src/index.ts",
|
|
39
|
+
build: "tsc -b",
|
|
40
|
+
};
|
|
41
|
+
const orderedPkg = {
|
|
42
|
+
...pkg, // starting from existing package.json
|
|
43
|
+
name: pkg.name,
|
|
44
|
+
version: pkg.version,
|
|
45
|
+
description: "Generated by Stack Forge",
|
|
46
|
+
main: "index.js",
|
|
47
|
+
...(language === "js" ? { type: "module" } : {}),
|
|
48
|
+
scripts: { ...pkg.scripts, ...scripts },
|
|
49
|
+
};
|
|
50
|
+
await fse.writeJson(packagePath, orderedPkg, { spaces: 2 });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
throw new Error(`Error updating package.json: ${message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function showSuccessfulMessage(project_config) {
|
|
58
|
+
console.log(chalk.bold.green(`\nš Successfully forged "${project_config.name}"!\n`));
|
|
59
|
+
console.log(chalk.white("Next steps:"));
|
|
60
|
+
console.log(chalk.cyan(` cd ${project_config.name}`));
|
|
61
|
+
console.log(chalk.cyan(` npm run dev\n`));
|
|
62
|
+
console.log(chalk.gray("Happy coding! š"));
|
|
63
|
+
}
|
|
64
|
+
export async function updateTsconfig(file_path) {
|
|
65
|
+
try {
|
|
66
|
+
const tsconfigPath = path.join(file_path, "tsconfig.json");
|
|
67
|
+
const config = {
|
|
68
|
+
compilerOptions: {
|
|
69
|
+
target: "ES2022",
|
|
70
|
+
module: "CommonJs",
|
|
71
|
+
moduleResolution: "Node",
|
|
72
|
+
lib: ["ES2022"],
|
|
73
|
+
rootDir: "./src",
|
|
74
|
+
outDir: "./dist",
|
|
75
|
+
baseUrl: ".",
|
|
76
|
+
esModuleInterop: true,
|
|
77
|
+
forceConsistentCasingInFileNames: true,
|
|
78
|
+
resolveJsonModule: true,
|
|
79
|
+
isolatedModules: true,
|
|
80
|
+
strict: true,
|
|
81
|
+
skipLibCheck: true,
|
|
82
|
+
noImplicitAny: true,
|
|
83
|
+
sourceMap: false,
|
|
84
|
+
declaration: false,
|
|
85
|
+
declarationMap: false,
|
|
86
|
+
removeComments: true,
|
|
87
|
+
},
|
|
88
|
+
include: ["src/**/*"],
|
|
89
|
+
exclude: ["node_modules", "dist"],
|
|
90
|
+
};
|
|
91
|
+
await fse.writeJSON(tsconfigPath, config, { spaces: 2 });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
throw new Error(`Error updating tsconfig.json: ${message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function validateProjectName(name) {
|
|
99
|
+
if (!name || name.trim() === "")
|
|
100
|
+
throw new Error("Project name cannot be empty");
|
|
101
|
+
if (name.includes(".."))
|
|
102
|
+
throw new Error("Project name cannot contain '..'");
|
|
103
|
+
if (/[\/\\]/.test(name))
|
|
104
|
+
throw new Error("Project name cannot contain path separators");
|
|
105
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(name))
|
|
106
|
+
throw new Error("Invalid characters in project name");
|
|
107
|
+
if (name.length > 40) {
|
|
108
|
+
throw new Error("Name Should be less than 50 Characters");
|
|
109
|
+
}
|
|
110
|
+
if (name.length <= 3) {
|
|
111
|
+
// console.log(name);
|
|
112
|
+
throw new Error("Name should contain more than 3 characters");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function getSafeProjectPath(name) {
|
|
116
|
+
validateProjectName(name);
|
|
117
|
+
const base = process.cwd();
|
|
118
|
+
const resolved = path.resolve(base, name);
|
|
119
|
+
// console.log(resolved);
|
|
120
|
+
if (!resolved.startsWith(base + path.sep)) {
|
|
121
|
+
throw new Error("Path traversal attempt blocked");
|
|
122
|
+
}
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
export async function checkEnv() {
|
|
126
|
+
const requiredTools = ["npm", "npx"];
|
|
127
|
+
for (const tool of requiredTools) {
|
|
128
|
+
try {
|
|
129
|
+
await execa(tool, ["--version"]);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw new Error(`Required tool "${tool}" is not installed or not in your PATH.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!(await isToolInstalled("git"))) {
|
|
136
|
+
console.warn(chalk.yellow('Warning: "git" is not installed. Project setup will continue without git initialization.'));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function isToolInstalled(tool) {
|
|
140
|
+
try {
|
|
141
|
+
await execa(tool, ["--version"]);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function removeProjectDirectory(projectPath) {
|
|
149
|
+
try {
|
|
150
|
+
await fse.remove(projectPath);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
+
throw new Error(`Failed to remove project directory "${projectPath}": ${message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dhruvinjs/appinit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"appinit": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"chalk": "^5.6.2",
|
|
15
|
+
"commander": "^14.0.3",
|
|
16
|
+
"cors": "^2.8.6",
|
|
17
|
+
"ejs": "^4.0.1",
|
|
18
|
+
"execa": "^9.6.1",
|
|
19
|
+
"express": "^5.2.1",
|
|
20
|
+
"fs-extra": "^11.3.3",
|
|
21
|
+
"helmet": "^8.1.0",
|
|
22
|
+
"inquirer": "^13.2.2",
|
|
23
|
+
"jsonwebtoken": "^9.0.3",
|
|
24
|
+
"ora": "^9.1.0",
|
|
25
|
+
"unique-names-generator": "^4.7.1",
|
|
26
|
+
"appinit-templates": "1.0.1"
|
|
27
|
+
},
|
|
28
|
+
"author": "dhruvinjs",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/ejs": "^3.1.5",
|
|
32
|
+
"@types/fs-extra": "^11.0.4",
|
|
33
|
+
"@types/node": "^22.15.3",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
38
|
+
"build": "npx tsc -b",
|
|
39
|
+
"dev": "tsc -b && node dist/index.js"
|
|
40
|
+
}
|
|
41
|
+
}
|