servcraft 0.2.0 ā 0.4.2
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/.github/workflows/ci.yml +9 -4
- package/README.md +70 -2
- package/ROADMAP.md +124 -47
- package/dist/cli/index.cjs +1331 -407
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1298 -389
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/completion.ts +146 -0
- package/src/cli/commands/doctor.ts +116 -1
- package/src/cli/commands/generate.ts +52 -7
- package/src/cli/commands/list.ts +1 -1
- package/src/cli/commands/scaffold.ts +211 -0
- package/src/cli/commands/templates.ts +147 -0
- package/src/cli/commands/update.ts +221 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/templates/controller-test.ts +110 -0
- package/src/cli/templates/integration-test.ts +139 -0
- package/src/cli/templates/service-test.ts +100 -0
- package/src/cli/utils/template-loader.ts +80 -0
- package/tests/cli/add.test.ts +32 -0
- package/tests/cli/completion.test.ts +35 -0
- package/tests/cli/doctor.test.ts +23 -0
- package/tests/cli/dry-run.test.ts +39 -0
- package/tests/cli/errors.test.ts +29 -0
- package/tests/cli/generate.test.ts +39 -0
- package/tests/cli/init.test.ts +63 -0
- package/tests/cli/list.test.ts +25 -0
- package/tests/cli/remove.test.ts +28 -0
- package/tests/cli/update.test.ts +34 -0
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* eslint-disable no-useless-escape */
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
|
|
5
|
+
const bashScript = `
|
|
6
|
+
# servcraft bash completion script
|
|
7
|
+
_servcraft_completions() {
|
|
8
|
+
local cur prev words cword
|
|
9
|
+
_init_completion || return
|
|
10
|
+
|
|
11
|
+
# Main commands
|
|
12
|
+
local commands="init add generate list remove doctor update completion docs --version --help"
|
|
13
|
+
|
|
14
|
+
# Generate subcommands
|
|
15
|
+
local generate_subcommands="module controller service repository types schema routes m c s r t"
|
|
16
|
+
|
|
17
|
+
case "\${words[1]}" in
|
|
18
|
+
generate|g)
|
|
19
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
20
|
+
COMPREPLY=( \$(compgen -W "\${generate_subcommands}" -- "\${cur}") )
|
|
21
|
+
fi
|
|
22
|
+
;;
|
|
23
|
+
add|remove|rm|update)
|
|
24
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
25
|
+
# Get available modules
|
|
26
|
+
local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
|
|
27
|
+
COMPREPLY=( \$(compgen -W "\${modules}" -- "\${cur}") )
|
|
28
|
+
fi
|
|
29
|
+
;;
|
|
30
|
+
completion)
|
|
31
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
32
|
+
COMPREPLY=( \$(compgen -W "bash zsh" -- "\${cur}") )
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
*)
|
|
36
|
+
if [[ \${cword} -eq 1 ]]; then
|
|
37
|
+
COMPREPLY=( \$(compgen -W "\${commands}" -- "\${cur}") )
|
|
38
|
+
fi
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
complete -F _servcraft_completions servcraft
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const zshScript = `
|
|
47
|
+
#compdef servcraft
|
|
48
|
+
|
|
49
|
+
_servcraft() {
|
|
50
|
+
local context state state_descr line
|
|
51
|
+
typeset -A opt_args
|
|
52
|
+
|
|
53
|
+
_arguments -C \\
|
|
54
|
+
'1: :_servcraft_commands' \\
|
|
55
|
+
'*::arg:->args'
|
|
56
|
+
|
|
57
|
+
case $state in
|
|
58
|
+
args)
|
|
59
|
+
case $line[1] in
|
|
60
|
+
generate|g)
|
|
61
|
+
_servcraft_generate
|
|
62
|
+
;;
|
|
63
|
+
add|remove|rm|update)
|
|
64
|
+
_servcraft_modules
|
|
65
|
+
;;
|
|
66
|
+
completion)
|
|
67
|
+
_arguments '1: :(bash zsh)'
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_servcraft_commands() {
|
|
75
|
+
local commands
|
|
76
|
+
commands=(
|
|
77
|
+
'init:Initialize a new ServCraft project'
|
|
78
|
+
'add:Add a pre-built module to your project'
|
|
79
|
+
'generate:Generate code files (controller, service, etc.)'
|
|
80
|
+
'list:List available and installed modules'
|
|
81
|
+
'remove:Remove an installed module'
|
|
82
|
+
'doctor:Diagnose project configuration'
|
|
83
|
+
'update:Update installed modules'
|
|
84
|
+
'completion:Generate shell completion scripts'
|
|
85
|
+
'docs:Open documentation'
|
|
86
|
+
'--version:Show version'
|
|
87
|
+
'--help:Show help'
|
|
88
|
+
)
|
|
89
|
+
_describe 'command' commands
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_servcraft_generate() {
|
|
93
|
+
local subcommands
|
|
94
|
+
subcommands=(
|
|
95
|
+
'module:Generate a complete module (controller + service + routes)'
|
|
96
|
+
'controller:Generate a controller'
|
|
97
|
+
'service:Generate a service'
|
|
98
|
+
'repository:Generate a repository'
|
|
99
|
+
'types:Generate TypeScript types'
|
|
100
|
+
'schema:Generate validation schema'
|
|
101
|
+
'routes:Generate routes file'
|
|
102
|
+
'm:Alias for module'
|
|
103
|
+
'c:Alias for controller'
|
|
104
|
+
's:Alias for service'
|
|
105
|
+
'r:Alias for repository'
|
|
106
|
+
't:Alias for types'
|
|
107
|
+
)
|
|
108
|
+
_describe 'subcommand' subcommands
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_servcraft_modules() {
|
|
112
|
+
local modules
|
|
113
|
+
modules=(
|
|
114
|
+
'auth:Authentication & Authorization'
|
|
115
|
+
'cache:Redis caching'
|
|
116
|
+
'rate-limit:Rate limiting'
|
|
117
|
+
'notification:Email/SMS notifications'
|
|
118
|
+
'payment:Payment integration'
|
|
119
|
+
'oauth:OAuth providers'
|
|
120
|
+
'mfa:Multi-factor authentication'
|
|
121
|
+
'queue:Background jobs'
|
|
122
|
+
'websocket:WebSocket support'
|
|
123
|
+
'upload:File upload handling'
|
|
124
|
+
)
|
|
125
|
+
_describe 'module' modules
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_servcraft "$@"
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
export const completionCommand = new Command('completion')
|
|
132
|
+
.description('Generate shell completion scripts')
|
|
133
|
+
.argument('<shell>', 'Shell type (bash or zsh)')
|
|
134
|
+
.action((shell: string) => {
|
|
135
|
+
const shellLower = shell.toLowerCase();
|
|
136
|
+
|
|
137
|
+
if (shellLower === 'bash') {
|
|
138
|
+
console.log(bashScript);
|
|
139
|
+
} else if (shellLower === 'zsh') {
|
|
140
|
+
console.log(zshScript);
|
|
141
|
+
} else {
|
|
142
|
+
console.error(`Unsupported shell: ${shell}`);
|
|
143
|
+
console.error('Supported shells: bash, zsh');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
@@ -1,8 +1,123 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
import { Command } from 'commander';
|
|
2
3
|
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
interface Check {
|
|
7
|
+
name: string;
|
|
8
|
+
status: 'pass' | 'warn' | 'fail';
|
|
9
|
+
message: string;
|
|
10
|
+
suggestion?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function checkNodeVersion(): Promise<Check> {
|
|
14
|
+
const version = process.version;
|
|
15
|
+
const major = parseInt(version.slice(1).split('.')[0] || '0', 10);
|
|
16
|
+
|
|
17
|
+
if (major >= 18) {
|
|
18
|
+
return { name: 'Node.js', status: 'pass', message: `${version} ā` };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
name: 'Node.js',
|
|
22
|
+
status: 'fail',
|
|
23
|
+
message: `${version} (< 18)`,
|
|
24
|
+
suggestion: 'Upgrade to Node.js 18+',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function checkPackageJson(): Promise<Check[]> {
|
|
29
|
+
const checks: Check[] = [];
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile('package.json', 'utf-8');
|
|
32
|
+
const pkg = JSON.parse(content);
|
|
33
|
+
|
|
34
|
+
checks.push({ name: 'package.json', status: 'pass', message: 'Found' });
|
|
35
|
+
|
|
36
|
+
if (pkg.dependencies?.fastify) {
|
|
37
|
+
checks.push({ name: 'Fastify', status: 'pass', message: 'Installed' });
|
|
38
|
+
} else {
|
|
39
|
+
checks.push({
|
|
40
|
+
name: 'Fastify',
|
|
41
|
+
status: 'fail',
|
|
42
|
+
message: 'Missing',
|
|
43
|
+
suggestion: 'npm install fastify',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
checks.push({
|
|
48
|
+
name: 'package.json',
|
|
49
|
+
status: 'fail',
|
|
50
|
+
message: 'Not found',
|
|
51
|
+
suggestion: 'Run servcraft init',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return checks;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function checkDirectories(): Promise<Check[]> {
|
|
58
|
+
const checks: Check[] = [];
|
|
59
|
+
const dirs = ['src', 'node_modules', '.git', '.env'];
|
|
60
|
+
|
|
61
|
+
for (const dir of dirs) {
|
|
62
|
+
try {
|
|
63
|
+
await fs.access(dir);
|
|
64
|
+
checks.push({ name: dir, status: 'pass', message: 'Exists' });
|
|
65
|
+
} catch {
|
|
66
|
+
const isCritical = dir === 'src' || dir === 'node_modules';
|
|
67
|
+
checks.push({
|
|
68
|
+
name: dir,
|
|
69
|
+
status: isCritical ? 'fail' : 'warn',
|
|
70
|
+
message: 'Not found',
|
|
71
|
+
suggestion:
|
|
72
|
+
dir === 'node_modules' ? 'npm install' : dir === '.env' ? 'Create .env file' : undefined,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return checks;
|
|
77
|
+
}
|
|
3
78
|
|
|
4
79
|
export const doctorCommand = new Command('doctor')
|
|
5
80
|
.description('Diagnose project configuration and dependencies')
|
|
6
81
|
.action(async () => {
|
|
7
|
-
console.log(chalk.bold.cyan('\
|
|
82
|
+
console.log(chalk.bold.cyan('\nš ServCraft Doctor\n'));
|
|
83
|
+
|
|
84
|
+
const allChecks: Check[] = [];
|
|
85
|
+
|
|
86
|
+
allChecks.push(await checkNodeVersion());
|
|
87
|
+
allChecks.push(...(await checkPackageJson()));
|
|
88
|
+
allChecks.push(...(await checkDirectories()));
|
|
89
|
+
|
|
90
|
+
// Display results
|
|
91
|
+
allChecks.forEach((check) => {
|
|
92
|
+
const icon =
|
|
93
|
+
check.status === 'pass'
|
|
94
|
+
? chalk.green('ā')
|
|
95
|
+
: check.status === 'warn'
|
|
96
|
+
? chalk.yellow('ā ')
|
|
97
|
+
: chalk.red('ā');
|
|
98
|
+
const color =
|
|
99
|
+
check.status === 'pass' ? chalk.green : check.status === 'warn' ? chalk.yellow : chalk.red;
|
|
100
|
+
|
|
101
|
+
console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
|
|
102
|
+
if (check.suggestion) {
|
|
103
|
+
console.log(chalk.gray(` ā ${check.suggestion}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const pass = allChecks.filter((c) => c.status === 'pass').length;
|
|
108
|
+
const warn = allChecks.filter((c) => c.status === 'warn').length;
|
|
109
|
+
const fail = allChecks.filter((c) => c.status === 'fail').length;
|
|
110
|
+
|
|
111
|
+
console.log(chalk.gray('\n' + 'ā'.repeat(60)));
|
|
112
|
+
console.log(
|
|
113
|
+
`\n${chalk.green(pass + ' passed')} | ${chalk.yellow(warn + ' warnings')} | ${chalk.red(fail + ' failed')}\n`
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (fail === 0 && warn === 0) {
|
|
117
|
+
console.log(chalk.green.bold('⨠Everything looks good!\n'));
|
|
118
|
+
} else if (fail > 0) {
|
|
119
|
+
console.log(chalk.red.bold('ā Fix critical issues before using ServCraft.\n'));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.yellow.bold('ā Some warnings, but should work.\n'));
|
|
122
|
+
}
|
|
8
123
|
});
|
|
@@ -45,6 +45,10 @@ import { prismaModelTemplate } from '../templates/prisma-model.js';
|
|
|
45
45
|
import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
|
|
46
46
|
import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
|
|
47
47
|
import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
|
|
48
|
+
import { controllerTestTemplate } from '../templates/controller-test.js';
|
|
49
|
+
import { serviceTestTemplate } from '../templates/service-test.js';
|
|
50
|
+
import { integrationTestTemplate } from '../templates/integration-test.js';
|
|
51
|
+
import { getTemplate } from '../utils/template-loader.js';
|
|
48
52
|
|
|
49
53
|
export const generateCommand = new Command('generate')
|
|
50
54
|
.alias('g')
|
|
@@ -62,6 +66,7 @@ generateCommand
|
|
|
62
66
|
.option('--prisma', 'Generate Prisma model suggestion')
|
|
63
67
|
.option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
|
|
64
68
|
.option('-i, --interactive', 'Interactive mode to define fields')
|
|
69
|
+
.option('--with-tests', 'Generate test files (__tests__ directory)')
|
|
65
70
|
.option('--dry-run', 'Preview changes without writing files')
|
|
66
71
|
.action(async (name: string, fieldsArgs: string[], options) => {
|
|
67
72
|
enableDryRunIfNeeded(options);
|
|
@@ -96,41 +101,50 @@ generateCommand
|
|
|
96
101
|
// Use dynamic templates if fields are provided
|
|
97
102
|
const hasFields = fields.length > 0;
|
|
98
103
|
|
|
104
|
+
// Load templates (custom or built-in)
|
|
105
|
+
const controllerTpl = await getTemplate('controller', controllerTemplate);
|
|
106
|
+
const serviceTpl = await getTemplate('service', serviceTemplate);
|
|
107
|
+
const repositoryTpl = await getTemplate('repository', repositoryTemplate);
|
|
108
|
+
const typesTpl = await getTemplate('types', typesTemplate);
|
|
109
|
+
const schemasTpl = await getTemplate('schemas', schemasTemplate);
|
|
110
|
+
const routesTpl = await getTemplate('routes', routesTemplate);
|
|
111
|
+
const moduleIndexTpl = await getTemplate('module-index', moduleIndexTemplate);
|
|
112
|
+
|
|
99
113
|
const files = [
|
|
100
114
|
{
|
|
101
115
|
name: `${kebabName}.types.ts`,
|
|
102
116
|
content: hasFields
|
|
103
117
|
? dynamicTypesTemplate(kebabName, pascalName, fields)
|
|
104
|
-
:
|
|
118
|
+
: typesTpl(kebabName, pascalName),
|
|
105
119
|
},
|
|
106
120
|
{
|
|
107
121
|
name: `${kebabName}.schemas.ts`,
|
|
108
122
|
content: hasFields
|
|
109
123
|
? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType)
|
|
110
|
-
:
|
|
124
|
+
: schemasTpl(kebabName, pascalName, camelName),
|
|
111
125
|
},
|
|
112
126
|
{
|
|
113
127
|
name: `${kebabName}.service.ts`,
|
|
114
|
-
content:
|
|
128
|
+
content: serviceTpl(kebabName, pascalName, camelName),
|
|
115
129
|
},
|
|
116
130
|
{
|
|
117
131
|
name: `${kebabName}.controller.ts`,
|
|
118
|
-
content:
|
|
132
|
+
content: controllerTpl(kebabName, pascalName, camelName),
|
|
119
133
|
},
|
|
120
|
-
{ name: 'index.ts', content:
|
|
134
|
+
{ name: 'index.ts', content: moduleIndexTpl(kebabName, pascalName, camelName) },
|
|
121
135
|
];
|
|
122
136
|
|
|
123
137
|
if (options.repository !== false) {
|
|
124
138
|
files.push({
|
|
125
139
|
name: `${kebabName}.repository.ts`,
|
|
126
|
-
content:
|
|
140
|
+
content: repositoryTpl(kebabName, pascalName, camelName, pluralName),
|
|
127
141
|
});
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
if (options.routes !== false) {
|
|
131
145
|
files.push({
|
|
132
146
|
name: `${kebabName}.routes.ts`,
|
|
133
|
-
content:
|
|
147
|
+
content: routesTpl(kebabName, pascalName, camelName, pluralName),
|
|
134
148
|
});
|
|
135
149
|
}
|
|
136
150
|
|
|
@@ -139,6 +153,31 @@ generateCommand
|
|
|
139
153
|
await writeFile(path.join(moduleDir, file.name), file.content);
|
|
140
154
|
}
|
|
141
155
|
|
|
156
|
+
// Generate test files if --with-tests flag is provided
|
|
157
|
+
if (options.withTests) {
|
|
158
|
+
const testDir = path.join(moduleDir, '__tests__');
|
|
159
|
+
|
|
160
|
+
// Load test templates (custom or built-in)
|
|
161
|
+
const controllerTestTpl = await getTemplate('controller-test', controllerTestTemplate);
|
|
162
|
+
const serviceTestTpl = await getTemplate('service-test', serviceTestTemplate);
|
|
163
|
+
const integrationTestTpl = await getTemplate('integration-test', integrationTestTemplate);
|
|
164
|
+
|
|
165
|
+
await writeFile(
|
|
166
|
+
path.join(testDir, `${kebabName}.controller.test.ts`),
|
|
167
|
+
controllerTestTpl(kebabName, pascalName, camelName)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
await writeFile(
|
|
171
|
+
path.join(testDir, `${kebabName}.service.test.ts`),
|
|
172
|
+
serviceTestTpl(kebabName, pascalName, camelName)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await writeFile(
|
|
176
|
+
path.join(testDir, `${kebabName}.integration.test.ts`),
|
|
177
|
+
integrationTestTpl(kebabName, pascalName, camelName)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
142
181
|
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
143
182
|
|
|
144
183
|
// Show Prisma model if requested or fields provided
|
|
@@ -169,6 +208,12 @@ generateCommand
|
|
|
169
208
|
console.log('\nš Files created:');
|
|
170
209
|
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
171
210
|
|
|
211
|
+
if (options.withTests) {
|
|
212
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
213
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
214
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
215
|
+
}
|
|
216
|
+
|
|
172
217
|
console.log('\nš Next steps:');
|
|
173
218
|
if (!hasFields) {
|
|
174
219
|
info(' 1. Update the types in ' + `${kebabName}.types.ts`);
|
package/src/cli/commands/list.ts
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import {
|
|
7
|
+
toPascalCase,
|
|
8
|
+
toCamelCase,
|
|
9
|
+
toKebabCase,
|
|
10
|
+
pluralize,
|
|
11
|
+
writeFile,
|
|
12
|
+
success,
|
|
13
|
+
info,
|
|
14
|
+
getModulesDir,
|
|
15
|
+
} from '../utils/helpers.js';
|
|
16
|
+
import { DryRunManager } from '../utils/dry-run.js';
|
|
17
|
+
import { parseFields } from '../utils/field-parser.js';
|
|
18
|
+
import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
|
|
19
|
+
import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
|
|
20
|
+
import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
|
|
21
|
+
import { controllerTemplate } from '../templates/controller.js';
|
|
22
|
+
import { serviceTemplate } from '../templates/service.js';
|
|
23
|
+
import { repositoryTemplate } from '../templates/repository.js';
|
|
24
|
+
import { routesTemplate } from '../templates/routes.js';
|
|
25
|
+
import { moduleIndexTemplate } from '../templates/module-index.js';
|
|
26
|
+
import { controllerTestTemplate } from '../templates/controller-test.js';
|
|
27
|
+
import { serviceTestTemplate } from '../templates/service-test.js';
|
|
28
|
+
import { integrationTestTemplate } from '../templates/integration-test.js';
|
|
29
|
+
import { getTemplate } from '../utils/template-loader.js';
|
|
30
|
+
|
|
31
|
+
export const scaffoldCommand = new Command('scaffold')
|
|
32
|
+
.description('Generate complete CRUD with Prisma model')
|
|
33
|
+
.argument('<name>', 'Resource name (e.g., product, user)')
|
|
34
|
+
.option(
|
|
35
|
+
'--fields <fields>',
|
|
36
|
+
'Field definitions: "name:string email:string? age:number category:relation"'
|
|
37
|
+
)
|
|
38
|
+
.option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
|
|
39
|
+
.option('--dry-run', 'Preview changes without writing files')
|
|
40
|
+
.action(
|
|
41
|
+
async (
|
|
42
|
+
name: string,
|
|
43
|
+
options: { fields?: string; validator?: ValidatorType; dryRun?: boolean }
|
|
44
|
+
) => {
|
|
45
|
+
const dryRun = DryRunManager.getInstance();
|
|
46
|
+
if (options.dryRun) {
|
|
47
|
+
dryRun.enable();
|
|
48
|
+
console.log(chalk.yellow('\nā DRY RUN MODE - No files will be written\n'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!options.fields) {
|
|
52
|
+
console.log(chalk.red('\nā Error: --fields option is required\n'));
|
|
53
|
+
console.log(chalk.gray('Example:'));
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.cyan(
|
|
56
|
+
' servcraft scaffold product --fields "name:string price:number category:relation"'
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const spinner = ora('Scaffolding resource...').start();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Parse fields
|
|
66
|
+
const fields = parseFields(options.fields || '');
|
|
67
|
+
|
|
68
|
+
if (!fields || fields.length === 0) {
|
|
69
|
+
spinner.fail('No valid fields provided');
|
|
70
|
+
console.log(chalk.gray(`\nReceived: ${options.fields}`));
|
|
71
|
+
console.log(chalk.gray(`Parsed: ${JSON.stringify(fields)}`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate names
|
|
76
|
+
const kebabName = toKebabCase(name);
|
|
77
|
+
const pascalName = toPascalCase(name);
|
|
78
|
+
const camelName = toCamelCase(name);
|
|
79
|
+
const pluralName = pluralize(camelName);
|
|
80
|
+
const tableName = pluralize(kebabName);
|
|
81
|
+
|
|
82
|
+
// Create module directory
|
|
83
|
+
const modulesDir = getModulesDir();
|
|
84
|
+
const moduleDir = path.join(modulesDir, kebabName);
|
|
85
|
+
|
|
86
|
+
// Load templates (custom or built-in)
|
|
87
|
+
const controllerTpl = await getTemplate('controller', controllerTemplate);
|
|
88
|
+
const serviceTpl = await getTemplate('service', serviceTemplate);
|
|
89
|
+
const repositoryTpl = await getTemplate('repository', repositoryTemplate);
|
|
90
|
+
const routesTpl = await getTemplate('routes', routesTemplate);
|
|
91
|
+
const moduleIndexTpl = await getTemplate('module-index', moduleIndexTemplate);
|
|
92
|
+
|
|
93
|
+
// Generate all files
|
|
94
|
+
const files = [
|
|
95
|
+
{
|
|
96
|
+
name: `${kebabName}.types.ts`,
|
|
97
|
+
content: dynamicTypesTemplate(kebabName, pascalName, fields),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: `${kebabName}.schemas.ts`,
|
|
101
|
+
content: dynamicSchemasTemplate(
|
|
102
|
+
kebabName,
|
|
103
|
+
pascalName,
|
|
104
|
+
camelName,
|
|
105
|
+
fields,
|
|
106
|
+
options.validator || 'zod'
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: `${kebabName}.service.ts`,
|
|
111
|
+
content: serviceTpl(kebabName, pascalName, camelName),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: `${kebabName}.controller.ts`,
|
|
115
|
+
content: controllerTpl(kebabName, pascalName, camelName),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'index.ts',
|
|
119
|
+
content: moduleIndexTpl(kebabName, pascalName, camelName),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: `${kebabName}.repository.ts`,
|
|
123
|
+
content: repositoryTpl(kebabName, pascalName, camelName, pluralName),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: `${kebabName}.routes.ts`,
|
|
127
|
+
content: routesTpl(kebabName, pascalName, camelName, pluralName),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// Write all files
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
await writeFile(path.join(moduleDir, file.name), file.content);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Generate test files
|
|
137
|
+
const testDir = path.join(moduleDir, '__tests__');
|
|
138
|
+
|
|
139
|
+
// Load test templates (custom or built-in)
|
|
140
|
+
const controllerTestTpl = await getTemplate('controller-test', controllerTestTemplate);
|
|
141
|
+
const serviceTestTpl = await getTemplate('service-test', serviceTestTemplate);
|
|
142
|
+
const integrationTestTpl = await getTemplate('integration-test', integrationTestTemplate);
|
|
143
|
+
|
|
144
|
+
await writeFile(
|
|
145
|
+
path.join(testDir, `${kebabName}.controller.test.ts`),
|
|
146
|
+
controllerTestTpl(kebabName, pascalName, camelName)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await writeFile(
|
|
150
|
+
path.join(testDir, `${kebabName}.service.test.ts`),
|
|
151
|
+
serviceTestTpl(kebabName, pascalName, camelName)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await writeFile(
|
|
155
|
+
path.join(testDir, `${kebabName}.integration.test.ts`),
|
|
156
|
+
integrationTestTpl(kebabName, pascalName, camelName)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
spinner.succeed(`Resource "${pascalName}" scaffolded successfully!`);
|
|
160
|
+
|
|
161
|
+
// Show Prisma model
|
|
162
|
+
console.log('\n' + 'ā'.repeat(70));
|
|
163
|
+
info('š Prisma model to add to schema.prisma:');
|
|
164
|
+
console.log(chalk.gray('\n// Copy this to your schema.prisma file:\n'));
|
|
165
|
+
console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
|
|
166
|
+
console.log('ā'.repeat(70));
|
|
167
|
+
|
|
168
|
+
// Show fields summary
|
|
169
|
+
console.log('\nš Fields scaffolded:');
|
|
170
|
+
fields.forEach((f) => {
|
|
171
|
+
const opts = [];
|
|
172
|
+
if (f.isOptional) opts.push('optional');
|
|
173
|
+
if (f.isArray) opts.push('array');
|
|
174
|
+
if (f.isUnique) opts.push('unique');
|
|
175
|
+
if (f.relation) opts.push(`relation: ${f.relation.model}`);
|
|
176
|
+
const optsStr = opts.length > 0 ? ` (${opts.join(', ')})` : '';
|
|
177
|
+
success(` ${f.name}: ${f.type}${optsStr}`);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Show files created
|
|
181
|
+
console.log('\nš Files created:');
|
|
182
|
+
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
183
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
184
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
185
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
186
|
+
|
|
187
|
+
// Show next steps
|
|
188
|
+
console.log('\nš Next steps:');
|
|
189
|
+
info(' 1. Add the Prisma model to your schema.prisma file');
|
|
190
|
+
info(' 2. Run: npx prisma db push (or prisma migrate dev)');
|
|
191
|
+
info(' 3. Run: npx prisma generate');
|
|
192
|
+
info(' 4. Register the module routes in your app');
|
|
193
|
+
info(' 5. Update the test files with actual test data');
|
|
194
|
+
|
|
195
|
+
console.log(
|
|
196
|
+
chalk.gray('\nš” Tip: Use --dry-run to preview changes before applying them\n')
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (options.dryRun) {
|
|
200
|
+
dryRun.printSummary();
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
spinner.fail('Failed to scaffold resource');
|
|
204
|
+
if (error instanceof Error) {
|
|
205
|
+
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
206
|
+
console.error(chalk.gray(error.stack));
|
|
207
|
+
}
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
);
|