stackkit 0.2.8 → 0.3.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/README.md +4 -0
- package/bin/stackkit.js +8 -5
- package/dist/cli/add.js +4 -9
- package/dist/cli/create.js +4 -9
- package/dist/cli/doctor.js +11 -32
- package/dist/lib/constants.js +3 -4
- package/dist/lib/conversion/js-conversion.js +20 -16
- package/dist/lib/discovery/installed-detection.js +28 -38
- package/dist/lib/discovery/module-discovery.d.ts +0 -15
- package/dist/lib/discovery/module-discovery.js +15 -50
- package/dist/lib/framework/framework-utils.d.ts +4 -5
- package/dist/lib/framework/framework-utils.js +38 -49
- package/dist/lib/fs/files.js +1 -1
- package/dist/lib/generation/code-generator.d.ts +13 -19
- package/dist/lib/generation/code-generator.js +159 -175
- package/dist/lib/generation/generator-utils.js +3 -15
- package/dist/lib/project/detect.js +11 -19
- package/dist/lib/utils/fs-helpers.d.ts +1 -1
- package/modules/auth/authjs/generator.json +16 -16
- package/modules/auth/better-auth/files/express/middlewares/authorize.ts +178 -40
- package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +264 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.route.ts +27 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +537 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.type.ts +33 -0
- package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +91 -0
- package/modules/auth/better-auth/files/express/templates/otp.ejs +87 -0
- package/modules/auth/better-auth/files/express/types/express.d.ts +6 -8
- package/modules/auth/better-auth/files/express/utils/cookie.ts +19 -0
- package/modules/auth/better-auth/files/express/utils/jwt.ts +34 -0
- package/modules/auth/better-auth/files/express/utils/token.ts +66 -0
- package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +1 -1
- package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +11 -1
- package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +74 -0
- package/modules/auth/better-auth/files/shared/config/env.ts +117 -0
- package/modules/auth/better-auth/files/shared/lib/auth-client.ts +1 -1
- package/modules/auth/better-auth/files/shared/lib/auth.ts +167 -79
- package/modules/auth/better-auth/files/shared/mongoose/auth/constants.ts +11 -0
- package/modules/auth/better-auth/files/shared/mongoose/auth/helper.ts +51 -0
- package/modules/auth/better-auth/files/shared/prisma/schema.prisma +22 -11
- package/modules/auth/better-auth/files/shared/utils/email.ts +70 -0
- package/modules/auth/better-auth/generator.json +162 -80
- package/modules/database/mongoose/files/lib/mongoose.ts +28 -3
- package/modules/database/mongoose/generator.json +18 -18
- package/modules/database/prisma/generator.json +44 -44
- package/package.json +2 -2
- package/templates/express/env.example +3 -2
- package/templates/express/eslint.config.mjs +7 -0
- package/templates/express/node_modules/.bin/acorn +17 -0
- package/templates/express/node_modules/.bin/eslint +17 -0
- package/templates/express/node_modules/.bin/tsc +17 -0
- package/templates/express/node_modules/.bin/tsserver +17 -0
- package/templates/express/node_modules/.bin/tsx +17 -0
- package/templates/express/package.json +12 -6
- package/templates/express/src/app.ts +15 -7
- package/templates/express/src/config/cors.ts +8 -7
- package/templates/express/src/config/env.ts +28 -5
- package/templates/express/src/config/logger.ts +2 -2
- package/templates/express/src/config/rate-limit.ts +2 -2
- package/templates/express/src/modules/health/health.controller.ts +13 -11
- package/templates/express/src/routes/index.ts +1 -6
- package/templates/express/src/server.ts +12 -12
- package/templates/express/src/shared/errors/app-error.ts +16 -0
- package/templates/express/src/shared/middlewares/error.middleware.ts +154 -12
- package/templates/express/src/shared/middlewares/not-found.middleware.ts +2 -1
- package/templates/express/src/shared/utils/catch-async.ts +11 -0
- package/templates/express/src/shared/utils/pagination.ts +6 -1
- package/templates/express/src/shared/utils/send-response.ts +25 -0
- package/templates/nextjs/lib/env.ts +19 -8
- package/modules/auth/better-auth/files/shared/lib/email/email-service.ts +0 -33
- package/modules/auth/better-auth/files/shared/lib/email/email-templates.ts +0 -89
- package/templates/express/eslint.config.cjs +0 -42
- package/templates/express/src/config/helmet.ts +0 -5
- package/templates/express/src/modules/health/health.service.ts +0 -6
- package/templates/express/src/shared/errors/error-codes.ts +0 -9
- package/templates/express/src/shared/logger/logger.ts +0 -20
- package/templates/express/src/shared/utils/async-handler.ts +0 -9
- package/templates/express/src/shared/utils/response.ts +0 -9
|
@@ -46,6 +46,25 @@ class AdvancedCodeGenerator {
|
|
|
46
46
|
this.createdFiles = [];
|
|
47
47
|
this.frameworkConfig = frameworkConfig;
|
|
48
48
|
}
|
|
49
|
+
initializeContext(selectedModules, features) {
|
|
50
|
+
const context = {
|
|
51
|
+
...selectedModules,
|
|
52
|
+
features,
|
|
53
|
+
combo: `${selectedModules.database || ""}:${selectedModules.framework || ""}`,
|
|
54
|
+
};
|
|
55
|
+
if (selectedModules.database === "prisma" && !context.prismaProvider) {
|
|
56
|
+
const providers = (0, shared_1.getPrismaProvidersFromGenerator)((0, package_root_1.getPackageRoot)());
|
|
57
|
+
if (providers.length > 0) {
|
|
58
|
+
context.prismaProvider = providers[0];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return context;
|
|
62
|
+
}
|
|
63
|
+
isGeneratorSelected(genType, name, selectedModules) {
|
|
64
|
+
return ((genType === "framework" && name === selectedModules.framework) ||
|
|
65
|
+
(genType === "database" && name === selectedModules.database) ||
|
|
66
|
+
(genType === "auth" && name === selectedModules.auth));
|
|
67
|
+
}
|
|
49
68
|
async loadGenerators(modulesPath) {
|
|
50
69
|
const moduleTypes = ["auth", "database"];
|
|
51
70
|
for (const type of moduleTypes) {
|
|
@@ -62,7 +81,7 @@ class AdvancedCodeGenerator {
|
|
|
62
81
|
this.generators.set(`${type}:${moduleName}`, config);
|
|
63
82
|
}
|
|
64
83
|
catch {
|
|
65
|
-
|
|
84
|
+
continue;
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
87
|
}
|
|
@@ -96,15 +115,10 @@ class AdvancedCodeGenerator {
|
|
|
96
115
|
return true;
|
|
97
116
|
}
|
|
98
117
|
processTemplate(content, context) {
|
|
99
|
-
// Create a copy of context for template variables
|
|
100
118
|
const templateContext = { ...context };
|
|
101
|
-
// Handle variable definitions {{#var name = value}} at the top of the file
|
|
102
119
|
content = this.processVariableDefinitions(content, templateContext);
|
|
103
|
-
// Process the rest of the template with the extended context
|
|
104
120
|
content = this.processTemplateRecursive(content, templateContext);
|
|
105
|
-
// Remove leading newlines that might be left from {{#var}} removal
|
|
106
121
|
content = content.replace(/^\n+/, "");
|
|
107
|
-
// Reduce multiple consecutive newlines to maximum 2
|
|
108
122
|
content = content.replace(/\n{3,}/g, "\n\n");
|
|
109
123
|
return content;
|
|
110
124
|
}
|
|
@@ -128,15 +142,13 @@ class AdvancedCodeGenerator {
|
|
|
128
142
|
const varNameMatch = result.substring(varStart + 7, equalsIndex).trim();
|
|
129
143
|
if (!varNameMatch)
|
|
130
144
|
break;
|
|
131
|
-
// Find the end of the variable value by counting braces
|
|
132
|
-
// Start with braceCount = 1 because {{#var is already open
|
|
133
145
|
let braceCount = 1;
|
|
134
146
|
const valueStart = equalsIndex + 1;
|
|
135
147
|
let valueEnd = valueStart;
|
|
136
148
|
for (let i = valueStart; i < result.length; i++) {
|
|
137
149
|
if (result[i] === "{" && result[i + 1] === "{") {
|
|
138
150
|
braceCount++;
|
|
139
|
-
i++;
|
|
151
|
+
i++;
|
|
140
152
|
}
|
|
141
153
|
else if (result[i] === "}" && result[i + 1] === "}") {
|
|
142
154
|
braceCount--;
|
|
@@ -144,17 +156,15 @@ class AdvancedCodeGenerator {
|
|
|
144
156
|
valueEnd = i;
|
|
145
157
|
break;
|
|
146
158
|
}
|
|
147
|
-
i++;
|
|
159
|
+
i++;
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
162
|
if (valueEnd === valueStart)
|
|
151
163
|
break;
|
|
152
164
|
const varValue = result.substring(valueStart, valueEnd).trim();
|
|
153
165
|
const fullMatch = result.substring(varStart, valueEnd + 2);
|
|
154
|
-
// Process the variable value with current context (allowing nested variables/conditionals)
|
|
155
166
|
const processedValue = this.processTemplateRecursive(varValue, context);
|
|
156
167
|
context[varNameMatch] = processedValue;
|
|
157
|
-
// Remove the variable definition
|
|
158
168
|
result = result.replace(fullMatch, "");
|
|
159
169
|
index = varStart;
|
|
160
170
|
}
|
|
@@ -189,7 +199,6 @@ class AdvancedCodeGenerator {
|
|
|
189
199
|
.replace(/^\n+/, "")
|
|
190
200
|
.replace(/\n+$/, "");
|
|
191
201
|
});
|
|
192
|
-
// Handle simple conditional blocks {{#if condition}}...{{/if}} (backward compatibility)
|
|
193
202
|
content = content.replace(/\{\{#if\s+([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g, (match, condition, blockContent, elseContent) => {
|
|
194
203
|
const conditionParts = condition.split("==");
|
|
195
204
|
if (conditionParts.length === 2) {
|
|
@@ -211,10 +220,8 @@ class AdvancedCodeGenerator {
|
|
|
211
220
|
}
|
|
212
221
|
return "";
|
|
213
222
|
});
|
|
214
|
-
// Handle switch statements {{#switch variable}}...{{/switch}}
|
|
215
223
|
content = content.replace(/\{\{#switch\s+([^}]+)\}\}([\s\S]*?)\{\{\/switch\}\}/g, (match, varName, switchContent) => {
|
|
216
224
|
const actualVal = context[varName.trim()];
|
|
217
|
-
// Parse cases
|
|
218
225
|
const caseRegex = /\{\{#case\s+([^}]+)\}\}([\s\S]*?)(?=\{\{#case|\{\{\/case\}|\{\{\/switch\})/g;
|
|
219
226
|
let result = "";
|
|
220
227
|
let defaultCase = "";
|
|
@@ -233,10 +240,8 @@ class AdvancedCodeGenerator {
|
|
|
233
240
|
const chosen = (result || defaultCase || "").trim();
|
|
234
241
|
return this.processTemplateRecursive(chosen, context);
|
|
235
242
|
});
|
|
236
|
-
// Handle variable replacement with advanced expressions
|
|
237
243
|
content = content.replace(/\{\{([^}]+)\}\}/g, (match, varExpr) => {
|
|
238
244
|
const trimmedExpr = varExpr.trim();
|
|
239
|
-
// Handle ternary expressions like framework=='nextjs' ? '@/lib' : '.'
|
|
240
245
|
const ternaryMatch = trimmedExpr.match(/^(.+?)\s*\?\s*(.+?)\s*:\s*(.+?)$/);
|
|
241
246
|
if (ternaryMatch) {
|
|
242
247
|
const [, condition, trueVal, falseVal] = ternaryMatch;
|
|
@@ -247,10 +252,9 @@ class AdvancedCodeGenerator {
|
|
|
247
252
|
const cleanExpectedVal = expectedVal.trim().replace(/['"]/g, "");
|
|
248
253
|
const actualVal = context[cleanVarName];
|
|
249
254
|
const result = actualVal === cleanExpectedVal ? trueVal.trim() : falseVal.trim();
|
|
250
|
-
return result.replace(/['"]/g, "");
|
|
255
|
+
return result.replace(/['"]/g, "");
|
|
251
256
|
}
|
|
252
257
|
}
|
|
253
|
-
// Handle switch expressions {{switch variable case1: value1, case2: value2, default: defaultValue}}
|
|
254
258
|
const switchMatch = trimmedExpr.match(/^switch\s+([^}\s]+)\s+(.+)$/);
|
|
255
259
|
if (switchMatch) {
|
|
256
260
|
const [, varName, casesStr] = switchMatch;
|
|
@@ -265,17 +269,14 @@ class AdvancedCodeGenerator {
|
|
|
265
269
|
}
|
|
266
270
|
return "";
|
|
267
271
|
}
|
|
268
|
-
// Handle heading helper {{heading:depthVar}} -> '#', '##', ... up to '######'
|
|
269
272
|
if (trimmedExpr.startsWith("heading:")) {
|
|
270
273
|
return this.renderHeadingFromExpr(trimmedExpr, context);
|
|
271
274
|
}
|
|
272
|
-
// Handle feature flags {{feature:name}}
|
|
273
275
|
if (trimmedExpr.startsWith("feature:")) {
|
|
274
276
|
const featureName = trimmedExpr.substring(8).trim();
|
|
275
277
|
const features = Array.isArray(context.features) ? context.features : [];
|
|
276
278
|
return features.includes(featureName) ? "true" : "false";
|
|
277
279
|
}
|
|
278
|
-
// Handle conditional expressions {{if condition then:value else:value}}
|
|
279
280
|
const conditionalMatch = trimmedExpr.match(/^if\s+(.+?)\s+then:([^,]+),\s*else:(.+)$/);
|
|
280
281
|
if (conditionalMatch) {
|
|
281
282
|
const [, condition, thenVal, elseVal] = conditionalMatch;
|
|
@@ -289,50 +290,23 @@ class AdvancedCodeGenerator {
|
|
|
289
290
|
return result.replace(/['"]/g, "");
|
|
290
291
|
}
|
|
291
292
|
}
|
|
292
|
-
// Simple variable replacement
|
|
293
293
|
const value = context[trimmedExpr];
|
|
294
294
|
return value !== undefined ? String(value) : match;
|
|
295
295
|
});
|
|
296
296
|
return content;
|
|
297
297
|
}
|
|
298
298
|
async generate(selectedModules, features, outputPath) {
|
|
299
|
-
// First, copy the base template
|
|
300
299
|
await this.copyTemplate(selectedModules.framework, outputPath);
|
|
301
|
-
const context =
|
|
302
|
-
...selectedModules,
|
|
303
|
-
features,
|
|
304
|
-
};
|
|
305
|
-
// Derived combined key to simplify template conditionals (e.g. "prisma:express")
|
|
306
|
-
context.combo = `${context.database || ""}:${context.framework || ""}`;
|
|
307
|
-
// Set default prismaProvider if database is prisma but no provider specified
|
|
308
|
-
if (selectedModules.database === "prisma" && !context.prismaProvider) {
|
|
309
|
-
const providers = (0, shared_1.getPrismaProvidersFromGenerator)((0, package_root_1.getPackageRoot)());
|
|
310
|
-
if (providers && providers.length > 0) {
|
|
311
|
-
context.prismaProvider = providers[0];
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// Collect all applicable operations
|
|
300
|
+
const context = this.initializeContext(selectedModules, features);
|
|
315
301
|
const applicableOperations = [];
|
|
316
302
|
for (const [key, generator] of this.generators) {
|
|
317
303
|
const [genType, name] = key.split(":");
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// Framework is always included
|
|
321
|
-
}
|
|
322
|
-
else if (genType === "database" && name === selectedModules.database) {
|
|
323
|
-
// Database is selected
|
|
324
|
-
}
|
|
325
|
-
else if (genType === "auth" && name === selectedModules.auth) {
|
|
326
|
-
// Auth is selected
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
continue; // Skip unselected generators
|
|
304
|
+
if (!this.isGeneratorSelected(genType, name, selectedModules)) {
|
|
305
|
+
continue;
|
|
330
306
|
}
|
|
331
|
-
// Collect postInstall commands from selected generators
|
|
332
307
|
if (generator.postInstall && Array.isArray(generator.postInstall)) {
|
|
333
308
|
this.postInstallCommands.push(...generator.postInstall);
|
|
334
309
|
}
|
|
335
|
-
// Handle operations
|
|
336
310
|
const items = generator.operations || [];
|
|
337
311
|
for (const item of items) {
|
|
338
312
|
if (this.evaluateCondition(item.condition, context)) {
|
|
@@ -345,25 +319,21 @@ class AdvancedCodeGenerator {
|
|
|
345
319
|
}
|
|
346
320
|
}
|
|
347
321
|
}
|
|
348
|
-
// Sort operations by priority
|
|
349
322
|
applicableOperations.sort((a, b) => {
|
|
350
323
|
const priorityA = a.priority || 0;
|
|
351
324
|
const priorityB = b.priority || 0;
|
|
352
325
|
return priorityA - priorityB;
|
|
353
326
|
});
|
|
354
|
-
// Execute operations
|
|
355
327
|
for (const operation of applicableOperations) {
|
|
356
328
|
await this.executeOperation(operation, context, outputPath);
|
|
357
329
|
}
|
|
358
|
-
|
|
359
|
-
await this.generatePackageJson(selectedModules, features, outputPath);
|
|
330
|
+
await this.generatePackageJson(selectedModules, outputPath);
|
|
360
331
|
return this.postInstallCommands;
|
|
361
332
|
}
|
|
362
333
|
getCreatedFiles() {
|
|
363
334
|
return this.createdFiles.slice();
|
|
364
335
|
}
|
|
365
336
|
async executeOperation(operation, context, outputPath) {
|
|
366
|
-
// Process templates in operation content
|
|
367
337
|
const processedOperation = this.processOperationTemplates(operation, context);
|
|
368
338
|
switch (processedOperation.type) {
|
|
369
339
|
case "create-file":
|
|
@@ -373,10 +343,10 @@ class AdvancedCodeGenerator {
|
|
|
373
343
|
await this.executePatchFile(processedOperation, context, outputPath);
|
|
374
344
|
break;
|
|
375
345
|
case "add-dependency":
|
|
376
|
-
await this.executeAddDependency(processedOperation,
|
|
346
|
+
await this.executeAddDependency(processedOperation, outputPath);
|
|
377
347
|
break;
|
|
378
348
|
case "add-script":
|
|
379
|
-
await this.executeAddScript(processedOperation,
|
|
349
|
+
await this.executeAddScript(processedOperation, outputPath);
|
|
380
350
|
break;
|
|
381
351
|
case "add-env":
|
|
382
352
|
await this.executeAddEnv(processedOperation, context, outputPath);
|
|
@@ -385,7 +355,7 @@ class AdvancedCodeGenerator {
|
|
|
385
355
|
this.executeRunCommand(processedOperation, context);
|
|
386
356
|
break;
|
|
387
357
|
default:
|
|
388
|
-
|
|
358
|
+
return;
|
|
389
359
|
}
|
|
390
360
|
}
|
|
391
361
|
async copyTemplate(frameworkName, outputPath) {
|
|
@@ -400,20 +370,6 @@ class AdvancedCodeGenerator {
|
|
|
400
370
|
!relativePath.startsWith("node_modules/"));
|
|
401
371
|
},
|
|
402
372
|
});
|
|
403
|
-
// If template provides a .env.example, create a .env from it when
|
|
404
|
-
// the target project does not already have a .env file.
|
|
405
|
-
try {
|
|
406
|
-
const envExampleSrc = path.join(templatePath, ".env.example");
|
|
407
|
-
const envDest = path.join(outputPath, ".env");
|
|
408
|
-
if ((await fs.pathExists(envExampleSrc)) && !(await fs.pathExists(envDest))) {
|
|
409
|
-
await fs.copy(envExampleSrc, envDest);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
// ignore (not critical)
|
|
414
|
-
}
|
|
415
|
-
// Ensure gitignore is present in target even if template authors
|
|
416
|
-
// renamed it to avoid npm/package issues (e.g. 'gitignore' or '_gitignore')
|
|
417
373
|
try {
|
|
418
374
|
const gitCandidates = [".gitignore", "gitignore", "_gitignore"];
|
|
419
375
|
for (const g of gitCandidates) {
|
|
@@ -427,10 +383,9 @@ class AdvancedCodeGenerator {
|
|
|
427
383
|
}
|
|
428
384
|
}
|
|
429
385
|
}
|
|
430
|
-
catch {
|
|
431
|
-
|
|
386
|
+
catch (error) {
|
|
387
|
+
void error;
|
|
432
388
|
}
|
|
433
|
-
// Ensure any top-level dotfiles declared in template.json are created
|
|
434
389
|
try {
|
|
435
390
|
const templateJsonPath = path.join(templatePath, "template.json");
|
|
436
391
|
if (await fs.pathExists(templateJsonPath)) {
|
|
@@ -440,8 +395,7 @@ class AdvancedCodeGenerator {
|
|
|
440
395
|
if (typeof f === "string" && f.startsWith(".")) {
|
|
441
396
|
const targetDest = path.join(outputPath, f);
|
|
442
397
|
if (await fs.pathExists(targetDest))
|
|
443
|
-
continue;
|
|
444
|
-
// Special-case: allow creating .gitignore from non-dot fallbacks
|
|
398
|
+
continue;
|
|
445
399
|
if (f === ".gitignore") {
|
|
446
400
|
const nameWithoutDot = f.slice(1);
|
|
447
401
|
const candidates = [f, nameWithoutDot, `_${nameWithoutDot}`];
|
|
@@ -454,9 +408,6 @@ class AdvancedCodeGenerator {
|
|
|
454
408
|
}
|
|
455
409
|
continue;
|
|
456
410
|
}
|
|
457
|
-
// For other dotfiles (e.g. .env.example), only create if the template
|
|
458
|
-
// actually contains a dotfile source. Don't synthesize from non-dot fallbacks
|
|
459
|
-
// to avoid creating files unintentionally.
|
|
460
411
|
const srcDot = path.join(templatePath, f);
|
|
461
412
|
if (await fs.pathExists(srcDot)) {
|
|
462
413
|
await fs.copy(srcDot, targetDest);
|
|
@@ -466,8 +417,8 @@ class AdvancedCodeGenerator {
|
|
|
466
417
|
}
|
|
467
418
|
}
|
|
468
419
|
}
|
|
469
|
-
catch {
|
|
470
|
-
|
|
420
|
+
catch (error) {
|
|
421
|
+
void error;
|
|
471
422
|
}
|
|
472
423
|
try {
|
|
473
424
|
const templateJsonPath2 = path.join(templatePath, "template.json");
|
|
@@ -480,7 +431,6 @@ class AdvancedCodeGenerator {
|
|
|
480
431
|
const nameWithoutDot = f.slice(1);
|
|
481
432
|
const nonDot = path.join(outputPath, nameWithoutDot);
|
|
482
433
|
const underscore = path.join(outputPath, `_${nameWithoutDot}`);
|
|
483
|
-
// If dot already exists, remove non-dot fallbacks
|
|
484
434
|
if (await fs.pathExists(dotDest)) {
|
|
485
435
|
if (await fs.pathExists(nonDot)) {
|
|
486
436
|
await fs.remove(nonDot);
|
|
@@ -490,7 +440,6 @@ class AdvancedCodeGenerator {
|
|
|
490
440
|
}
|
|
491
441
|
continue;
|
|
492
442
|
}
|
|
493
|
-
// If dot doesn't exist but a non-dot fallback was copied, rename it
|
|
494
443
|
if (await fs.pathExists(nonDot)) {
|
|
495
444
|
await fs.move(nonDot, dotDest, { overwrite: true });
|
|
496
445
|
}
|
|
@@ -502,14 +451,23 @@ class AdvancedCodeGenerator {
|
|
|
502
451
|
}
|
|
503
452
|
}
|
|
504
453
|
}
|
|
505
|
-
catch {
|
|
506
|
-
|
|
454
|
+
catch (error) {
|
|
455
|
+
void error;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const envExampleDest = path.join(outputPath, ".env.example");
|
|
459
|
+
const envDest = path.join(outputPath, ".env");
|
|
460
|
+
if ((await fs.pathExists(envExampleDest)) && !(await fs.pathExists(envDest))) {
|
|
461
|
+
await fs.copy(envExampleDest, envDest);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
void error;
|
|
507
466
|
}
|
|
508
467
|
}
|
|
509
468
|
}
|
|
510
469
|
processOperationTemplates(operation, context) {
|
|
511
470
|
const processed = { ...operation };
|
|
512
|
-
// Process templates in string fields
|
|
513
471
|
if (processed.source) {
|
|
514
472
|
processed.source = this.processTemplate(processed.source, context);
|
|
515
473
|
}
|
|
@@ -519,7 +477,6 @@ class AdvancedCodeGenerator {
|
|
|
519
477
|
if (processed.content) {
|
|
520
478
|
processed.content = this.processTemplate(processed.content, context);
|
|
521
479
|
}
|
|
522
|
-
// Process templates in patch operations
|
|
523
480
|
if (processed.operations) {
|
|
524
481
|
processed.operations = processed.operations.map((op) => {
|
|
525
482
|
const processedOp = { ...op };
|
|
@@ -559,90 +516,162 @@ class AdvancedCodeGenerator {
|
|
|
559
516
|
async executeCreateFile(operation, context, outputPath) {
|
|
560
517
|
if (!operation.destination)
|
|
561
518
|
return;
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
await fs.ensureDir(path.dirname(destinationPath));
|
|
565
|
-
let content;
|
|
519
|
+
const processedDestination = this.processTemplate(operation.destination, context);
|
|
520
|
+
const { basePath: destinationBasePath, mode: destinationMode } = this.parsePathPattern(processedDestination);
|
|
566
521
|
if (operation.content) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
522
|
+
const destinationPath = path.join(outputPath, processedDestination);
|
|
523
|
+
await fs.ensureDir(path.dirname(destinationPath));
|
|
524
|
+
const content = this.processTemplate(operation.content, context);
|
|
525
|
+
await fs.writeFile(destinationPath, content, "utf-8");
|
|
526
|
+
try {
|
|
527
|
+
const rel = path.relative(outputPath, destinationPath);
|
|
528
|
+
if (rel && !this.createdFiles.includes(rel))
|
|
529
|
+
this.createdFiles.push(rel);
|
|
575
530
|
}
|
|
576
|
-
|
|
577
|
-
|
|
531
|
+
catch (error) {
|
|
532
|
+
void error;
|
|
578
533
|
}
|
|
534
|
+
return;
|
|
579
535
|
}
|
|
580
|
-
|
|
536
|
+
if (!operation.source) {
|
|
581
537
|
throw new Error(`Create file operation must have either 'content' or 'source' field`);
|
|
582
538
|
}
|
|
583
|
-
|
|
539
|
+
const processedSource = this.processTemplate(operation.source, context);
|
|
540
|
+
const { basePath: sourceBasePathRel, mode: sourceMode } = this.parsePathPattern(processedSource);
|
|
541
|
+
const sourcePath = (0, generator_utils_1.locateOperationSource)(operation.generatorType, operation.generator, sourceBasePathRel);
|
|
542
|
+
if (!sourcePath || !(await fs.pathExists(sourcePath))) {
|
|
543
|
+
throw new Error(`Source file not found: ${sourcePath}`);
|
|
544
|
+
}
|
|
545
|
+
const sourceStat = await fs.stat(sourcePath);
|
|
546
|
+
const shouldCreateMultipleFiles = sourceStat.isDirectory() || sourceMode !== "single";
|
|
547
|
+
if (shouldCreateMultipleFiles) {
|
|
548
|
+
if (!sourceStat.isDirectory()) {
|
|
549
|
+
throw new Error(`Source path must be a directory for wildcard copy: ${processedSource}`);
|
|
550
|
+
}
|
|
551
|
+
let sourceFiles = [];
|
|
552
|
+
if (sourceMode === "flat") {
|
|
553
|
+
const entries = await fs.readdir(sourcePath);
|
|
554
|
+
for (const entry of entries) {
|
|
555
|
+
const candidatePath = path.join(sourcePath, entry);
|
|
556
|
+
const candidateStat = await fs.stat(candidatePath);
|
|
557
|
+
if (candidateStat.isFile()) {
|
|
558
|
+
sourceFiles.push(candidatePath);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
sourceFiles = await this.collectFilesRecursively(sourcePath);
|
|
564
|
+
}
|
|
565
|
+
for (const sourceFilePath of sourceFiles) {
|
|
566
|
+
let relativeDestinationPath;
|
|
567
|
+
if (destinationMode === "recursive") {
|
|
568
|
+
relativeDestinationPath = path.relative(sourcePath, sourceFilePath);
|
|
569
|
+
}
|
|
570
|
+
else if (destinationMode === "flat") {
|
|
571
|
+
relativeDestinationPath = path.basename(sourceFilePath);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
relativeDestinationPath =
|
|
575
|
+
sourceMode === "flat"
|
|
576
|
+
? path.basename(sourceFilePath)
|
|
577
|
+
: path.relative(sourcePath, sourceFilePath);
|
|
578
|
+
}
|
|
579
|
+
const destinationPath = path.join(outputPath, destinationBasePath, relativeDestinationPath);
|
|
580
|
+
await fs.ensureDir(path.dirname(destinationPath));
|
|
581
|
+
let content = await fs.readFile(sourceFilePath, "utf-8");
|
|
582
|
+
content = this.processTemplate(content, context);
|
|
583
|
+
await fs.writeFile(destinationPath, content, "utf-8");
|
|
584
|
+
try {
|
|
585
|
+
const rel = path.relative(outputPath, destinationPath);
|
|
586
|
+
if (rel && !this.createdFiles.includes(rel))
|
|
587
|
+
this.createdFiles.push(rel);
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
void error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const destinationPath = destinationMode === "single"
|
|
596
|
+
? path.join(outputPath, processedDestination)
|
|
597
|
+
: path.join(outputPath, destinationBasePath, path.basename(sourcePath));
|
|
598
|
+
await fs.ensureDir(path.dirname(destinationPath));
|
|
599
|
+
let content = await fs.readFile(sourcePath, "utf-8");
|
|
600
|
+
content = this.processTemplate(content, context);
|
|
584
601
|
await fs.writeFile(destinationPath, content, "utf-8");
|
|
585
602
|
try {
|
|
586
603
|
const rel = path.relative(outputPath, destinationPath);
|
|
587
604
|
if (rel && !this.createdFiles.includes(rel))
|
|
588
605
|
this.createdFiles.push(rel);
|
|
589
606
|
}
|
|
590
|
-
catch {
|
|
591
|
-
|
|
607
|
+
catch (error) {
|
|
608
|
+
void error;
|
|
592
609
|
}
|
|
593
610
|
}
|
|
611
|
+
parsePathPattern(inputPath) {
|
|
612
|
+
const normalizedPath = inputPath.replace(/\\/g, "/");
|
|
613
|
+
if (normalizedPath.endsWith("/**")) {
|
|
614
|
+
return { basePath: normalizedPath.slice(0, -3), mode: "recursive" };
|
|
615
|
+
}
|
|
616
|
+
if (normalizedPath.endsWith("/*")) {
|
|
617
|
+
return { basePath: normalizedPath.slice(0, -2), mode: "flat" };
|
|
618
|
+
}
|
|
619
|
+
return { basePath: normalizedPath, mode: "single" };
|
|
620
|
+
}
|
|
621
|
+
async collectFilesRecursively(rootDirPath) {
|
|
622
|
+
const files = [];
|
|
623
|
+
const entries = await fs.readdir(rootDirPath);
|
|
624
|
+
for (const entry of entries) {
|
|
625
|
+
const fullPath = path.join(rootDirPath, entry);
|
|
626
|
+
const stat = await fs.stat(fullPath);
|
|
627
|
+
if (stat.isDirectory()) {
|
|
628
|
+
files.push(...(await this.collectFilesRecursively(fullPath)));
|
|
629
|
+
}
|
|
630
|
+
else if (stat.isFile()) {
|
|
631
|
+
files.push(fullPath);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return files;
|
|
635
|
+
}
|
|
594
636
|
async executePatchFile(operation, context, outputPath) {
|
|
595
637
|
if (!operation.destination)
|
|
596
638
|
return;
|
|
597
639
|
const filePath = path.join(outputPath, this.processTemplate(operation.destination, context));
|
|
598
|
-
// Read existing file
|
|
599
640
|
let content = await fs.readFile(filePath, "utf-8");
|
|
600
641
|
if (operation.content) {
|
|
601
642
|
content += this.processTemplate(operation.content, context).trim();
|
|
602
643
|
}
|
|
603
644
|
else if (operation.operations) {
|
|
604
|
-
// Execute patch operations
|
|
605
645
|
for (const patchOp of operation.operations) {
|
|
606
646
|
if (!this.evaluateCondition(patchOp.condition, context))
|
|
607
647
|
continue;
|
|
608
648
|
switch (patchOp.type) {
|
|
609
649
|
case "add-import":
|
|
610
650
|
if (patchOp.imports) {
|
|
611
|
-
// Process imports and trim any accidental surrounding blank lines
|
|
612
651
|
const imports = patchOp.imports
|
|
613
652
|
.map((imp) => this.processTemplate(imp, context))
|
|
614
653
|
.join("\n")
|
|
615
654
|
.replace(/^\n+/, "")
|
|
616
655
|
.replace(/\n+$/, "");
|
|
617
|
-
// Add imports at the top, after existing imports without introducing
|
|
618
|
-
// extra blank lines.
|
|
619
656
|
const lines = content.split("\n");
|
|
620
|
-
// Find the last import line index
|
|
621
657
|
let lastImportIndex = -1;
|
|
622
658
|
for (let i = 0; i < lines.length; i++) {
|
|
623
659
|
if (lines[i].trim().startsWith("import"))
|
|
624
660
|
lastImportIndex = i;
|
|
625
661
|
}
|
|
626
|
-
// Insert right after the last import. If there's a blank line
|
|
627
|
-
// immediately after the imports, overwrite that blank line to
|
|
628
|
-
// avoid introducing an extra empty line above the inserted imports.
|
|
629
662
|
const insertIndex = lastImportIndex === -1 ? 0 : lastImportIndex + 1;
|
|
630
|
-
// Only add imports that don't already exist in the file
|
|
631
663
|
const importLines = imports
|
|
632
664
|
.split("\n")
|
|
633
665
|
.map((l) => l.trim())
|
|
634
666
|
.filter(Boolean);
|
|
635
667
|
const newImportLines = importLines.filter((imp) => !lines.some((ln) => ln.trim() === imp));
|
|
636
668
|
if (newImportLines.length > 0) {
|
|
637
|
-
// Insert imports
|
|
638
669
|
if (insertIndex < lines.length && lines[insertIndex].trim() === "") {
|
|
639
670
|
lines.splice(insertIndex, 1, ...newImportLines);
|
|
640
671
|
}
|
|
641
672
|
else {
|
|
642
673
|
lines.splice(insertIndex, 0, ...newImportLines);
|
|
643
674
|
}
|
|
644
|
-
// After insertion, ensure exactly one blank line after the import block
|
|
645
|
-
// Find last import line index again
|
|
646
675
|
let lastIdx = -1;
|
|
647
676
|
for (let i = 0; i < lines.length; i++) {
|
|
648
677
|
if (lines[i].trim().startsWith("import"))
|
|
@@ -650,12 +679,10 @@ class AdvancedCodeGenerator {
|
|
|
650
679
|
}
|
|
651
680
|
const nextIdx = lastIdx + 1;
|
|
652
681
|
if (lastIdx !== -1) {
|
|
653
|
-
// Remove multiple blank lines after imports
|
|
654
682
|
let j = nextIdx;
|
|
655
683
|
while (j < lines.length && lines[j].trim() === "") {
|
|
656
684
|
j++;
|
|
657
685
|
}
|
|
658
|
-
// Ensure exactly one blank line after imports unless imports end at EOF
|
|
659
686
|
if (nextIdx < lines.length) {
|
|
660
687
|
lines.splice(nextIdx, j - nextIdx, "");
|
|
661
688
|
}
|
|
@@ -670,22 +697,17 @@ class AdvancedCodeGenerator {
|
|
|
670
697
|
if (Array.isArray(codeValue))
|
|
671
698
|
codeValue = codeValue.join("\n");
|
|
672
699
|
const processedCode = this.processTemplate(codeValue, context);
|
|
673
|
-
// Skip insertion if the exact code already exists in the file
|
|
674
700
|
const codeTrimmed = processedCode.trim();
|
|
675
701
|
if (codeTrimmed && content.includes(codeTrimmed)) {
|
|
676
702
|
break;
|
|
677
703
|
}
|
|
678
|
-
// Insert after pattern if provided
|
|
679
704
|
if (patchOp.after) {
|
|
680
705
|
const afterPattern = this.processTemplate(patchOp.after, context);
|
|
681
706
|
const index = content.indexOf(afterPattern);
|
|
682
707
|
if (index !== -1) {
|
|
683
708
|
const left = content.slice(0, index + afterPattern.length);
|
|
684
709
|
const right = content.slice(index + afterPattern.length);
|
|
685
|
-
// Normalize code: trim surrounding newlines and ensure single trailing newline
|
|
686
710
|
let codeNormalized = processedCode.replace(/^\n+|\n+$/g, "") + "\n";
|
|
687
|
-
// If right already starts with a newline, avoid double-blank by
|
|
688
|
-
// removing trailing newline from codeNormalized so only one newline remains
|
|
689
711
|
const rightStartsWithNewline = right.startsWith("\n");
|
|
690
712
|
if (rightStartsWithNewline && codeNormalized.endsWith("\n")) {
|
|
691
713
|
codeNormalized = codeNormalized.replace(/\n+$/, "");
|
|
@@ -694,17 +716,13 @@ class AdvancedCodeGenerator {
|
|
|
694
716
|
content = left + (leftNeedsNewline ? "\n" : "") + codeNormalized + right;
|
|
695
717
|
}
|
|
696
718
|
}
|
|
697
|
-
// Insert before pattern if provided
|
|
698
719
|
if (patchOp.before) {
|
|
699
720
|
const beforePattern = this.processTemplate(patchOp.before, context);
|
|
700
721
|
const index = content.indexOf(beforePattern);
|
|
701
722
|
if (index !== -1) {
|
|
702
723
|
const left = content.slice(0, index);
|
|
703
724
|
const right = content.slice(index);
|
|
704
|
-
// Normalize code: trim surrounding newlines and ensure single trailing newline
|
|
705
725
|
let codeNormalized = processedCode.replace(/^\n+|\n+$/g, "") + "\n";
|
|
706
|
-
// If right already starts with a newline, avoid double-blank by
|
|
707
|
-
// removing trailing newline from codeNormalized so only one newline remains
|
|
708
726
|
const rightStartsWithNewline = right.startsWith("\n");
|
|
709
727
|
if (rightStartsWithNewline && codeNormalized.endsWith("\n")) {
|
|
710
728
|
codeNormalized = codeNormalized.replace(/\n+$/, "");
|
|
@@ -764,12 +782,10 @@ class AdvancedCodeGenerator {
|
|
|
764
782
|
}
|
|
765
783
|
}
|
|
766
784
|
}
|
|
767
|
-
// Normalize excessive blank lines introduced during patching
|
|
768
785
|
content = content.replace(/\n{3,}/g, "\n\n");
|
|
769
|
-
// Write back the modified content
|
|
770
786
|
await fs.writeFile(filePath, content, "utf-8");
|
|
771
787
|
}
|
|
772
|
-
async executeAddDependency(operation,
|
|
788
|
+
async executeAddDependency(operation, outputPath) {
|
|
773
789
|
const packageJsonPath = path.join(outputPath, "package.json");
|
|
774
790
|
let packageJson = {};
|
|
775
791
|
if (await fs.pathExists(packageJsonPath)) {
|
|
@@ -789,7 +805,7 @@ class AdvancedCodeGenerator {
|
|
|
789
805
|
}
|
|
790
806
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
791
807
|
}
|
|
792
|
-
async executeAddScript(operation,
|
|
808
|
+
async executeAddScript(operation, outputPath) {
|
|
793
809
|
const packageJsonPath = path.join(outputPath, "package.json");
|
|
794
810
|
let packageJson = {};
|
|
795
811
|
if (await fs.pathExists(packageJsonPath)) {
|
|
@@ -820,32 +836,27 @@ class AdvancedCodeGenerator {
|
|
|
820
836
|
}
|
|
821
837
|
executeRunCommand(operation, context) {
|
|
822
838
|
if (operation.command) {
|
|
823
|
-
// Process template variables in the command
|
|
824
839
|
const processedCommand = this.processTemplate(operation.command, context);
|
|
825
840
|
this.postInstallCommands.push(processedCommand);
|
|
826
841
|
}
|
|
827
842
|
}
|
|
828
|
-
async generatePackageJson(selectedModules,
|
|
843
|
+
async generatePackageJson(selectedModules, outputPath) {
|
|
829
844
|
const packageJsonPath = path.join(outputPath, "package.json");
|
|
830
845
|
let packageJson = {};
|
|
831
846
|
if (await fs.pathExists(packageJsonPath)) {
|
|
832
847
|
packageJson = await fs.readJson(packageJsonPath);
|
|
833
848
|
}
|
|
834
|
-
// Collect dependencies from selected generators
|
|
835
849
|
const allDeps = {};
|
|
836
850
|
const allDevDeps = {};
|
|
837
851
|
const allScripts = {};
|
|
838
852
|
for (const [key, generator] of this.generators) {
|
|
839
853
|
const [type, name] = key.split(":");
|
|
840
|
-
if ((type
|
|
841
|
-
(
|
|
842
|
-
(
|
|
843
|
-
// Dependencies and devDependencies are now provided via `add-dependency`
|
|
844
|
-
// operations. Keep merging scripts from generator configs for now.
|
|
854
|
+
if (this.isGeneratorSelected(type, name, selectedModules)) {
|
|
855
|
+
Object.assign(allDeps, generator.dependencies);
|
|
856
|
+
Object.assign(allDevDeps, generator.devDependencies);
|
|
845
857
|
Object.assign(allScripts, generator.scripts);
|
|
846
858
|
}
|
|
847
859
|
}
|
|
848
|
-
// Update package.json
|
|
849
860
|
packageJson.dependencies = {
|
|
850
861
|
...(packageJson.dependencies || {}),
|
|
851
862
|
...allDeps,
|
|
@@ -860,36 +871,12 @@ class AdvancedCodeGenerator {
|
|
|
860
871
|
};
|
|
861
872
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
862
873
|
}
|
|
863
|
-
/**
|
|
864
|
-
* Apply generators to an existing project directory instead of creating a new template.
|
|
865
|
-
* This executes applicable operations and updates package.json in-place.
|
|
866
|
-
*/
|
|
867
874
|
async applyToProject(selectedModules, features, projectPath) {
|
|
868
|
-
const context =
|
|
869
|
-
...selectedModules,
|
|
870
|
-
features,
|
|
871
|
-
};
|
|
872
|
-
// Derived combined key to simplify template conditionals (e.g. "prisma:express")
|
|
873
|
-
context.combo = `${context.database || ""}:${context.framework || ""}`;
|
|
874
|
-
if (selectedModules.database === "prisma" && !context.prismaProvider) {
|
|
875
|
-
const providers = (0, shared_1.getPrismaProvidersFromGenerator)((0, package_root_1.getPackageRoot)());
|
|
876
|
-
if (providers && providers.length > 0) {
|
|
877
|
-
context.prismaProvider = providers[0];
|
|
878
|
-
}
|
|
879
|
-
}
|
|
875
|
+
const context = this.initializeContext(selectedModules, features);
|
|
880
876
|
const applicableOperations = [];
|
|
881
877
|
for (const [key, generator] of this.generators) {
|
|
882
878
|
const [genType, name] = key.split(":");
|
|
883
|
-
if (genType
|
|
884
|
-
// framework
|
|
885
|
-
}
|
|
886
|
-
else if (genType === "database" && name === selectedModules.database) {
|
|
887
|
-
// database
|
|
888
|
-
}
|
|
889
|
-
else if (genType === "auth" && name === selectedModules.auth) {
|
|
890
|
-
// auth
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
879
|
+
if (!this.isGeneratorSelected(genType, name, selectedModules)) {
|
|
893
880
|
continue;
|
|
894
881
|
}
|
|
895
882
|
if (generator.postInstall && Array.isArray(generator.postInstall)) {
|
|
@@ -911,7 +898,7 @@ class AdvancedCodeGenerator {
|
|
|
911
898
|
for (const operation of applicableOperations) {
|
|
912
899
|
await this.executeOperation(operation, context, projectPath);
|
|
913
900
|
}
|
|
914
|
-
await this.generatePackageJson(selectedModules,
|
|
901
|
+
await this.generatePackageJson(selectedModules, projectPath);
|
|
915
902
|
return this.postInstallCommands;
|
|
916
903
|
}
|
|
917
904
|
getAvailableGenerators() {
|
|
@@ -934,9 +921,6 @@ class AdvancedCodeGenerator {
|
|
|
934
921
|
}
|
|
935
922
|
return { frameworks, databases, auths };
|
|
936
923
|
}
|
|
937
|
-
/**
|
|
938
|
-
* Register a generator config dynamically (used for modules that only provide module.json patches).
|
|
939
|
-
*/
|
|
940
924
|
registerGenerator(type, name, config) {
|
|
941
925
|
this.generators.set(`${type}:${name}`, config);
|
|
942
926
|
}
|