servcraft 0.1.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/.dockerignore +45 -0
- package/.env.example +46 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +11 -0
- package/Dockerfile +76 -0
- package/Dockerfile.dev +31 -0
- package/README.md +232 -0
- package/commitlint.config.js +24 -0
- package/dist/cli/index.cjs +3968 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +3945 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +2458 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +828 -0
- package/dist/index.d.ts +828 -0
- package/dist/index.js +2332 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.prod.yml +118 -0
- package/docker-compose.yml +147 -0
- package/eslint.config.js +27 -0
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
- package/npm-cache/_update-notifier-last-checked +0 -0
- package/package.json +112 -0
- package/prisma/schema.prisma +157 -0
- package/src/cli/commands/add-module.ts +422 -0
- package/src/cli/commands/db.ts +137 -0
- package/src/cli/commands/docs.ts +16 -0
- package/src/cli/commands/generate.ts +459 -0
- package/src/cli/commands/init.ts +640 -0
- package/src/cli/index.ts +32 -0
- package/src/cli/templates/controller.ts +67 -0
- package/src/cli/templates/dynamic-prisma.ts +89 -0
- package/src/cli/templates/dynamic-schemas.ts +232 -0
- package/src/cli/templates/dynamic-types.ts +60 -0
- package/src/cli/templates/module-index.ts +33 -0
- package/src/cli/templates/prisma-model.ts +17 -0
- package/src/cli/templates/repository.ts +104 -0
- package/src/cli/templates/routes.ts +70 -0
- package/src/cli/templates/schemas.ts +26 -0
- package/src/cli/templates/service.ts +58 -0
- package/src/cli/templates/types.ts +27 -0
- package/src/cli/utils/docs-generator.ts +47 -0
- package/src/cli/utils/field-parser.ts +315 -0
- package/src/cli/utils/helpers.ts +89 -0
- package/src/config/env.ts +80 -0
- package/src/config/index.ts +97 -0
- package/src/core/index.ts +5 -0
- package/src/core/logger.ts +43 -0
- package/src/core/server.ts +132 -0
- package/src/database/index.ts +7 -0
- package/src/database/prisma.ts +54 -0
- package/src/database/seed.ts +59 -0
- package/src/index.ts +63 -0
- package/src/middleware/error-handler.ts +73 -0
- package/src/middleware/index.ts +3 -0
- package/src/middleware/security.ts +116 -0
- package/src/modules/audit/audit.service.ts +192 -0
- package/src/modules/audit/index.ts +2 -0
- package/src/modules/audit/types.ts +37 -0
- package/src/modules/auth/auth.controller.ts +182 -0
- package/src/modules/auth/auth.middleware.ts +87 -0
- package/src/modules/auth/auth.routes.ts +123 -0
- package/src/modules/auth/auth.service.ts +142 -0
- package/src/modules/auth/index.ts +49 -0
- package/src/modules/auth/schemas.ts +52 -0
- package/src/modules/auth/types.ts +69 -0
- package/src/modules/email/email.service.ts +212 -0
- package/src/modules/email/index.ts +10 -0
- package/src/modules/email/templates.ts +213 -0
- package/src/modules/email/types.ts +57 -0
- package/src/modules/swagger/index.ts +3 -0
- package/src/modules/swagger/schema-builder.ts +263 -0
- package/src/modules/swagger/swagger.service.ts +169 -0
- package/src/modules/swagger/types.ts +68 -0
- package/src/modules/user/index.ts +30 -0
- package/src/modules/user/schemas.ts +49 -0
- package/src/modules/user/types.ts +78 -0
- package/src/modules/user/user.controller.ts +139 -0
- package/src/modules/user/user.repository.ts +156 -0
- package/src/modules/user/user.routes.ts +199 -0
- package/src/modules/user/user.service.ts +145 -0
- package/src/modules/validation/index.ts +18 -0
- package/src/modules/validation/validator.ts +104 -0
- package/src/types/common.ts +61 -0
- package/src/types/index.ts +10 -0
- package/src/utils/errors.ts +66 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/pagination.ts +38 -0
- package/src/utils/response.ts +63 -0
- package/tests/integration/auth.test.ts +59 -0
- package/tests/setup.ts +17 -0
- package/tests/unit/modules/validation.test.ts +88 -0
- package/tests/unit/utils/errors.test.ts +113 -0
- package/tests/unit/utils/pagination.test.ts +82 -0
- package/tsconfig.json +33 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +34 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { ensureDir, writeFile, success, error, info, warn } from '../utils/helpers.js';
|
|
9
|
+
|
|
10
|
+
interface InitOptions {
|
|
11
|
+
name: string;
|
|
12
|
+
language: 'typescript' | 'javascript';
|
|
13
|
+
database: 'postgresql' | 'mysql' | 'sqlite' | 'mongodb' | 'none';
|
|
14
|
+
validator: 'zod' | 'joi' | 'yup';
|
|
15
|
+
features: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const initCommand = new Command('init')
|
|
19
|
+
.alias('new')
|
|
20
|
+
.description('Initialize a new Servcraft project')
|
|
21
|
+
.argument('[name]', 'Project name')
|
|
22
|
+
.option('-y, --yes', 'Skip prompts and use defaults')
|
|
23
|
+
.option('--ts, --typescript', 'Use TypeScript (default)')
|
|
24
|
+
.option('--js, --javascript', 'Use JavaScript')
|
|
25
|
+
.option('--db <database>', 'Database type (postgresql, mysql, sqlite, mongodb, none)')
|
|
26
|
+
.action(async (name?: string, cmdOptions?: { yes?: boolean; typescript?: boolean; javascript?: boolean; db?: string }) => {
|
|
27
|
+
console.log(chalk.blue(`
|
|
28
|
+
╔═══════════════════════════════════════════╗
|
|
29
|
+
║ ║
|
|
30
|
+
║ ${chalk.bold('🚀 Servcraft Project Generator')} ║
|
|
31
|
+
║ ║
|
|
32
|
+
╚═══════════════════════════════════════════╝
|
|
33
|
+
`));
|
|
34
|
+
|
|
35
|
+
let options: InitOptions;
|
|
36
|
+
|
|
37
|
+
if (cmdOptions?.yes) {
|
|
38
|
+
options = {
|
|
39
|
+
name: name || 'my-servcraft-app',
|
|
40
|
+
language: cmdOptions.javascript ? 'javascript' : 'typescript',
|
|
41
|
+
database: (cmdOptions.db as InitOptions['database']) || 'postgresql',
|
|
42
|
+
validator: 'zod',
|
|
43
|
+
features: ['auth', 'users', 'email'],
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
const answers = await inquirer.prompt([
|
|
47
|
+
{
|
|
48
|
+
type: 'input',
|
|
49
|
+
name: 'name',
|
|
50
|
+
message: 'Project name:',
|
|
51
|
+
default: name || 'my-servcraft-app',
|
|
52
|
+
validate: (input: string) => {
|
|
53
|
+
if (!/^[a-z0-9-_]+$/i.test(input)) {
|
|
54
|
+
return 'Project name can only contain letters, numbers, hyphens, and underscores';
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'language',
|
|
62
|
+
message: 'Select language:',
|
|
63
|
+
choices: [
|
|
64
|
+
{ name: 'TypeScript (Recommended)', value: 'typescript' },
|
|
65
|
+
{ name: 'JavaScript', value: 'javascript' },
|
|
66
|
+
],
|
|
67
|
+
default: 'typescript',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'database',
|
|
72
|
+
message: 'Select database:',
|
|
73
|
+
choices: [
|
|
74
|
+
{ name: 'PostgreSQL (Recommended)', value: 'postgresql' },
|
|
75
|
+
{ name: 'MySQL', value: 'mysql' },
|
|
76
|
+
{ name: 'SQLite (Development)', value: 'sqlite' },
|
|
77
|
+
{ name: 'MongoDB', value: 'mongodb' },
|
|
78
|
+
{ name: 'None (Add later)', value: 'none' },
|
|
79
|
+
],
|
|
80
|
+
default: 'postgresql',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: 'list',
|
|
84
|
+
name: 'validator',
|
|
85
|
+
message: 'Select validation library:',
|
|
86
|
+
choices: [
|
|
87
|
+
{ name: 'Zod (Recommended - TypeScript-first)', value: 'zod' },
|
|
88
|
+
{ name: 'Joi (Battle-tested, feature-rich)', value: 'joi' },
|
|
89
|
+
{ name: 'Yup (Inspired by Joi, lighter)', value: 'yup' },
|
|
90
|
+
],
|
|
91
|
+
default: 'zod',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'checkbox',
|
|
95
|
+
name: 'features',
|
|
96
|
+
message: 'Select features to include:',
|
|
97
|
+
choices: [
|
|
98
|
+
{ name: 'Authentication (JWT)', value: 'auth', checked: true },
|
|
99
|
+
{ name: 'User Management', value: 'users', checked: true },
|
|
100
|
+
{ name: 'Email Service', value: 'email', checked: true },
|
|
101
|
+
{ name: 'Audit Logs', value: 'audit', checked: false },
|
|
102
|
+
{ name: 'File Upload', value: 'upload', checked: false },
|
|
103
|
+
{ name: 'Redis Cache', value: 'redis', checked: false },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
options = answers as InitOptions;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const projectDir = path.resolve(process.cwd(), options.name);
|
|
112
|
+
const spinner = ora('Creating project...').start();
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Check if directory exists
|
|
116
|
+
try {
|
|
117
|
+
await fs.access(projectDir);
|
|
118
|
+
spinner.stop();
|
|
119
|
+
error(`Directory "${options.name}" already exists`);
|
|
120
|
+
return;
|
|
121
|
+
} catch {
|
|
122
|
+
// Directory doesn't exist, continue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create project directory
|
|
126
|
+
await ensureDir(projectDir);
|
|
127
|
+
|
|
128
|
+
spinner.text = 'Generating project files...';
|
|
129
|
+
|
|
130
|
+
// Generate package.json
|
|
131
|
+
const packageJson = generatePackageJson(options);
|
|
132
|
+
await writeFile(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
133
|
+
|
|
134
|
+
// Generate tsconfig or jsconfig
|
|
135
|
+
if (options.language === 'typescript') {
|
|
136
|
+
await writeFile(path.join(projectDir, 'tsconfig.json'), generateTsConfig());
|
|
137
|
+
await writeFile(path.join(projectDir, 'tsup.config.ts'), generateTsupConfig());
|
|
138
|
+
} else {
|
|
139
|
+
await writeFile(path.join(projectDir, 'jsconfig.json'), generateJsConfig());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Generate .env files
|
|
143
|
+
await writeFile(path.join(projectDir, '.env.example'), generateEnvExample(options));
|
|
144
|
+
await writeFile(path.join(projectDir, '.env'), generateEnvExample(options));
|
|
145
|
+
|
|
146
|
+
// Generate .gitignore
|
|
147
|
+
await writeFile(path.join(projectDir, '.gitignore'), generateGitignore());
|
|
148
|
+
|
|
149
|
+
// Generate Docker files
|
|
150
|
+
await writeFile(path.join(projectDir, 'Dockerfile'), generateDockerfile(options));
|
|
151
|
+
await writeFile(path.join(projectDir, 'docker-compose.yml'), generateDockerCompose(options));
|
|
152
|
+
|
|
153
|
+
// Create directory structure
|
|
154
|
+
const ext = options.language === 'typescript' ? 'ts' : 'js';
|
|
155
|
+
const dirs = [
|
|
156
|
+
'src/core',
|
|
157
|
+
'src/config',
|
|
158
|
+
'src/modules',
|
|
159
|
+
'src/middleware',
|
|
160
|
+
'src/utils',
|
|
161
|
+
'src/types',
|
|
162
|
+
'tests/unit',
|
|
163
|
+
'tests/integration',
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
if (options.database !== 'none' && options.database !== 'mongodb') {
|
|
167
|
+
dirs.push('prisma');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const dir of dirs) {
|
|
171
|
+
await ensureDir(path.join(projectDir, dir));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Generate main entry file
|
|
175
|
+
await writeFile(
|
|
176
|
+
path.join(projectDir, `src/index.${ext}`),
|
|
177
|
+
generateEntryFile(options)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Generate core files
|
|
181
|
+
await writeFile(
|
|
182
|
+
path.join(projectDir, `src/core/server.${ext}`),
|
|
183
|
+
generateServerFile(options)
|
|
184
|
+
);
|
|
185
|
+
await writeFile(
|
|
186
|
+
path.join(projectDir, `src/core/logger.${ext}`),
|
|
187
|
+
generateLoggerFile(options)
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Generate Prisma schema if database is selected
|
|
191
|
+
if (options.database !== 'none' && options.database !== 'mongodb') {
|
|
192
|
+
await writeFile(
|
|
193
|
+
path.join(projectDir, 'prisma/schema.prisma'),
|
|
194
|
+
generatePrismaSchema(options)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
spinner.succeed('Project files generated!');
|
|
199
|
+
|
|
200
|
+
// Install dependencies
|
|
201
|
+
const installSpinner = ora('Installing dependencies...').start();
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
|
|
205
|
+
installSpinner.succeed('Dependencies installed!');
|
|
206
|
+
} catch {
|
|
207
|
+
installSpinner.warn('Failed to install dependencies automatically');
|
|
208
|
+
warn(' Run "npm install" manually in the project directory');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Print success message
|
|
212
|
+
console.log('\n' + chalk.green('✨ Project created successfully!'));
|
|
213
|
+
console.log('\n' + chalk.bold('📁 Project structure:'));
|
|
214
|
+
console.log(`
|
|
215
|
+
${options.name}/
|
|
216
|
+
├── src/
|
|
217
|
+
│ ├── core/ # Core server, logger
|
|
218
|
+
│ ├── config/ # Configuration
|
|
219
|
+
│ ├── modules/ # Feature modules
|
|
220
|
+
│ ├── middleware/ # Middlewares
|
|
221
|
+
│ ├── utils/ # Utilities
|
|
222
|
+
│ └── index.${ext} # Entry point
|
|
223
|
+
├── tests/ # Tests
|
|
224
|
+
├── prisma/ # Database schema
|
|
225
|
+
├── docker-compose.yml
|
|
226
|
+
└── package.json
|
|
227
|
+
`);
|
|
228
|
+
|
|
229
|
+
console.log(chalk.bold('🚀 Get started:'));
|
|
230
|
+
console.log(`
|
|
231
|
+
${chalk.cyan(`cd ${options.name}`)}
|
|
232
|
+
${options.database !== 'none' ? chalk.cyan('npm run db:push # Setup database') : ''}
|
|
233
|
+
${chalk.cyan('npm run dev # Start development server')}
|
|
234
|
+
`);
|
|
235
|
+
|
|
236
|
+
console.log(chalk.bold('📚 Available commands:'));
|
|
237
|
+
console.log(`
|
|
238
|
+
${chalk.yellow('servcraft generate module <name>')} Generate a new module
|
|
239
|
+
${chalk.yellow('servcraft generate controller <name>')} Generate a controller
|
|
240
|
+
${chalk.yellow('servcraft generate service <name>')} Generate a service
|
|
241
|
+
${chalk.yellow('servcraft add auth')} Add authentication module
|
|
242
|
+
`);
|
|
243
|
+
|
|
244
|
+
} catch (err) {
|
|
245
|
+
spinner.fail('Failed to create project');
|
|
246
|
+
error(err instanceof Error ? err.message : String(err));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
function generatePackageJson(options: InitOptions): Record<string, unknown> {
|
|
251
|
+
const isTS = options.language === 'typescript';
|
|
252
|
+
|
|
253
|
+
const pkg: Record<string, unknown> = {
|
|
254
|
+
name: options.name,
|
|
255
|
+
version: '0.1.0',
|
|
256
|
+
description: 'A Servcraft application',
|
|
257
|
+
main: isTS ? 'dist/index.js' : 'src/index.js',
|
|
258
|
+
type: 'module',
|
|
259
|
+
scripts: {
|
|
260
|
+
dev: isTS ? 'tsx watch src/index.ts' : 'node --watch src/index.js',
|
|
261
|
+
build: isTS ? 'tsup' : 'echo "No build needed for JS"',
|
|
262
|
+
start: isTS ? 'node dist/index.js' : 'node src/index.js',
|
|
263
|
+
test: 'vitest',
|
|
264
|
+
lint: isTS ? 'eslint src --ext .ts' : 'eslint src --ext .js',
|
|
265
|
+
},
|
|
266
|
+
dependencies: {
|
|
267
|
+
fastify: '^4.28.1',
|
|
268
|
+
'@fastify/cors': '^9.0.1',
|
|
269
|
+
'@fastify/helmet': '^11.1.1',
|
|
270
|
+
'@fastify/jwt': '^8.0.1',
|
|
271
|
+
'@fastify/rate-limit': '^9.1.0',
|
|
272
|
+
'@fastify/cookie': '^9.3.1',
|
|
273
|
+
pino: '^9.5.0',
|
|
274
|
+
'pino-pretty': '^11.3.0',
|
|
275
|
+
bcryptjs: '^2.4.3',
|
|
276
|
+
dotenv: '^16.4.5',
|
|
277
|
+
},
|
|
278
|
+
devDependencies: {
|
|
279
|
+
vitest: '^2.1.8',
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Add validator library based on choice
|
|
284
|
+
switch (options.validator) {
|
|
285
|
+
case 'zod':
|
|
286
|
+
(pkg.dependencies as Record<string, string>).zod = '^3.23.8';
|
|
287
|
+
break;
|
|
288
|
+
case 'joi':
|
|
289
|
+
(pkg.dependencies as Record<string, string>).joi = '^17.13.3';
|
|
290
|
+
break;
|
|
291
|
+
case 'yup':
|
|
292
|
+
(pkg.dependencies as Record<string, string>).yup = '^1.4.0';
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (isTS) {
|
|
297
|
+
(pkg.devDependencies as Record<string, string>).typescript = '^5.7.2';
|
|
298
|
+
(pkg.devDependencies as Record<string, string>).tsx = '^4.19.2';
|
|
299
|
+
(pkg.devDependencies as Record<string, string>).tsup = '^8.3.5';
|
|
300
|
+
(pkg.devDependencies as Record<string, string>)['@types/node'] = '^22.10.1';
|
|
301
|
+
(pkg.devDependencies as Record<string, string>)['@types/bcryptjs'] = '^2.4.6';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (options.database !== 'none' && options.database !== 'mongodb') {
|
|
305
|
+
(pkg.dependencies as Record<string, string>)['@prisma/client'] = '^5.22.0';
|
|
306
|
+
(pkg.devDependencies as Record<string, string>).prisma = '^5.22.0';
|
|
307
|
+
(pkg.scripts as Record<string, string>)['db:generate'] = 'prisma generate';
|
|
308
|
+
(pkg.scripts as Record<string, string>)['db:migrate'] = 'prisma migrate dev';
|
|
309
|
+
(pkg.scripts as Record<string, string>)['db:push'] = 'prisma db push';
|
|
310
|
+
(pkg.scripts as Record<string, string>)['db:studio'] = 'prisma studio';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (options.database === 'mongodb') {
|
|
314
|
+
(pkg.dependencies as Record<string, string>).mongoose = '^8.8.4';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (options.features.includes('email')) {
|
|
318
|
+
(pkg.dependencies as Record<string, string>).nodemailer = '^6.9.15';
|
|
319
|
+
(pkg.dependencies as Record<string, string>).handlebars = '^4.7.8';
|
|
320
|
+
if (isTS) {
|
|
321
|
+
(pkg.devDependencies as Record<string, string>)['@types/nodemailer'] = '^6.4.17';
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (options.features.includes('redis')) {
|
|
326
|
+
(pkg.dependencies as Record<string, string>).ioredis = '^5.4.1';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return pkg;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function generateTsConfig(): string {
|
|
333
|
+
return JSON.stringify({
|
|
334
|
+
compilerOptions: {
|
|
335
|
+
target: 'ES2022',
|
|
336
|
+
module: 'NodeNext',
|
|
337
|
+
moduleResolution: 'NodeNext',
|
|
338
|
+
lib: ['ES2022'],
|
|
339
|
+
outDir: './dist',
|
|
340
|
+
rootDir: './src',
|
|
341
|
+
strict: true,
|
|
342
|
+
esModuleInterop: true,
|
|
343
|
+
skipLibCheck: true,
|
|
344
|
+
forceConsistentCasingInFileNames: true,
|
|
345
|
+
resolveJsonModule: true,
|
|
346
|
+
declaration: true,
|
|
347
|
+
sourceMap: true,
|
|
348
|
+
},
|
|
349
|
+
include: ['src/**/*'],
|
|
350
|
+
exclude: ['node_modules', 'dist'],
|
|
351
|
+
}, null, 2);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function generateJsConfig(): string {
|
|
355
|
+
return JSON.stringify({
|
|
356
|
+
compilerOptions: {
|
|
357
|
+
module: 'NodeNext',
|
|
358
|
+
moduleResolution: 'NodeNext',
|
|
359
|
+
target: 'ES2022',
|
|
360
|
+
checkJs: true,
|
|
361
|
+
},
|
|
362
|
+
include: ['src/**/*'],
|
|
363
|
+
exclude: ['node_modules'],
|
|
364
|
+
}, null, 2);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function generateTsupConfig(): string {
|
|
368
|
+
return `import { defineConfig } from 'tsup';
|
|
369
|
+
|
|
370
|
+
export default defineConfig({
|
|
371
|
+
entry: ['src/index.ts'],
|
|
372
|
+
format: ['esm'],
|
|
373
|
+
dts: true,
|
|
374
|
+
clean: true,
|
|
375
|
+
sourcemap: true,
|
|
376
|
+
target: 'node18',
|
|
377
|
+
});
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function generateEnvExample(options: InitOptions): string {
|
|
382
|
+
let env = `# Server
|
|
383
|
+
NODE_ENV=development
|
|
384
|
+
PORT=3000
|
|
385
|
+
HOST=0.0.0.0
|
|
386
|
+
|
|
387
|
+
# JWT
|
|
388
|
+
JWT_SECRET=your-super-secret-key-min-32-characters
|
|
389
|
+
JWT_ACCESS_EXPIRES_IN=15m
|
|
390
|
+
JWT_REFRESH_EXPIRES_IN=7d
|
|
391
|
+
|
|
392
|
+
# Security
|
|
393
|
+
CORS_ORIGIN=http://localhost:3000
|
|
394
|
+
RATE_LIMIT_MAX=100
|
|
395
|
+
|
|
396
|
+
# Logging
|
|
397
|
+
LOG_LEVEL=info
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
if (options.database === 'postgresql') {
|
|
401
|
+
env += `
|
|
402
|
+
# Database (PostgreSQL)
|
|
403
|
+
DATABASE_PROVIDER=postgresql
|
|
404
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
|
|
405
|
+
`;
|
|
406
|
+
} else if (options.database === 'mysql') {
|
|
407
|
+
env += `
|
|
408
|
+
# Database (MySQL)
|
|
409
|
+
DATABASE_PROVIDER=mysql
|
|
410
|
+
DATABASE_URL="mysql://user:password@localhost:3306/mydb"
|
|
411
|
+
`;
|
|
412
|
+
} else if (options.database === 'sqlite') {
|
|
413
|
+
env += `
|
|
414
|
+
# Database (SQLite)
|
|
415
|
+
DATABASE_PROVIDER=sqlite
|
|
416
|
+
DATABASE_URL="file:./dev.db"
|
|
417
|
+
`;
|
|
418
|
+
} else if (options.database === 'mongodb') {
|
|
419
|
+
env += `
|
|
420
|
+
# Database (MongoDB)
|
|
421
|
+
MONGODB_URI="mongodb://localhost:27017/mydb"
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (options.features.includes('email')) {
|
|
426
|
+
env += `
|
|
427
|
+
# Email
|
|
428
|
+
SMTP_HOST=smtp.example.com
|
|
429
|
+
SMTP_PORT=587
|
|
430
|
+
SMTP_USER=
|
|
431
|
+
SMTP_PASS=
|
|
432
|
+
SMTP_FROM="App <noreply@example.com>"
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (options.features.includes('redis')) {
|
|
437
|
+
env += `
|
|
438
|
+
# Redis
|
|
439
|
+
REDIS_URL=redis://localhost:6379
|
|
440
|
+
`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return env;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function generateGitignore(): string {
|
|
447
|
+
return `node_modules/
|
|
448
|
+
dist/
|
|
449
|
+
.env
|
|
450
|
+
.env.local
|
|
451
|
+
*.log
|
|
452
|
+
coverage/
|
|
453
|
+
.DS_Store
|
|
454
|
+
*.db
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function generateDockerfile(options: InitOptions): string {
|
|
459
|
+
const isTS = options.language === 'typescript';
|
|
460
|
+
|
|
461
|
+
return `FROM node:20-alpine
|
|
462
|
+
WORKDIR /app
|
|
463
|
+
COPY package*.json ./
|
|
464
|
+
RUN npm ci --only=production
|
|
465
|
+
COPY ${isTS ? 'dist' : 'src'} ./${isTS ? 'dist' : 'src'}
|
|
466
|
+
${options.database !== 'none' && options.database !== 'mongodb' ? 'COPY prisma ./prisma\nRUN npx prisma generate' : ''}
|
|
467
|
+
EXPOSE 3000
|
|
468
|
+
CMD ["node", "${isTS ? 'dist' : 'src'}/index.js"]
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function generateDockerCompose(options: InitOptions): string {
|
|
473
|
+
let compose = `version: '3.8'
|
|
474
|
+
|
|
475
|
+
services:
|
|
476
|
+
app:
|
|
477
|
+
build: .
|
|
478
|
+
ports:
|
|
479
|
+
- "\${PORT:-3000}:3000"
|
|
480
|
+
environment:
|
|
481
|
+
- NODE_ENV=development
|
|
482
|
+
`;
|
|
483
|
+
|
|
484
|
+
if (options.database === 'postgresql') {
|
|
485
|
+
compose += ` - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/mydb
|
|
486
|
+
depends_on:
|
|
487
|
+
- postgres
|
|
488
|
+
|
|
489
|
+
postgres:
|
|
490
|
+
image: postgres:16-alpine
|
|
491
|
+
environment:
|
|
492
|
+
POSTGRES_USER: postgres
|
|
493
|
+
POSTGRES_PASSWORD: postgres
|
|
494
|
+
POSTGRES_DB: mydb
|
|
495
|
+
ports:
|
|
496
|
+
- "5432:5432"
|
|
497
|
+
volumes:
|
|
498
|
+
- postgres-data:/var/lib/postgresql/data
|
|
499
|
+
|
|
500
|
+
volumes:
|
|
501
|
+
postgres-data:
|
|
502
|
+
`;
|
|
503
|
+
} else if (options.database === 'mysql') {
|
|
504
|
+
compose += ` - DATABASE_URL=mysql://root:root@mysql:3306/mydb
|
|
505
|
+
depends_on:
|
|
506
|
+
- mysql
|
|
507
|
+
|
|
508
|
+
mysql:
|
|
509
|
+
image: mysql:8.0
|
|
510
|
+
environment:
|
|
511
|
+
MYSQL_ROOT_PASSWORD: root
|
|
512
|
+
MYSQL_DATABASE: mydb
|
|
513
|
+
ports:
|
|
514
|
+
- "3306:3306"
|
|
515
|
+
volumes:
|
|
516
|
+
- mysql-data:/var/lib/mysql
|
|
517
|
+
|
|
518
|
+
volumes:
|
|
519
|
+
mysql-data:
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (options.features.includes('redis')) {
|
|
524
|
+
compose += `
|
|
525
|
+
redis:
|
|
526
|
+
image: redis:7-alpine
|
|
527
|
+
ports:
|
|
528
|
+
- "6379:6379"
|
|
529
|
+
`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return compose;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function generatePrismaSchema(options: InitOptions): string {
|
|
536
|
+
const provider = options.database === 'sqlite' ? 'sqlite' : options.database;
|
|
537
|
+
|
|
538
|
+
return `generator client {
|
|
539
|
+
provider = "prisma-client-js"
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
datasource db {
|
|
543
|
+
provider = "${provider}"
|
|
544
|
+
url = env("DATABASE_URL")
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
model User {
|
|
548
|
+
id String @id @default(uuid())
|
|
549
|
+
email String @unique
|
|
550
|
+
password String
|
|
551
|
+
name String?
|
|
552
|
+
role String @default("user")
|
|
553
|
+
status String @default("active")
|
|
554
|
+
emailVerified Boolean @default(false)
|
|
555
|
+
createdAt DateTime @default(now())
|
|
556
|
+
updatedAt DateTime @updatedAt
|
|
557
|
+
|
|
558
|
+
@@map("users")
|
|
559
|
+
}
|
|
560
|
+
`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function generateEntryFile(options: InitOptions): string {
|
|
564
|
+
const isTS = options.language === 'typescript';
|
|
565
|
+
|
|
566
|
+
return `${isTS ? "import { createServer } from './core/server.js';\nimport { logger } from './core/logger.js';" : "const { createServer } = require('./core/server.js');\nconst { logger } = require('./core/logger.js');"}
|
|
567
|
+
|
|
568
|
+
async function main()${isTS ? ': Promise<void>' : ''} {
|
|
569
|
+
const server = createServer();
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
await server.start();
|
|
573
|
+
} catch (error) {
|
|
574
|
+
logger.error({ err: error }, 'Failed to start server');
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
main();
|
|
580
|
+
`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function generateServerFile(options: InitOptions): string {
|
|
584
|
+
const isTS = options.language === 'typescript';
|
|
585
|
+
|
|
586
|
+
return `${isTS ? `import Fastify from 'fastify';
|
|
587
|
+
import type { FastifyInstance } from 'fastify';
|
|
588
|
+
import { logger } from './logger.js';` : `const Fastify = require('fastify');
|
|
589
|
+
const { logger } = require('./logger.js');`}
|
|
590
|
+
|
|
591
|
+
${isTS ? 'export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }' : 'function createServer()'} {
|
|
592
|
+
const app = Fastify({ logger });
|
|
593
|
+
|
|
594
|
+
// Health check
|
|
595
|
+
app.get('/health', async () => ({
|
|
596
|
+
status: 'ok',
|
|
597
|
+
timestamp: new Date().toISOString(),
|
|
598
|
+
}));
|
|
599
|
+
|
|
600
|
+
// Graceful shutdown
|
|
601
|
+
const signals${isTS ? ': NodeJS.Signals[]' : ''} = ['SIGINT', 'SIGTERM'];
|
|
602
|
+
signals.forEach((signal) => {
|
|
603
|
+
process.on(signal, async () => {
|
|
604
|
+
logger.info(\`Received \${signal}, shutting down...\`);
|
|
605
|
+
await app.close();
|
|
606
|
+
process.exit(0);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
instance: app,
|
|
612
|
+
start: async ()${isTS ? ': Promise<void>' : ''} => {
|
|
613
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
614
|
+
const host = process.env.HOST || '0.0.0.0';
|
|
615
|
+
await app.listen({ port, host });
|
|
616
|
+
logger.info(\`Server listening on \${host}:\${port}\`);
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
${isTS ? '' : 'module.exports = { createServer };'}
|
|
622
|
+
`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function generateLoggerFile(options: InitOptions): string {
|
|
626
|
+
const isTS = options.language === 'typescript';
|
|
627
|
+
|
|
628
|
+
return `${isTS ? "import pino from 'pino';\nimport type { Logger } from 'pino';" : "const pino = require('pino');"}
|
|
629
|
+
|
|
630
|
+
${isTS ? 'export const logger: Logger' : 'const logger'} = pino({
|
|
631
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
632
|
+
transport: process.env.NODE_ENV !== 'production' ? {
|
|
633
|
+
target: 'pino-pretty',
|
|
634
|
+
options: { colorize: true },
|
|
635
|
+
} : undefined,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
${isTS ? '' : 'module.exports = { logger };'}
|
|
639
|
+
`;
|
|
640
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { generateCommand } from './commands/generate.js';
|
|
6
|
+
import { addModuleCommand } from './commands/add-module.js';
|
|
7
|
+
import { dbCommand } from './commands/db.js';
|
|
8
|
+
import { docsCommand } from './commands/docs.js';
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('servcraft')
|
|
14
|
+
.description('Servcraft - A modular Node.js backend framework CLI')
|
|
15
|
+
.version('0.1.0');
|
|
16
|
+
|
|
17
|
+
// Initialize new project
|
|
18
|
+
program.addCommand(initCommand);
|
|
19
|
+
|
|
20
|
+
// Generate resources (controller, service, model, etc.)
|
|
21
|
+
program.addCommand(generateCommand);
|
|
22
|
+
|
|
23
|
+
// Add pre-built modules
|
|
24
|
+
program.addCommand(addModuleCommand);
|
|
25
|
+
|
|
26
|
+
// Database commands
|
|
27
|
+
program.addCommand(dbCommand);
|
|
28
|
+
|
|
29
|
+
// Documentation commands
|
|
30
|
+
program.addCommand(docsCommand);
|
|
31
|
+
|
|
32
|
+
program.parse();
|