nexpgen 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/.gitattributes +2 -0
- package/README.md +517 -0
- package/bin/cli.js +58 -0
- package/package.json +44 -0
- package/src/commands/generate.js +205 -0
- package/src/commands/init.js +191 -0
- package/src/templates/controller.template +127 -0
- package/src/templates/error.template +24 -0
- package/src/templates/helpers/imageUpload.template +191 -0
- package/src/templates/helpers/timeConverter.template +171 -0
- package/src/templates/middleware.template +302 -0
- package/src/templates/route.template +56 -0
- package/src/templates/servers/clean-arrow.template +42 -0
- package/src/templates/servers/clean-class.template +46 -0
- package/src/templates/servers/layered-arrow.template +42 -0
- package/src/templates/servers/layered-class.template +46 -0
- package/src/templates/servers/microservices-arrow.template +41 -0
- package/src/templates/servers/microservices-class.template +45 -0
- package/src/templates/servers/mvc-arrow.template +44 -0
- package/src/templates/servers/mvc-class.template +48 -0
- package/src/templates/servers/simple-arrow.template +42 -0
- package/src/templates/servers/simple-class.template +46 -0
- package/src/templates/service.template +121 -0
- package/src/templates/util.template +39 -0
- package/src/utils/architectureGenerator.js +925 -0
- package/src/utils/fileGenerator.js +617 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TemplateError extends Error {
|
|
6
|
+
constructor(message, templateName) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'TemplateError';
|
|
9
|
+
this.templateName = templateName;
|
|
10
|
+
Error.captureStackTrace(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class ConfigError extends Error {
|
|
15
|
+
constructor(message, configPath) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ConfigError';
|
|
18
|
+
this.configPath = configPath;
|
|
19
|
+
Error.captureStackTrace(this, this.constructor);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class ValidationError extends Error {
|
|
24
|
+
constructor(message, field) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'ValidationError';
|
|
27
|
+
this.field = field;
|
|
28
|
+
Error.captureStackTrace(this, this.constructor);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class FileSystemError extends Error {
|
|
33
|
+
constructor(message, filePath, code) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'FileSystemError';
|
|
36
|
+
this.filePath = filePath;
|
|
37
|
+
this.code = code;
|
|
38
|
+
Error.captureStackTrace(this, this.constructor);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
const logger = {
|
|
44
|
+
_isVerbose: process.env.nexpgen_VERBOSE === 'true' || process.env.NODE_ENV === 'development',
|
|
45
|
+
|
|
46
|
+
info(message) {
|
|
47
|
+
console.log(`ℹ️ ${message}`);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
success(message) {
|
|
51
|
+
console.log(`✅ ${message}`);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
warn(message) {
|
|
55
|
+
console.warn(`⚠️ ${message}`);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
error(message, error = null) {
|
|
59
|
+
console.error(`❌ ${message}`);
|
|
60
|
+
if (error && this._isVerbose && error.stack) {
|
|
61
|
+
console.error('Stack trace:', error.stack);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
debug(message) {
|
|
66
|
+
if (this._isVerbose) {
|
|
67
|
+
console.log(`🔍 DEBUG: ${message}`);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
verbose(message) {
|
|
72
|
+
if (this._isVerbose) {
|
|
73
|
+
console.log(`📝 ${message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
const utils = {
|
|
80
|
+
/**
|
|
81
|
+
* Safely read file with error handling
|
|
82
|
+
*/
|
|
83
|
+
readFileSafe(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
if (!fs.existsSync(filePath)) {
|
|
86
|
+
throw new FileSystemError(`File not found: ${filePath}`, filePath, 'ENOENT');
|
|
87
|
+
}
|
|
88
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
|
+
logger.debug(`Read file: ${filePath}`);
|
|
90
|
+
return content;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof FileSystemError) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
if (error.code === 'EACCES') {
|
|
96
|
+
throw new FileSystemError(`Permission denied reading: ${filePath}`, filePath, 'EACCES');
|
|
97
|
+
}
|
|
98
|
+
throw new FileSystemError(`Failed to read file: ${error.message}`, filePath, error.code);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Safely write file with directory creation
|
|
104
|
+
*/
|
|
105
|
+
writeFileSafe(filePath, content) {
|
|
106
|
+
try {
|
|
107
|
+
const dir = path.dirname(filePath);
|
|
108
|
+
if (!fs.existsSync(dir)) {
|
|
109
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
110
|
+
logger.debug(`Created directory: ${dir}`);
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
113
|
+
logger.debug(`Wrote file: ${filePath}`);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code === 'EACCES') {
|
|
117
|
+
throw new FileSystemError(`Permission denied writing: ${filePath}`, filePath, 'EACCES');
|
|
118
|
+
}
|
|
119
|
+
throw new FileSystemError(`Failed to write file: ${error.message}`, filePath, error.code);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if path is safe (within cwd) - prevents directory traversal
|
|
125
|
+
*/
|
|
126
|
+
isPathSafe(targetPath, basePath = process.cwd()) {
|
|
127
|
+
try {
|
|
128
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
129
|
+
const resolvedBase = path.resolve(basePath);
|
|
130
|
+
const normalizedTarget = path.normalize(resolvedTarget);
|
|
131
|
+
const normalizedBase = path.normalize(resolvedBase);
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
const isWithin = normalizedTarget.startsWith(normalizedBase);
|
|
135
|
+
const hasDangerousPatterns = normalizedTarget.includes('..') ||
|
|
136
|
+
normalizedTarget.includes('\0') ||
|
|
137
|
+
normalizedTarget.includes('\x00');
|
|
138
|
+
|
|
139
|
+
return isWithin && !hasDangerousPatterns;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger.debug(`Path safety check failed: ${error.message}`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Deep clone object (prevents accidental mutations)
|
|
148
|
+
*/
|
|
149
|
+
deepClone(obj) {
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(JSON.stringify(obj));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.warn(`Deep clone failed, returning original: ${error.message}`);
|
|
154
|
+
return obj;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Validate file path
|
|
160
|
+
*/
|
|
161
|
+
validateFilePath(filePath) {
|
|
162
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
163
|
+
throw new ValidationError('File path must be a non-empty string', 'filePath');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if (filePath.includes('..') || filePath.includes('\0') || filePath.includes('\x00')) {
|
|
168
|
+
throw new ValidationError('File path contains dangerous patterns', 'filePath');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Escape regex special characters
|
|
176
|
+
*/
|
|
177
|
+
escapeRegex(str) {
|
|
178
|
+
if (!str || typeof str !== 'string') return '';
|
|
179
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
class ArchitectureGenerator {
|
|
184
|
+
constructor() {
|
|
185
|
+
this.templateDir = path.join(__dirname, '../templates');
|
|
186
|
+
this.serverTemplateDir = path.join(this.templateDir, 'servers');
|
|
187
|
+
this.helperTemplateDir = path.join(this.templateDir, 'helpers');
|
|
188
|
+
this.validatePaths();
|
|
189
|
+
|
|
190
|
+
this._templateCache = new Map();
|
|
191
|
+
|
|
192
|
+
this._ifRegex = /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{else\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
193
|
+
|
|
194
|
+
this._fileExistsCache = new Map();
|
|
195
|
+
this._fileExistsCacheTime = 0;
|
|
196
|
+
this._CACHE_TTL = 5000;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
validatePaths() {
|
|
200
|
+
try {
|
|
201
|
+
|
|
202
|
+
const pathsToCheck = [
|
|
203
|
+
{ path: this.templateDir, name: 'Template directory' },
|
|
204
|
+
{ path: this.serverTemplateDir, name: 'Server template directory' },
|
|
205
|
+
{ path: this.helperTemplateDir, name: 'Helper template directory' }
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
for (const { path: checkPath, name } of pathsToCheck) {
|
|
209
|
+
if (!fs.existsSync(checkPath)) {
|
|
210
|
+
throw new FileSystemError(`${name} not found: ${checkPath}`, checkPath, 'ENOENT');
|
|
211
|
+
}
|
|
212
|
+
const stats = fs.statSync(checkPath);
|
|
213
|
+
if (!stats.isDirectory()) {
|
|
214
|
+
throw new FileSystemError(`${name} is not a directory: ${checkPath}`, checkPath, 'ENOTDIR');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
logger.debug('All template paths validated successfully');
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error instanceof FileSystemError) {
|
|
220
|
+
logger.error(`Path validation failed: ${error.message}`);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
throw new FileSystemError(`Path validation failed: ${error.message}`, '', error.code);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_clearFileExistsCache() {
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
if (now - this._fileExistsCacheTime > this._CACHE_TTL) {
|
|
230
|
+
this._fileExistsCache.clear();
|
|
231
|
+
this._fileExistsCacheTime = now;
|
|
232
|
+
logger.debug('File existence cache cleared');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_cacheFileExists(filePath) {
|
|
237
|
+
this._clearFileExistsCache();
|
|
238
|
+
if (!this._fileExistsCache.has(filePath)) {
|
|
239
|
+
this._fileExistsCache.set(filePath, fs.existsSync(filePath));
|
|
240
|
+
}
|
|
241
|
+
return this._fileExistsCache.get(filePath);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
readTemplate(templatePath) {
|
|
245
|
+
try {
|
|
246
|
+
if (!templatePath || typeof templatePath !== 'string') {
|
|
247
|
+
throw new ValidationError('Template path must be a non-empty string', 'templatePath');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
utils.validateFilePath(templatePath);
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if (this._templateCache.has(templatePath)) {
|
|
255
|
+
logger.debug(`Template cache hit: ${templatePath}`);
|
|
256
|
+
return this._templateCache.get(templatePath);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const normalizedPath = path.normalize(templatePath);
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if (!utils.isPathSafe(normalizedPath, this.templateDir)) {
|
|
263
|
+
throw new TemplateError(`Template path outside template directory: ${templatePath}`, templatePath);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!this._cacheFileExists(normalizedPath)) {
|
|
267
|
+
throw new TemplateError(`Template not found: ${templatePath}`, templatePath);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const stats = fs.statSync(normalizedPath);
|
|
271
|
+
if (!stats.isFile()) {
|
|
272
|
+
throw new TemplateError(`Path is not a file: ${templatePath}`, templatePath);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const content = utils.readFileSafe(normalizedPath);
|
|
276
|
+
|
|
277
|
+
if (!content || content.trim().length === 0) {
|
|
278
|
+
throw new TemplateError(`Template file is empty: ${templatePath}`, templatePath);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
this._templateCache.set(templatePath, content);
|
|
283
|
+
logger.debug(`Template cached: ${templatePath}`);
|
|
284
|
+
return content;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error.code === 'ENOENT') {
|
|
287
|
+
throw new TemplateError(`Template file not found: ${templatePath}`, templatePath);
|
|
288
|
+
} else if (error.code === 'EACCES') {
|
|
289
|
+
throw new TemplateError(`Permission denied reading template: ${templatePath}`, templatePath);
|
|
290
|
+
}
|
|
291
|
+
if (error instanceof TemplateError || error instanceof ValidationError || error instanceof FileSystemError) {
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
throw new TemplateError(`Failed to read template: ${error.message}`, templatePath);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
processTemplate(template, data) {
|
|
299
|
+
try {
|
|
300
|
+
if (!template || typeof template !== 'string') {
|
|
301
|
+
throw new ValidationError('Template must be a non-empty string', 'template');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!data || typeof data !== 'object') {
|
|
305
|
+
throw new ValidationError('Template data must be an object', 'data');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let content = template;
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
this._ifRegex.lastIndex = 0;
|
|
313
|
+
content = content.replace(this._ifRegex, (match, condition, ifBlock, elseBlock) => {
|
|
314
|
+
if (!condition || typeof condition !== 'string') {
|
|
315
|
+
return match;
|
|
316
|
+
}
|
|
317
|
+
return data[condition] ? ifBlock : elseBlock;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
const sortedKeys = Object.keys(data).sort((a, b) => b.length - a.length);
|
|
323
|
+
sortedKeys.forEach(key => {
|
|
324
|
+
if (key && typeof key === 'string') {
|
|
325
|
+
const value = data[key];
|
|
326
|
+
const safeValue = value !== null && value !== undefined ? String(value) : '';
|
|
327
|
+
|
|
328
|
+
const escapedKey = utils.escapeRegex(key);
|
|
329
|
+
const regex = new RegExp(`\\{\\{${escapedKey}\\}\\}`, 'g');
|
|
330
|
+
content = content.replace(regex, safeValue);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return content;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (error instanceof ValidationError) {
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`Template processing failed: ${error.message}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
validateInputs(architecture, functionStyle, packageName, packages) {
|
|
344
|
+
const errors = [];
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
const validArchitectures = ['mvc', 'microservices', 'layered', 'clean', 'simple'];
|
|
348
|
+
if (!architecture || typeof architecture !== 'string') {
|
|
349
|
+
errors.push('Architecture must be a non-empty string');
|
|
350
|
+
} else if (!validArchitectures.includes(architecture)) {
|
|
351
|
+
errors.push(`Invalid architecture. Must be one of: ${validArchitectures.join(', ')}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
const validStyles = ['class', 'arrow'];
|
|
356
|
+
if (!functionStyle || typeof functionStyle !== 'string') {
|
|
357
|
+
errors.push('Function style must be a non-empty string');
|
|
358
|
+
} else if (!validStyles.includes(functionStyle)) {
|
|
359
|
+
errors.push(`Invalid function style. Must be one of: ${validStyles.join(', ')}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
if (!packageName || typeof packageName !== 'string' || packageName.trim().length === 0) {
|
|
364
|
+
errors.push('Package name must be a non-empty string');
|
|
365
|
+
} else {
|
|
366
|
+
|
|
367
|
+
const npmNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
368
|
+
if (!npmNameRegex.test(packageName)) {
|
|
369
|
+
errors.push('Invalid package name format. Must be a valid npm package name');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
if (!Array.isArray(packages)) {
|
|
375
|
+
errors.push('Packages must be an array');
|
|
376
|
+
} else {
|
|
377
|
+
const validPackages = ['dotenv', 'helmet', 'imageUpload', 'timeConverter'];
|
|
378
|
+
packages.forEach(pkg => {
|
|
379
|
+
if (typeof pkg !== 'string') {
|
|
380
|
+
errors.push('All package names must be strings');
|
|
381
|
+
} else if (!validPackages.includes(pkg)) {
|
|
382
|
+
errors.push(`Invalid package: ${pkg}. Valid packages: ${validPackages.join(', ')}`);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (errors.length > 0) {
|
|
388
|
+
throw new ValidationError(`Validation errors:\n${errors.map(e => ` - ${e}`).join('\n')}`, 'inputs');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
generate(architecture, functionStyle, packageName, packages = []) {
|
|
393
|
+
try {
|
|
394
|
+
|
|
395
|
+
this.validateInputs(architecture, functionStyle, packageName, packages);
|
|
396
|
+
|
|
397
|
+
const structures = {
|
|
398
|
+
mvc: this.getMVCStructure,
|
|
399
|
+
microservices: this.getMicroservicesStructure,
|
|
400
|
+
layered: this.getLayeredStructure,
|
|
401
|
+
clean: this.getCleanArchitectureStructure,
|
|
402
|
+
simple: this.getSimpleStructure
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const structureGenerator = structures[architecture];
|
|
406
|
+
if (!structureGenerator) {
|
|
407
|
+
throw new ValidationError(`Unknown architecture: ${architecture}`, 'architecture');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const structure = structureGenerator.call(this);
|
|
411
|
+
this.createStructure(process.cwd(), structure);
|
|
412
|
+
this.createServerJs(architecture, functionStyle, packages);
|
|
413
|
+
this.createHelperFiles(packages, functionStyle);
|
|
414
|
+
this.createPackageJson(packageName, packages);
|
|
415
|
+
|
|
416
|
+
logger.success('Architecture generation completed successfully');
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error instanceof ValidationError || error instanceof FileSystemError || error instanceof TemplateError) {
|
|
419
|
+
logger.error(`Error generating architecture: ${error.message}`, error);
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
logger.error(`Error generating architecture: ${error.message}`, error);
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
getMVCStructure() {
|
|
428
|
+
return {
|
|
429
|
+
'src': {
|
|
430
|
+
'controllers': {},
|
|
431
|
+
'models': {},
|
|
432
|
+
'views': {},
|
|
433
|
+
'routes': {},
|
|
434
|
+
'middleware': {},
|
|
435
|
+
'config': {},
|
|
436
|
+
'utils': {}
|
|
437
|
+
},
|
|
438
|
+
'public': {
|
|
439
|
+
'css': {},
|
|
440
|
+
'js': {},
|
|
441
|
+
'images': {}
|
|
442
|
+
},
|
|
443
|
+
'config': {
|
|
444
|
+
'uploads': {
|
|
445
|
+
'images': {}
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
'tests': {}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
getMicroservicesStructure() {
|
|
453
|
+
return {
|
|
454
|
+
'services': {
|
|
455
|
+
'api-gateway': {
|
|
456
|
+
'src': {
|
|
457
|
+
'controllers': {},
|
|
458
|
+
'routes': {},
|
|
459
|
+
'middleware': {}
|
|
460
|
+
},
|
|
461
|
+
'public': {
|
|
462
|
+
'css': {},
|
|
463
|
+
'js': {},
|
|
464
|
+
'images': {}
|
|
465
|
+
},
|
|
466
|
+
'config': {
|
|
467
|
+
'uploads': {
|
|
468
|
+
'images': {}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
'user-service': {
|
|
473
|
+
'src': {
|
|
474
|
+
'controllers': {},
|
|
475
|
+
'services': {},
|
|
476
|
+
'models': {},
|
|
477
|
+
'routes': {}
|
|
478
|
+
},
|
|
479
|
+
'config': {
|
|
480
|
+
'uploads': {
|
|
481
|
+
'images': {}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
'shared': {
|
|
486
|
+
'utils': {},
|
|
487
|
+
'errors': {}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
'public': {
|
|
491
|
+
'css': {},
|
|
492
|
+
'js': {},
|
|
493
|
+
'images': {}
|
|
494
|
+
},
|
|
495
|
+
'config': {
|
|
496
|
+
'uploads': {
|
|
497
|
+
'images': {}
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
'tests': {}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
getLayeredStructure() {
|
|
505
|
+
return {
|
|
506
|
+
'src': {
|
|
507
|
+
'presentation': {
|
|
508
|
+
'controllers': {},
|
|
509
|
+
'routes': {},
|
|
510
|
+
'middleware': {}
|
|
511
|
+
},
|
|
512
|
+
'business': {
|
|
513
|
+
'services': {},
|
|
514
|
+
'interfaces': {}
|
|
515
|
+
},
|
|
516
|
+
'data': {
|
|
517
|
+
'repositories': {},
|
|
518
|
+
'models': {}
|
|
519
|
+
},
|
|
520
|
+
'infrastructure': {
|
|
521
|
+
'config': {},
|
|
522
|
+
'database': {}
|
|
523
|
+
},
|
|
524
|
+
'utils': {}
|
|
525
|
+
},
|
|
526
|
+
'public': {
|
|
527
|
+
'css': {},
|
|
528
|
+
'js': {},
|
|
529
|
+
'images': {}
|
|
530
|
+
},
|
|
531
|
+
'config': {
|
|
532
|
+
'uploads': {
|
|
533
|
+
'images': {}
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
'tests': {}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
getCleanArchitectureStructure() {
|
|
541
|
+
return {
|
|
542
|
+
'src': {
|
|
543
|
+
'domain': {
|
|
544
|
+
'entities': {},
|
|
545
|
+
'interfaces': {},
|
|
546
|
+
'errors': {}
|
|
547
|
+
},
|
|
548
|
+
'application': {
|
|
549
|
+
'use-cases': {},
|
|
550
|
+
'dto': {}
|
|
551
|
+
},
|
|
552
|
+
'infrastructure': {
|
|
553
|
+
'controllers': {},
|
|
554
|
+
'repositories': {},
|
|
555
|
+
'database': {},
|
|
556
|
+
'config': {}
|
|
557
|
+
},
|
|
558
|
+
'presentation': {
|
|
559
|
+
'routes': {},
|
|
560
|
+
'middleware': {}
|
|
561
|
+
},
|
|
562
|
+
'utils': {}
|
|
563
|
+
},
|
|
564
|
+
'public': {
|
|
565
|
+
'css': {},
|
|
566
|
+
'js': {},
|
|
567
|
+
'images': {}
|
|
568
|
+
},
|
|
569
|
+
'config': {
|
|
570
|
+
'uploads': {
|
|
571
|
+
'images': {}
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
'tests': {}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
getSimpleStructure() {
|
|
579
|
+
return {
|
|
580
|
+
'src': {
|
|
581
|
+
'controllers': {},
|
|
582
|
+
'services': {},
|
|
583
|
+
'routes': {},
|
|
584
|
+
'utils': {},
|
|
585
|
+
'config': {}
|
|
586
|
+
},
|
|
587
|
+
'public': {
|
|
588
|
+
'css': {},
|
|
589
|
+
'js': {},
|
|
590
|
+
'images': {}
|
|
591
|
+
},
|
|
592
|
+
'config': {
|
|
593
|
+
'uploads': {
|
|
594
|
+
'images': {}
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
'tests': {}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
createStructure(basePath, structure) {
|
|
602
|
+
try {
|
|
603
|
+
if (!basePath || typeof basePath !== 'string') {
|
|
604
|
+
throw new ValidationError('Base path must be a non-empty string', 'basePath');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const normalizedBasePath = path.resolve(basePath);
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
if (!utils.isPathSafe(normalizedBasePath)) {
|
|
611
|
+
throw new ValidationError(`Base path is not safe: ${normalizedBasePath}`, 'basePath');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
if (!this._cacheFileExists(normalizedBasePath)) {
|
|
616
|
+
throw new FileSystemError(`Base path does not exist: ${normalizedBasePath}`, normalizedBasePath, 'ENOENT');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const stats = fs.statSync(normalizedBasePath);
|
|
620
|
+
if (!stats.isDirectory()) {
|
|
621
|
+
throw new FileSystemError(`Base path is not a directory: ${normalizedBasePath}`, normalizedBasePath, 'ENOTDIR');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const createDir = (currentPath, structureObj) => {
|
|
625
|
+
if (!structureObj || typeof structureObj !== 'object') {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
Object.keys(structureObj).forEach(dir => {
|
|
630
|
+
if (!dir || typeof dir !== 'string') {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
const sanitizedDir = dir.replace(/[<>:"|?*\x00-\x1f]/g, '');
|
|
636
|
+
if (sanitizedDir !== dir) {
|
|
637
|
+
logger.warn(`Directory name sanitized: ${dir} -> ${sanitizedDir}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const dirPath = path.join(currentPath, sanitizedDir);
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
if (!utils.isPathSafe(dirPath, normalizedBasePath)) {
|
|
644
|
+
logger.warn(`Skipping unsafe path: ${dirPath}`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
|
|
650
|
+
if (!this._cacheFileExists(dirPath)) {
|
|
651
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
652
|
+
logger.info(`Created directory: ${dirPath}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (typeof structureObj[dir] === 'object' && structureObj[dir] !== null && Object.keys(structureObj[dir]).length > 0) {
|
|
656
|
+
createDir(dirPath, structureObj[dir]);
|
|
657
|
+
}
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (error.code === 'EACCES') {
|
|
660
|
+
throw new FileSystemError(`Permission denied creating directory: ${dirPath}`, dirPath, 'EACCES');
|
|
661
|
+
}
|
|
662
|
+
throw new FileSystemError(`Failed to create directory ${dirPath}: ${error.message}`, dirPath, error.code);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
createDir(normalizedBasePath, structure);
|
|
668
|
+
logger.debug('Directory structure created successfully');
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (error instanceof ValidationError || error instanceof FileSystemError) {
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
throw new FileSystemError(`Failed to create structure: ${error.message}`, '', error.code);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
createServerJs(architecture, functionStyle, packages) {
|
|
678
|
+
try {
|
|
679
|
+
const templateFileName = `${architecture}-${functionStyle}.template`;
|
|
680
|
+
const templatePath = path.join(this.serverTemplateDir, templateFileName);
|
|
681
|
+
|
|
682
|
+
const template = this.readTemplate(templatePath);
|
|
683
|
+
const templateData = {
|
|
684
|
+
useDotenv: Array.isArray(packages) && packages.includes('dotenv'),
|
|
685
|
+
useHelmet: Array.isArray(packages) && packages.includes('helmet')
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const content = this.processTemplate(template, templateData);
|
|
689
|
+
const serverPath = path.resolve(process.cwd(), 'server.js');
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
if (!utils.isPathSafe(serverPath)) {
|
|
693
|
+
throw new ValidationError(`Server path is not safe: ${serverPath}`, 'serverPath');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
if (this._cacheFileExists(serverPath)) {
|
|
698
|
+
logger.warn('server.js already exists, skipping...');
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
utils.writeFileSafe(serverPath, content);
|
|
703
|
+
logger.success('Created: server.js');
|
|
704
|
+
} catch (error) {
|
|
705
|
+
if (error instanceof TemplateError || error instanceof ValidationError || error instanceof FileSystemError) {
|
|
706
|
+
logger.error(`Error creating server.js: ${error.message}`, error);
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
logger.error(`Error creating server.js: ${error.message}`, error);
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
createHelperFiles(packages, functionStyle) {
|
|
715
|
+
if (!Array.isArray(packages) || packages.length === 0) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const helpers = {
|
|
720
|
+
imageUpload: {
|
|
721
|
+
template: 'imageUpload.template',
|
|
722
|
+
fileName: 'imageUploadHelper.js',
|
|
723
|
+
directory: 'src/utils'
|
|
724
|
+
},
|
|
725
|
+
timeConverter: {
|
|
726
|
+
template: 'timeConverter.template',
|
|
727
|
+
fileName: 'customHelp.js',
|
|
728
|
+
directory: 'src/utils'
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
packages.forEach(pkg => {
|
|
733
|
+
if (!pkg || typeof pkg !== 'string') {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (helpers[pkg]) {
|
|
738
|
+
const helper = helpers[pkg];
|
|
739
|
+
const templatePath = path.join(this.helperTemplateDir, helper.template);
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const template = this.readTemplate(templatePath);
|
|
743
|
+
const templateData = {
|
|
744
|
+
isClass: functionStyle === 'class'
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const content = this.processTemplate(template, templateData);
|
|
748
|
+
const helperDir = path.resolve(process.cwd(), helper.directory);
|
|
749
|
+
const helperPath = path.join(helperDir, helper.fileName);
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
if (!utils.isPathSafe(helperPath)) {
|
|
753
|
+
logger.warn(`Helper path is not safe, skipping: ${helperPath}`);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
if (!this._cacheFileExists(helperDir)) {
|
|
759
|
+
try {
|
|
760
|
+
fs.mkdirSync(helperDir, { recursive: true });
|
|
761
|
+
this._fileExistsCache.delete(helperDir);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (error.code === 'EACCES') {
|
|
764
|
+
throw new FileSystemError(`Permission denied creating directory: ${helperDir}`, helperDir, 'EACCES');
|
|
765
|
+
}
|
|
766
|
+
throw new FileSystemError(`Failed to create directory ${helperDir}: ${error.message}`, helperDir, error.code);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
if (this._cacheFileExists(helperPath)) {
|
|
772
|
+
logger.warn(`${helper.fileName} already exists, skipping...`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
utils.writeFileSafe(helperPath, content);
|
|
777
|
+
logger.success(`Created: ${helper.directory}/${helper.fileName}`);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
logger.error(`Error creating ${helper.fileName}: ${error.message}`, error);
|
|
780
|
+
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
if (packages.includes('dotenv')) {
|
|
787
|
+
try {
|
|
788
|
+
const envExamplePath = path.resolve(process.cwd(), '.env.example');
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
if (!utils.isPathSafe(envExamplePath)) {
|
|
792
|
+
logger.warn(`.env.example path is not safe, skipping`);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (!this._cacheFileExists(envExamplePath)) {
|
|
797
|
+
const envExample = `PORT=3000
|
|
798
|
+
NODE_ENV=development
|
|
799
|
+
# Add your environment variables here
|
|
800
|
+
`;
|
|
801
|
+
utils.writeFileSafe(envExamplePath, envExample);
|
|
802
|
+
logger.success('Created: .env.example');
|
|
803
|
+
}
|
|
804
|
+
} catch (error) {
|
|
805
|
+
logger.error(`Error creating .env.example: ${error.message}`, error);
|
|
806
|
+
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
createPackageJson(packageName, packages = []) {
|
|
812
|
+
try {
|
|
813
|
+
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
if (!utils.isPathSafe(packageJsonPath)) {
|
|
817
|
+
throw new ValidationError(`Package.json path is not safe: ${packageJsonPath}`, 'packageJsonPath');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
const packageDependencies = {
|
|
822
|
+
dotenv: '^16.3.1',
|
|
823
|
+
helmet: '^7.1.0',
|
|
824
|
+
imageUpload: { multer: '^1.4.5-lts.1' },
|
|
825
|
+
timeConverter: {}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
const dependencies = {
|
|
829
|
+
express: '^4.18.2'
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
if (Array.isArray(packages)) {
|
|
834
|
+
packages.forEach(pkg => {
|
|
835
|
+
if (pkg && typeof pkg === 'string' && packageDependencies[pkg]) {
|
|
836
|
+
if (typeof packageDependencies[pkg] === 'object') {
|
|
837
|
+
Object.assign(dependencies, packageDependencies[pkg]);
|
|
838
|
+
} else {
|
|
839
|
+
dependencies[pkg] = packageDependencies[pkg];
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
let packageJson;
|
|
846
|
+
const fileExists = this._cacheFileExists(packageJsonPath);
|
|
847
|
+
|
|
848
|
+
if (fileExists) {
|
|
849
|
+
try {
|
|
850
|
+
const existingContent = utils.readFileSafe(packageJsonPath);
|
|
851
|
+
packageJson = JSON.parse(existingContent);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (error instanceof SyntaxError) {
|
|
854
|
+
throw new ConfigError(`Invalid JSON in existing package.json: ${error.message}`, packageJsonPath);
|
|
855
|
+
}
|
|
856
|
+
throw new FileSystemError(`Failed to read package.json: ${error.message}`, packageJsonPath, error.code);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
packageJson.name = packageName;
|
|
860
|
+
packageJson.main = 'server.js';
|
|
861
|
+
packageJson.scripts = {
|
|
862
|
+
...(packageJson.scripts || {}),
|
|
863
|
+
start: 'node server.js',
|
|
864
|
+
dev: 'node server.js'
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
if (!packageJson.dependencies) {
|
|
868
|
+
packageJson.dependencies = {};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
Object.assign(packageJson.dependencies, dependencies);
|
|
873
|
+
} else {
|
|
874
|
+
packageJson = {
|
|
875
|
+
name: packageName,
|
|
876
|
+
version: '1.0.0',
|
|
877
|
+
description: '',
|
|
878
|
+
main: 'server.js',
|
|
879
|
+
scripts: {
|
|
880
|
+
start: 'node server.js',
|
|
881
|
+
dev: 'node server.js'
|
|
882
|
+
},
|
|
883
|
+
dependencies: dependencies,
|
|
884
|
+
author: '',
|
|
885
|
+
license: 'ISC'
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
try {
|
|
890
|
+
const jsonContent = JSON.stringify(packageJson, null, 2);
|
|
891
|
+
utils.writeFileSafe(packageJsonPath, jsonContent);
|
|
892
|
+
logger.success(`${fileExists ? 'Updated' : 'Created'}: package.json`);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
if (error.code === 'EACCES') {
|
|
895
|
+
throw new FileSystemError(`Permission denied writing package.json`, packageJsonPath, 'EACCES');
|
|
896
|
+
}
|
|
897
|
+
throw new FileSystemError(`Failed to write package.json: ${error.message}`, packageJsonPath, error.code);
|
|
898
|
+
}
|
|
899
|
+
} catch (error) {
|
|
900
|
+
if (error instanceof ValidationError || error instanceof ConfigError || error instanceof FileSystemError) {
|
|
901
|
+
logger.error(`Error creating/updating package.json: ${error.message}`, error);
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
logger.error(`Error creating/updating package.json: ${error.message}`, error);
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
clearCache() {
|
|
911
|
+
this._templateCache.clear();
|
|
912
|
+
this._fileExistsCache.clear();
|
|
913
|
+
this._fileExistsCacheTime = 0;
|
|
914
|
+
logger.debug('Architecture generator cache cleared');
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
module.exports = new ArchitectureGenerator();
|
|
920
|
+
module.exports.utils = utils;
|
|
921
|
+
module.exports.logger = logger;
|
|
922
|
+
module.exports.TemplateError = TemplateError;
|
|
923
|
+
module.exports.ConfigError = ConfigError;
|
|
924
|
+
module.exports.ValidationError = ValidationError;
|
|
925
|
+
module.exports.FileSystemError = FileSystemError;
|