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.
Files changed (77) hide show
  1. package/README.md +4 -0
  2. package/bin/stackkit.js +8 -5
  3. package/dist/cli/add.js +4 -9
  4. package/dist/cli/create.js +4 -9
  5. package/dist/cli/doctor.js +11 -32
  6. package/dist/lib/constants.js +3 -4
  7. package/dist/lib/conversion/js-conversion.js +20 -16
  8. package/dist/lib/discovery/installed-detection.js +28 -38
  9. package/dist/lib/discovery/module-discovery.d.ts +0 -15
  10. package/dist/lib/discovery/module-discovery.js +15 -50
  11. package/dist/lib/framework/framework-utils.d.ts +4 -5
  12. package/dist/lib/framework/framework-utils.js +38 -49
  13. package/dist/lib/fs/files.js +1 -1
  14. package/dist/lib/generation/code-generator.d.ts +13 -19
  15. package/dist/lib/generation/code-generator.js +159 -175
  16. package/dist/lib/generation/generator-utils.js +3 -15
  17. package/dist/lib/project/detect.js +11 -19
  18. package/dist/lib/utils/fs-helpers.d.ts +1 -1
  19. package/modules/auth/authjs/generator.json +16 -16
  20. package/modules/auth/better-auth/files/express/middlewares/authorize.ts +178 -40
  21. package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +264 -0
  22. package/modules/auth/better-auth/files/express/modules/auth/auth.route.ts +27 -0
  23. package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +537 -0
  24. package/modules/auth/better-auth/files/express/modules/auth/auth.type.ts +33 -0
  25. package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +91 -0
  26. package/modules/auth/better-auth/files/express/templates/otp.ejs +87 -0
  27. package/modules/auth/better-auth/files/express/types/express.d.ts +6 -8
  28. package/modules/auth/better-auth/files/express/utils/cookie.ts +19 -0
  29. package/modules/auth/better-auth/files/express/utils/jwt.ts +34 -0
  30. package/modules/auth/better-auth/files/express/utils/token.ts +66 -0
  31. package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +1 -1
  32. package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +11 -1
  33. package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +74 -0
  34. package/modules/auth/better-auth/files/shared/config/env.ts +117 -0
  35. package/modules/auth/better-auth/files/shared/lib/auth-client.ts +1 -1
  36. package/modules/auth/better-auth/files/shared/lib/auth.ts +167 -79
  37. package/modules/auth/better-auth/files/shared/mongoose/auth/constants.ts +11 -0
  38. package/modules/auth/better-auth/files/shared/mongoose/auth/helper.ts +51 -0
  39. package/modules/auth/better-auth/files/shared/prisma/schema.prisma +22 -11
  40. package/modules/auth/better-auth/files/shared/utils/email.ts +70 -0
  41. package/modules/auth/better-auth/generator.json +162 -80
  42. package/modules/database/mongoose/files/lib/mongoose.ts +28 -3
  43. package/modules/database/mongoose/generator.json +18 -18
  44. package/modules/database/prisma/generator.json +44 -44
  45. package/package.json +2 -2
  46. package/templates/express/env.example +3 -2
  47. package/templates/express/eslint.config.mjs +7 -0
  48. package/templates/express/node_modules/.bin/acorn +17 -0
  49. package/templates/express/node_modules/.bin/eslint +17 -0
  50. package/templates/express/node_modules/.bin/tsc +17 -0
  51. package/templates/express/node_modules/.bin/tsserver +17 -0
  52. package/templates/express/node_modules/.bin/tsx +17 -0
  53. package/templates/express/package.json +12 -6
  54. package/templates/express/src/app.ts +15 -7
  55. package/templates/express/src/config/cors.ts +8 -7
  56. package/templates/express/src/config/env.ts +28 -5
  57. package/templates/express/src/config/logger.ts +2 -2
  58. package/templates/express/src/config/rate-limit.ts +2 -2
  59. package/templates/express/src/modules/health/health.controller.ts +13 -11
  60. package/templates/express/src/routes/index.ts +1 -6
  61. package/templates/express/src/server.ts +12 -12
  62. package/templates/express/src/shared/errors/app-error.ts +16 -0
  63. package/templates/express/src/shared/middlewares/error.middleware.ts +154 -12
  64. package/templates/express/src/shared/middlewares/not-found.middleware.ts +2 -1
  65. package/templates/express/src/shared/utils/catch-async.ts +11 -0
  66. package/templates/express/src/shared/utils/pagination.ts +6 -1
  67. package/templates/express/src/shared/utils/send-response.ts +25 -0
  68. package/templates/nextjs/lib/env.ts +19 -8
  69. package/modules/auth/better-auth/files/shared/lib/email/email-service.ts +0 -33
  70. package/modules/auth/better-auth/files/shared/lib/email/email-templates.ts +0 -89
  71. package/templates/express/eslint.config.cjs +0 -42
  72. package/templates/express/src/config/helmet.ts +0 -5
  73. package/templates/express/src/modules/health/health.service.ts +0 -6
  74. package/templates/express/src/shared/errors/error-codes.ts +0 -9
  75. package/templates/express/src/shared/logger/logger.ts +0 -20
  76. package/templates/express/src/shared/utils/async-handler.ts +0 -9
  77. 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
- // ignore invalid generator files
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++; // Skip next character
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++; // Skip next character
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, ""); // Remove quotes from result
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
- // Check if this generator is selected
319
- if (genType === "framework" && name === selectedModules.framework) {
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
- // Generate package.json updates
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, context, outputPath);
346
+ await this.executeAddDependency(processedOperation, outputPath);
377
347
  break;
378
348
  case "add-script":
379
- await this.executeAddScript(processedOperation, context, outputPath);
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
- // Unknown operation type - skip silently
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
- // ignore
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; // already present
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
- // ignore
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
- // ignore
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 destinationPath = path.join(outputPath, this.processTemplate(operation.destination, context));
563
- // Ensure directory exists
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
- // Use content directly
568
- content = this.processTemplate(operation.content, context);
569
- }
570
- else if (operation.source) {
571
- const sourcePath = (0, generator_utils_1.locateOperationSource)(operation.generatorType, operation.generator, operation.source || "");
572
- if (sourcePath && (await fs.pathExists(sourcePath))) {
573
- content = await fs.readFile(sourcePath, "utf-8");
574
- content = this.processTemplate(content, context);
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
- else {
577
- throw new Error(`Source file not found: ${sourcePath}`);
531
+ catch (error) {
532
+ void error;
578
533
  }
534
+ return;
579
535
  }
580
- else {
536
+ if (!operation.source) {
581
537
  throw new Error(`Create file operation must have either 'content' or 'source' field`);
582
538
  }
583
- // Write destination file
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
- // ignore logging failures
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, context, outputPath) {
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, context, outputPath) {
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, features, outputPath) {
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 === "framework" && name === selectedModules.framework) ||
841
- (type === "database" && name === selectedModules.database) ||
842
- (type === "auth" && name === selectedModules.auth)) {
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 === "framework" && name === selectedModules.framework) {
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, features, projectPath);
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
  }