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,617 @@
|
|
|
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
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class ConfigError extends Error {
|
|
14
|
+
constructor(message, configPath) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'ConfigError';
|
|
17
|
+
this.configPath = configPath;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class ValidationError extends Error {
|
|
22
|
+
constructor(message, field) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'ValidationError';
|
|
25
|
+
this.field = field;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const utils = {
|
|
31
|
+
/**
|
|
32
|
+
* Safely read file with error handling
|
|
33
|
+
*/
|
|
34
|
+
readFileSafe(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(filePath)) {
|
|
37
|
+
throw new Error(`File not found: ${filePath}`);
|
|
38
|
+
}
|
|
39
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code === 'EACCES') {
|
|
42
|
+
throw new Error(`Permission denied reading: ${filePath}`);
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Safely write file with directory creation
|
|
50
|
+
*/
|
|
51
|
+
writeFileSafe(filePath, content) {
|
|
52
|
+
try {
|
|
53
|
+
const dir = path.dirname(filePath);
|
|
54
|
+
if (!fs.existsSync(dir)) {
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === 'EACCES') {
|
|
61
|
+
throw new Error(`Permission denied writing: ${filePath}`);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if path is safe (within cwd)
|
|
69
|
+
*/
|
|
70
|
+
isPathSafe(targetPath, basePath = process.cwd()) {
|
|
71
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
72
|
+
const resolvedBase = path.resolve(basePath);
|
|
73
|
+
const normalizedTarget = path.normalize(resolvedTarget);
|
|
74
|
+
const normalizedBase = path.normalize(resolvedBase);
|
|
75
|
+
return normalizedTarget.startsWith(normalizedBase);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Deep clone object
|
|
80
|
+
*/
|
|
81
|
+
deepClone(obj) {
|
|
82
|
+
return JSON.parse(JSON.stringify(obj));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
class FileGenerator {
|
|
87
|
+
constructor() {
|
|
88
|
+
this.templateDir = path.join(__dirname, '../templates');
|
|
89
|
+
this.validateTemplateDir();
|
|
90
|
+
|
|
91
|
+
this._configCache = null;
|
|
92
|
+
this._templateCache = new Map();
|
|
93
|
+
this._configPath = null;
|
|
94
|
+
this._configMtime = null;
|
|
95
|
+
|
|
96
|
+
this._ifRegex = /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{else\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
97
|
+
|
|
98
|
+
this._fileExistsCache = new Map();
|
|
99
|
+
this._fileExistsCacheTime = 0;
|
|
100
|
+
this._CACHE_TTL = 5000;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
validateTemplateDir() {
|
|
104
|
+
try {
|
|
105
|
+
if (!fs.existsSync(this.templateDir)) {
|
|
106
|
+
throw new TemplateError(`Template directory not found: ${this.templateDir}`, 'templateDir');
|
|
107
|
+
}
|
|
108
|
+
const stats = fs.statSync(this.templateDir);
|
|
109
|
+
if (!stats.isDirectory()) {
|
|
110
|
+
throw new TemplateError(`Template path is not a directory: ${this.templateDir}`, 'templateDir');
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof TemplateError) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
throw new TemplateError(`Template directory validation failed: ${error.message}`, 'templateDir');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_clearFileExistsCache() {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
if (now - this._fileExistsCacheTime > this._CACHE_TTL) {
|
|
123
|
+
this._fileExistsCache.clear();
|
|
124
|
+
this._fileExistsCacheTime = now;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_cacheFileExists(filePath) {
|
|
129
|
+
this._clearFileExistsCache();
|
|
130
|
+
if (!this._fileExistsCache.has(filePath)) {
|
|
131
|
+
this._fileExistsCache.set(filePath, fs.existsSync(filePath));
|
|
132
|
+
}
|
|
133
|
+
return this._fileExistsCache.get(filePath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
readConfig() {
|
|
137
|
+
try {
|
|
138
|
+
const configPath = path.resolve(process.cwd(), '.nexpgenrc');
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if (this._configCache && this._configPath === configPath) {
|
|
142
|
+
try {
|
|
143
|
+
const stats = fs.statSync(configPath);
|
|
144
|
+
if (stats.mtimeMs === this._configMtime) {
|
|
145
|
+
return utils.deepClone(this._configCache);
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this._configPath = configPath;
|
|
153
|
+
|
|
154
|
+
if (!this._cacheFileExists(configPath)) {
|
|
155
|
+
this._configCache = { functionStyle: 'class' };
|
|
156
|
+
return utils.deepClone(this._configCache);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const stats = fs.statSync(configPath);
|
|
160
|
+
this._configMtime = stats.mtimeMs;
|
|
161
|
+
|
|
162
|
+
if (!stats.isFile()) {
|
|
163
|
+
throw new ConfigError('.nexpgenrc exists but is not a file', configPath);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const content = utils.readFileSafe(configPath);
|
|
167
|
+
|
|
168
|
+
if (!content || content.trim().length === 0) {
|
|
169
|
+
this._configCache = { functionStyle: 'class' };
|
|
170
|
+
return utils.deepClone(this._configCache);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const config = JSON.parse(content);
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if (config.functionStyle && !['class', 'arrow'].includes(config.functionStyle)) {
|
|
178
|
+
console.warn(`ā ļø Invalid functionStyle in config, using default: class`);
|
|
179
|
+
config.functionStyle = 'class';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if (config.packages && !Array.isArray(config.packages)) {
|
|
184
|
+
config.packages = [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this._configCache = config;
|
|
188
|
+
return utils.deepClone(this._configCache);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof SyntaxError) {
|
|
191
|
+
throw new ConfigError(`Invalid JSON in .nexpgenrc: ${error.message}`, configPath);
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error.code === 'EACCES') {
|
|
197
|
+
throw new ConfigError(`Permission denied reading .nexpgenrc`, this._configPath);
|
|
198
|
+
}
|
|
199
|
+
if (error instanceof ConfigError) {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
console.warn(`ā ļø Failed to read config, using defaults: ${error.message}`);
|
|
203
|
+
this._configCache = { functionStyle: 'class' };
|
|
204
|
+
return utils.deepClone(this._configCache);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
readTemplate(templateName) {
|
|
209
|
+
try {
|
|
210
|
+
if (!templateName || typeof templateName !== 'string' || templateName.trim().length === 0) {
|
|
211
|
+
throw new ValidationError('Template name must be a non-empty string', 'templateName');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if (this._templateCache.has(templateName)) {
|
|
216
|
+
return this._templateCache.get(templateName);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
const sanitizedName = path.basename(templateName);
|
|
221
|
+
if (sanitizedName !== templateName) {
|
|
222
|
+
throw new ValidationError(`Invalid template name: ${templateName}`, 'templateName');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const templatePath = path.join(this.templateDir, `${sanitizedName}.template`);
|
|
226
|
+
const normalizedPath = path.normalize(templatePath);
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if (!normalizedPath.startsWith(path.normalize(this.templateDir))) {
|
|
230
|
+
throw new TemplateError(`Template path outside template directory: ${templateName}`, templateName);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!this._cacheFileExists(normalizedPath)) {
|
|
234
|
+
throw new TemplateError(`Template not found: ${templateName}`, templateName);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const stats = fs.statSync(normalizedPath);
|
|
238
|
+
if (!stats.isFile()) {
|
|
239
|
+
throw new TemplateError(`Template path is not a file: ${templateName}`, templateName);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const content = utils.readFileSafe(normalizedPath);
|
|
243
|
+
|
|
244
|
+
if (!content || content.trim().length === 0) {
|
|
245
|
+
throw new TemplateError(`Template file is empty: ${templateName}`, templateName);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
this._templateCache.set(templateName, content);
|
|
250
|
+
return content;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error.code === 'ENOENT') {
|
|
253
|
+
throw new TemplateError(`Template file not found: ${templateName}`, templateName);
|
|
254
|
+
} else if (error.code === 'EACCES') {
|
|
255
|
+
throw new TemplateError(`Permission denied reading template: ${templateName}`, templateName);
|
|
256
|
+
}
|
|
257
|
+
if (error instanceof TemplateError || error instanceof ValidationError) {
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
throw new TemplateError(`Failed to read template: ${error.message}`, templateName);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
processTemplate(template, data) {
|
|
265
|
+
try {
|
|
266
|
+
if (!template || typeof template !== 'string') {
|
|
267
|
+
throw new ValidationError('Template must be a non-empty string', 'template');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!data || typeof data !== 'object') {
|
|
271
|
+
throw new ValidationError('Template data must be an object', 'data');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let content = template;
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
this._ifRegex.lastIndex = 0;
|
|
279
|
+
content = content.replace(this._ifRegex, (match, condition, ifBlock, elseBlock) => {
|
|
280
|
+
if (!condition || typeof condition !== 'string') {
|
|
281
|
+
return match;
|
|
282
|
+
}
|
|
283
|
+
return data[condition] ? ifBlock : elseBlock;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
const sortedKeys = Object.keys(data).sort((a, b) => b.length - a.length);
|
|
289
|
+
sortedKeys.forEach(key => {
|
|
290
|
+
if (key && typeof key === 'string') {
|
|
291
|
+
const value = data[key];
|
|
292
|
+
const safeValue = value !== null && value !== undefined ? String(value) : '';
|
|
293
|
+
|
|
294
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
295
|
+
const regex = new RegExp(`\\{\\{${escapedKey}\\}\\}`, 'g');
|
|
296
|
+
content = content.replace(regex, safeValue);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return content;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (error instanceof ValidationError) {
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
throw new Error(`Template processing failed: ${error.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
toCamelCase(str) {
|
|
310
|
+
if (!str || typeof str !== 'string') {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return str
|
|
315
|
+
.split(/[-_/]/)
|
|
316
|
+
.filter(word => word.length > 0)
|
|
317
|
+
.map((word, index) => {
|
|
318
|
+
const cleaned = word.trim();
|
|
319
|
+
if (cleaned.length === 0) return '';
|
|
320
|
+
if (index === 0) {
|
|
321
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
322
|
+
}
|
|
323
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
324
|
+
})
|
|
325
|
+
.filter(word => word.length > 0)
|
|
326
|
+
.join('');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
toPascalCase(str) {
|
|
330
|
+
if (!str || typeof str !== 'string') {
|
|
331
|
+
return '';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return str
|
|
335
|
+
.split(/[-_/]/)
|
|
336
|
+
.filter(word => word.length > 0)
|
|
337
|
+
.map(word => {
|
|
338
|
+
const cleaned = word.trim();
|
|
339
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
340
|
+
})
|
|
341
|
+
.filter(word => word.length > 0)
|
|
342
|
+
.join('');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
sanitizeFileName(fileName) {
|
|
346
|
+
if (!fileName || typeof fileName !== 'string') {
|
|
347
|
+
return 'file';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
let sanitized = fileName
|
|
352
|
+
.replace(/[<>:"|?*\x00-\x1f]/g, '')
|
|
353
|
+
.replace(/^\.+/, '')
|
|
354
|
+
.replace(/\.+$/, '')
|
|
355
|
+
.trim();
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
if (!sanitized || sanitized.length === 0) {
|
|
359
|
+
return 'file';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return sanitized;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
generateFromTemplate(templateType, name, outputDir) {
|
|
366
|
+
try {
|
|
367
|
+
|
|
368
|
+
if (!templateType || typeof templateType !== 'string') {
|
|
369
|
+
throw new ValidationError('Template type must be a non-empty string', 'templateType');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
373
|
+
throw new ValidationError('Name must be a non-empty string', 'name');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!outputDir || typeof outputDir !== 'string') {
|
|
377
|
+
throw new ValidationError('Output directory must be a non-empty string', 'outputDir');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const template = this.readTemplate(templateType);
|
|
381
|
+
const config = this.readConfig();
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
const nameParts = name.split('/').filter(part => part.trim().length > 0);
|
|
385
|
+
|
|
386
|
+
if (nameParts.length === 0) {
|
|
387
|
+
throw new ValidationError('Invalid name: empty after splitting', 'name');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const finalName = nameParts[nameParts.length - 1];
|
|
391
|
+
const sanitizedName = this.sanitizeFileName(finalName);
|
|
392
|
+
|
|
393
|
+
if (sanitizedName !== finalName) {
|
|
394
|
+
console.warn(`ā ļø Name sanitized: ${finalName} -> ${sanitizedName}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const className = this.toPascalCase(sanitizedName);
|
|
398
|
+
const camelCaseName = this.toCamelCase(sanitizedName);
|
|
399
|
+
|
|
400
|
+
if (!className || className.length === 0) {
|
|
401
|
+
throw new ValidationError('Failed to generate valid class name', 'className');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
let relativePath = '';
|
|
406
|
+
if (nameParts.length > 1) {
|
|
407
|
+
|
|
408
|
+
const dirParts = nameParts.slice(0, -1).map(part => this.sanitizeFileName(part));
|
|
409
|
+
relativePath = dirParts.join('/');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const fileName = this.getFileName(templateType, sanitizedName);
|
|
413
|
+
const resolvedOutputDir = path.resolve(outputDir);
|
|
414
|
+
const fullOutputDir = relativePath
|
|
415
|
+
? path.join(resolvedOutputDir, relativePath)
|
|
416
|
+
: resolvedOutputDir;
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if (!utils.isPathSafe(fullOutputDir)) {
|
|
420
|
+
throw new ValidationError(`Output directory outside current working directory: ${fullOutputDir}`, 'outputDir');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const templateData = {
|
|
424
|
+
name: sanitizedName,
|
|
425
|
+
className: className,
|
|
426
|
+
camelCaseName: camelCaseName,
|
|
427
|
+
isClass: config.functionStyle === 'class'
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const content = this.processTemplate(template, templateData);
|
|
431
|
+
|
|
432
|
+
const filePath = path.join(fullOutputDir, fileName);
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
if (!this._cacheFileExists(fullOutputDir)) {
|
|
436
|
+
try {
|
|
437
|
+
fs.mkdirSync(fullOutputDir, { recursive: true });
|
|
438
|
+
|
|
439
|
+
this._fileExistsCache.delete(fullOutputDir);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error.code === 'EACCES') {
|
|
442
|
+
throw new Error(`Permission denied creating directory: ${fullOutputDir}`);
|
|
443
|
+
}
|
|
444
|
+
throw new Error(`Failed to create directory ${fullOutputDir}: ${error.message}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
if (this._cacheFileExists(filePath)) {
|
|
450
|
+
const errorMsg = `File already exists: ${filePath}`;
|
|
451
|
+
console.error(`ā ${errorMsg}`);
|
|
452
|
+
throw new Error(errorMsg);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
utils.writeFileSafe(filePath, content);
|
|
456
|
+
console.log(`ā
Generated: ${filePath}`);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
const errorMsg = error instanceof ValidationError || error instanceof TemplateError
|
|
459
|
+
? error.message
|
|
460
|
+
: `Error generating file: ${error.message}`;
|
|
461
|
+
console.error(`ā ${errorMsg}`);
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
getFileName(templateType, name) {
|
|
467
|
+
const extensions = {
|
|
468
|
+
controller: '.controller.js',
|
|
469
|
+
service: '.service.js',
|
|
470
|
+
util: '.util.js',
|
|
471
|
+
error: '.error.js',
|
|
472
|
+
route: '.routes.js',
|
|
473
|
+
middleware: '.middleware.js'
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const extension = extensions[templateType] || '.js';
|
|
477
|
+
const sanitizedName = this.sanitizeFileName(name);
|
|
478
|
+
|
|
479
|
+
return `${sanitizedName}${extension}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
generateMiddleware(name, outputDir) {
|
|
483
|
+
try {
|
|
484
|
+
const nameParts = name.split('/').filter(part => part.trim().length > 0);
|
|
485
|
+
if (nameParts.length === 0) {
|
|
486
|
+
throw new ValidationError('Invalid middleware name: empty after splitting', 'name');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const finalName = nameParts[nameParts.length - 1];
|
|
490
|
+
const sanitizedName = this.sanitizeFileName(finalName);
|
|
491
|
+
const className = this.toPascalCase(sanitizedName);
|
|
492
|
+
const camelCaseName = this.toCamelCase(sanitizedName);
|
|
493
|
+
const config = this.readConfig();
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
const middlewareTypes = {
|
|
497
|
+
auth: { isAuth: true },
|
|
498
|
+
authentication: { isAuth: true },
|
|
499
|
+
validate: { isValidation: true },
|
|
500
|
+
validation: { isValidation: true },
|
|
501
|
+
error: { isErrorHandler: true },
|
|
502
|
+
errorhandler: { isErrorHandler: true },
|
|
503
|
+
ratelimit: { isRateLimit: true },
|
|
504
|
+
'rate-limit': { isRateLimit: true }
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const typeKey = sanitizedName.toLowerCase().replace(/[-_]/g, '');
|
|
508
|
+
const middlewareType = middlewareTypes[typeKey] || { isAuth: true };
|
|
509
|
+
|
|
510
|
+
const template = this.readTemplate('middleware');
|
|
511
|
+
const templateData = {
|
|
512
|
+
name: sanitizedName,
|
|
513
|
+
className: className,
|
|
514
|
+
camelCaseName: camelCaseName,
|
|
515
|
+
isClass: config.functionStyle === 'class',
|
|
516
|
+
...middlewareType
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const content = this.processTemplate(template, templateData);
|
|
520
|
+
|
|
521
|
+
let relativePath = '';
|
|
522
|
+
if (nameParts.length > 1) {
|
|
523
|
+
const dirParts = nameParts.slice(0, -1).map(part => this.sanitizeFileName(part));
|
|
524
|
+
relativePath = dirParts.join('/');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const fileName = this.getFileName('middleware', sanitizedName);
|
|
528
|
+
const resolvedOutputDir = path.resolve(outputDir);
|
|
529
|
+
const fullOutputDir = relativePath
|
|
530
|
+
? path.join(resolvedOutputDir, relativePath)
|
|
531
|
+
: resolvedOutputDir;
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
if (!utils.isPathSafe(fullOutputDir)) {
|
|
535
|
+
throw new ValidationError(`Output directory outside current working directory: ${fullOutputDir}`, 'outputDir');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
if (!this._cacheFileExists(fullOutputDir)) {
|
|
540
|
+
try {
|
|
541
|
+
fs.mkdirSync(fullOutputDir, { recursive: true });
|
|
542
|
+
this._fileExistsCache.delete(fullOutputDir);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error.code === 'EACCES') {
|
|
545
|
+
throw new Error(`Permission denied creating directory: ${fullOutputDir}`);
|
|
546
|
+
}
|
|
547
|
+
throw new Error(`Failed to create directory ${fullOutputDir}: ${error.message}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const filePath = path.join(fullOutputDir, fileName);
|
|
552
|
+
|
|
553
|
+
if (this._cacheFileExists(filePath)) {
|
|
554
|
+
throw new Error(`File already exists: ${filePath}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
utils.writeFileSafe(filePath, content);
|
|
558
|
+
console.log(`ā
Generated: ${filePath}`);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
const errorMsg = error instanceof ValidationError || error instanceof TemplateError
|
|
561
|
+
? error.message
|
|
562
|
+
: `Failed to generate middleware: ${error.message}`;
|
|
563
|
+
throw new Error(errorMsg);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
generateCRUD(name, outputDir) {
|
|
568
|
+
try {
|
|
569
|
+
console.log(`\nš Generating CRUD for: ${name}\n`);
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
const nameParts = name.split('/').filter(part => part.trim().length > 0);
|
|
573
|
+
if (nameParts.length === 0) {
|
|
574
|
+
throw new ValidationError('Invalid CRUD name: empty after splitting', 'name');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const finalName = nameParts[nameParts.length - 1];
|
|
578
|
+
const camelCaseName = this.toCamelCase(finalName);
|
|
579
|
+
const pluralName = `${finalName}s`;
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
this.generateFromTemplate('controller', name, outputDir);
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
this.generateFromTemplate('service', name, outputDir);
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
this.generateFromTemplate('route', name, outputDir);
|
|
589
|
+
|
|
590
|
+
console.log(`\n⨠CRUD generation complete for: ${name}`);
|
|
591
|
+
console.log(`š Don't forget to register routes in your server.js:\n`);
|
|
592
|
+
console.log(` const ${camelCaseName}Routes = require('./src/routes/${finalName}.routes');`);
|
|
593
|
+
console.log(` app.use('/api/${pluralName}', ${camelCaseName}Routes);\n`);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
const errorMsg = error instanceof ValidationError || error instanceof TemplateError
|
|
596
|
+
? error.message
|
|
597
|
+
: `Failed to generate CRUD: ${error.message}`;
|
|
598
|
+
throw new Error(errorMsg);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
clearCache() {
|
|
604
|
+
this._configCache = null;
|
|
605
|
+
this._configMtime = null;
|
|
606
|
+
this._templateCache.clear();
|
|
607
|
+
this._fileExistsCache.clear();
|
|
608
|
+
this._fileExistsCacheTime = 0;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
module.exports = new FileGenerator();
|
|
614
|
+
module.exports.utils = utils;
|
|
615
|
+
module.exports.TemplateError = TemplateError;
|
|
616
|
+
module.exports.ConfigError = ConfigError;
|
|
617
|
+
module.exports.ValidationError = ValidationError;
|