@ingenyus/swarm-wasp 0.1.0 → 0.2.1

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 (126) hide show
  1. package/README.md +229 -21
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/common/filesystem.d.ts +0 -14
  4. package/dist/common/filesystem.d.ts.map +1 -1
  5. package/dist/common/filesystem.js +123 -0
  6. package/dist/common/index.d.ts +0 -1
  7. package/dist/common/index.d.ts.map +1 -1
  8. package/dist/common/index.js +366 -0
  9. package/dist/common/prisma.js +140 -0
  10. package/dist/common/schemas.d.ts +8 -42
  11. package/dist/common/schemas.d.ts.map +1 -1
  12. package/dist/common/schemas.js +54 -0
  13. package/dist/common/templates.js +52 -0
  14. package/dist/generators/action/action-generator.d.ts +16 -29
  15. package/dist/generators/action/action-generator.d.ts.map +1 -1
  16. package/dist/generators/action/action-generator.js +1425 -0
  17. package/dist/generators/action/index.js +1425 -0
  18. package/dist/generators/action/schema.d.ts +11 -23
  19. package/dist/generators/action/schema.d.ts.map +1 -1
  20. package/dist/generators/action/schema.js +115 -0
  21. package/dist/generators/api/api-generator.d.ts +19 -26
  22. package/dist/generators/api/api-generator.d.ts.map +1 -1
  23. package/dist/generators/api/api-generator.js +1104 -0
  24. package/dist/generators/api/index.js +1104 -0
  25. package/dist/generators/api/schema.d.ts +13 -21
  26. package/dist/generators/api/schema.d.ts.map +1 -1
  27. package/dist/generators/api/schema.js +117 -0
  28. package/dist/generators/api-namespace/api-namespace-generator.d.ts +10 -17
  29. package/dist/generators/api-namespace/api-namespace-generator.d.ts.map +1 -1
  30. package/dist/generators/api-namespace/api-namespace-generator.js +1028 -0
  31. package/dist/generators/api-namespace/index.js +1028 -0
  32. package/dist/generators/api-namespace/schema.d.ts +4 -12
  33. package/dist/generators/api-namespace/schema.d.ts.map +1 -1
  34. package/dist/generators/api-namespace/schema.js +89 -0
  35. package/dist/generators/base/{entity-generator.base.d.ts → component-generator.base.d.ts} +9 -9
  36. package/dist/generators/base/component-generator.base.d.ts.map +1 -0
  37. package/dist/generators/base/component-generator.base.js +931 -0
  38. package/dist/generators/base/index.d.ts +1 -1
  39. package/dist/generators/base/index.d.ts.map +1 -1
  40. package/dist/generators/base/index.js +1330 -0
  41. package/dist/generators/base/operation-generator.base.d.ts +12 -3
  42. package/dist/generators/base/operation-generator.base.d.ts.map +1 -1
  43. package/dist/generators/base/operation-generator.base.js +1331 -0
  44. package/dist/generators/base/wasp-generator.base.d.ts +2 -1
  45. package/dist/generators/base/wasp-generator.base.d.ts.map +1 -1
  46. package/dist/generators/base/wasp-generator.base.js +706 -0
  47. package/dist/generators/config/config-generator.d.ts +7 -4
  48. package/dist/generators/config/config-generator.d.ts.map +1 -1
  49. package/dist/generators/config/config-generator.js +0 -0
  50. package/dist/generators/config/index.js +596 -0
  51. package/dist/generators/config/wasp-config-generator.d.ts +1 -1
  52. package/dist/generators/config/wasp-config-generator.d.ts.map +1 -1
  53. package/dist/generators/config/wasp-config-generator.js +596 -0
  54. package/dist/generators/crud/crud-generator.d.ts +34 -22
  55. package/dist/generators/crud/crud-generator.d.ts.map +1 -1
  56. package/dist/generators/crud/crud-generator.js +1550 -0
  57. package/dist/generators/crud/index.js +1550 -0
  58. package/dist/generators/crud/schema.d.ts +25 -18
  59. package/dist/generators/crud/schema.d.ts.map +1 -1
  60. package/dist/generators/crud/schema.js +133 -0
  61. package/dist/generators/feature/feature-generator.d.ts +20 -0
  62. package/dist/generators/feature/feature-generator.d.ts.map +1 -0
  63. package/dist/generators/feature/feature-generator.js +765 -0
  64. package/dist/generators/feature/index.d.ts +2 -0
  65. package/dist/generators/feature/index.d.ts.map +1 -0
  66. package/dist/generators/feature/index.js +765 -0
  67. package/dist/generators/feature/schema.d.ts +5 -0
  68. package/dist/generators/feature/schema.d.ts.map +1 -0
  69. package/dist/generators/feature/schema.js +86 -0
  70. package/dist/generators/index.d.ts +1 -1
  71. package/dist/generators/index.d.ts.map +1 -1
  72. package/dist/generators/index.js +2211 -0
  73. package/dist/generators/job/index.js +1099 -0
  74. package/dist/generators/job/job-generator.d.ts +12 -23
  75. package/dist/generators/job/job-generator.d.ts.map +1 -1
  76. package/dist/generators/job/job-generator.js +1099 -0
  77. package/dist/generators/job/schema.d.ts +6 -18
  78. package/dist/generators/job/schema.d.ts.map +1 -1
  79. package/dist/generators/job/schema.js +152 -0
  80. package/dist/generators/query/index.js +1425 -0
  81. package/dist/generators/query/query-generator.d.ts +16 -29
  82. package/dist/generators/query/query-generator.d.ts.map +1 -1
  83. package/dist/generators/query/query-generator.js +1425 -0
  84. package/dist/generators/query/schema.d.ts +11 -23
  85. package/dist/generators/query/schema.d.ts.map +1 -1
  86. package/dist/generators/query/schema.js +115 -0
  87. package/dist/generators/route/index.js +1038 -0
  88. package/dist/generators/route/route-generator.d.ts +11 -20
  89. package/dist/generators/route/route-generator.d.ts.map +1 -1
  90. package/dist/generators/route/route-generator.js +1038 -0
  91. package/dist/generators/route/schema.d.ts +5 -15
  92. package/dist/generators/route/schema.d.ts.map +1 -1
  93. package/dist/generators/route/schema.js +90 -0
  94. package/dist/index.d.ts +2 -10
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +1980 -2115
  97. package/dist/plugins/index.d.ts +2 -0
  98. package/dist/plugins/index.d.ts.map +1 -0
  99. package/dist/plugins/wasp.d.ts +3 -0
  100. package/dist/plugins/wasp.d.ts.map +1 -0
  101. package/dist/types/constants.d.ts +4 -22
  102. package/dist/types/constants.d.ts.map +1 -1
  103. package/dist/types/constants.js +8 -2
  104. package/dist/types/index.d.ts +2 -2
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/types/index.js +8 -2
  107. package/dist/wasp-config/app.d.ts +2 -1
  108. package/dist/wasp-config/app.d.ts.map +1 -1
  109. package/dist/wasp-config/app.js +357 -0
  110. package/dist/wasp-config/index.js +357 -0
  111. package/dist/wasp-config/stubs/index.js +48 -0
  112. package/package.json +5 -14
  113. package/dist/common/plugin.d.ts +0 -2
  114. package/dist/common/plugin.d.ts.map +0 -1
  115. package/dist/generators/args.types.d.ts +0 -85
  116. package/dist/generators/args.types.d.ts.map +0 -1
  117. package/dist/generators/base/entity-generator.base.d.ts.map +0 -1
  118. package/dist/generators/feature-directory/feature-directory-generator.d.ts +0 -18
  119. package/dist/generators/feature-directory/feature-directory-generator.d.ts.map +0 -1
  120. package/dist/generators/feature-directory/index.d.ts +0 -2
  121. package/dist/generators/feature-directory/index.d.ts.map +0 -1
  122. package/dist/generators/feature-directory/schema.d.ts +0 -8
  123. package/dist/generators/feature-directory/schema.d.ts.map +0 -1
  124. package/dist/plugin.d.ts +0 -6
  125. package/dist/plugin.d.ts.map +0 -1
  126. /package/dist/generators/{feature-directory → feature}/templates/feature.wasp.eta +0 -0
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- // src/common/filesystem.ts
2
- import { toPascalCase, validateFeaturePath } from "@ingenyus/swarm";
3
- import fs from "fs";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
-
7
1
  // src/types/constants.ts
8
2
  var PLUGIN_NAME = "wasp";
9
3
  var OPERATION_TYPES = ["query", "action"];
10
- var HTTP_METHODS = ["ALL", "GET", "POST", "PUT", "DELETE"];
4
+ var API_HTTP_METHODS = [
5
+ "ALL",
6
+ "GET",
7
+ "POST",
8
+ "PUT",
9
+ "DELETE"
10
+ ];
11
11
  var OPERATIONS = {
12
12
  CREATE: "create",
13
13
  UPDATE: "update",
@@ -57,7 +57,20 @@ var CONFIG_TYPES = {
57
57
  CRUD: "Crud"
58
58
  };
59
59
 
60
+ // src/generators/base/component-generator.base.ts
61
+ import {
62
+ hasHelperMethodCall,
63
+ logger as singletonLogger4,
64
+ toCamelCase,
65
+ toKebabCase as toKebabCase2,
66
+ validateFeaturePath as validateFeaturePath3
67
+ } from "@ingenyus/swarm";
68
+ import path6 from "path";
69
+
60
70
  // src/common/filesystem.ts
71
+ import { toPascalCase, validateFeaturePath } from "@ingenyus/swarm";
72
+ import fs from "fs";
73
+ import path from "path";
61
74
  var realFileSystem = {
62
75
  readFileSync: fs.readFileSync,
63
76
  writeFileSync: fs.writeFileSync,
@@ -102,13 +115,6 @@ function ensureDirectoryExists(fileSystem, dir) {
102
115
  fileSystem.mkdirSync(dir, { recursive: true });
103
116
  }
104
117
  }
105
- function featureExists(fileSystem, featurePath) {
106
- return fileSystem.existsSync(getFeatureDir(fileSystem, featurePath));
107
- }
108
- function getConfigDir(fileSystem) {
109
- const waspRoot = findWaspRoot(fileSystem);
110
- return path.join(waspRoot, "config");
111
- }
112
118
  function normaliseFeaturePath(featurePath) {
113
119
  const segments = validateFeaturePath(featurePath);
114
120
  const normalisedSegments = [];
@@ -131,81 +137,21 @@ function getFeatureImportPath(featurePath) {
131
137
  const segments = validateFeaturePath(featurePath);
132
138
  return segments.join("/");
133
139
  }
134
- function getFeatureTargetDir(fileSystem, featurePath, type) {
135
- validateFeaturePath(featurePath);
136
- const normalisedPath = normaliseFeaturePath(featurePath);
137
- const featureDir = getFeatureDir(fileSystem, normalisedPath);
138
- const typeKey = type.toLowerCase();
139
- const typeDirectory = TYPE_DIRECTORIES[typeKey];
140
- const targetDirectory = path.join(featureDir, typeDirectory);
141
- const importDirectory = `@src/${normalisedPath}/${typeDirectory}`;
142
- return { targetDirectory, importDirectory };
143
- }
144
140
  function getRouteNameFromPath(routePath) {
145
141
  const lastSegment = routePath.split("/").filter(Boolean).pop() || "index";
146
142
  const cleanSegment = lastSegment.replace(/[:*]/g, "");
147
143
  return `${toPascalCase(cleanSegment)}Page`;
148
144
  }
149
- function getAppRootDir(fileSystem) {
150
- const currentFile = fileURLToPath(import.meta.url);
151
- let currentDir = path.dirname(currentFile);
152
- while (currentDir !== path.dirname(currentDir)) {
153
- const dirName = path.basename(currentDir);
154
- if (dirName === "src" || dirName === "dist") {
155
- return currentDir;
156
- }
157
- const packageJsonPath = path.join(currentDir, "package.json");
158
- if (fileSystem.existsSync(packageJsonPath)) {
159
- const srcPath = path.join(currentDir, "src");
160
- const distPath = path.join(currentDir, "dist");
161
- if (fileSystem.existsSync(distPath)) {
162
- return distPath;
163
- } else if (fileSystem.existsSync(srcPath)) {
164
- return srcPath;
165
- }
166
- }
167
- currentDir = path.dirname(currentDir);
168
- }
169
- throw new Error("Could not determine application root directory");
170
- }
171
- function getTemplatesDir(fileSystem) {
172
- const appRoot = getAppRootDir(fileSystem);
173
- return path.join(appRoot, "templates");
174
- }
175
-
176
- // src/common/plugin.ts
177
- import path2 from "path";
178
- import { fileURLToPath as fileURLToPath2 } from "url";
179
- function getPluginVersion() {
180
- const __dirname = path2.dirname(fileURLToPath2(import.meta.url));
181
- let currentDir = __dirname;
182
- while (currentDir !== path2.dirname(currentDir)) {
183
- const packageJsonPath = path2.join(currentDir, "package.json");
184
- if (realFileSystem.existsSync(packageJsonPath)) {
185
- try {
186
- const packageJson = JSON.parse(
187
- realFileSystem.readFileSync(packageJsonPath, "utf8")
188
- );
189
- if (packageJson.name === "@ingenyus/swarm-wasp") {
190
- return packageJson.version;
191
- }
192
- } catch (e) {
193
- }
194
- }
195
- currentDir = path2.dirname(currentDir);
196
- }
197
- return "0.1.0";
198
- }
199
145
 
200
146
  // src/common/prisma.ts
201
147
  import {
202
148
  getSchema
203
149
  } from "@mrleebo/prisma-ast";
204
150
  import fs2 from "fs";
205
- import path3 from "path";
151
+ import path2 from "path";
206
152
  async function getEntityMetadata(modelName) {
207
153
  try {
208
- const schemaPath = path3.join(process.cwd(), "schema.prisma");
154
+ const schemaPath = path2.join(process.cwd(), "schema.prisma");
209
155
  const schemaContent = fs2.readFileSync(schemaPath, "utf8");
210
156
  const schema9 = getSchema(schemaContent);
211
157
  const model = schema9.list?.find(
@@ -326,97 +272,61 @@ function getPrismaToTsType(type) {
326
272
  }
327
273
 
328
274
  // src/common/schemas.ts
329
- import { extend } from "@ingenyus/swarm";
275
+ import { commandRegistry } from "@ingenyus/swarm";
330
276
  import { z } from "zod";
331
277
  var commonSchemas = {
332
- feature: extend(z.string().min(1, "Feature is required"), {
333
- description: "The feature directory this resource will be generated in",
334
- friendlyName: "Feature",
278
+ feature: z.string().min(1, "Feature is required").meta({
279
+ description: "The feature directory this component will be generated in"
280
+ }).register(commandRegistry, {
335
281
  shortName: "f",
336
282
  examples: ["root", "auth", "dashboard/users"],
337
- helpText: 'Can be nested as a logical or relative path, e.g. "dashboard/users" or "features/dashboard/features/users"'
283
+ helpText: "Can be nested as a logical or relative path, e.g. 'dashboard/users' or 'features/dashboard/features/users'"
338
284
  }),
339
- name: extend(z.string().min(1, "Name is required"), {
340
- description: "The name of the generated resource",
341
- friendlyName: "Name",
285
+ name: z.string().min(1, "Name is required").meta({ description: "The name of the generated component" }).register(commandRegistry, {
342
286
  shortName: "n",
343
287
  examples: ["users", "task"],
344
288
  helpText: "Will be used for generated files and configuration entries"
345
289
  }),
346
- path: extend(z.string().min(1, "Path is required"), {
347
- description: "The path that this resource will be accessible at",
348
- friendlyName: "Path",
290
+ target: z.string().min(1, "Target directory is required").meta({ description: "The target path of the generated directory" }).register(commandRegistry, {
291
+ shortName: "t",
292
+ examples: ["dashboard/users", "features/dashboard/features/users"],
293
+ helpText: "A logical or relative path, e.g. 'dashboard/users' or 'features/dashboard/features/users'"
294
+ }),
295
+ path: z.string().min(1, "Path is required").meta({ description: "The path that this component will be accessible at" }).register(commandRegistry, {
349
296
  shortName: "p",
350
297
  examples: ["/api/users/:id", "/api/products"],
351
- helpText: 'Supports Express-style placeholders, e.g. "/api/users/:id"'
298
+ helpText: "Supports Express-style placeholders, e.g. '/api/users/:id'"
299
+ }),
300
+ dataType: z.string().min(1, "Data type is required").meta({ description: "The data type/model name for this operation" }).register(commandRegistry, {
301
+ shortName: "d",
302
+ examples: ["User", "Product", "Task"],
303
+ helpText: "The Wasp entity or model name this operation will interact with"
352
304
  }),
353
- entities: extend(z.string().optional(), {
354
- description: "The Wasp entities that this resource will have access to (optional)",
355
- friendlyName: "Entities",
305
+ entities: z.array(z.string()).optional().meta({
306
+ description: "The Wasp entities that will be available to this component (optional)"
307
+ }).register(commandRegistry, {
356
308
  shortName: "e",
357
- examples: ["User,Product"],
358
- helpText: "A comma-separated list of Wasp entities"
309
+ examples: ["User", "User Task"],
310
+ helpText: "An array of Wasp entity names"
359
311
  }),
360
- force: extend(z.boolean().optional(), {
361
- description: "Force overwrite of existing files and configuration entries (optional)",
362
- friendlyName: "Force",
312
+ force: z.boolean().optional().meta({
313
+ description: "Force overwrite of existing files and configuration entries (optional)"
314
+ }).register(commandRegistry, {
363
315
  shortName: "F",
364
316
  helpText: "CAUTION: Will overwrite existing files and configuration entries with current parameters"
365
317
  }),
366
- auth: extend(z.boolean().optional(), {
367
- description: "Require authentication for this resource (optional)",
368
- friendlyName: "Auth",
318
+ auth: z.boolean().optional().meta({
319
+ description: "Require authentication for this component (optional)"
320
+ }).register(commandRegistry, {
369
321
  shortName: "a",
370
322
  helpText: "Will generate authentication checks"
371
323
  })
372
324
  };
373
- var getTypedArrayValidator = (validValues) => {
374
- return (input) => {
375
- if (!input) return true;
376
- const values = input.split(",").map((s) => s.trim()).filter(Boolean);
377
- const normalisedValues = validValues.map((value) => value.toLowerCase());
378
- for (const value of values) {
379
- const normalisedValue = value.toLowerCase();
380
- if (!normalisedValues.includes(normalisedValue)) {
381
- return false;
382
- }
383
- }
384
- return true;
385
- };
386
- };
387
- var getTypedArrayTransformer = (validValues) => {
388
- return (input) => {
389
- if (!input) return void 0;
390
- return input.split(",").map((s) => s.trim()).filter(Boolean).map((value) => {
391
- const normalisedValue = value.toLowerCase();
392
- const validValue = validValues.find(
393
- (val) => val.toLowerCase() === normalisedValue
394
- );
395
- return validValue;
396
- });
397
- };
398
- };
399
- var getTypedValueValidator = (validValues) => {
400
- return (value) => {
401
- const normalisedValue = value.toLowerCase();
402
- const normalisedValidOps = validValues.map((val) => val.toLowerCase());
403
- return normalisedValidOps.includes(normalisedValue);
404
- };
405
- };
406
- var getTypedValueTransformer = (validValues) => {
407
- return (value) => {
408
- const normalisedValue = value.toLowerCase();
409
- const validValue = validValues.find(
410
- (val) => val.toLowerCase() === normalisedValue
411
- );
412
- return validValue;
413
- };
414
- };
415
325
 
416
326
  // src/common/templates.ts
417
327
  import { toKebabCase } from "@ingenyus/swarm";
418
328
  import { Eta } from "eta";
419
- import path4 from "path";
329
+ import path3 from "path";
420
330
  var TemplateUtility = class {
421
331
  constructor(fileSystem) {
422
332
  this.fileSystem = fileSystem;
@@ -424,14 +334,14 @@ var TemplateUtility = class {
424
334
  processTemplate(templatePath, replacements) {
425
335
  const declarations = Object.keys(replacements).map((key) => `${key}=it.${key}`).join(", ");
426
336
  const functionHeader = declarations ? `const ${declarations};` : void 0;
427
- const templateDir = path4.dirname(templatePath);
337
+ const templateDir = path3.dirname(templatePath);
428
338
  const eta = new Eta({
429
339
  autoTrim: false,
430
340
  autoEscape: false,
431
341
  views: templateDir,
432
342
  functionHeader
433
343
  });
434
- const templateName = path4.basename(templatePath).replace(/\.eta$/, "");
344
+ const templateName = path3.basename(templatePath).replace(/\.eta$/, "");
435
345
  if (this.fileSystem.existsSync(templatePath)) {
436
346
  return eta.render(templateName, replacements);
437
347
  } else {
@@ -449,11 +359,11 @@ var TemplateUtility = class {
449
359
  resolveTemplatePath(relativePath, generatorName, currentFileUrl) {
450
360
  const generatorDirName = toKebabCase(generatorName);
451
361
  const currentFilePath = new URL(currentFileUrl).pathname;
452
- const currentFileDir = path4.dirname(currentFilePath);
453
- const currentFileName = path4.basename(currentFilePath);
362
+ const currentFileDir = path3.dirname(currentFilePath);
363
+ const currentFileName = path3.basename(currentFilePath);
454
364
  const isInstalledPackage = currentFileDir.includes("node_modules") && currentFileDir.endsWith("/dist") && currentFileName === "index.js";
455
- const startDir = isInstalledPackage ? currentFileDir : path4.dirname(path4.dirname(currentFileDir));
456
- return path4.join(
365
+ const startDir = isInstalledPackage ? currentFileDir : path3.dirname(path3.dirname(currentFileDir));
366
+ return path3.join(
457
367
  startDir,
458
368
  "generators",
459
369
  generatorDirName,
@@ -463,2212 +373,2199 @@ var TemplateUtility = class {
463
373
  }
464
374
  };
465
375
 
466
- // src/plugin.ts
467
- function createWaspPlugin() {
468
- return {
469
- name: PLUGIN_NAME,
470
- version: getPluginVersion(),
471
- description: "Wasp Plugin for Swarm",
472
- swarmVersion: "0.1.0",
473
- generators: [
474
- new ActionGenerator(),
475
- new ApiGenerator(),
476
- new ApiNamespaceGenerator(),
477
- new CrudGenerator(),
478
- new FeatureDirectoryGenerator(),
479
- new JobGenerator(),
480
- new QueryGenerator(),
481
- new RouteGenerator()
482
- ]
483
- };
484
- }
485
- var _apiPlugin = null;
486
- function getWaspPlugin() {
487
- if (!_apiPlugin) {
488
- _apiPlugin = createWaspPlugin();
489
- }
490
- return _apiPlugin;
491
- }
492
- var wasp = getWaspPlugin;
493
-
494
- // src/wasp-config/app.ts
495
- import fs3 from "fs";
376
+ // src/generators/feature/feature-generator.ts
377
+ import {
378
+ handleFatalError as handleFatalError2,
379
+ logger as singletonLogger3,
380
+ validateFeaturePath as validateFeaturePath2
381
+ } from "@ingenyus/swarm";
496
382
  import path5 from "path";
383
+
384
+ // src/generators/base/wasp-generator.base.ts
497
385
  import {
498
- App as WaspApp
499
- } from "wasp-config";
500
- var App = class _App extends WaspApp {
501
- constructor(name, config) {
502
- super(name, config);
386
+ GeneratorBase,
387
+ logger as singletonLogger2,
388
+ SwarmConfigManager,
389
+ TemplateResolver
390
+ } from "@ingenyus/swarm";
391
+
392
+ // src/generators/config/wasp-config-generator.ts
393
+ import {
394
+ handleFatalError,
395
+ parseHelperMethodDefinition,
396
+ logger as singletonLogger
397
+ } from "@ingenyus/swarm";
398
+ import path4 from "path";
399
+ var WaspConfigGenerator = class {
400
+ constructor(logger = singletonLogger, fileSystem = realFileSystem) {
401
+ this.logger = logger;
402
+ this.fileSystem = fileSystem;
403
+ this.templateUtility = new TemplateUtility(fileSystem);
503
404
  }
405
+ path = path4;
406
+ templateUtility;
504
407
  /**
505
- * Static factory method that creates and initializes Swarm with configuration
506
- * dynamically loaded from feature directories
507
- *
508
- * @param name The name of the application
509
- * @param config The base configuration for the application
510
- * @returns An initialized Swarm instance
408
+ * Gets the template path for feature config templates.
409
+ * Feature config templates are located in the feature generator's templates directory.
410
+ * @param templateName - The name of the template file (e.g., 'feature.wasp.eta')
411
+ * @returns The full path to the template file
511
412
  */
512
- static async create(name, config) {
513
- const app = new _App(name, config);
514
- await app.configureFeatures();
515
- return app;
516
- }
517
- // Chainable configuration methods
518
- auth(authConfig) {
519
- super.auth(authConfig);
520
- return this;
521
- }
522
- client(clientConfig) {
523
- super.client(clientConfig);
524
- return this;
525
- }
526
- db(dbConfig) {
527
- super.db(dbConfig);
528
- return this;
529
- }
530
- emailSender(emailSenderConfig) {
531
- super.emailSender(emailSenderConfig);
532
- return this;
533
- }
534
- job(name, jobConfig) {
535
- super.job(name, jobConfig);
536
- return this;
537
- }
538
- query(name, queryConfig) {
539
- super.query(name, queryConfig);
540
- return this;
541
- }
542
- route(name, routeConfig) {
543
- super.route(name, routeConfig);
544
- return this;
545
- }
546
- api(name, apiConfig) {
547
- super.api(name, apiConfig);
548
- return this;
549
- }
550
- apiNamespace(name, apiNamespaceConfig) {
551
- super.apiNamespace(name, apiNamespaceConfig);
552
- return this;
553
- }
554
- crud(name, crudConfig) {
555
- super.crud(name, crudConfig);
556
- return this;
557
- }
558
- action(name, actionConfig) {
559
- super.action(name, actionConfig);
560
- return this;
413
+ getTemplatePath(templateName) {
414
+ return this.templateUtility.resolveTemplatePath(
415
+ templateName,
416
+ "feature",
417
+ import.meta.url
418
+ );
561
419
  }
562
420
  /**
563
- * Helper method to add routes with simplified parameters
564
- * @param featureName The name of the feature
565
- * @param name Route name, e.g. "DashboardRoute"
566
- * @param options Route configuration options
421
+ * Generate a TypeScript Wasp config file in a feature directory
422
+ * @param featurePath - The feature directory path
567
423
  */
568
- addRoute(featureName, name, options) {
569
- const componentName = name.charAt(0).toUpperCase() + name.slice(1);
570
- const importPath = this.getFeatureImportPath(
571
- featureName,
572
- "client",
573
- "pages",
574
- componentName
575
- );
576
- const routeConfig = {
577
- path: options.path,
578
- to: this.page(componentName, {
579
- authRequired: options.auth || false,
580
- component: {
581
- import: componentName,
582
- from: `@src/${importPath}`
583
- }
584
- })
585
- };
586
- super.route(name, routeConfig);
587
- return this;
424
+ generate(featurePath) {
425
+ const featureDir = getFeatureDir(this.fileSystem, featurePath);
426
+ if (!this.fileSystem.existsSync(featureDir)) {
427
+ this.fileSystem.mkdirSync(featureDir, { recursive: true });
428
+ }
429
+ const templatePath = this.getTemplatePath("feature.wasp.eta");
430
+ if (!this.fileSystem.existsSync(templatePath)) {
431
+ this.logger.error(`Template not found: ${templatePath}`);
432
+ return;
433
+ }
434
+ const configFilePath = path4.join(featureDir, `feature.wasp.ts`);
435
+ if (this.fileSystem.existsSync(configFilePath)) {
436
+ this.logger.warn(`Feature config already exists: ${configFilePath}`);
437
+ return;
438
+ }
439
+ this.fileSystem.copyFileSync(templatePath, configFilePath);
440
+ this.logger.success(`Generated feature config: ${configFilePath}`);
588
441
  }
589
442
  /**
590
- * Helper method to add API endpoints with simplified parameters
591
- * @param featureName The name of the feature
592
- * @param name API endpoint name, e.g. "getTasksApi"
593
- * @param options API configuration options
443
+ * Updates or creates a feature configuration file with a pre-built declaration.
444
+ * @param featurePath - The path to the feature
445
+ * @param declaration - The pre-built declaration string to add or update
446
+ * @returns The updated feature configuration file
594
447
  */
595
- addApi(featureName, name, options) {
596
- const importPath = this.getFeatureImportPath(
597
- featureName,
598
- "server",
599
- "apis",
600
- name
448
+ update(featurePath, declaration) {
449
+ const configDir = getFeatureDir(this.fileSystem, featurePath);
450
+ const configFilePath = path4.join(configDir, `feature.wasp.ts`);
451
+ if (!this.fileSystem.existsSync(configFilePath)) {
452
+ const templatePath = this.getTemplatePath("feature.wasp.eta");
453
+ if (!this.fileSystem.existsSync(templatePath)) {
454
+ handleFatalError(`Feature config template not found: ${templatePath}`);
455
+ }
456
+ this.fileSystem.copyFileSync(templatePath, configFilePath);
457
+ }
458
+ let content = this.fileSystem.readFileSync(configFilePath, "utf8");
459
+ content = this.normaliseSemicolons(content);
460
+ const parsed = parseHelperMethodDefinition(declaration);
461
+ if (!parsed) {
462
+ handleFatalError(`Could not parse definition: ${declaration}`);
463
+ return content;
464
+ }
465
+ const { methodName } = parsed;
466
+ const hadExistingDefinitions = this.hasExistingDefinitions(
467
+ content,
468
+ methodName
601
469
  );
602
- const middlewareImportPath = this.getFeatureImportPath(
603
- featureName,
604
- "server",
605
- "middleware",
606
- name
470
+ content = this.removeExistingDefinition(content, declaration);
471
+ const hasExistingDefinitions = this.hasExistingDefinitions(
472
+ content,
473
+ methodName
607
474
  );
608
- super.api(name, {
609
- fn: {
610
- import: name,
611
- from: `@src/${importPath}`
612
- },
613
- ...options.customMiddleware && {
614
- import: name,
615
- from: `@src/${middlewareImportPath}`
616
- },
617
- entities: options.entities,
618
- httpRoute: { method: options.method, route: options.route },
619
- auth: options.auth || false
620
- });
621
- return this;
622
- }
623
- /**
624
- * Helper method to add CRUD operations with simplified parameters
625
- * @param featureName The name of the feature
626
- * @param name The CRUD name
627
- * @param options CRUD configuration options
475
+ const lines = content.split("\n");
476
+ const configureFunctionStart = lines.findIndex(
477
+ (line) => line.trim().startsWith("export default function")
478
+ );
479
+ if (configureFunctionStart === -1) {
480
+ handleFatalError("Could not find configure function in feature config");
481
+ }
482
+ const appLineIndex = lines.findIndex(
483
+ (line, index) => index > configureFunctionStart && line.trim() === "app"
484
+ );
485
+ if (appLineIndex === -1) {
486
+ const insertIndex = configureFunctionStart + 1;
487
+ const itemsToInsert = [" app"];
488
+ const comment = this.getMethodComment(methodName);
489
+ itemsToInsert.push(` ${comment}`);
490
+ itemsToInsert.push(declaration.trimEnd());
491
+ lines.splice(insertIndex, 0, ...itemsToInsert);
492
+ } else {
493
+ const { insertIndex, addComment } = this.findGroupInsertionPoint(
494
+ lines,
495
+ methodName,
496
+ declaration,
497
+ hadExistingDefinitions || hasExistingDefinitions
498
+ );
499
+ const newLines = this.insertWithSpacing(
500
+ lines,
501
+ declaration,
502
+ insertIndex,
503
+ methodName,
504
+ addComment
505
+ );
506
+ const normalisedContent2 = this.normaliseSemicolons(newLines.join("\n"));
507
+ this.fileSystem.writeFileSync(configFilePath, normalisedContent2);
508
+ return configFilePath;
509
+ }
510
+ const normalisedContent = this.normaliseSemicolons(lines.join("\n"));
511
+ this.fileSystem.writeFileSync(configFilePath, normalisedContent);
512
+ return configFilePath;
513
+ }
514
+ /**
515
+ * Determines the insertion index for a method name based on alphabetical ordering
516
+ * of existing groups in the configuration file.
517
+ * @param groups - Object containing existing method groups
518
+ * @param methodName - The method name to find insertion index for
519
+ * @returns The insertion index for the method name
628
520
  */
629
- addCrud(featureName, name, options) {
630
- const processOperationOptions = (operationName, operationOptions) => {
631
- if (!operationOptions) return void 0;
632
- const processedOptions = { ...operationOptions };
633
- if (operationOptions.override) {
634
- const operationDataType = operationName === "getAll" ? this.getPlural(options.entity) : options.entity;
635
- const operationComponent = `${operationName}${operationDataType}`;
636
- const importPath = this.getFeatureImportPath(
637
- featureName,
638
- "server",
639
- "cruds",
640
- name.charAt(0).toLowerCase() + name.slice(1)
521
+ getInsertionIndexForMethod(groups, methodName) {
522
+ const existingMethods = Object.keys(groups).filter(
523
+ (method) => groups[method].length > 0
524
+ );
525
+ const allMethods = [...existingMethods, methodName].sort();
526
+ return allMethods.indexOf(methodName);
527
+ }
528
+ /**
529
+ * Gets the comment text for a method type.
530
+ * @param methodName The method name (e.g., 'addApi')
531
+ * @returns The comment text for the method type
532
+ */
533
+ getMethodComment(methodName) {
534
+ const entityName = methodName.startsWith("add") ? methodName.slice(3) : methodName;
535
+ return `// ${entityName} definitions`;
536
+ }
537
+ /**
538
+ * Finds the correct insertion point for a new configuration item.
539
+ * @param lines - Array of file lines
540
+ * @param methodName - The method name (e.g., 'addApi')
541
+ * @param definition - The definition string to parse for item name
542
+ * @returns Object with insertion index and whether to add a comment
543
+ */
544
+ findGroupInsertionPoint(lines, methodName, definition, hasExistingDefinitionsOfType) {
545
+ const appLineIndex = lines.findIndex((line) => line.trim() === "app");
546
+ if (appLineIndex === -1) {
547
+ return { insertIndex: appLineIndex + 1, addComment: false };
548
+ }
549
+ const methodCalls = [];
550
+ for (let i = appLineIndex + 1; i < lines.length; i++) {
551
+ const line = lines[i].trim();
552
+ if (line.startsWith(".") && line.includes("(")) {
553
+ let methodCallContent = line;
554
+ let j = i;
555
+ let closingParenCount = 0;
556
+ let foundClosingParen = false;
557
+ for (let k = 0; k < methodCallContent.length; k++) {
558
+ if (methodCallContent[k] === "(") closingParenCount++;
559
+ if (methodCallContent[k] === ")") closingParenCount--;
560
+ if (closingParenCount === 0 && methodCallContent[k] === ")") {
561
+ foundClosingParen = true;
562
+ break;
563
+ }
564
+ }
565
+ while (!foundClosingParen && j < lines.length - 1) {
566
+ j++;
567
+ methodCallContent += " " + lines[j].trim();
568
+ for (let k = 0; k < lines[j].length; k++) {
569
+ if (lines[j][k] === "(") closingParenCount++;
570
+ if (lines[j][k] === ")") closingParenCount--;
571
+ if (closingParenCount === 0 && lines[j][k] === ")") {
572
+ foundClosingParen = true;
573
+ break;
574
+ }
575
+ }
576
+ }
577
+ const match = methodCallContent.match(
578
+ /\.(\w+)\([^,]+,\s*['"`]([^'"`]+)['"`]/
641
579
  );
642
- processedOptions.overrideFn = {
643
- import: operationComponent,
644
- from: `@src/${importPath}`
645
- };
646
- delete processedOptions.override;
580
+ if (match) {
581
+ methodCalls.push({
582
+ lineIndex: i,
583
+ endLineIndex: j,
584
+ methodName: match[1],
585
+ itemName: match[2]
586
+ });
587
+ }
647
588
  }
648
- return processedOptions;
649
- };
650
- super.crud(this.getPlural(options.entity), {
651
- entity: options.entity,
652
- operations: {
653
- getAll: processOperationOptions("getAll", options.getAll),
654
- get: processOperationOptions("get", options.get),
655
- create: processOperationOptions("create", options.create),
656
- update: processOperationOptions("update", options.update),
657
- delete: processOperationOptions("delete", options.delete)
589
+ }
590
+ const groups = {};
591
+ methodCalls.forEach((call) => {
592
+ if (!groups[call.methodName]) {
593
+ groups[call.methodName] = [];
658
594
  }
595
+ groups[call.methodName].push({
596
+ lineIndex: call.lineIndex,
597
+ endLineIndex: call.endLineIndex,
598
+ itemName: call.itemName
599
+ });
659
600
  });
660
- return this;
601
+ const targetGroup = groups[methodName] || [];
602
+ if (targetGroup.length === 0) {
603
+ const targetGroupIndex = this.getInsertionIndexForMethod(
604
+ groups,
605
+ methodName
606
+ );
607
+ const existingMethods = Object.keys(groups).filter((method) => groups[method].length > 0).sort();
608
+ for (let i = targetGroupIndex; i < existingMethods.length; i++) {
609
+ const groupMethod = existingMethods[i];
610
+ if (groups[groupMethod] && groups[groupMethod].length > 0) {
611
+ const firstItem = groups[groupMethod][0];
612
+ let insertIndex = firstItem.lineIndex;
613
+ for (let j = firstItem.lineIndex - 1; j > appLineIndex; j--) {
614
+ const line = lines[j].trim();
615
+ if (line.startsWith("//") && line.includes("definitions")) {
616
+ insertIndex = j;
617
+ break;
618
+ } else if (line.startsWith(".") || line === "") {
619
+ continue;
620
+ } else {
621
+ break;
622
+ }
623
+ }
624
+ return { insertIndex, addComment: !hasExistingDefinitionsOfType };
625
+ }
626
+ }
627
+ for (let i = targetGroupIndex - 1; i >= 0; i--) {
628
+ const groupMethod = existingMethods[i];
629
+ if (groups[groupMethod] && groups[groupMethod].length > 0) {
630
+ const lastItem2 = groups[groupMethod][groups[groupMethod].length - 1];
631
+ return {
632
+ insertIndex: lastItem2.endLineIndex + 1,
633
+ addComment: !hasExistingDefinitionsOfType
634
+ };
635
+ }
636
+ }
637
+ return {
638
+ insertIndex: appLineIndex + 1,
639
+ addComment: !hasExistingDefinitionsOfType
640
+ };
641
+ }
642
+ const parsed = parseHelperMethodDefinition(definition);
643
+ if (!parsed) {
644
+ return { insertIndex: appLineIndex + 1, addComment: false };
645
+ }
646
+ const { firstParam: itemName } = parsed;
647
+ for (let i = 0; i < targetGroup.length; i++) {
648
+ if (itemName.localeCompare(targetGroup[i].itemName) < 0) {
649
+ return { insertIndex: targetGroup[i].lineIndex, addComment: false };
650
+ }
651
+ }
652
+ const lastItem = targetGroup[targetGroup.length - 1];
653
+ return { insertIndex: lastItem.endLineIndex + 1, addComment: false };
661
654
  }
662
655
  /**
663
- * Helper method to add actions with simplified parameters
664
- * @param featureName The name of the feature
665
- * @param name The action name
666
- * @param options Action configuration options
656
+ * Inserts a definition with optional comment header.
657
+ * @param lines - Array of file lines
658
+ * @param declaration - The declaration to insert
659
+ * @param insertIndex - The index where to insert
660
+ * @param methodName - The method name for comment generation
661
+ * @param addComment - Whether to add a comment before the declaration
662
+ * @returns The modified lines array
667
663
  */
668
- addAction(featureName, name, options) {
669
- const importPath = this.getFeatureImportPath(
670
- featureName,
671
- "server",
672
- "actions",
673
- name
674
- );
675
- const config = this.getOperationConfig(
676
- name,
677
- importPath,
678
- options.entities,
679
- options.auth
680
- );
681
- super.action(name, config);
682
- return this;
664
+ insertWithSpacing(lines, declaration, insertIndex, methodName, addComment = false) {
665
+ const newLines = [...lines];
666
+ if (addComment) {
667
+ const comment = this.getMethodComment(methodName);
668
+ newLines.splice(insertIndex, 0, ` ${comment}`);
669
+ insertIndex += 1;
670
+ }
671
+ newLines.splice(insertIndex, 0, declaration.trimEnd());
672
+ return newLines;
683
673
  }
684
674
  /**
685
- * Helper method to add queries with simplified parameters
686
- * @param featureName The name of the feature
687
- * @param name The query name
688
- * @param options Query configuration options
675
+ * Checks if there are any existing definitions of a specific type in the content.
676
+ * @param content - The file content to search
677
+ * @param methodName - The method name to check for (e.g., 'addJob', 'addApi')
678
+ * @returns true if there are existing definitions of this type, false otherwise
689
679
  */
690
- addQuery(featureName, name, options) {
691
- const importPath = this.getFeatureImportPath(
692
- featureName,
693
- "server",
694
- "queries",
695
- name
696
- );
697
- const config = this.getOperationConfig(
698
- name,
699
- importPath,
700
- options.entities,
701
- options.auth
702
- );
703
- super.query(name, config);
704
- return this;
680
+ hasExistingDefinitions(content, methodName) {
681
+ const lines = content.split("\n");
682
+ for (const line of lines) {
683
+ if (line.trim().startsWith(`.${methodName}(`)) {
684
+ return true;
685
+ }
686
+ }
687
+ return false;
705
688
  }
706
689
  /**
707
- * Helper method to add background jobs with simplified parameters
708
- * @param featureName The name of the feature
709
- * @param name Job name
710
- * @param options Job configuration options
690
+ * Removes an existing definition from the content by finding the helper method call
691
+ * and removing the entire method call block.
692
+ * @param content - The file content
693
+ * @param definition - The new definition to find the existing one from
694
+ * @returns The content with the existing definition removed
711
695
  */
712
- addJob(featureName, name, options) {
713
- const importPath = this.getFeatureImportPath(
714
- featureName,
715
- "server",
716
- "jobs",
717
- name
718
- );
719
- super.job(name, {
720
- executor: "PgBoss",
721
- perform: {
722
- fn: {
723
- import: name,
724
- from: `@src/${importPath}`
696
+ removeExistingDefinition(content, definition) {
697
+ const parsed = parseHelperMethodDefinition(definition);
698
+ if (!parsed) {
699
+ return content;
700
+ }
701
+ const { methodName, firstParam } = parsed;
702
+ let contentLines = content.split("\n");
703
+ let openingLineIndex = -1;
704
+ for (let i = 0; i < contentLines.length; i++) {
705
+ const line = contentLines[i];
706
+ if (line.trim().startsWith(`.${methodName}(`)) {
707
+ if (firstParam && line.includes(firstParam)) {
708
+ openingLineIndex = i;
709
+ break;
725
710
  }
726
- },
727
- entities: options.entities,
728
- ...options.cron && {
729
- schedule: {
730
- cron: options.cron,
731
- args: options.args || {}
711
+ }
712
+ }
713
+ if (openingLineIndex === -1) {
714
+ return content;
715
+ }
716
+ let closingLineIndex = -1;
717
+ let parenCount = 0;
718
+ let braceCount = 0;
719
+ let foundOpening = false;
720
+ for (let i = openingLineIndex; i < contentLines.length; i++) {
721
+ const line = contentLines[i];
722
+ for (const char of line) {
723
+ if (char === "(") {
724
+ parenCount++;
725
+ foundOpening = true;
726
+ } else if (char === ")") {
727
+ parenCount--;
728
+ if (foundOpening && parenCount === 0 && braceCount === 0) {
729
+ closingLineIndex = i;
730
+ break;
731
+ }
732
+ } else if (char === "{") {
733
+ braceCount++;
734
+ } else if (char === "}") {
735
+ braceCount--;
732
736
  }
733
737
  }
734
- });
735
- return this;
738
+ if (closingLineIndex !== -1) {
739
+ break;
740
+ }
741
+ }
742
+ if (closingLineIndex === -1) {
743
+ this.logger.warn(
744
+ "Could not find closing parenthesis for existing definition"
745
+ );
746
+ return content;
747
+ }
748
+ contentLines.splice(
749
+ openingLineIndex,
750
+ closingLineIndex - openingLineIndex + 1
751
+ );
752
+ return contentLines.join("\n");
736
753
  }
737
754
  /**
738
- * Helper method to add API namespaces with simplified parameters
739
- * @param featureName The name of the feature
740
- * @param name Namespace name
741
- * @param options API namespace configuration options
755
+ * Adds a definition to the content by finding the appropriate place to insert it.
756
+ * @param content - The current file content
757
+ * @param definition - The definition to add
758
+ * @returns The updated content with the new definition
742
759
  */
743
- addApiNamespace(featureName, name, options) {
744
- const importPath = this.getFeatureImportPath(
745
- featureName,
746
- "server",
747
- "middleware",
748
- name
760
+ addDefinitionToContent(content, definition) {
761
+ const lines = content.split("\n");
762
+ const lastLineIndex = lines.length - 1;
763
+ let insertIndex = lastLineIndex;
764
+ for (let i = lastLineIndex; i >= 0; i--) {
765
+ const line = lines[i].trim();
766
+ if (line && !line.startsWith("}")) {
767
+ insertIndex = i;
768
+ break;
769
+ }
770
+ }
771
+ lines.splice(insertIndex + 1, 0, ` ${definition}`);
772
+ return lines.join("\n");
773
+ }
774
+ /**
775
+ * Normalises semicolons in the config file by removing them from method chain calls
776
+ * while preserving them in other contexts (imports, declarations, etc.).
777
+ * @param content - The file content to normalise
778
+ * @returns The normalised content
779
+ */
780
+ normaliseSemicolons(content) {
781
+ const lines = content.split("\n");
782
+ const configureFunctionStart = lines.findIndex(
783
+ (line) => line.trim().startsWith("export default function")
749
784
  );
750
- super.apiNamespace(name, {
751
- path: options.path,
752
- middlewareConfigFn: {
753
- import: name,
754
- from: `@src/${importPath}`
785
+ if (configureFunctionStart === -1) {
786
+ return content;
787
+ }
788
+ const appLineIndex = lines.findIndex(
789
+ (line, index) => index > configureFunctionStart && line.trim().startsWith("app")
790
+ );
791
+ if (appLineIndex === -1) {
792
+ return content;
793
+ }
794
+ let braceCount = 0;
795
+ let functionEndIndex = lines.length - 1;
796
+ for (let i = configureFunctionStart; i < lines.length; i++) {
797
+ const line = lines[i];
798
+ for (const char of line) {
799
+ if (char === "{") braceCount++;
800
+ if (char === "}") {
801
+ braceCount--;
802
+ if (braceCount === 0) {
803
+ functionEndIndex = i;
804
+ break;
805
+ }
806
+ }
755
807
  }
756
- });
757
- return this;
808
+ if (braceCount === 0 && i > configureFunctionStart) {
809
+ break;
810
+ }
811
+ }
812
+ let lastMethodCallIndex = -1;
813
+ for (let i = appLineIndex + 1; i < functionEndIndex; i++) {
814
+ const line = lines[i];
815
+ const trimmed = line.trim();
816
+ if ((trimmed.endsWith(")") || trimmed.endsWith(");")) && !trimmed.startsWith("//")) {
817
+ lines[i] = line.replace(/;\s*$/, "");
818
+ lastMethodCallIndex = i;
819
+ }
820
+ }
821
+ if (lastMethodCallIndex !== -1 && !lines[lastMethodCallIndex].trim().endsWith(";")) {
822
+ lines[lastMethodCallIndex] = lines[lastMethodCallIndex] + ";";
823
+ }
824
+ return lines.join("\n");
825
+ }
826
+ };
827
+
828
+ // src/generators/base/wasp-generator.base.ts
829
+ var WaspGeneratorBase = class extends GeneratorBase {
830
+ constructor(fileSystem = realFileSystem, logger = singletonLogger2) {
831
+ super(fileSystem, logger);
832
+ this.fileSystem = fileSystem;
833
+ this.logger = logger;
834
+ this.configGenerator = new WaspConfigGenerator(logger, fileSystem);
835
+ this.templateUtility = new TemplateUtility(fileSystem);
836
+ this.templateResolver = new TemplateResolver(fileSystem);
837
+ }
838
+ configGenerator;
839
+ templateUtility;
840
+ templateResolver;
841
+ swarmConfig;
842
+ configLoaded = false;
843
+ // Plugin name from swarm.config.json
844
+ pluginName = PLUGIN_NAME;
845
+ async loadSwarmConfig() {
846
+ if (this.configLoaded) return;
847
+ const configManager = new SwarmConfigManager();
848
+ this.swarmConfig = await configManager.loadConfig();
849
+ this.configLoaded = true;
850
+ }
851
+ async getCustomTemplateDir() {
852
+ await this.loadSwarmConfig();
853
+ return this.swarmConfig?.templateDirectory;
758
854
  }
759
855
  /**
760
- * Calculates the import path for a feature component
761
- * @param featureName The name of the feature
762
- * @param type The type of component (client, server, etc.)
763
- * @param subPath The sub-path within the feature directory
764
- * @param fileName The name of the file (optional, defaults to featureName)
765
- * @returns The calculated import path
856
+ * Resolves template path with override support
857
+ */
858
+ async getTemplatePath(templateName) {
859
+ const defaultPath = this.getDefaultTemplatePath(templateName);
860
+ const customPath = await this.getCustomTemplateDir();
861
+ if (!customPath) {
862
+ return defaultPath;
863
+ }
864
+ const { path: resolvedPath, isCustom } = this.templateResolver.resolveTemplatePath(
865
+ this.pluginName,
866
+ this.name,
867
+ templateName,
868
+ defaultPath,
869
+ customPath
870
+ );
871
+ if (isCustom) {
872
+ this.logger.info(`Using custom template: ${resolvedPath}`);
873
+ }
874
+ return resolvedPath;
875
+ }
876
+ /**
877
+ * Processes a template and writes the result to a file
766
878
  */
767
- getFeatureImportPath(featureName, type, subPath, fileName) {
768
- const file = fileName || featureName;
769
- return `features/${featureName}/${type}/${subPath}/${file}`;
879
+ async renderTemplateToFile(templateName, replacements, outputPath, readableFileType, force) {
880
+ const templatePath = await this.getTemplatePath(templateName);
881
+ const fileExists = this.checkFileExists(
882
+ outputPath,
883
+ force,
884
+ readableFileType
885
+ );
886
+ const content = this.templateUtility.processTemplate(
887
+ templatePath,
888
+ replacements
889
+ );
890
+ this.writeFile(outputPath, content, readableFileType, fileExists);
891
+ return fileExists;
770
892
  }
771
893
  /**
772
- * Converts a singular word to its plural form
773
- * @param word The singular word to pluralize
774
- * @returns The plural form of the word
894
+ * Generic existence check with force flag handling
895
+ * Consolidates the pattern used in both file and config checks
775
896
  */
776
- getPlural(word) {
777
- if (word.endsWith("y")) {
778
- return word.slice(0, -1) + "ies";
779
- } else if (word.endsWith("s") || word.endsWith("sh") || word.endsWith("ch") || word.endsWith("x") || word.endsWith("z")) {
780
- return word + "es";
781
- } else {
782
- return word + "s";
897
+ checkExistence(exists, itemDescription, force, errorMessage) {
898
+ if (exists && !force) {
899
+ this.logger.error(`${itemDescription}. Use --force to overwrite`);
900
+ throw new Error(errorMessage || itemDescription);
783
901
  }
902
+ return exists;
784
903
  }
785
904
  /**
786
- * Configures all feature modules by scanning the features directory
905
+ * Checks if a file exists and handles force flag logic
787
906
  */
788
- async configureFeatures() {
789
- const featuresDir = path5.join(process.cwd(), "src", "features");
790
- if (!fs3.existsSync(featuresDir)) {
791
- console.warn(
792
- "Features directory not found, skipping feature configuration"
793
- );
794
- return this;
795
- }
796
- const getAllFeatureFiles = (dir) => {
797
- let results = [];
798
- const list = fs3.readdirSync(dir, { withFileTypes: true });
799
- for (const entry of list) {
800
- const fullPath = path5.join(dir, entry.name);
801
- if (entry.isDirectory()) {
802
- results = results.concat(getAllFeatureFiles(fullPath));
803
- } else if (entry.isFile() && entry.name.endsWith(".wasp.ts")) {
804
- results.push(path5.relative(featuresDir, fullPath));
805
- }
806
- }
807
- return results;
808
- };
809
- const featureFiles = getAllFeatureFiles(featuresDir);
810
- for (const file of featureFiles) {
811
- try {
812
- const featureName = path5.dirname(file);
813
- const modulePath = path5.join(
814
- process.cwd(),
815
- ".wasp",
816
- "src",
817
- "features",
818
- file.replace(".ts", ".js")
819
- );
820
- const module = await import(modulePath);
821
- if (module.default) {
822
- module.default(this, featureName);
823
- }
824
- } catch (error) {
825
- console.error(`Failed to load feature module ${file}:`, error);
826
- }
827
- }
828
- return this;
907
+ checkFileExists(filePath, force, fileType) {
908
+ const fileExists = this.fileSystem.existsSync(filePath);
909
+ return this.checkExistence(
910
+ fileExists,
911
+ `${fileType} already exists: ${filePath}`,
912
+ force,
913
+ `${fileType} already exists`
914
+ );
829
915
  }
830
916
  /**
831
- * Helper method to get the configuration for an action or query
832
- * @param name The operation name
833
- * @param importPath Import path (excluding `@src/` prefix), e.g. "features/dashboard/server/queries/getTasks"
834
- * @param entities Comma-separated list of entities (optional, defaults to datatype)
835
- * @param auth Require authentication (optional)
917
+ * Safely writes a file with proper error handling and logging
836
918
  */
837
- getOperationConfig(name, importPath, entities, auth) {
838
- return {
839
- fn: {
840
- import: name,
841
- from: `@src/${importPath}`
842
- },
843
- entities,
844
- auth: auth || false
845
- };
919
+ writeFile(filePath, content, fileType, fileExists) {
920
+ this.fileSystem.writeFileSync(filePath, content);
921
+ this.logger.success(
922
+ `${fileExists ? "Overwrote" : "Generated"} ${fileType}: ${filePath}`
923
+ );
846
924
  }
847
925
  };
848
926
 
849
- // src/generators/base/entity-generator.base.ts
850
- import {
851
- hasHelperMethodCall,
852
- SignaleLogger as SignaleLogger4,
853
- toCamelCase,
854
- toKebabCase as toKebabCase2,
855
- validateFeaturePath as validateFeaturePath3
856
- } from "@ingenyus/swarm";
857
- import path8 from "path";
858
-
859
- // src/generators/feature-directory/feature-directory-generator.ts
860
- import {
861
- handleFatalError as handleFatalError2,
862
- SignaleLogger as SignaleLogger3,
863
- validateFeaturePath as validateFeaturePath2
864
- } from "@ingenyus/swarm";
865
- import path7 from "path";
866
-
867
- // src/generators/base/wasp-generator.base.ts
868
- import {
869
- GeneratorBase,
870
- SignaleLogger as SignaleLogger2,
871
- SwarmConfigManager,
872
- TemplateResolver
873
- } from "@ingenyus/swarm";
927
+ // src/generators/feature/schema.ts
928
+ import { z as z2 } from "zod";
929
+ var schema = z2.object({
930
+ target: commonSchemas.target
931
+ });
874
932
 
875
- // src/generators/config/wasp-config-generator.ts
876
- import {
877
- handleFatalError,
878
- parseHelperMethodDefinition,
879
- SignaleLogger
880
- } from "@ingenyus/swarm";
881
- import path6 from "path";
882
- var WaspConfigGenerator = class {
883
- constructor(logger = new SignaleLogger(), fileSystem = realFileSystem) {
933
+ // src/generators/feature/feature-generator.ts
934
+ var FeatureGenerator = class extends WaspGeneratorBase {
935
+ constructor(logger = singletonLogger3, fileSystem = realFileSystem) {
936
+ super(fileSystem, logger);
884
937
  this.logger = logger;
885
938
  this.fileSystem = fileSystem;
886
- this.templateUtility = new TemplateUtility(fileSystem);
939
+ this.name = "feature";
940
+ this.description = "Generates a feature directory containing a Wasp configuration file";
887
941
  }
888
- path = path6;
889
- templateUtility;
890
- /**
891
- * Gets the template path for feature config templates.
892
- * Feature config templates are located in the feature-directory generator's templates directory.
893
- * @param templateName - The name of the template file (e.g., 'feature.wasp.eta')
894
- * @returns The full path to the template file
895
- */
896
- getTemplatePath(templateName) {
942
+ name;
943
+ description;
944
+ schema = schema;
945
+ getDefaultTemplatePath(templateName) {
897
946
  return this.templateUtility.resolveTemplatePath(
898
947
  templateName,
899
- "feature-directory",
948
+ this.name,
900
949
  import.meta.url
901
950
  );
902
951
  }
903
952
  /**
904
- * Generate a TypeScript Wasp config file in a feature directory
905
- * @param featurePath - The feature directory path
906
- */
907
- generate(featurePath) {
908
- const featureDir = getFeatureDir(this.fileSystem, featurePath);
909
- if (!this.fileSystem.existsSync(featureDir)) {
910
- this.fileSystem.mkdirSync(featureDir, { recursive: true });
911
- }
912
- const templatePath = this.getTemplatePath("feature.wasp.eta");
913
- if (!this.fileSystem.existsSync(templatePath)) {
914
- this.logger.error(`Template not found: ${templatePath}`);
915
- return;
916
- }
917
- const configFilePrefix = featurePath.split("/").at(-1);
918
- const configFilePath = path6.join(featureDir, `${configFilePrefix}.wasp.ts`);
919
- if (this.fileSystem.existsSync(configFilePath)) {
920
- this.logger.warn(`Feature config already exists: ${configFilePath}`);
921
- return;
922
- }
923
- this.fileSystem.copyFileSync(templatePath, configFilePath);
924
- this.logger.success(`Generated feature config: ${configFilePath}`);
925
- }
926
- /**
927
- * Updates or creates a feature configuration file with a pre-built declaration.
928
- * @param featurePath - The path to the feature
929
- * @param declaration - The pre-built declaration string to add or update
930
- * @returns The updated feature configuration file
953
+ * Generates a feature directory containing a Wasp configuration file
954
+ * @param target - The target path of the generated directory
931
955
  */
932
- update(featurePath, declaration) {
933
- const configFilePrefix = featurePath.split("/").at(-1);
934
- const configDir = getFeatureDir(this.fileSystem, featurePath);
935
- const configFilePath = path6.join(configDir, `${configFilePrefix}.wasp.ts`);
936
- if (!this.fileSystem.existsSync(configFilePath)) {
937
- const templatePath = this.getTemplatePath("feature.wasp.eta");
938
- if (!this.fileSystem.existsSync(templatePath)) {
939
- handleFatalError(`Feature config template not found: ${templatePath}`);
956
+ async generate(args) {
957
+ const { target } = args;
958
+ const segments = validateFeaturePath2(target);
959
+ const normalisedPath = normaliseFeaturePath(target);
960
+ const sourceRoot = path5.join(findWaspRoot(this.fileSystem), "src");
961
+ if (segments.length > 1) {
962
+ const parentPath = segments.slice(0, -1).join("/");
963
+ const parentNormalisedPath = normaliseFeaturePath(parentPath);
964
+ const parentFeatureDir = path5.join(sourceRoot, parentNormalisedPath);
965
+ if (!this.fileSystem.existsSync(parentFeatureDir)) {
966
+ handleFatalError2(
967
+ `Parent feature '${parentPath}' does not exist. Please create it first.`
968
+ );
940
969
  }
941
- this.fileSystem.copyFileSync(templatePath, configFilePath);
942
- }
943
- let content = this.fileSystem.readFileSync(configFilePath, "utf8");
944
- content = this.normaliseSemicolons(content);
945
- const parsed = parseHelperMethodDefinition(declaration);
946
- if (!parsed) {
947
- handleFatalError(`Could not parse definition: ${declaration}`);
948
- return content;
949
- }
950
- const { methodName } = parsed;
951
- const hadExistingDefinitions = this.hasExistingDefinitions(
952
- content,
953
- methodName
954
- );
955
- content = this.removeExistingDefinition(content, declaration);
956
- const hasExistingDefinitions = this.hasExistingDefinitions(
957
- content,
958
- methodName
959
- );
960
- const lines = content.split("\n");
961
- const configureFunctionStart = lines.findIndex(
962
- (line) => line.trim().startsWith("export default function")
963
- );
964
- if (configureFunctionStart === -1) {
965
- handleFatalError("Could not find configure function in feature config");
966
970
  }
967
- const appLineIndex = lines.findIndex(
968
- (line, index) => index > configureFunctionStart && line.trim() === "app"
971
+ const featureDir = path5.join(sourceRoot, normalisedPath);
972
+ this.fileSystem.mkdirSync(featureDir, { recursive: true });
973
+ this.configGenerator.generate(normalisedPath);
974
+ this.logger.success(`Generated feature: ${normalisedPath}`);
975
+ }
976
+ };
977
+
978
+ // src/generators/base/component-generator.base.ts
979
+ var ComponentGeneratorBase = class extends WaspGeneratorBase {
980
+ constructor(logger = singletonLogger4, fileSystem = realFileSystem, featureDirectoryGenerator = new FeatureGenerator(logger, fileSystem)) {
981
+ super(fileSystem, logger);
982
+ this.logger = logger;
983
+ this.fileSystem = fileSystem;
984
+ this.featureDirectoryGenerator = featureDirectoryGenerator;
985
+ this.featureDirectoryGenerator = featureDirectoryGenerator;
986
+ }
987
+ getDefaultTemplatePath(templateName) {
988
+ return this.templateUtility.resolveTemplatePath(
989
+ templateName,
990
+ this.name,
991
+ import.meta.url
969
992
  );
970
- if (appLineIndex === -1) {
971
- const insertIndex = configureFunctionStart + 1;
972
- const itemsToInsert = [" app"];
973
- const comment = this.getMethodComment(methodName);
974
- itemsToInsert.push(` ${comment}`);
975
- itemsToInsert.push(declaration.trimEnd());
976
- lines.splice(insertIndex, 0, ...itemsToInsert);
977
- } else {
978
- const { insertIndex, addComment } = this.findGroupInsertionPoint(
979
- lines,
980
- methodName,
981
- declaration,
982
- hadExistingDefinitions || hasExistingDefinitions
983
- );
984
- const newLines = this.insertWithSpacing(
985
- lines,
986
- declaration,
987
- insertIndex,
988
- methodName,
989
- addComment
990
- );
991
- const normalisedContent2 = this.normaliseSemicolons(newLines.join("\n"));
992
- this.fileSystem.writeFileSync(configFilePath, normalisedContent2);
993
- return configFilePath;
993
+ }
994
+ get name() {
995
+ return toKebabCase2(this.componentType);
996
+ }
997
+ /**
998
+ * Validates that the feature config file exists in the target or ancestor directories
999
+ */
1000
+ validateFeatureConfig(featurePath) {
1001
+ const normalisedPath = normaliseFeaturePath(featurePath);
1002
+ const segments = normalisedPath.split("/");
1003
+ for (let i = segments.length; i > 0; i--) {
1004
+ const pathSegments = segments.slice(0, i);
1005
+ const currentPath = pathSegments.join("/");
1006
+ const featureName = pathSegments[pathSegments.length - 1];
1007
+ const featureDir = getFeatureDir(this.fileSystem, currentPath);
1008
+ const configPath = path6.join(featureDir, `feature.wasp.ts`);
1009
+ if (this.fileSystem.existsSync(configPath)) {
1010
+ return configPath;
1011
+ }
994
1012
  }
995
- const normalisedContent = this.normaliseSemicolons(lines.join("\n"));
996
- this.fileSystem.writeFileSync(configFilePath, normalisedContent);
997
- return configFilePath;
1013
+ this.logger.error(
1014
+ `Feature config file not found in '${normalisedPath}' or any ancestor directories`
1015
+ );
1016
+ this.logger.error(
1017
+ `Expected to find a feature.wasp.ts config file in one of the feature directories`
1018
+ );
1019
+ throw new Error("Feature config file not found");
998
1020
  }
999
1021
  /**
1000
- * Determines the insertion index for a method name based on alphabetical ordering
1001
- * of existing groups in the configuration file.
1002
- * @param groups - Object containing existing method groups
1003
- * @param methodName - The method name to find insertion index for
1004
- * @returns The insertion index for the method name
1022
+ * Checks if a config item already exists in the feature config
1005
1023
  */
1006
- getInsertionIndexForMethod(groups, methodName) {
1007
- const existingMethods = Object.keys(groups).filter(
1008
- (method) => groups[method].length > 0
1024
+ checkConfigExists(configPath, methodName, itemName, force) {
1025
+ const configContent = this.fileSystem.readFileSync(configPath, "utf8");
1026
+ const configExists = hasHelperMethodCall(
1027
+ configContent,
1028
+ methodName,
1029
+ itemName
1030
+ );
1031
+ return this.checkExistence(
1032
+ configExists,
1033
+ `${methodName} config already exists in ${configPath}`,
1034
+ force,
1035
+ `${methodName} config already exists`
1009
1036
  );
1010
- const allMethods = [...existingMethods, methodName].sort();
1011
- return allMethods.indexOf(methodName);
1012
1037
  }
1013
1038
  /**
1014
- * Gets the comment text for a method type.
1015
- * @param methodName The method name (e.g., 'addApi')
1016
- * @returns The comment text for the method type
1039
+ * Updates the feature config with a new definition
1017
1040
  */
1018
- getMethodComment(methodName) {
1019
- const entityName = methodName.startsWith("add") ? methodName.slice(3) : methodName;
1020
- return `// ${entityName} definitions`;
1041
+ updateFeatureConfig(featurePath, definition, configPath, configExists, methodName) {
1042
+ this.configGenerator.update(featurePath, definition);
1043
+ this.logger.success(
1044
+ `${configExists ? "Updated" : "Added"} ${methodName} config in: ${configPath}`
1045
+ );
1021
1046
  }
1022
1047
  /**
1023
- * Finds the correct insertion point for a new configuration item.
1024
- * @param lines - Array of file lines
1025
- * @param methodName - The method name (e.g., 'addApi')
1026
- * @param definition - The definition string to parse for item name
1027
- * @returns Object with insertion index and whether to add a comment
1048
+ * Consolidated helper for updating config files with existence check
1049
+ * This replaces the duplicated updateConfigFile pattern in concrete generators
1028
1050
  */
1029
- findGroupInsertionPoint(lines, methodName, definition, hasExistingDefinitionsOfType) {
1030
- const appLineIndex = lines.findIndex((line) => line.trim() === "app");
1031
- if (appLineIndex === -1) {
1032
- return { insertIndex: appLineIndex + 1, addComment: false };
1033
- }
1034
- const methodCalls = [];
1035
- for (let i = appLineIndex + 1; i < lines.length; i++) {
1036
- const line = lines[i].trim();
1037
- if (line.startsWith(".") && line.includes("(")) {
1038
- let methodCallContent = line;
1039
- let j = i;
1040
- let closingParenCount = 0;
1041
- let foundClosingParen = false;
1042
- for (let k = 0; k < methodCallContent.length; k++) {
1043
- if (methodCallContent[k] === "(") closingParenCount++;
1044
- if (methodCallContent[k] === ")") closingParenCount--;
1045
- if (closingParenCount === 0 && methodCallContent[k] === ")") {
1046
- foundClosingParen = true;
1047
- break;
1048
- }
1049
- }
1050
- while (!foundClosingParen && j < lines.length - 1) {
1051
- j++;
1052
- methodCallContent += " " + lines[j].trim();
1053
- for (let k = 0; k < lines[j].length; k++) {
1054
- if (lines[j][k] === "(") closingParenCount++;
1055
- if (lines[j][k] === ")") closingParenCount--;
1056
- if (closingParenCount === 0 && lines[j][k] === ")") {
1057
- foundClosingParen = true;
1058
- break;
1059
- }
1060
- }
1061
- }
1062
- const match = methodCallContent.match(
1063
- /\.(\w+)\([^,]+,\s*['"`]([^'"`]+)['"`]/
1064
- );
1065
- if (match) {
1066
- methodCalls.push({
1067
- lineIndex: i,
1068
- endLineIndex: j,
1069
- methodName: match[1],
1070
- itemName: match[2]
1071
- });
1072
- }
1073
- }
1074
- }
1075
- const groups = {};
1076
- methodCalls.forEach((call) => {
1077
- if (!groups[call.methodName]) {
1078
- groups[call.methodName] = [];
1079
- }
1080
- groups[call.methodName].push({
1081
- lineIndex: call.lineIndex,
1082
- endLineIndex: call.endLineIndex,
1083
- itemName: call.itemName
1084
- });
1085
- });
1086
- const targetGroup = groups[methodName] || [];
1087
- if (targetGroup.length === 0) {
1088
- const targetGroupIndex = this.getInsertionIndexForMethod(
1089
- groups,
1051
+ updateConfigWithCheck(configPath, methodName, entityName, definition, featurePath, force) {
1052
+ const configExists = this.checkConfigExists(
1053
+ configPath,
1054
+ methodName,
1055
+ entityName,
1056
+ force
1057
+ );
1058
+ if (!configExists || force) {
1059
+ this.updateFeatureConfig(
1060
+ featurePath,
1061
+ definition,
1062
+ configPath,
1063
+ configExists,
1090
1064
  methodName
1091
1065
  );
1092
- const existingMethods = Object.keys(groups).filter((method) => groups[method].length > 0).sort();
1093
- for (let i = targetGroupIndex; i < existingMethods.length; i++) {
1094
- const groupMethod = existingMethods[i];
1095
- if (groups[groupMethod] && groups[groupMethod].length > 0) {
1096
- const firstItem = groups[groupMethod][0];
1097
- let insertIndex = firstItem.lineIndex;
1098
- for (let j = firstItem.lineIndex - 1; j > appLineIndex; j--) {
1099
- const line = lines[j].trim();
1100
- if (line.startsWith("//") && line.includes("definitions")) {
1101
- insertIndex = j;
1102
- break;
1103
- } else if (line.startsWith(".") || line === "") {
1104
- continue;
1105
- } else {
1106
- break;
1107
- }
1108
- }
1109
- return { insertIndex, addComment: !hasExistingDefinitionsOfType };
1110
- }
1111
- }
1112
- for (let i = targetGroupIndex - 1; i >= 0; i--) {
1113
- const groupMethod = existingMethods[i];
1114
- if (groups[groupMethod] && groups[groupMethod].length > 0) {
1115
- const lastItem2 = groups[groupMethod][groups[groupMethod].length - 1];
1116
- return {
1117
- insertIndex: lastItem2.endLineIndex + 1,
1118
- addComment: !hasExistingDefinitionsOfType
1119
- };
1120
- }
1121
- }
1122
- return {
1123
- insertIndex: appLineIndex + 1,
1124
- addComment: !hasExistingDefinitionsOfType
1125
- };
1126
- }
1127
- const parsed = parseHelperMethodDefinition(definition);
1128
- if (!parsed) {
1129
- return { insertIndex: appLineIndex + 1, addComment: false };
1130
- }
1131
- const { firstParam: itemName } = parsed;
1132
- for (let i = 0; i < targetGroup.length; i++) {
1133
- if (itemName.localeCompare(targetGroup[i].itemName) < 0) {
1134
- return { insertIndex: targetGroup[i].lineIndex, addComment: false };
1135
- }
1136
1066
  }
1137
- const lastItem = targetGroup[targetGroup.length - 1];
1138
- return { insertIndex: lastItem.endLineIndex + 1, addComment: false };
1139
1067
  }
1140
1068
  /**
1141
- * Inserts a definition with optional comment header.
1142
- * @param lines - Array of file lines
1143
- * @param declaration - The declaration to insert
1144
- * @param insertIndex - The index where to insert
1145
- * @param methodName - The method name for comment generation
1146
- * @param addComment - Whether to add a comment before the declaration
1147
- * @returns The modified lines array
1069
+ * Gets the appropriate directory for a feature based on its path.
1070
+ * @param fileSystem - The filesystem abstraction
1071
+ * @param featurePath - The full feature path
1072
+ * @param type - The type of file being generated
1073
+ * @returns The target directory and import path
1148
1074
  */
1149
- insertWithSpacing(lines, declaration, insertIndex, methodName, addComment = false) {
1150
- const newLines = [...lines];
1151
- if (addComment) {
1152
- const comment = this.getMethodComment(methodName);
1153
- newLines.splice(insertIndex, 0, ` ${comment}`);
1154
- insertIndex += 1;
1075
+ getFeatureTargetDir(fileSystem, featurePath, type) {
1076
+ validateFeaturePath3(featurePath);
1077
+ const normalisedPath = normaliseFeaturePath(featurePath);
1078
+ const featureDir = getFeatureDir(fileSystem, normalisedPath);
1079
+ const typeKey = type.toLowerCase();
1080
+ const typeDirectory = TYPE_DIRECTORIES[typeKey];
1081
+ const targetDirectory = path6.join(featureDir, typeDirectory);
1082
+ const importDirectory = `@src/${normalisedPath}/${typeDirectory}`;
1083
+ return { targetDirectory, importDirectory };
1084
+ }
1085
+ /**
1086
+ * Ensures a target directory exists and returns its path
1087
+ */
1088
+ ensureTargetDirectory(featurePath, type) {
1089
+ const { targetDirectory, importDirectory } = this.getFeatureTargetDir(
1090
+ this.fileSystem,
1091
+ featurePath,
1092
+ type
1093
+ );
1094
+ ensureDirectoryExists(this.fileSystem, targetDirectory);
1095
+ return { targetDirectory, importDirectory };
1096
+ }
1097
+ /**
1098
+ * Generate middleware file for API or API namespace
1099
+ */
1100
+ async generateMiddlewareFile(targetFile, name, force) {
1101
+ const replacements = {
1102
+ name,
1103
+ middlewareType: toCamelCase(this.componentType || "")
1104
+ };
1105
+ await this.renderTemplateToFile(
1106
+ "middleware.eta",
1107
+ replacements,
1108
+ targetFile,
1109
+ "Middleware file",
1110
+ force
1111
+ );
1112
+ }
1113
+ };
1114
+
1115
+ // src/generators/base/operation-generator.base.ts
1116
+ import {
1117
+ capitalise,
1118
+ getPlural,
1119
+ handleFatalError as handleFatalError3,
1120
+ toPascalCase as toPascalCase2
1121
+ } from "@ingenyus/swarm";
1122
+ var OperationGeneratorBase = class extends ComponentGeneratorBase {
1123
+ /**
1124
+ * Gets the operation name based on operation type and model name.
1125
+ */
1126
+ getOperationName(operation, modelName, customName) {
1127
+ if (customName) {
1128
+ return customName;
1129
+ }
1130
+ switch (operation) {
1131
+ case OPERATIONS.GETALL:
1132
+ return `getAll${getPlural(modelName)}`;
1133
+ case OPERATIONS.GETFILTERED:
1134
+ return `getFiltered${getPlural(modelName)}`;
1135
+ default:
1136
+ return `${operation}${modelName}`;
1155
1137
  }
1156
- newLines.splice(insertIndex, 0, declaration.trimEnd());
1157
- return newLines;
1158
1138
  }
1159
1139
  /**
1160
- * Checks if there are any existing definitions of a specific type in the content.
1161
- * @param content - The file content to search
1162
- * @param methodName - The method name to check for (e.g., 'addJob', 'addApi')
1163
- * @returns true if there are existing definitions of this type, false otherwise
1140
+ * Gets the template path for operation templates.
1141
+ * This method resolves operation templates to the operation generator's directory
1142
+ * instead of the current generator's directory.
1164
1143
  */
1165
- hasExistingDefinitions(content, methodName) {
1166
- const lines = content.split("\n");
1167
- for (const line of lines) {
1168
- if (line.trim().startsWith(`.${methodName}(`)) {
1169
- return true;
1170
- }
1171
- }
1172
- return false;
1144
+ getOperationTemplatePath(templateName) {
1145
+ return this.templateUtility.resolveTemplatePath(
1146
+ templateName,
1147
+ "operation",
1148
+ import.meta.url
1149
+ );
1173
1150
  }
1174
1151
  /**
1175
- * Removes an existing definition from the content by finding the helper method call
1176
- * and removing the entire method call block.
1177
- * @param content - The file content
1178
- * @param definition - The new definition to find the existing one from
1179
- * @returns The content with the existing definition removed
1152
+ * Gets the TypeScript type name for an operation.
1180
1153
  */
1181
- removeExistingDefinition(content, definition) {
1182
- const parsed = parseHelperMethodDefinition(definition);
1183
- if (!parsed) {
1184
- return content;
1185
- }
1186
- const { methodName, firstParam } = parsed;
1187
- let contentLines = content.split("\n");
1188
- let openingLineIndex = -1;
1189
- for (let i = 0; i < contentLines.length; i++) {
1190
- const line = contentLines[i];
1191
- if (line.trim().startsWith(`.${methodName}(`)) {
1192
- if (firstParam && line.includes(firstParam)) {
1193
- openingLineIndex = i;
1194
- break;
1195
- }
1196
- }
1197
- }
1198
- if (openingLineIndex === -1) {
1199
- return content;
1200
- }
1201
- let closingLineIndex = -1;
1202
- let parenCount = 0;
1203
- let braceCount = 0;
1204
- let foundOpening = false;
1205
- for (let i = openingLineIndex; i < contentLines.length; i++) {
1206
- const line = contentLines[i];
1207
- for (const char of line) {
1208
- if (char === "(") {
1209
- parenCount++;
1210
- foundOpening = true;
1211
- } else if (char === ")") {
1212
- parenCount--;
1213
- if (foundOpening && parenCount === 0 && braceCount === 0) {
1214
- closingLineIndex = i;
1215
- break;
1216
- }
1217
- } else if (char === "{") {
1218
- braceCount++;
1219
- } else if (char === "}") {
1220
- braceCount--;
1221
- }
1222
- }
1223
- if (closingLineIndex !== -1) {
1224
- break;
1154
+ getOperationTypeName(operation, modelName) {
1155
+ return toPascalCase2(this.getOperationName(operation, modelName));
1156
+ }
1157
+ /**
1158
+ * Generates import statements for an operation.
1159
+ */
1160
+ generateImports(model, modelName, operation) {
1161
+ const imports = [];
1162
+ if (operation !== OPERATIONS.GETALL) {
1163
+ if (needsPrismaImport(model)) {
1164
+ imports.push('import { Prisma } from "@prisma/client";');
1225
1165
  }
1166
+ imports.push(`import { ${modelName} } from "wasp/entities";`);
1226
1167
  }
1227
- if (closingLineIndex === -1) {
1228
- this.logger.warn(
1229
- "Could not find closing parenthesis for existing definition"
1230
- );
1231
- return content;
1232
- }
1233
- contentLines.splice(
1234
- openingLineIndex,
1235
- closingLineIndex - openingLineIndex + 1
1168
+ imports.push('import { HttpError } from "wasp/server";');
1169
+ imports.push(
1170
+ `import type { ${this.getOperationTypeName(
1171
+ operation,
1172
+ modelName
1173
+ )} } from "wasp/server/operations";`
1236
1174
  );
1237
- return contentLines.join("\n");
1175
+ return imports.join("\n");
1238
1176
  }
1239
1177
  /**
1240
- * Adds a definition to the content by finding the appropriate place to insert it.
1241
- * @param content - The current file content
1242
- * @param definition - The definition to add
1243
- * @returns The updated content with the new definition
1178
+ * Gets the operation type ("query" or "action") for a given operation.
1244
1179
  */
1245
- addDefinitionToContent(content, definition) {
1246
- const lines = content.split("\n");
1247
- const lastLineIndex = lines.length - 1;
1248
- let insertIndex = lastLineIndex;
1249
- for (let i = lastLineIndex; i >= 0; i--) {
1250
- const line = lines[i].trim();
1251
- if (line && !line.startsWith("}")) {
1252
- insertIndex = i;
1253
- break;
1254
- }
1255
- }
1256
- lines.splice(insertIndex + 1, 0, ` ${definition}`);
1257
- return lines.join("\n");
1180
+ getOperationType(operation) {
1181
+ return operation === OPERATIONS.GETALL || operation === OPERATIONS.GET || operation === OPERATIONS.GETFILTERED ? "query" : "action";
1258
1182
  }
1259
1183
  /**
1260
- * Normalises semicolons in the config file by removing them from method chain calls
1261
- * while preserving them in other contexts (imports, declarations, etc.).
1262
- * @param content - The file content to normalise
1263
- * @returns The normalised content
1184
+ * Generates the operation components needed for file and config generation.
1264
1185
  */
1265
- normaliseSemicolons(content) {
1266
- const lines = content.split("\n");
1267
- const configureFunctionStart = lines.findIndex(
1268
- (line) => line.trim().startsWith("export default function")
1186
+ async generateOperationComponents(modelName, operation, auth = false, entities = [modelName], isCrudOverride = false, crudName = null, customName) {
1187
+ const model = await getEntityMetadata(modelName);
1188
+ const operationType = this.getOperationType(operation);
1189
+ const operationName = this.getOperationName(
1190
+ operation,
1191
+ modelName,
1192
+ customName
1269
1193
  );
1270
- if (configureFunctionStart === -1) {
1271
- return content;
1272
- }
1273
- const appLineIndex = lines.findIndex(
1274
- (line, index) => index > configureFunctionStart && line.trim().startsWith("app")
1194
+ const operationCode = await this.generateOperationCode(
1195
+ model,
1196
+ operation,
1197
+ auth,
1198
+ isCrudOverride,
1199
+ crudName
1275
1200
  );
1276
- if (appLineIndex === -1) {
1277
- return content;
1278
- }
1279
- let braceCount = 0;
1280
- let functionEndIndex = lines.length - 1;
1281
- for (let i = configureFunctionStart; i < lines.length; i++) {
1282
- const line = lines[i];
1283
- for (const char of line) {
1284
- if (char === "{") braceCount++;
1285
- if (char === "}") {
1286
- braceCount--;
1287
- if (braceCount === 0) {
1288
- functionEndIndex = i;
1289
- break;
1290
- }
1291
- }
1201
+ const configEntry = {
1202
+ operationName,
1203
+ entities,
1204
+ authRequired: auth
1205
+ };
1206
+ return {
1207
+ operationCode,
1208
+ configEntry,
1209
+ operationType,
1210
+ operationName
1211
+ };
1212
+ }
1213
+ /**
1214
+ * Generates the code for an operation.
1215
+ */
1216
+ async generateOperationCode(model, operation, auth = false, isCrudOverride = false, crudName = null) {
1217
+ const operationType = this.getOperationType(operation);
1218
+ const templatePath = this.getOperationTemplatePath(`${operation}.eta`);
1219
+ const allFieldNames = model.fields.map((f) => f.name);
1220
+ const idFields = getIdFields(model);
1221
+ const requiredFields = getRequiredFields(model);
1222
+ const optionalFields = getOptionalFields(model);
1223
+ const jsonFields = getJsonFields(model);
1224
+ const pluralModelName = getPlural(model.name);
1225
+ const pluralModelNameLower = pluralModelName.toLowerCase();
1226
+ const modelNameLower = model.name.toLowerCase();
1227
+ const operationName = this.getOperationName(operation, model.name);
1228
+ const imports = isCrudOverride ? "" : this.generateImports(model, model.name, operation);
1229
+ const jsonTypeHandling = generateJsonTypeHandling(jsonFields);
1230
+ let typeParams = "";
1231
+ switch (operation) {
1232
+ case "create": {
1233
+ const pickRequired = generatePickType(
1234
+ model.name,
1235
+ requiredFields,
1236
+ allFieldNames
1237
+ );
1238
+ const partialOptional = generatePartialType(
1239
+ generatePickType(model.name, optionalFields, allFieldNames)
1240
+ );
1241
+ typeParams = `<${generateIntersectionType(pickRequired, partialOptional)}>`;
1242
+ break;
1292
1243
  }
1293
- if (braceCount === 0 && i > configureFunctionStart) {
1244
+ case "update": {
1245
+ const pickId = generatePickType(model.name, idFields, allFieldNames);
1246
+ const omitId = generateOmitType(model.name, idFields, allFieldNames);
1247
+ const partialRest = generatePartialType(omitId);
1248
+ typeParams = `<${generateIntersectionType(pickId, partialRest)}>`;
1294
1249
  break;
1295
1250
  }
1251
+ case "delete":
1252
+ case "get":
1253
+ typeParams = `<${generatePickType(model.name, idFields, allFieldNames)}>`;
1254
+ break;
1255
+ case "getAll":
1256
+ typeParams = `<void>`;
1257
+ break;
1258
+ case "getFiltered":
1259
+ typeParams = `<${generatePartialType(model.name)}>`;
1260
+ break;
1296
1261
  }
1297
- let lastMethodCallIndex = -1;
1298
- for (let i = appLineIndex + 1; i < functionEndIndex; i++) {
1299
- const line = lines[i];
1300
- const trimmed = line.trim();
1301
- if ((trimmed.endsWith(")") || trimmed.endsWith(");")) && !trimmed.startsWith("//")) {
1302
- lines[i] = line.replace(/;\s*$/, "");
1303
- lastMethodCallIndex = i;
1262
+ const authCheck = auth ? ` if (!context.user) {
1263
+ throw new HttpError(401);
1264
+ }
1265
+
1266
+ ` : "";
1267
+ let typeAnnotation = "";
1268
+ let satisfiesType = "";
1269
+ if (isCrudOverride && crudName) {
1270
+ const opCap = capitalise(operation);
1271
+ if (operationType === "action") {
1272
+ typeAnnotation = `: ${crudName}.${opCap}Action${typeParams}`;
1273
+ } else {
1274
+ typeAnnotation = "";
1275
+ }
1276
+ if (operationType === "query") {
1277
+ satisfiesType = `satisfies ${crudName}.${opCap}Query${typeParams}`;
1278
+ } else {
1279
+ satisfiesType = "";
1280
+ }
1281
+ } else {
1282
+ if (operationType === "action") {
1283
+ typeAnnotation = `: ${this.getOperationTypeName(operation, model.name)}${typeParams}`;
1284
+ } else {
1285
+ typeAnnotation = "";
1286
+ }
1287
+ if (operationType === "query") {
1288
+ satisfiesType = `satisfies ${this.getOperationTypeName(operation, model.name)}${typeParams}`;
1289
+ } else {
1290
+ satisfiesType = "";
1304
1291
  }
1305
1292
  }
1306
- if (lastMethodCallIndex !== -1 && !lines[lastMethodCallIndex].trim().endsWith(";")) {
1307
- lines[lastMethodCallIndex] = lines[lastMethodCallIndex] + ";";
1308
- }
1309
- return lines.join("\n");
1310
- }
1311
- };
1312
-
1313
- // src/generators/base/wasp-generator.base.ts
1314
- var WaspGeneratorBase = class extends GeneratorBase {
1315
- constructor(fileSystem = realFileSystem, logger = new SignaleLogger2()) {
1316
- super(fileSystem, logger);
1317
- this.fileSystem = fileSystem;
1318
- this.logger = logger;
1319
- this.configGenerator = new WaspConfigGenerator(logger, fileSystem);
1320
- this.templateUtility = new TemplateUtility(fileSystem);
1321
- this.templateResolver = new TemplateResolver(fileSystem);
1322
- }
1323
- configGenerator;
1324
- templateUtility;
1325
- templateResolver;
1326
- swarmConfig;
1327
- configLoaded = false;
1328
- // Plugin name from swarm.config.json
1329
- pluginName = PLUGIN_NAME;
1330
- async loadSwarmConfig() {
1331
- if (this.configLoaded) return;
1332
- const configManager = new SwarmConfigManager();
1333
- this.swarmConfig = await configManager.loadConfig();
1334
- this.configLoaded = true;
1335
- }
1336
- async getCustomTemplateDir() {
1337
- await this.loadSwarmConfig();
1338
- return this.swarmConfig?.templateDirectory;
1293
+ const isCompositeKey = idFields.length > 1;
1294
+ const compositeKeyName = isCompositeKey ? idFields.join("_") : "";
1295
+ const idFieldParams = isCompositeKey ? idFields.join(", ") : idFields[0];
1296
+ const whereClause = isCompositeKey ? `${compositeKeyName}: { ${idFields.map((f) => `${f}`).join(", ")} }` : idFields[0];
1297
+ const replacements = {
1298
+ operationName,
1299
+ modelName: model.name,
1300
+ authCheck,
1301
+ imports,
1302
+ idField: idFields[0],
1303
+ idFieldParams,
1304
+ whereClause,
1305
+ isCompositeKey: String(isCompositeKey),
1306
+ compositeKeyName,
1307
+ jsonTypeHandling,
1308
+ typeAnnotation,
1309
+ satisfiesType,
1310
+ modelNameLower,
1311
+ pluralModelNameLower
1312
+ };
1313
+ return this.templateUtility.processTemplate(templatePath, replacements);
1339
1314
  }
1340
1315
  /**
1341
- * Resolves template path with override support
1316
+ * Generates an operation file for a given operation.
1342
1317
  */
1343
- async getTemplatePath(templateName) {
1344
- const defaultPath = this.getDefaultTemplatePath(templateName);
1345
- const customPath = await this.getCustomTemplateDir();
1346
- if (!customPath) {
1347
- return defaultPath;
1348
- }
1349
- const { path: resolvedPath, isCustom } = this.templateResolver.resolveTemplatePath(
1350
- this.pluginName,
1351
- this.name,
1352
- templateName,
1353
- defaultPath,
1354
- customPath
1318
+ generateOperationFile(operationsDir, operationName, operationCode, force = false) {
1319
+ const operationFile = `${operationsDir}/${operationName}.ts`;
1320
+ const fileExists = this.checkFileExists(
1321
+ operationFile,
1322
+ force,
1323
+ "Operation file"
1355
1324
  );
1356
- if (isCustom) {
1357
- this.logger.info(`Using custom template: ${resolvedPath}`);
1358
- }
1359
- return resolvedPath;
1325
+ this.writeFile(operationFile, operationCode, "operation file", fileExists);
1360
1326
  }
1361
1327
  /**
1362
- * Processes a template and writes the result to a file
1328
+ * Copies a directory of operation templates to the target feature directory.
1329
+ * @param templateDir - The source template directory
1330
+ * @param targetDir - The target feature directory
1363
1331
  */
1364
- async renderTemplateToFile(templateName, replacements, outputPath, readableFileType, force) {
1365
- const templatePath = await this.getTemplatePath(templateName);
1366
- const fileExists = this.checkFileExists(
1367
- outputPath,
1368
- force,
1369
- readableFileType
1370
- );
1371
- const content = this.templateUtility.processTemplate(
1372
- templatePath,
1373
- replacements
1332
+ copyOperationTemplates(templateDir, targetDir) {
1333
+ copyDirectory(this.fileSystem, templateDir, targetDir);
1334
+ this.logger.debug(
1335
+ `Copied operation templates from ${templateDir} to ${targetDir}`
1374
1336
  );
1375
- this.writeFile(outputPath, content, readableFileType, fileExists);
1376
- return fileExists;
1377
1337
  }
1378
1338
  /**
1379
- * Generic existence check with force flag handling
1380
- * Consolidates the pattern used in both file and config checks
1339
+ * Generates an operation definition for the feature configuration.
1381
1340
  */
1382
- checkExistence(exists, itemDescription, force, errorMessage) {
1383
- if (exists && !force) {
1384
- this.logger.error(`${itemDescription}. Use --force to overwrite`);
1385
- throw new Error(errorMessage || itemDescription);
1341
+ getDefinition(operationName, featurePath, entities, operationType, importPath, auth = false) {
1342
+ if (!OPERATION_TYPES.includes(operationType)) {
1343
+ handleFatalError3(`Unknown operation type: ${operationType}`);
1386
1344
  }
1387
- return exists;
1345
+ const directory = TYPE_DIRECTORIES[operationType];
1346
+ const featureDir = getFeatureImportPath(featurePath);
1347
+ const templatePath = this.templateUtility.resolveTemplatePath(
1348
+ "operation.eta",
1349
+ "config",
1350
+ import.meta.url
1351
+ );
1352
+ return this.templateUtility.processTemplate(templatePath, {
1353
+ operationType: capitalise(operationType),
1354
+ operationName,
1355
+ featureDir,
1356
+ directory,
1357
+ entities: entities.map((e) => `"${e}"`).join(", "),
1358
+ importPath,
1359
+ auth: String(auth)
1360
+ });
1388
1361
  }
1389
- /**
1390
- * Checks if a file exists and handles force flag logic
1391
- */
1392
- checkFileExists(filePath, force, fileType) {
1393
- const fileExists = this.fileSystem.existsSync(filePath);
1394
- return this.checkExistence(
1395
- fileExists,
1396
- `${fileType} already exists: ${filePath}`,
1397
- force,
1398
- `${fileType} already exists`
1362
+ };
1363
+
1364
+ // src/generators/action/schema.ts
1365
+ import { SchemaManager, commandRegistry as commandRegistry2 } from "@ingenyus/swarm";
1366
+ import { z as z3 } from "zod";
1367
+ var validActions = Object.values(ACTION_OPERATIONS);
1368
+ var actionSchema = z3.string().min(1, "Action type is required").transform((val) => SchemaManager.findEnumValue(ACTION_OPERATIONS, val)).pipe(
1369
+ z3.enum(ACTION_OPERATIONS, {
1370
+ message: `Invalid action. Must be one of: ${validActions.join(", ")}`
1371
+ })
1372
+ ).meta({ description: "The action operation to generate" }).register(commandRegistry2, {
1373
+ shortName: "o",
1374
+ examples: validActions,
1375
+ helpText: `Available actions: ${validActions.join(", ")}`
1376
+ });
1377
+ var schema2 = z3.object({
1378
+ feature: commonSchemas.feature,
1379
+ operation: actionSchema,
1380
+ dataType: commonSchemas.dataType,
1381
+ name: commonSchemas.name.optional().meta({
1382
+ ...commonSchemas.name.meta() ?? {},
1383
+ description: `${commonSchemas.name.meta()?.description ?? ""} (optional)`
1384
+ }).register(commandRegistry2, commandRegistry2.get(commonSchemas.name) ?? {}),
1385
+ entities: commonSchemas.entities,
1386
+ force: commonSchemas.force,
1387
+ auth: commonSchemas.auth
1388
+ });
1389
+
1390
+ // src/generators/action/action-generator.ts
1391
+ var ActionGenerator = class extends OperationGeneratorBase {
1392
+ get componentType() {
1393
+ return CONFIG_TYPES.ACTION;
1394
+ }
1395
+ description = "Generates a Wasp Action";
1396
+ schema = schema2;
1397
+ async generate(args) {
1398
+ const { dataType, feature, name } = args;
1399
+ const operation = args.operation;
1400
+ const operationType = "action";
1401
+ const entities = args.entities ?? [];
1402
+ if (dataType && !entities.includes(dataType)) {
1403
+ entities.unshift(dataType);
1404
+ }
1405
+ const { operationCode, operationName } = await this.generateOperationComponents(
1406
+ dataType,
1407
+ operation,
1408
+ args.auth,
1409
+ entities,
1410
+ false,
1411
+ null,
1412
+ name
1413
+ );
1414
+ return this.handleGeneratorError(
1415
+ this.componentType,
1416
+ operationName,
1417
+ async () => {
1418
+ const configPath = this.validateFeatureConfig(feature);
1419
+ const { targetDirectory: operationsDir, importDirectory } = this.ensureTargetDirectory(feature, operationType);
1420
+ const importPath = `${importDirectory}/${operationName}`;
1421
+ this.generateOperationFile(
1422
+ operationsDir,
1423
+ operationName,
1424
+ operationCode,
1425
+ args.force || false
1426
+ );
1427
+ const definition = this.getDefinition(
1428
+ operationName,
1429
+ feature,
1430
+ entities,
1431
+ "action",
1432
+ importPath,
1433
+ args.auth
1434
+ );
1435
+ this.updateConfigWithCheck(
1436
+ configPath,
1437
+ "addAction",
1438
+ operationName,
1439
+ definition,
1440
+ feature,
1441
+ args.force || false
1442
+ );
1443
+ }
1444
+ );
1445
+ }
1446
+ };
1447
+
1448
+ // src/generators/api/api-generator.ts
1449
+ import { toCamelCase as toCamelCase2, toPascalCase as toPascalCase3 } from "@ingenyus/swarm";
1450
+
1451
+ // src/generators/api/schema.ts
1452
+ import { commandRegistry as commandRegistry3 } from "@ingenyus/swarm";
1453
+ import { z as z4 } from "zod";
1454
+ var validHttpMethods = API_HTTP_METHODS.map((method) => `${method}`);
1455
+ var schema3 = z4.object({
1456
+ method: z4.string().min(1, "HTTP method is required").transform((val) => val.toUpperCase()).pipe(
1457
+ z4.enum(API_HTTP_METHODS, {
1458
+ message: `Invalid HTTP method. Must be one of: ${validHttpMethods.join(", ")}`
1459
+ })
1460
+ ).meta({ description: "The HTTP method used for this API Endpoint" }).register(commandRegistry3, {
1461
+ shortName: "m",
1462
+ examples: validHttpMethods,
1463
+ helpText: `Must be one of: ${validHttpMethods.join(", ")}`
1464
+ }),
1465
+ feature: commonSchemas.feature,
1466
+ name: commonSchemas.name,
1467
+ path: commonSchemas.path,
1468
+ entities: commonSchemas.entities,
1469
+ auth: commonSchemas.auth,
1470
+ force: commonSchemas.force,
1471
+ customMiddleware: z4.boolean().meta({ description: "Enable custom middleware for this API Endpoint" }).optional().register(commandRegistry3, {
1472
+ shortName: "c",
1473
+ helpText: "Will generate custom middleware file"
1474
+ })
1475
+ });
1476
+
1477
+ // src/generators/api/api-generator.ts
1478
+ var ApiGenerator = class extends ComponentGeneratorBase {
1479
+ get componentType() {
1480
+ return CONFIG_TYPES.API;
1481
+ }
1482
+ description = "Generates a Wasp API Endpoint";
1483
+ schema = schema3;
1484
+ async generate(args) {
1485
+ const apiName = toCamelCase2(args.name);
1486
+ return this.handleGeneratorError(this.componentType, apiName, async () => {
1487
+ const configPath = this.validateFeatureConfig(args.feature);
1488
+ const {
1489
+ targetDirectory: apiTargetDirectory,
1490
+ importDirectory: apiImportDirectory
1491
+ } = this.ensureTargetDirectory(args.feature, this.name);
1492
+ const fileName = `${apiName}.ts`;
1493
+ const targetFile = `${apiTargetDirectory}/${fileName}`;
1494
+ await this.generateApiFile(targetFile, apiName, args);
1495
+ if (args.customMiddleware) {
1496
+ const { targetDirectory: middlewareTargetDirectory } = this.ensureTargetDirectory(args.feature, "middleware");
1497
+ const middlewareFile = `${middlewareTargetDirectory}/${apiName}.ts`;
1498
+ this.generateMiddlewareFile(
1499
+ middlewareFile,
1500
+ apiName,
1501
+ args.force || false
1502
+ );
1503
+ }
1504
+ await this.updateConfigFile(
1505
+ apiName,
1506
+ fileName,
1507
+ apiImportDirectory,
1508
+ args,
1509
+ configPath
1510
+ );
1511
+ });
1512
+ }
1513
+ async generateApiFile(targetFile, apiName, { method, auth = false, force = false }) {
1514
+ const replacements = this.buildTemplateData(apiName, method, auth);
1515
+ await this.renderTemplateToFile(
1516
+ "api.eta",
1517
+ replacements,
1518
+ targetFile,
1519
+ "API Endpoint file",
1520
+ force
1399
1521
  );
1400
1522
  }
1401
- /**
1402
- * Safely writes a file with proper error handling and logging
1403
- */
1404
- writeFile(filePath, content, fileType, fileExists) {
1405
- this.fileSystem.writeFileSync(filePath, content);
1406
- this.logger.success(
1407
- `${fileExists ? "Overwrote" : "Generated"} ${fileType}: ${filePath}`
1523
+ async updateConfigFile(apiName, apiFile, importDirectory, args, configFilePath) {
1524
+ const {
1525
+ feature,
1526
+ force = false,
1527
+ entities,
1528
+ method,
1529
+ path: path9,
1530
+ auth,
1531
+ customMiddleware
1532
+ } = args;
1533
+ const importPath = this.path.join(importDirectory, apiFile);
1534
+ const definition = await this.getConfigDefinition(
1535
+ apiName,
1536
+ feature,
1537
+ Array.isArray(entities) ? entities : entities ? [entities] : [],
1538
+ method,
1539
+ path9,
1540
+ apiFile,
1541
+ auth,
1542
+ importPath,
1543
+ customMiddleware || false
1544
+ );
1545
+ this.updateConfigWithCheck(
1546
+ configFilePath,
1547
+ "addApi",
1548
+ apiName,
1549
+ definition,
1550
+ feature,
1551
+ force
1408
1552
  );
1409
1553
  }
1410
- };
1554
+ async getConfigDefinition(apiName, featurePath, entities, method, route, apiFile, auth = false, importPath, customMiddleware = false) {
1555
+ const featureDir = getFeatureImportPath(featurePath);
1556
+ const configTemplatePath = await this.getTemplatePath("config/api.eta");
1557
+ return this.templateUtility.processTemplate(configTemplatePath, {
1558
+ apiName,
1559
+ featureDir,
1560
+ entities: entities.map((e) => `"${e}"`).join(", "),
1561
+ method,
1562
+ route,
1563
+ apiFile,
1564
+ auth: String(auth),
1565
+ importPath,
1566
+ customMiddleware: String(customMiddleware)
1567
+ });
1568
+ }
1569
+ buildTemplateData(apiName, method, auth) {
1570
+ const apiType = toPascalCase3(apiName);
1571
+ const authCheck = auth ? ` if (!context.user) {
1572
+ throw new HttpError(401);
1573
+ }
1411
1574
 
1412
- // src/generators/feature-directory/schema.ts
1413
- import { z as z2 } from "zod";
1414
- var schema = z2.object({
1415
- path: commonSchemas.path
1416
- });
1575
+ ` : "";
1576
+ const methodCheck = method !== "ALL" ? ` if (req.method !== '${method}') {
1577
+ throw new HttpError(405);
1578
+ }
1417
1579
 
1418
- // src/generators/feature-directory/feature-directory-generator.ts
1419
- var FeatureDirectoryGenerator = class extends WaspGeneratorBase {
1420
- constructor(logger = new SignaleLogger3(), fileSystem = realFileSystem) {
1421
- super(fileSystem, logger);
1422
- this.logger = logger;
1423
- this.fileSystem = fileSystem;
1424
- this.name = "feature-directory";
1425
- this.description = "Generate feature directory structure";
1426
- this.schema = schema;
1427
- }
1428
- name;
1429
- description;
1430
- schema;
1431
- getDefaultTemplatePath(templateName) {
1432
- return this.templateUtility.resolveTemplatePath(
1433
- templateName,
1434
- this.name,
1435
- import.meta.url
1436
- );
1437
- }
1438
- /**
1439
- * Generate feature directory structure (main entry point)
1440
- * @param featurePath - The path to the feature
1441
- */
1442
- async generate(flags) {
1443
- const { path: featurePath } = flags;
1444
- console.log("generate feature directory:", featurePath);
1445
- const segments = validateFeaturePath2(featurePath);
1446
- const normalisedPath = normaliseFeaturePath(featurePath);
1447
- const sourceRoot = path7.join(findWaspRoot(this.fileSystem), "src");
1448
- if (segments.length > 1) {
1449
- const parentPath = segments.slice(0, -1).join("/");
1450
- const parentNormalisedPath = normaliseFeaturePath(parentPath);
1451
- const parentFeatureDir = path7.join(sourceRoot, parentNormalisedPath);
1452
- if (!this.fileSystem.existsSync(parentFeatureDir)) {
1453
- handleFatalError2(
1454
- `Parent feature '${parentPath}' does not exist. Please create it first.`
1455
- );
1456
- }
1457
- }
1458
- const featureDir = path7.join(sourceRoot, normalisedPath);
1459
- this.fileSystem.mkdirSync(featureDir, { recursive: true });
1460
- this.configGenerator.generate(normalisedPath);
1461
- this.logger.success(`Generated feature: ${normalisedPath}`);
1580
+ ` : "";
1581
+ const errorImport = auth || method !== "ALL" ? 'import { HttpError } from "wasp/server";\n' : "";
1582
+ const imports = `${errorImport}import type { ${apiType} } from "wasp/server/api";`;
1583
+ return {
1584
+ imports,
1585
+ apiType,
1586
+ apiName,
1587
+ methodCheck,
1588
+ authCheck
1589
+ };
1462
1590
  }
1463
1591
  };
1464
1592
 
1465
- // src/generators/base/entity-generator.base.ts
1466
- var EntityGeneratorBase = class extends WaspGeneratorBase {
1467
- constructor(logger = new SignaleLogger4(), fileSystem = realFileSystem, featureDirectoryGenerator = new FeatureDirectoryGenerator(
1468
- logger,
1469
- fileSystem
1470
- )) {
1471
- super(fileSystem, logger);
1472
- this.logger = logger;
1473
- this.fileSystem = fileSystem;
1474
- this.featureDirectoryGenerator = featureDirectoryGenerator;
1475
- this.featureDirectoryGenerator = featureDirectoryGenerator;
1476
- }
1477
- getDefaultTemplatePath(templateName) {
1478
- return this.templateUtility.resolveTemplatePath(
1479
- templateName,
1480
- this.name,
1481
- import.meta.url
1482
- );
1483
- }
1484
- get name() {
1485
- return toKebabCase2(this.entityType.toString());
1593
+ // src/generators/api-namespace/api-namespace-generator.ts
1594
+ import { toCamelCase as toCamelCase3 } from "@ingenyus/swarm";
1595
+ import path7 from "path";
1596
+
1597
+ // src/generators/api-namespace/schema.ts
1598
+ import { z as z5 } from "zod";
1599
+ var schema4 = z5.object({
1600
+ feature: commonSchemas.feature,
1601
+ name: commonSchemas.name,
1602
+ path: commonSchemas.path,
1603
+ force: commonSchemas.force
1604
+ });
1605
+
1606
+ // src/generators/api-namespace/api-namespace-generator.ts
1607
+ var ApiNamespaceGenerator = class extends ComponentGeneratorBase {
1608
+ get componentType() {
1609
+ return CONFIG_TYPES.API_NAMESPACE;
1486
1610
  }
1487
- /**
1488
- * Validates that the feature config file exists in the target or ancestor directories
1489
- */
1490
- validateFeatureConfig(featurePath) {
1491
- const normalisedPath = normaliseFeaturePath(featurePath);
1492
- const segments = normalisedPath.split("/");
1493
- for (let i = segments.length; i > 0; i--) {
1494
- const pathSegments = segments.slice(0, i);
1495
- const currentPath = pathSegments.join("/");
1496
- const featureName = pathSegments[pathSegments.length - 1];
1497
- const featureDir = getFeatureDir(this.fileSystem, currentPath);
1498
- const configPath = path8.join(featureDir, `${featureName}.wasp.ts`);
1499
- if (this.fileSystem.existsSync(configPath)) {
1500
- return configPath;
1611
+ description = "Generates a Wasp API Namespace";
1612
+ schema = schema4;
1613
+ async generate(args) {
1614
+ const { name, path: namespacePath, feature } = args;
1615
+ const namespaceName = toCamelCase3(name);
1616
+ return this.handleGeneratorError(
1617
+ this.componentType,
1618
+ namespaceName,
1619
+ async () => {
1620
+ const configPath = this.validateFeatureConfig(feature);
1621
+ const { targetDirectory, importDirectory } = this.ensureTargetDirectory(
1622
+ feature,
1623
+ "middleware"
1624
+ );
1625
+ const targetFile = `${targetDirectory}/${namespaceName}.ts`;
1626
+ await this.generateMiddlewareFile(
1627
+ targetFile,
1628
+ namespaceName,
1629
+ args.force || false
1630
+ );
1631
+ await this.updateConfigFile(
1632
+ namespaceName,
1633
+ importDirectory,
1634
+ namespacePath,
1635
+ args,
1636
+ configPath
1637
+ );
1501
1638
  }
1502
- }
1503
- this.logger.error(
1504
- `Feature config file not found in '${normalisedPath}' or any ancestor directories`
1505
- );
1506
- this.logger.error(
1507
- `Expected to find a .wasp.ts config file in one of the feature directories`
1508
1639
  );
1509
- throw new Error("Feature config file not found");
1510
1640
  }
1511
- /**
1512
- * Checks if a config item already exists in the feature config
1513
- */
1514
- checkConfigExists(configPath, methodName, itemName, force) {
1515
- const configContent = this.fileSystem.readFileSync(configPath, "utf8");
1516
- const configExists = hasHelperMethodCall(
1517
- configContent,
1518
- methodName,
1519
- itemName
1641
+ async updateConfigFile(namespaceName, importDirectory, namespacePath, args, configFilePath) {
1642
+ const { force = false } = args;
1643
+ const importPath = path7.join(importDirectory, namespaceName);
1644
+ const definition = await this.getDefinition(
1645
+ namespaceName,
1646
+ importPath,
1647
+ namespacePath
1520
1648
  );
1521
- return this.checkExistence(
1522
- configExists,
1523
- `${methodName} config already exists in ${configPath}`,
1524
- force,
1525
- `${methodName} config already exists`
1649
+ this.updateConfigWithCheck(
1650
+ configFilePath,
1651
+ "addApiNamespace",
1652
+ namespaceName,
1653
+ definition,
1654
+ args.feature,
1655
+ force
1526
1656
  );
1527
1657
  }
1528
1658
  /**
1529
- * Updates the feature config with a new definition
1659
+ * Generates an apiNamespace definition for the feature configuration.
1530
1660
  */
1531
- updateFeatureConfig(featurePath, definition, configPath, configExists, methodName) {
1532
- this.configGenerator.update(featurePath, definition);
1533
- this.logger.success(
1534
- `${configExists ? "Updated" : "Added"} ${methodName} config in: ${configPath}`
1661
+ async getDefinition(namespaceName, middlewareImportPath, pathValue) {
1662
+ const templatePath = this.templateUtility.resolveTemplatePath(
1663
+ "config/api-namespace.eta",
1664
+ "api-namespace",
1665
+ import.meta.url
1535
1666
  );
1667
+ return this.templateUtility.processTemplate(templatePath, {
1668
+ namespaceName,
1669
+ middlewareImportPath,
1670
+ pathValue
1671
+ });
1536
1672
  }
1537
- /**
1538
- * Consolidated helper for updating config files with existence check
1539
- * This replaces the duplicated updateConfigFile pattern in concrete generators
1540
- */
1541
- updateConfigWithCheck(configPath, methodName, entityName, definition, featurePath, force) {
1542
- const configExists = this.checkConfigExists(
1543
- configPath,
1544
- methodName,
1545
- entityName,
1546
- force
1547
- );
1548
- if (!configExists || force) {
1549
- this.updateFeatureConfig(
1550
- featurePath,
1551
- definition,
1552
- configPath,
1553
- configExists,
1554
- methodName
1555
- );
1556
- }
1673
+ };
1674
+
1675
+ // src/generators/crud/crud-generator.ts
1676
+ import { getPlural as getPlural2, toCamelCase as toCamelCase4, toPascalCase as toPascalCase4 } from "@ingenyus/swarm";
1677
+
1678
+ // src/generators/crud/schema.ts
1679
+ import { SchemaManager as SchemaManager2, commandRegistry as commandRegistry4 } from "@ingenyus/swarm";
1680
+ import { z as z6 } from "zod";
1681
+ var validCrudOperations = Object.values(CRUD_OPERATIONS);
1682
+ var publicOperations = getCrudOperationsArray();
1683
+ var overrideOperations = getCrudOperationsArray();
1684
+ var excludeOperations = getCrudOperationsArray();
1685
+ var schema5 = z6.object({
1686
+ feature: commonSchemas.feature,
1687
+ name: commonSchemas.name,
1688
+ dataType: commonSchemas.dataType,
1689
+ public: publicOperations.meta({
1690
+ description: "Public CRUD operations (accessible without authentication)"
1691
+ }).register(commandRegistry4, {
1692
+ shortName: "b",
1693
+ examples: ["'get'", "'get' 'getAll'"],
1694
+ helpText: "Operations that can be accessed without authentication."
1695
+ }),
1696
+ override: overrideOperations.meta({ description: "Override existing CRUD operations" }).register(commandRegistry4, {
1697
+ shortName: "v",
1698
+ examples: ["'create'", "'create' 'update'"],
1699
+ helpText: "Operations that will have overriden implementations"
1700
+ }),
1701
+ exclude: excludeOperations.meta({ description: "Exclude specific CRUD operations from generation" }).register(commandRegistry4, {
1702
+ shortName: "x",
1703
+ examples: ["'delete'", "'delete' 'update'"],
1704
+ helpText: "Operations to exclude from generation"
1705
+ }),
1706
+ force: commonSchemas.force
1707
+ });
1708
+ function getCrudOperationsArray() {
1709
+ return z6.array(
1710
+ z6.string().transform((val) => {
1711
+ return SchemaManager2.findEnumValue(CRUD_OPERATIONS, val);
1712
+ }).pipe(
1713
+ z6.enum(CRUD_OPERATIONS, {
1714
+ message: `Must be one or more of: ${validCrudOperations.join(", ")}`
1715
+ })
1716
+ )
1717
+ ).optional();
1718
+ }
1719
+
1720
+ // src/generators/crud/crud-generator.ts
1721
+ var CRUD_OPERATIONS_LIST = [
1722
+ "get",
1723
+ "getAll",
1724
+ "create",
1725
+ "update",
1726
+ "delete"
1727
+ ];
1728
+ var CrudGenerator = class extends OperationGeneratorBase {
1729
+ get componentType() {
1730
+ return CONFIG_TYPES.CRUD;
1557
1731
  }
1558
- /**
1559
- * Gets the appropriate directory for a feature based on its path.
1560
- * @param fileSystem - The filesystem abstraction
1561
- * @param featurePath - The full feature path
1562
- * @param type - The type of file being generated
1563
- * @returns The target directory and import path
1564
- */
1565
- getFeatureTargetDir(fileSystem, featurePath, type) {
1566
- validateFeaturePath3(featurePath);
1567
- const normalisedPath = normaliseFeaturePath(featurePath);
1568
- const featureDir = getFeatureDir(fileSystem, normalisedPath);
1569
- const typeKey = type.toLowerCase();
1570
- const typeDirectory = TYPE_DIRECTORIES[typeKey];
1571
- const targetDirectory = path8.join(featureDir, typeDirectory);
1572
- const importDirectory = `@src/${normalisedPath}/${typeDirectory}`;
1573
- return { targetDirectory, importDirectory };
1732
+ description = "Generates a Wasp CRUD operation";
1733
+ schema = schema5;
1734
+ async generate(args) {
1735
+ const { dataType, feature } = args;
1736
+ const crudName = toCamelCase4(getPlural2(dataType));
1737
+ const crudType = toPascalCase4(crudName);
1738
+ return this.handleGeneratorError(this.componentType, crudName, async () => {
1739
+ const configPath = this.validateFeatureConfig(feature);
1740
+ const { targetDirectory } = this.ensureTargetDirectory(
1741
+ feature,
1742
+ this.componentType.toLowerCase()
1743
+ );
1744
+ if ((args.override?.length ?? 0) > 0) {
1745
+ const targetFile = `${targetDirectory}/${crudName}.ts`;
1746
+ const operations = await this.getOperationsCode(
1747
+ dataType,
1748
+ crudName,
1749
+ args
1750
+ );
1751
+ await this.generateCrudFile(
1752
+ targetFile,
1753
+ dataType,
1754
+ operations,
1755
+ crudType,
1756
+ args
1757
+ );
1758
+ }
1759
+ await this.updateConfigFile(
1760
+ feature,
1761
+ crudName,
1762
+ dataType,
1763
+ args,
1764
+ configPath
1765
+ );
1766
+ });
1574
1767
  }
1575
- /**
1576
- * Ensures a target directory exists and returns its path
1577
- */
1578
- ensureTargetDirectory(featurePath, type) {
1579
- const { targetDirectory, importDirectory } = this.getFeatureTargetDir(
1580
- this.fileSystem,
1581
- featurePath,
1582
- type
1768
+ async generateCrudFile(targetFile, dataType, operations, crudName, args) {
1769
+ const { override = [], force = false } = args;
1770
+ const model = await getEntityMetadata(dataType);
1771
+ const imports = this.generateCrudImports(
1772
+ model,
1773
+ dataType,
1774
+ crudName,
1775
+ override
1583
1776
  );
1584
- ensureDirectoryExists(this.fileSystem, targetDirectory);
1585
- return { targetDirectory, importDirectory };
1586
- }
1587
- /**
1588
- * Generate middleware file for API or API namespace
1589
- */
1590
- async generateMiddlewareFile(targetFile, name, force) {
1591
1777
  const replacements = {
1592
- name,
1593
- middlewareType: toCamelCase(this.entityType || "")
1778
+ imports,
1779
+ operations
1594
1780
  };
1595
1781
  await this.renderTemplateToFile(
1596
- "middleware.eta",
1782
+ "crud.eta",
1597
1783
  replacements,
1598
1784
  targetFile,
1599
- "Middleware file",
1785
+ "CRUD file",
1600
1786
  force
1601
1787
  );
1602
1788
  }
1603
- };
1604
-
1605
- // src/generators/base/operation-generator.base.ts
1606
- import {
1607
- capitalise,
1608
- getPlural,
1609
- handleFatalError as handleFatalError3,
1610
- toPascalCase as toPascalCase2
1611
- } from "@ingenyus/swarm";
1612
- var OperationGeneratorBase = class extends EntityGeneratorBase {
1613
- /**
1614
- * Gets the operation name based on operation type and model name.
1615
- */
1616
- getOperationName(operation, modelName, customName) {
1617
- if (customName) {
1618
- return customName;
1619
- }
1620
- switch (operation) {
1621
- case OPERATIONS.GETALL:
1622
- return `getAll${getPlural(modelName)}`;
1623
- case OPERATIONS.GETFILTERED:
1624
- return `getFiltered${getPlural(modelName)}`;
1625
- default:
1626
- return `${operation}${modelName}`;
1627
- }
1628
- }
1629
- /**
1630
- * Gets the template path for operation templates.
1631
- * This method resolves operation templates to the operation generator's directory
1632
- * instead of the current generator's directory.
1633
- */
1634
- getOperationTemplatePath(templateName) {
1635
- return this.templateUtility.resolveTemplatePath(
1636
- templateName,
1637
- "operation",
1638
- import.meta.url
1639
- );
1640
- }
1641
- /**
1642
- * Gets the TypeScript type name for an operation.
1643
- */
1644
- getOperationTypeName(operation, modelName) {
1645
- return toPascalCase2(this.getOperationName(operation, modelName));
1646
- }
1647
1789
  /**
1648
1790
  * Generates import statements for an operation.
1649
1791
  */
1650
- generateImports(model, modelName, operation) {
1792
+ generateCrudImports(model, modelName, crudName, operations) {
1651
1793
  const imports = [];
1652
- if (operation !== OPERATIONS.GETALL) {
1794
+ if (operations.some((operation) => operation !== "getAll")) {
1653
1795
  if (needsPrismaImport(model)) {
1654
1796
  imports.push('import { Prisma } from "@prisma/client";');
1655
1797
  }
1656
- imports.push(`import { ${modelName} } from "wasp/entities";`);
1798
+ imports.push(`import { type ${modelName} } from "wasp/entities";`);
1657
1799
  }
1658
1800
  imports.push('import { HttpError } from "wasp/server";');
1659
- imports.push(
1660
- `import type { ${this.getOperationTypeName(
1661
- operation,
1662
- modelName
1663
- )} } from "wasp/server/operations";`
1664
- );
1801
+ imports.push(`import { type ${crudName} } from "wasp/server/crud";`);
1665
1802
  return imports.join("\n");
1666
1803
  }
1804
+ async updateConfigFile(feature, crudName, dataType, args, configPath) {
1805
+ const operations = this.buildOperations(args);
1806
+ const definition = this.getDefinition(crudName, dataType, operations);
1807
+ this.updateConfigWithCheck(
1808
+ configPath,
1809
+ "addCrud",
1810
+ crudName,
1811
+ definition,
1812
+ feature,
1813
+ args.force || false
1814
+ );
1815
+ }
1816
+ buildOperations(args) {
1817
+ const {
1818
+ public: publicOps = [],
1819
+ override: overrideOps = [],
1820
+ exclude: excludeOps = []
1821
+ } = args;
1822
+ return CRUD_OPERATIONS_LIST.reduce(
1823
+ (acc, operation) => {
1824
+ if (excludeOps.includes(operation)) {
1825
+ return acc;
1826
+ }
1827
+ const operationConfig = {};
1828
+ if (publicOps.includes(operation)) {
1829
+ operationConfig.isPublic = true;
1830
+ }
1831
+ if (overrideOps.includes(operation)) {
1832
+ operationConfig.override = true;
1833
+ }
1834
+ acc[operation] = operationConfig;
1835
+ return acc;
1836
+ },
1837
+ {}
1838
+ );
1839
+ }
1667
1840
  /**
1668
- * Gets the operation type ("query" or "action") for a given operation.
1841
+ * Generates operation code for overridden CRUD operations and returns as a single string.
1669
1842
  */
1670
- getOperationType(operation) {
1671
- return operation === OPERATIONS.GETALL || operation === OPERATIONS.GET || operation === OPERATIONS.GETFILTERED ? "query" : "action";
1843
+ async getOperationsCode(dataType, crudName, args) {
1844
+ const { override = [], public: publicOps = [] } = args;
1845
+ if (override.length === 0) {
1846
+ return "";
1847
+ }
1848
+ const operationCodes = [];
1849
+ for (const operation of override) {
1850
+ const { operationCode } = await this.generateOperationComponents(
1851
+ dataType,
1852
+ operation,
1853
+ !publicOps.includes(operation),
1854
+ [dataType],
1855
+ true,
1856
+ toPascalCase4(crudName)
1857
+ );
1858
+ operationCodes.push(operationCode.replace(/^[\r\n]/, ""));
1859
+ }
1860
+ return operationCodes.join("");
1672
1861
  }
1673
1862
  /**
1674
- * Generates the operation components needed for file and config generation.
1863
+ * Generates a CRUD definition for the feature configuration.
1675
1864
  */
1676
- async generateOperationComponents(modelName, operation, auth = false, entities = [modelName], isCrudOverride = false, crudName = null, customName) {
1677
- const model = await getEntityMetadata(modelName);
1678
- const operationType = this.getOperationType(operation);
1679
- const operationName = this.getOperationName(
1680
- operation,
1681
- modelName,
1682
- customName
1683
- );
1684
- const operationCode = await this.generateOperationCode(
1685
- model,
1686
- operation,
1687
- auth,
1688
- isCrudOverride,
1689
- crudName
1865
+ getDefinition(crudName, dataType, operations) {
1866
+ const templatePath = this.templateUtility.resolveTemplatePath(
1867
+ "config/crud.eta",
1868
+ "crud",
1869
+ import.meta.url
1690
1870
  );
1691
- const configEntry = {
1692
- operationName,
1693
- entities,
1694
- authRequired: auth
1695
- };
1696
- return {
1697
- operationCode,
1698
- configEntry,
1699
- operationType,
1700
- operationName
1701
- };
1871
+ const operationsStr = JSON.stringify(operations, null, 2).replace(/"([^"]+)":/g, "$1:").slice(1, -1).split("\n").filter((line) => line.trim() !== "").map((line, index) => index === 0 ? line.trimStart() : " " + line).join("\n");
1872
+ return this.templateUtility.processTemplate(templatePath, {
1873
+ crudName: toPascalCase4(crudName),
1874
+ dataType,
1875
+ operations: operationsStr
1876
+ });
1702
1877
  }
1703
- /**
1704
- * Generates the code for an operation.
1705
- */
1706
- async generateOperationCode(model, operation, auth = false, isCrudOverride = false, crudName = null) {
1707
- const operationType = this.getOperationType(operation);
1708
- const templatePath = this.getOperationTemplatePath(`${operation}.eta`);
1709
- const allFieldNames = model.fields.map((f) => f.name);
1710
- const idFields = getIdFields(model);
1711
- const requiredFields = getRequiredFields(model);
1712
- const optionalFields = getOptionalFields(model);
1713
- const jsonFields = getJsonFields(model);
1714
- const pluralModelName = getPlural(model.name);
1715
- const pluralModelNameLower = pluralModelName.toLowerCase();
1716
- const modelNameLower = model.name.toLowerCase();
1717
- const operationName = this.getOperationName(operation, model.name);
1718
- const imports = isCrudOverride ? "" : this.generateImports(model, model.name, operation);
1719
- const jsonTypeHandling = generateJsonTypeHandling(jsonFields);
1720
- let typeParams = "";
1721
- switch (operation) {
1722
- case "create": {
1723
- const pickRequired = generatePickType(
1724
- model.name,
1725
- requiredFields,
1726
- allFieldNames
1727
- );
1728
- const partialOptional = generatePartialType(
1729
- generatePickType(model.name, optionalFields, allFieldNames)
1730
- );
1731
- typeParams = `<${generateIntersectionType(pickRequired, partialOptional)}>`;
1732
- break;
1733
- }
1734
- case "update": {
1735
- const pickId = generatePickType(model.name, idFields, allFieldNames);
1736
- const omitId = generateOmitType(model.name, idFields, allFieldNames);
1737
- const partialRest = generatePartialType(omitId);
1738
- typeParams = `<${generateIntersectionType(pickId, partialRest)}>`;
1739
- break;
1740
- }
1741
- case "delete":
1742
- case "get":
1743
- typeParams = `<${generatePickType(model.name, idFields, allFieldNames)}>`;
1744
- break;
1745
- case "getAll":
1746
- typeParams = `<void>`;
1747
- break;
1748
- case "getFiltered":
1749
- typeParams = `<${generatePartialType(model.name)}>`;
1750
- break;
1878
+ };
1879
+
1880
+ // src/generators/job/job-generator.ts
1881
+ import { capitalise as capitalise2, toCamelCase as toCamelCase5 } from "@ingenyus/swarm";
1882
+
1883
+ // src/generators/job/schema.ts
1884
+ import { commandRegistry as commandRegistry5 } from "@ingenyus/swarm";
1885
+ import { z as z7 } from "zod";
1886
+ var cronSchema = z7.string().optional().refine(
1887
+ (val) => {
1888
+ if (!val) return true;
1889
+ const parts = val.trim().split(/\s+/);
1890
+ if (parts.length !== 5) return false;
1891
+ const [minute, hour, day, month, weekday] = parts;
1892
+ const validateCronField = (field, min, max) => {
1893
+ if (field === "*") return true;
1894
+ const rangeRegex = /^(\d+)(-(\d+))?(,(\d+)(-(\d+))?)*(\/(\d+))?$/;
1895
+ if (!rangeRegex.test(field)) return false;
1896
+ const items = field.split(",");
1897
+ for (const item of items) {
1898
+ if (item.includes("/")) {
1899
+ const [base, step] = item.split("/");
1900
+ const stepNum = parseInt(step, 10);
1901
+ if (isNaN(stepNum) || stepNum <= 0) return false;
1902
+ if (base === "*") continue;
1903
+ const baseNum = parseInt(base, 10);
1904
+ if (isNaN(baseNum) || baseNum < min || baseNum > max) return false;
1905
+ } else if (item.includes("-")) {
1906
+ const [start, end] = item.split("-");
1907
+ const startNum = parseInt(start, 10);
1908
+ const endNum = parseInt(end, 10);
1909
+ if (isNaN(startNum) || isNaN(endNum) || startNum < min || endNum > max || startNum > endNum)
1910
+ return false;
1911
+ } else {
1912
+ const num = parseInt(item, 10);
1913
+ if (isNaN(num) || num < min || num > max) return false;
1914
+ }
1915
+ }
1916
+ return true;
1917
+ };
1918
+ return validateCronField(minute, 0, 59) && validateCronField(hour, 0, 23) && validateCronField(day, 1, 31) && validateCronField(month, 1, 12) && validateCronField(weekday, 0, 6);
1919
+ },
1920
+ {
1921
+ message: 'Cron expression must be a valid five-field format: (minute hour day month weekday), e.g. "0 9 * * *"'
1922
+ }
1923
+ ).meta({ description: "Cron schedule expression for the job" }).register(commandRegistry5, {
1924
+ shortName: "c",
1925
+ examples: ["0 9 * * *", "*/15 * * * *", "0 0 1 * *"],
1926
+ helpText: "Five-field cron expression: minute hour day month weekday"
1927
+ });
1928
+ var argsSchema = z7.string().optional().refine(
1929
+ (val) => {
1930
+ if (!val) return true;
1931
+ try {
1932
+ const parsed = JSON.parse(val);
1933
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
1934
+ } catch {
1935
+ return false;
1751
1936
  }
1752
- const authCheck = auth ? ` if (!context.user) {
1753
- throw new HttpError(401);
1937
+ },
1938
+ {
1939
+ message: "Args must be a valid JSON object string"
1754
1940
  }
1941
+ ).meta({ description: "Arguments to pass to the job function when executed" }).register(commandRegistry5, {
1942
+ shortName: "a",
1943
+ examples: ['{"userId": 123}', '{"type": "cleanup", "batchSize": 100}'],
1944
+ helpText: "JSON object string that will be passed to the job function"
1945
+ });
1946
+ var schema6 = z7.object({
1947
+ feature: commonSchemas.feature,
1948
+ name: commonSchemas.name,
1949
+ entities: commonSchemas.entities,
1950
+ cron: cronSchema,
1951
+ args: argsSchema,
1952
+ force: commonSchemas.force
1953
+ });
1755
1954
 
1756
- ` : "";
1757
- let typeAnnotation = "";
1758
- let satisfiesType = "";
1759
- if (isCrudOverride && crudName) {
1760
- const opCap = capitalise(operation);
1761
- if (operationType === "action") {
1762
- typeAnnotation = `: ${crudName}.${opCap}Action${typeParams}`;
1763
- } else {
1764
- typeAnnotation = "";
1765
- }
1766
- if (operationType === "query") {
1767
- satisfiesType = `satisfies ${crudName}.${opCap}Query${typeParams}`;
1768
- } else {
1769
- satisfiesType = "";
1770
- }
1771
- } else {
1772
- if (operationType === "action") {
1773
- typeAnnotation = `: ${this.getOperationTypeName(operation, model.name)}${typeParams}`;
1774
- } else {
1775
- typeAnnotation = "";
1776
- }
1777
- if (operationType === "query") {
1778
- satisfiesType = `satisfies ${this.getOperationTypeName(operation, model.name)}${typeParams}`;
1779
- } else {
1780
- satisfiesType = "";
1781
- }
1955
+ // src/generators/job/job-generator.ts
1956
+ var JobGenerator = class extends ComponentGeneratorBase {
1957
+ get componentType() {
1958
+ return CONFIG_TYPES.JOB;
1959
+ }
1960
+ description = "Generates a Wasp Job";
1961
+ schema = schema6;
1962
+ async generate(args) {
1963
+ const jobName = toCamelCase5(args.name);
1964
+ return this.handleGeneratorError(this.componentType, jobName, async () => {
1965
+ const configPath = this.validateFeatureConfig(args.feature);
1966
+ const { targetDirectory } = this.ensureTargetDirectory(
1967
+ args.feature,
1968
+ this.componentType.toLowerCase()
1969
+ );
1970
+ const targetFile = `${targetDirectory}/${jobName}.ts`;
1971
+ await this.generateJobFile(targetFile, jobName, args);
1972
+ this.updateConfigFile(args.feature, jobName, args, configPath);
1973
+ });
1974
+ }
1975
+ async generateJobFile(targetFile, jobName, args) {
1976
+ const jobType = capitalise2(jobName);
1977
+ const entities = args.entities ?? [];
1978
+ let imports = `import type { ${jobType} } from 'wasp/server/jobs';
1979
+ `;
1980
+ if (entities.length > 0) {
1981
+ imports += `import { ${entities.join(", ")} } from 'wasp/entities';
1982
+ `;
1782
1983
  }
1783
- const isCompositeKey = idFields.length > 1;
1784
- const compositeKeyName = isCompositeKey ? idFields.join("_") : "";
1785
- const idFieldParams = isCompositeKey ? idFields.join(", ") : idFields[0];
1786
- const whereClause = isCompositeKey ? `${compositeKeyName}: { ${idFields.map((f) => `${f}`).join(", ")} }` : idFields[0];
1787
1984
  const replacements = {
1788
- operationName,
1789
- modelName: model.name,
1790
- authCheck,
1791
1985
  imports,
1792
- idField: idFields[0],
1793
- idFieldParams,
1794
- whereClause,
1795
- isCompositeKey: String(isCompositeKey),
1796
- compositeKeyName,
1797
- jsonTypeHandling,
1798
- typeAnnotation,
1799
- satisfiesType,
1800
- modelNameLower,
1801
- pluralModelNameLower
1986
+ jobType,
1987
+ jobName
1802
1988
  };
1803
- return this.templateUtility.processTemplate(templatePath, replacements);
1804
- }
1805
- /**
1806
- * Generates an operation file for a given operation.
1807
- */
1808
- generateOperationFile(operationsDir, operationName, operationCode, force = false) {
1809
- const operationFile = `${operationsDir}/${operationName}.ts`;
1810
- const fileExists = this.checkFileExists(
1811
- operationFile,
1812
- force,
1813
- "Operation file"
1989
+ await this.renderTemplateToFile(
1990
+ "job.eta",
1991
+ replacements,
1992
+ targetFile,
1993
+ "job worker",
1994
+ args.force || false
1814
1995
  );
1815
- this.writeFile(operationFile, operationCode, "operation file", fileExists);
1816
1996
  }
1817
- /**
1818
- * Copies a directory of operation templates to the target feature directory.
1819
- * @param templateDir - The source template directory
1820
- * @param targetDir - The target feature directory
1821
- */
1822
- copyOperationTemplates(templateDir, targetDir) {
1823
- copyDirectory(this.fileSystem, templateDir, targetDir);
1824
- this.logger.debug(
1825
- `Copied operation templates from ${templateDir} to ${targetDir}`
1997
+ updateConfigFile(featurePath, jobName, args, configPath) {
1998
+ const {
1999
+ entities = [],
2000
+ cron = "0 0 * * *",
2001
+ args: executionArgs = "{}",
2002
+ force = false
2003
+ } = args;
2004
+ const definition = this.getDefinition(
2005
+ jobName,
2006
+ entities,
2007
+ cron,
2008
+ executionArgs
2009
+ );
2010
+ this.updateConfigWithCheck(
2011
+ configPath,
2012
+ "job",
2013
+ jobName,
2014
+ definition,
2015
+ featurePath,
2016
+ force
1826
2017
  );
1827
2018
  }
1828
2019
  /**
1829
- * Generates an operation definition for the feature configuration.
2020
+ * Generates a job definition for the feature configuration.
1830
2021
  */
1831
- getDefinition(operationName, featurePath, entities, operationType, importPath, auth = false) {
1832
- if (!OPERATION_TYPES.includes(operationType)) {
1833
- handleFatalError3(`Unknown operation type: ${operationType}`);
1834
- }
1835
- const directory = TYPE_DIRECTORIES[operationType];
1836
- const featureDir = getFeatureImportPath(featurePath);
1837
- const templatePath = this.templateUtility.resolveTemplatePath(
1838
- "operation.eta",
1839
- "config",
1840
- import.meta.url
1841
- );
2022
+ getDefinition(jobName, entities, cron, args) {
2023
+ const templatePath = this.getDefaultTemplatePath("config/job.eta");
1842
2024
  return this.templateUtility.processTemplate(templatePath, {
1843
- operationType: capitalise(operationType),
1844
- operationName,
1845
- featureDir,
1846
- directory,
2025
+ jobName,
1847
2026
  entities: entities.map((e) => `"${e}"`).join(", "),
1848
- importPath,
1849
- auth: String(auth)
2027
+ cron,
2028
+ args
1850
2029
  });
1851
2030
  }
1852
2031
  };
1853
2032
 
1854
- // src/generators/action/schema.ts
1855
- import { extend as extend2 } from "@ingenyus/swarm";
1856
- import { z as z3 } from "zod";
1857
- var validActions = Object.values(ACTION_OPERATIONS);
1858
- var actionSchema = extend2(
1859
- z3.string().min(1, "Action type is required").refine(getTypedValueValidator(validActions), {
1860
- message: `Invalid action. Must be one of: ${validActions.join(", ")}`
1861
- }).transform(getTypedValueTransformer(validActions)),
1862
- {
1863
- description: "The action operation to generate",
1864
- friendlyName: "Action Operation",
1865
- shortName: "o",
1866
- examples: validActions,
1867
- helpText: "Available actions: create, update, delete"
1868
- }
1869
- );
1870
- var dataTypeSchema = extend2(z3.string().min(1, "Data type is required"), {
1871
- description: "The data type/model name for this action",
1872
- friendlyName: "Data Type",
1873
- shortName: "d",
1874
- examples: ["User", "Product", "Task"],
1875
- helpText: "The Wasp entity or model name this action will work with"
2033
+ // src/generators/query/schema.ts
2034
+ import { SchemaManager as SchemaManager3, commandRegistry as commandRegistry6 } from "@ingenyus/swarm";
2035
+ import { z as z8 } from "zod";
2036
+ var validQueries = Object.values(QUERY_OPERATIONS);
2037
+ var querySchema = z8.string().min(1, "Query type is required").transform((val) => SchemaManager3.findEnumValue(QUERY_OPERATIONS, val)).pipe(
2038
+ z8.enum(QUERY_OPERATIONS, {
2039
+ message: `Invalid query. Must be one of: ${validQueries.join(", ")}`
2040
+ })
2041
+ ).meta({ description: "The query operation to generate" }).register(commandRegistry6, {
2042
+ shortName: "o",
2043
+ examples: validQueries,
2044
+ helpText: `Available queries: ${validQueries.join(", ")}`
1876
2045
  });
1877
- var schema2 = z3.object({
2046
+ var schema7 = z8.object({
1878
2047
  feature: commonSchemas.feature,
1879
- operation: actionSchema,
1880
- dataType: dataTypeSchema,
1881
- name: extend2(commonSchemas.name.optional(), commonSchemas.name._metadata),
2048
+ operation: querySchema,
2049
+ dataType: commonSchemas.dataType,
2050
+ name: commonSchemas.name.optional().meta({
2051
+ ...commonSchemas.name.meta() ?? {},
2052
+ description: `${commonSchemas.name.meta()?.description ?? ""} (optional)`
2053
+ }).register(commandRegistry6, commandRegistry6.get(commonSchemas.name) ?? {}),
1882
2054
  entities: commonSchemas.entities,
1883
2055
  force: commonSchemas.force,
1884
2056
  auth: commonSchemas.auth
1885
2057
  });
1886
2058
 
1887
- // src/generators/action/action-generator.ts
1888
- var ActionGenerator = class extends OperationGeneratorBase {
1889
- get entityType() {
1890
- return CONFIG_TYPES.ACTION;
2059
+ // src/generators/query/query-generator.ts
2060
+ var QueryGenerator = class extends OperationGeneratorBase {
2061
+ get componentType() {
2062
+ return CONFIG_TYPES.QUERY;
1891
2063
  }
1892
- description = "Generate actions (mutations) for Wasp applications";
1893
- schema = schema2;
1894
- async generate(flags) {
1895
- const { dataType, feature, name } = flags;
1896
- const operation = flags.operation;
1897
- const operationType = "action";
1898
- const entities = flags.entities ? Array.isArray(flags.entities) ? flags.entities : flags.entities.split(",").map((e) => e.trim()).filter(Boolean) : [];
2064
+ description = "Generates a Wasp Query";
2065
+ schema = schema7;
2066
+ async generate(args) {
2067
+ const { dataType, feature, name } = args;
2068
+ const operation = args.operation;
2069
+ const operationType = "query";
2070
+ const entities = args.entities ?? [];
1899
2071
  if (dataType && !entities.includes(dataType)) {
1900
2072
  entities.unshift(dataType);
1901
2073
  }
1902
2074
  const { operationCode, operationName } = await this.generateOperationComponents(
1903
2075
  dataType,
1904
2076
  operation,
1905
- flags.auth,
2077
+ args.auth,
1906
2078
  entities,
1907
2079
  false,
1908
2080
  null,
1909
2081
  name
1910
2082
  );
1911
- return this.handleGeneratorError(
1912
- this.entityType,
1913
- operationName,
1914
- async () => {
1915
- const configPath = this.validateFeatureConfig(feature);
1916
- const { targetDirectory: operationsDir, importDirectory } = this.ensureTargetDirectory(feature, operationType);
1917
- const importPath = `${importDirectory}/${operationName}`;
1918
- this.generateOperationFile(
1919
- operationsDir,
1920
- operationName,
1921
- operationCode,
1922
- flags.force || false
1923
- );
1924
- const definition = this.getDefinition(
1925
- operationName,
1926
- feature,
1927
- entities,
1928
- "action",
1929
- importPath,
1930
- flags.auth
1931
- );
1932
- this.updateConfigWithCheck(
1933
- configPath,
1934
- "addAction",
1935
- operationName,
1936
- definition,
1937
- feature,
1938
- flags.force || false
1939
- );
1940
- }
1941
- );
1942
- }
1943
- };
1944
-
1945
- // src/generators/api/api-generator.ts
1946
- import { toCamelCase as toCamelCase2, toPascalCase as toPascalCase3 } from "@ingenyus/swarm";
1947
-
1948
- // src/generators/api/schema.ts
1949
- import { extend as extend3 } from "@ingenyus/swarm";
1950
- import { z as z4 } from "zod";
1951
- var validHttpMethods = Object.values(HTTP_METHODS);
1952
- var schema3 = z4.object({
1953
- method: extend3(
1954
- z4.string().min(1, "HTTP method is required").refine(getTypedValueValidator(validHttpMethods), {
1955
- message: `Invalid HTTP method. Must be one of: ${validHttpMethods.join(", ")}`
1956
- }).transform(getTypedValueTransformer(validHttpMethods)),
1957
- {
1958
- description: "The HTTP method used for this API endpoint",
1959
- friendlyName: "HTTP Method",
1960
- shortName: "m",
1961
- examples: validHttpMethods
1962
- }
1963
- ),
1964
- feature: commonSchemas.feature,
1965
- name: commonSchemas.name,
1966
- path: commonSchemas.path,
1967
- entities: commonSchemas.entities,
1968
- auth: commonSchemas.auth,
1969
- force: commonSchemas.force,
1970
- customMiddleware: z4.boolean().optional()
1971
- });
1972
-
1973
- // src/generators/api/api-generator.ts
1974
- var ApiGenerator = class extends EntityGeneratorBase {
1975
- get entityType() {
1976
- return CONFIG_TYPES.API;
1977
- }
1978
- description = "Generate API endpoints for Wasp applications";
1979
- schema = schema3;
1980
- async generate(flags) {
1981
- const apiName = toCamelCase2(flags?.name);
1982
- return this.handleGeneratorError(this.entityType, apiName, async () => {
1983
- const configPath = this.validateFeatureConfig(flags.feature);
1984
- const {
1985
- targetDirectory: apiTargetDirectory,
1986
- importDirectory: apiImportDirectory
1987
- } = this.ensureTargetDirectory(flags.feature, this.name);
1988
- const fileName = `${apiName}.ts`;
1989
- const targetFile = `${apiTargetDirectory}/${fileName}`;
1990
- await this.generateApiFile(targetFile, apiName, flags);
1991
- if (flags.customMiddleware) {
1992
- const { targetDirectory: middlewareTargetDirectory } = this.ensureTargetDirectory(flags.feature, "middleware");
1993
- const middlewareFile = `${middlewareTargetDirectory}/${apiName}.ts`;
1994
- this.generateMiddlewareFile(
1995
- middlewareFile,
1996
- apiName,
1997
- flags.force || false
1998
- );
1999
- }
2000
- await this.updateConfigFile(
2001
- flags.feature,
2002
- apiName,
2003
- fileName,
2004
- apiImportDirectory,
2005
- flags,
2006
- configPath
2007
- );
2008
- });
2009
- }
2010
- async generateApiFile(targetFile, apiName, { method, auth = false, force = false }) {
2011
- const replacements = this.buildTemplateData(apiName, method, auth);
2012
- await this.renderTemplateToFile(
2013
- "api.eta",
2014
- replacements,
2015
- targetFile,
2016
- "API endpoint file",
2017
- force
2018
- );
2019
- }
2020
- async updateConfigFile(featurePath, apiName, apiFile, importDirectory, flags, configFilePath) {
2021
- const { force = false, entities, method, route, auth } = flags;
2022
- const importPath = this.path.join(importDirectory, apiFile);
2023
- const definition = await this.getConfigDefinition(
2024
- apiName,
2025
- featurePath,
2026
- Array.isArray(entities) ? entities : entities ? [entities] : [],
2027
- method,
2028
- route,
2029
- apiFile,
2030
- auth,
2031
- importPath,
2032
- flags.customMiddleware || false
2033
- );
2034
- this.updateConfigWithCheck(
2035
- configFilePath,
2036
- "addApi",
2037
- apiName,
2038
- definition,
2039
- featurePath,
2040
- force
2083
+ return this.handleGeneratorError(
2084
+ this.componentType,
2085
+ operationName,
2086
+ async () => {
2087
+ const configPath = this.validateFeatureConfig(feature);
2088
+ const { targetDirectory: operationsDir, importDirectory } = this.ensureTargetDirectory(feature, operationType);
2089
+ const importPath = `${importDirectory}/${operationName}`;
2090
+ this.generateOperationFile(
2091
+ operationsDir,
2092
+ operationName,
2093
+ operationCode,
2094
+ args.force || false
2095
+ );
2096
+ const definition = this.getDefinition(
2097
+ operationName,
2098
+ feature,
2099
+ entities,
2100
+ "query",
2101
+ importPath,
2102
+ args.auth
2103
+ );
2104
+ this.updateConfigWithCheck(
2105
+ configPath,
2106
+ "addQuery",
2107
+ operationName,
2108
+ definition,
2109
+ feature,
2110
+ args.force || false
2111
+ );
2112
+ }
2041
2113
  );
2042
2114
  }
2043
- async getConfigDefinition(apiName, featurePath, entities, method, route, apiFile, auth = false, importPath, customMiddleware = false) {
2044
- const featureDir = getFeatureImportPath(featurePath);
2045
- const configTemplatePath = await this.getTemplatePath("config/api.eta");
2046
- return this.templateUtility.processTemplate(configTemplatePath, {
2047
- apiName,
2048
- featureDir,
2049
- entities: entities.map((e) => `"${e}"`).join(", "),
2050
- method,
2051
- route,
2052
- apiFile,
2053
- auth: String(auth),
2054
- importPath,
2055
- customMiddleware: String(customMiddleware)
2056
- });
2057
- }
2058
- buildTemplateData(apiName, method, auth) {
2059
- const apiType = toPascalCase3(apiName);
2060
- const authCheck = auth ? ` if (!context.user) {
2061
- throw new HttpError(401);
2062
- }
2063
-
2064
- ` : "";
2065
- const methodCheck = method !== "ALL" ? ` if (req.method !== '${method}') {
2066
- throw new HttpError(405);
2067
- }
2068
-
2069
- ` : "";
2070
- const errorImport = auth || method !== "ALL" ? 'import { HttpError } from "wasp/server";\n' : "";
2071
- const imports = `${errorImport}import type { ${apiType} } from "wasp/server/api";`;
2072
- return {
2073
- imports,
2074
- apiType,
2075
- apiName,
2076
- methodCheck,
2077
- authCheck
2078
- };
2079
- }
2080
2115
  };
2081
2116
 
2082
- // src/generators/api-namespace/api-namespace-generator.ts
2083
- import { toCamelCase as toCamelCase3 } from "@ingenyus/swarm";
2084
- import path9 from "path";
2117
+ // src/generators/route/route-generator.ts
2118
+ import {
2119
+ formatDisplayName,
2120
+ toCamelCase as toCamelCase6,
2121
+ toPascalCase as toPascalCase5
2122
+ } from "@ingenyus/swarm";
2085
2123
 
2086
- // src/generators/api-namespace/schema.ts
2087
- import { z as z5 } from "zod";
2088
- var schema4 = z5.object({
2124
+ // src/generators/route/schema.ts
2125
+ import { z as z9 } from "zod";
2126
+ var schema8 = z9.object({
2089
2127
  feature: commonSchemas.feature,
2090
2128
  name: commonSchemas.name,
2091
2129
  path: commonSchemas.path,
2130
+ auth: commonSchemas.auth,
2092
2131
  force: commonSchemas.force
2093
2132
  });
2094
2133
 
2095
- // src/generators/api-namespace/api-namespace-generator.ts
2096
- var ApiNamespaceGenerator = class extends EntityGeneratorBase {
2097
- get entityType() {
2098
- return CONFIG_TYPES.API_NAMESPACE;
2134
+ // src/generators/route/route-generator.ts
2135
+ var RouteGenerator = class extends ComponentGeneratorBase {
2136
+ get componentType() {
2137
+ return CONFIG_TYPES.ROUTE;
2099
2138
  }
2100
- description = "Generate API namespaces for Wasp applications";
2101
- schema = schema4;
2102
- async generate(flags) {
2103
- const { name, path: apiPath, feature } = flags;
2104
- const namespaceName = toCamelCase3(name);
2139
+ description = "Generates a Wasp Page and Route";
2140
+ schema = schema8;
2141
+ async generate(args) {
2142
+ const { path: routePath, name, feature } = args;
2143
+ const routeName = toCamelCase6(name || getRouteNameFromPath(routePath));
2144
+ const componentName = toPascalCase5(routeName);
2145
+ const fileName = `${componentName}.tsx`;
2105
2146
  return this.handleGeneratorError(
2106
- this.entityType,
2107
- namespaceName,
2147
+ this.componentType,
2148
+ routeName,
2108
2149
  async () => {
2109
2150
  const configPath = this.validateFeatureConfig(feature);
2110
- const { targetDirectory, importDirectory } = this.ensureTargetDirectory(
2111
- feature,
2112
- "middleware"
2113
- );
2114
- const targetFile = `${targetDirectory}/${namespaceName}.ts`;
2115
- await this.generateMiddlewareFile(
2116
- targetFile,
2117
- namespaceName,
2118
- flags.force || false
2119
- );
2120
- await this.updateConfigFile(
2121
- feature,
2122
- namespaceName,
2123
- importDirectory,
2124
- apiPath,
2125
- flags,
2126
- configPath
2127
- );
2151
+ const { targetDirectory } = this.ensureTargetDirectory(feature, "page");
2152
+ const targetFile = `${targetDirectory}/${fileName}`;
2153
+ await this.generatePageFile(targetFile, componentName, args);
2154
+ this.updateConfigFile(feature, routeName, routePath, args, configPath);
2128
2155
  }
2129
2156
  );
2130
2157
  }
2131
- async updateConfigFile(feature, namespaceName, importDirectory, apiPath, flags, configFilePath) {
2132
- const { force = false } = flags;
2133
- const importPath = path9.join(importDirectory, namespaceName);
2134
- const definition = await this.getDefinition(
2135
- namespaceName,
2136
- importPath,
2137
- apiPath
2158
+ async generatePageFile(targetFile, componentName, args) {
2159
+ const templatePath = "files/client/page.eta";
2160
+ const replacements = {
2161
+ componentName,
2162
+ displayName: formatDisplayName(componentName)
2163
+ };
2164
+ await this.renderTemplateToFile(
2165
+ "page.eta",
2166
+ replacements,
2167
+ targetFile,
2168
+ "Page file",
2169
+ args.force || false
2170
+ );
2171
+ }
2172
+ updateConfigFile(featurePath, routeName, routePath, args, configPath) {
2173
+ const definition = this.getDefinition(
2174
+ routeName,
2175
+ routePath,
2176
+ featurePath,
2177
+ args.auth
2138
2178
  );
2139
2179
  this.updateConfigWithCheck(
2140
- configFilePath,
2141
- "addApiNamespace",
2142
- namespaceName,
2180
+ configPath,
2181
+ "addRoute",
2182
+ routeName,
2143
2183
  definition,
2144
- feature,
2145
- force
2184
+ featurePath,
2185
+ args.force || false
2146
2186
  );
2147
2187
  }
2148
2188
  /**
2149
- * Generates an apiNamespace definition for the feature configuration.
2189
+ * Generates a route definition for the feature configuration.
2150
2190
  */
2151
- async getDefinition(namespaceName, middlewareImportPath, pathValue) {
2152
- const templatePath = this.templateUtility.resolveTemplatePath(
2153
- "config/api-namespace.eta",
2154
- "api-namespace",
2155
- import.meta.url
2156
- );
2191
+ getDefinition(routeName, routePath, featurePath, auth = false) {
2192
+ const templatePath = this.getDefaultTemplatePath("config/route.eta");
2157
2193
  return this.templateUtility.processTemplate(templatePath, {
2158
- namespaceName,
2159
- middlewareImportPath,
2160
- pathValue
2194
+ featureName: featurePath.split("/").pop() || featurePath,
2195
+ routeName,
2196
+ routePath,
2197
+ auth: String(auth)
2161
2198
  });
2162
2199
  }
2163
2200
  };
2164
2201
 
2165
- // src/generators/crud/crud-generator.ts
2166
- import { getPlural as getPlural2, toCamelCase as toCamelCase4, toPascalCase as toPascalCase4 } from "@ingenyus/swarm";
2167
-
2168
- // src/generators/crud/schema.ts
2169
- import { extend as extend4 } from "@ingenyus/swarm";
2170
- import { z as z6 } from "zod";
2171
- var validCrudOperations = Object.values(CRUD_OPERATIONS);
2172
- var publicOperations = getCrudOperationsArray();
2173
- var overrideOperations = getCrudOperationsArray();
2174
- var excludeOperations = getCrudOperationsArray();
2175
- var schema5 = z6.object({
2176
- feature: commonSchemas.feature,
2177
- name: commonSchemas.name,
2178
- public: extend4(publicOperations, {
2179
- description: "Public CRUD operations (accessible without authentication)",
2180
- friendlyName: "Public Operations",
2181
- shortName: "b",
2182
- examples: ["get,getAll", "create,update"],
2183
- helpText: "Operations that can be accessed without authentication"
2184
- }),
2185
- override: extend4(overrideOperations, {
2186
- description: "Override existing CRUD operations",
2187
- friendlyName: "Override Operations",
2188
- shortName: "o",
2189
- examples: ["create,update"],
2190
- helpText: "Operations to override if they already exist"
2191
- }),
2192
- exclude: extend4(excludeOperations, {
2193
- description: "Exclude specific CRUD operations from generation",
2194
- friendlyName: "Exclude Operations",
2195
- shortName: "x",
2196
- examples: ["delete", "update,delete"],
2197
- helpText: "Operations to exclude from generation"
2198
- }),
2199
- force: commonSchemas.force
2200
- });
2201
- function getCrudOperationsArray() {
2202
- return z6.string().optional().refine(getTypedArrayValidator(validCrudOperations), {
2203
- message: `Must be one or more of: ${validCrudOperations.join(", ")}`
2204
- }).transform(getTypedArrayTransformer(validCrudOperations));
2205
- }
2202
+ // src/plugins/wasp.ts
2203
+ var wasp = {
2204
+ name: PLUGIN_NAME,
2205
+ generators: [
2206
+ new ActionGenerator(),
2207
+ new ApiGenerator(),
2208
+ new ApiNamespaceGenerator(),
2209
+ new CrudGenerator(),
2210
+ new FeatureGenerator(),
2211
+ new JobGenerator(),
2212
+ new QueryGenerator(),
2213
+ new RouteGenerator()
2214
+ ]
2215
+ };
2206
2216
 
2207
- // src/generators/crud/crud-generator.ts
2208
- var CRUD_OPERATIONS_LIST = [
2209
- "get",
2210
- "getAll",
2211
- "create",
2212
- "update",
2213
- "delete"
2214
- ];
2215
- var CrudGenerator = class extends OperationGeneratorBase {
2216
- get entityType() {
2217
- return CONFIG_TYPES.CRUD;
2217
+ // src/wasp-config/app.ts
2218
+ import fs3 from "fs";
2219
+ import path8 from "path";
2220
+ import {
2221
+ App as WaspApp
2222
+ } from "wasp-config";
2223
+ var App = class _App extends WaspApp {
2224
+ constructor(name, config) {
2225
+ super(name, config);
2218
2226
  }
2219
- description = "Generate CRUD operations for Wasp applications";
2220
- schema = schema5;
2221
- async generate(flags) {
2222
- const { dataType, feature } = flags;
2223
- const crudName = toCamelCase4(getPlural2(dataType));
2224
- return this.handleGeneratorError(this.entityType, crudName, async () => {
2225
- const configPath = this.validateFeatureConfig(feature);
2226
- const { targetDirectory } = this.ensureTargetDirectory(
2227
- feature,
2228
- this.entityType.toLowerCase()
2229
- );
2230
- const targetFile = `${targetDirectory}/${crudName}.ts`;
2231
- const fileExists = this.fileSystem.existsSync(targetFile);
2232
- if (!fileExists || flags.force) {
2233
- const operations = await this.getOperationsCode(
2234
- dataType,
2235
- crudName,
2236
- flags
2237
- );
2238
- await this.generateCrudFile(
2239
- targetFile,
2240
- crudName,
2241
- dataType,
2242
- operations,
2243
- flags.force || false
2244
- );
2245
- }
2246
- await this.updateConfigFile(
2247
- feature,
2248
- crudName,
2249
- dataType,
2250
- flags,
2251
- configPath
2252
- );
2253
- });
2227
+ /**
2228
+ * Static factory method that creates and initializes Swarm with configuration
2229
+ * dynamically loaded from feature directories
2230
+ *
2231
+ * @param name The name of the application
2232
+ * @param config The base configuration for the application
2233
+ * @returns An initialized Swarm instance
2234
+ */
2235
+ static async create(name, config) {
2236
+ const app = new _App(name, config);
2237
+ await app.configureFeatures();
2238
+ return app;
2239
+ }
2240
+ // Chainable configuration methods
2241
+ auth(authConfig) {
2242
+ super.auth(authConfig);
2243
+ return this;
2244
+ }
2245
+ client(clientConfig) {
2246
+ super.client(clientConfig);
2247
+ return this;
2248
+ }
2249
+ db(dbConfig) {
2250
+ super.db(dbConfig);
2251
+ return this;
2252
+ }
2253
+ emailSender(emailSenderConfig) {
2254
+ super.emailSender(emailSenderConfig);
2255
+ return this;
2256
+ }
2257
+ job(name, jobConfig) {
2258
+ super.job(name, jobConfig);
2259
+ return this;
2260
+ }
2261
+ query(name, queryConfig) {
2262
+ super.query(name, queryConfig);
2263
+ return this;
2254
2264
  }
2255
- async generateCrudFile(targetFile, crudName, dataType, operations, force) {
2256
- const imports = `import { type ${toPascalCase4(dataType)} } from "wasp/entities";
2257
- import { HttpError } from "wasp/server";
2258
- import { type ${toPascalCase4(crudName)} } from "wasp/server/crud";`;
2259
- const replacements = {
2260
- imports,
2261
- operations
2262
- };
2263
- await this.renderTemplateToFile(
2264
- "crud.eta",
2265
- replacements,
2266
- targetFile,
2267
- "CRUD file",
2268
- force
2269
- );
2265
+ route(name, routeConfig) {
2266
+ super.route(name, routeConfig);
2267
+ return this;
2270
2268
  }
2271
- async updateConfigFile(feature, crudName, dataType, flags, configPath) {
2272
- const operations = this.buildOperations(flags);
2273
- const definition = await this.getDefinition(crudName, dataType, operations);
2274
- this.updateConfigWithCheck(
2275
- configPath,
2276
- "addCrud",
2277
- crudName,
2278
- definition,
2279
- feature,
2280
- flags.force || false
2281
- );
2269
+ api(name, apiConfig) {
2270
+ super.api(name, apiConfig);
2271
+ return this;
2282
2272
  }
2283
- buildOperations(flags) {
2284
- const {
2285
- public: publicOps = [],
2286
- override: overrideOps = [],
2287
- exclude: excludeOps = []
2288
- } = flags;
2289
- return CRUD_OPERATIONS_LIST.reduce(
2290
- (acc, operation) => {
2291
- if (excludeOps.includes(operation)) {
2292
- return acc;
2293
- }
2294
- const operationConfig = {};
2295
- if (publicOps.includes(operation)) {
2296
- operationConfig.isPublic = true;
2297
- }
2298
- if (overrideOps.includes(operation)) {
2299
- operationConfig.override = true;
2300
- }
2301
- acc[operation] = operationConfig;
2302
- return acc;
2303
- },
2304
- {}
2305
- );
2273
+ apiNamespace(name, apiNamespaceConfig) {
2274
+ super.apiNamespace(name, apiNamespaceConfig);
2275
+ return this;
2276
+ }
2277
+ crud(name, crudConfig) {
2278
+ super.crud(name, crudConfig);
2279
+ return this;
2280
+ }
2281
+ action(name, actionConfig) {
2282
+ super.action(name, actionConfig);
2283
+ return this;
2306
2284
  }
2307
2285
  /**
2308
- * Generates operation code for overridden CRUD operations and returns as a single string.
2286
+ * Helper method to add routes with simplified parameters
2287
+ * @param featureName The name of the feature
2288
+ * @param name Route name, e.g. "DashboardRoute"
2289
+ * @param options Route configuration options
2309
2290
  */
2310
- async getOperationsCode(dataType, crudName, flags) {
2311
- if (!flags.override || flags.override.length === 0) {
2312
- return "";
2313
- }
2314
- const operationCodes = [];
2315
- for (const operation of flags.override) {
2316
- const { operationCode } = await this.generateOperationComponents(
2317
- dataType,
2318
- operation,
2319
- flags.auth || false,
2320
- [dataType],
2321
- true,
2322
- toPascalCase4(crudName)
2323
- );
2324
- operationCodes.push(operationCode.replace(/^[\r\n]/, ""));
2325
- }
2326
- return operationCodes.join("");
2291
+ addRoute(featureName, name, options) {
2292
+ const componentName = name.charAt(0).toUpperCase() + name.slice(1);
2293
+ const importPath = this.getFeatureImportPath(
2294
+ featureName,
2295
+ "client",
2296
+ "pages",
2297
+ componentName
2298
+ );
2299
+ const routeConfig = {
2300
+ path: options.path,
2301
+ to: this.page(componentName, {
2302
+ authRequired: options.auth || false,
2303
+ component: {
2304
+ import: componentName,
2305
+ from: `@src/${importPath}`
2306
+ }
2307
+ })
2308
+ };
2309
+ super.route(name, routeConfig);
2310
+ return this;
2327
2311
  }
2328
2312
  /**
2329
- * Generates a CRUD definition for the feature configuration.
2313
+ * Helper method to add API endpoints with simplified parameters
2314
+ * @param featureName The name of the feature
2315
+ * @param name API endpoint name, e.g. "getTasksApi"
2316
+ * @param options API configuration options
2330
2317
  */
2331
- getDefinition(crudName, dataType, operations) {
2332
- const templatePath = this.templateUtility.resolveTemplatePath(
2333
- "config/crud.eta",
2334
- "crud",
2335
- import.meta.url
2318
+ addApi(featureName, name, options) {
2319
+ const importPath = this.getFeatureImportPath(
2320
+ featureName,
2321
+ "server",
2322
+ "apis",
2323
+ name
2336
2324
  );
2337
- const operationsStr = JSON.stringify(operations, null, 2).replace(/"([^"]+)":/g, "$1:").slice(1, -1).split("\n").filter((line) => line.trim() !== "").map((line, index) => index === 0 ? line.trimStart() : " " + line).join("\n");
2338
- return this.templateUtility.processTemplate(templatePath, {
2339
- crudName: toPascalCase4(crudName),
2340
- dataType,
2341
- operations: operationsStr
2325
+ const middlewareImportPath = this.getFeatureImportPath(
2326
+ featureName,
2327
+ "server",
2328
+ "middleware",
2329
+ name
2330
+ );
2331
+ super.api(name, {
2332
+ fn: {
2333
+ import: name,
2334
+ from: `@src/${importPath}`
2335
+ },
2336
+ ...options.customMiddleware && {
2337
+ import: name,
2338
+ from: `@src/${middlewareImportPath}`
2339
+ },
2340
+ entities: options.entities,
2341
+ httpRoute: { method: options.method, route: options.route },
2342
+ auth: options.auth || false
2342
2343
  });
2344
+ return this;
2343
2345
  }
2344
- };
2345
-
2346
- // src/generators/job/job-generator.ts
2347
- import { capitalise as capitalise2, toCamelCase as toCamelCase5 } from "@ingenyus/swarm";
2348
-
2349
- // src/generators/job/schema.ts
2350
- import { extend as extend5 } from "@ingenyus/swarm";
2351
- import { z as z7 } from "zod";
2352
- var cronSchema = extend5(
2353
- z7.string().optional().refine(
2354
- (val) => {
2355
- if (!val) return true;
2356
- const parts = val.trim().split(/\s+/);
2357
- if (parts.length !== 5) return false;
2358
- const [minute, hour, day, month, weekday] = parts;
2359
- const validateCronField = (field, min, max) => {
2360
- if (field === "*") return true;
2361
- const rangeRegex = /^(\d+)(-(\d+))?(,(\d+)(-(\d+))?)*(\/(\d+))?$/;
2362
- if (!rangeRegex.test(field)) return false;
2363
- const items = field.split(",");
2364
- for (const item of items) {
2365
- if (item.includes("/")) {
2366
- const [base, step] = item.split("/");
2367
- const stepNum = parseInt(step, 10);
2368
- if (isNaN(stepNum) || stepNum <= 0) return false;
2369
- if (base === "*") continue;
2370
- const baseNum = parseInt(base, 10);
2371
- if (isNaN(baseNum) || baseNum < min || baseNum > max)
2372
- return false;
2373
- } else if (item.includes("-")) {
2374
- const [start, end] = item.split("-");
2375
- const startNum = parseInt(start, 10);
2376
- const endNum = parseInt(end, 10);
2377
- if (isNaN(startNum) || isNaN(endNum) || startNum < min || endNum > max || startNum > endNum)
2378
- return false;
2379
- } else {
2380
- const num = parseInt(item, 10);
2381
- if (isNaN(num) || num < min || num > max) return false;
2382
- }
2383
- }
2384
- return true;
2385
- };
2386
- return validateCronField(minute, 0, 59) && validateCronField(hour, 0, 23) && validateCronField(day, 1, 31) && validateCronField(month, 1, 12) && validateCronField(weekday, 0, 6);
2387
- },
2388
- {
2389
- message: 'Cron expression must be a valid five-field format: (minute hour day month weekday), e.g. "0 9 * * *"'
2390
- }
2391
- ),
2392
- {
2393
- description: "Cron schedule expression for the job",
2394
- friendlyName: "Cron Schedule",
2395
- shortName: "c",
2396
- examples: ["0 9 * * *", "*/15 * * * *", "0 0 1 * *"],
2397
- helpText: "Five-field cron expression: minute hour day month weekday"
2398
- }
2399
- );
2400
- var argsSchema = extend5(
2401
- z7.string().optional().refine(
2402
- (val) => {
2403
- if (!val) return true;
2404
- try {
2405
- const parsed = JSON.parse(val);
2406
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
2407
- } catch {
2408
- return false;
2346
+ /**
2347
+ * Helper method to add CRUD operations with simplified parameters
2348
+ * @param featureName The name of the feature
2349
+ * @param name The CRUD name
2350
+ * @param options CRUD configuration options
2351
+ */
2352
+ addCrud(featureName, name, options) {
2353
+ const processOperationOptions = (operationName, operationOptions) => {
2354
+ if (!operationOptions) return void 0;
2355
+ const processedOptions = { ...operationOptions };
2356
+ if (operationOptions.override) {
2357
+ const operationDataType = operationName === "getAll" ? this.getPlural(options.entity) : options.entity;
2358
+ const operationComponent = `${operationName}${operationDataType}`;
2359
+ const importPath = this.getFeatureImportPath(
2360
+ featureName,
2361
+ "server",
2362
+ "cruds",
2363
+ name.charAt(0).toLowerCase() + name.slice(1)
2364
+ );
2365
+ processedOptions.overrideFn = {
2366
+ import: operationComponent,
2367
+ from: `@src/${importPath}`
2368
+ };
2369
+ delete processedOptions.override;
2370
+ }
2371
+ return processedOptions;
2372
+ };
2373
+ super.crud(this.getPlural(options.entity), {
2374
+ entity: options.entity,
2375
+ operations: {
2376
+ getAll: processOperationOptions("getAll", options.getAll),
2377
+ get: processOperationOptions("get", options.get),
2378
+ create: processOperationOptions("create", options.create),
2379
+ update: processOperationOptions("update", options.update),
2380
+ delete: processOperationOptions("delete", options.delete)
2409
2381
  }
2410
- },
2411
- {
2412
- message: "Args must be a valid JSON object string"
2413
- }
2414
- ),
2415
- {
2416
- description: "Arguments to pass to the job function when executed",
2417
- friendlyName: "Job Arguments",
2418
- shortName: "a",
2419
- examples: ['{"userId": 123}', '{"type": "cleanup", "batchSize": 100}'],
2420
- helpText: "JSON object string that will be passed to the job function"
2421
- }
2422
- );
2423
- var schema6 = z7.object({
2424
- feature: commonSchemas.feature,
2425
- name: commonSchemas.name,
2426
- entities: commonSchemas.entities,
2427
- cron: cronSchema,
2428
- args: argsSchema,
2429
- force: commonSchemas.force
2430
- });
2431
-
2432
- // src/generators/job/job-generator.ts
2433
- var JobGenerator = class extends EntityGeneratorBase {
2434
- get entityType() {
2435
- return CONFIG_TYPES.JOB;
2436
- }
2437
- description = "Generate job workers for Wasp applications";
2438
- schema = schema6;
2439
- async generate(flags) {
2440
- const jobName = toCamelCase5(flags.name);
2441
- return this.handleGeneratorError(this.entityType, jobName, async () => {
2442
- const configPath = this.validateFeatureConfig(flags.feature);
2443
- const { targetDirectory } = this.ensureTargetDirectory(
2444
- flags.feature,
2445
- this.entityType.toLowerCase()
2446
- );
2447
- const targetFile = `${targetDirectory}/${jobName}.ts`;
2448
- await this.generateJobFile(targetFile, jobName, flags);
2449
- this.updateConfigFile(flags.feature, jobName, flags, configPath);
2450
2382
  });
2383
+ return this;
2451
2384
  }
2452
- async generateJobFile(targetFile, jobName, flags) {
2453
- const jobType = capitalise2(jobName);
2454
- const entities = Array.isArray(flags.entities) ? flags.entities : flags.entities ? [flags.entities] : [];
2455
- let imports = `import type { ${jobType} } from 'wasp/server/jobs';
2456
- `;
2457
- if (entities.length > 0) {
2458
- imports += `import { ${entities.join(", ")} } from 'wasp/entities';
2459
- `;
2460
- }
2461
- const replacements = {
2462
- imports,
2463
- jobType,
2464
- jobName
2465
- };
2466
- await this.renderTemplateToFile(
2467
- "job.eta",
2468
- replacements,
2469
- targetFile,
2470
- "job worker",
2471
- flags.force || false
2385
+ /**
2386
+ * Helper method to add actions with simplified parameters
2387
+ * @param featureName The name of the feature
2388
+ * @param name The action name
2389
+ * @param options Action configuration options
2390
+ */
2391
+ addAction(featureName, name, options) {
2392
+ const importPath = this.getFeatureImportPath(
2393
+ featureName,
2394
+ "server",
2395
+ "actions",
2396
+ name
2397
+ );
2398
+ const config = this.getOperationConfig(
2399
+ name,
2400
+ importPath,
2401
+ options.entities,
2402
+ options.auth
2472
2403
  );
2404
+ super.action(name, config);
2405
+ return this;
2473
2406
  }
2474
- updateConfigFile(featurePath, jobName, flags, configPath) {
2475
- const entities = Array.isArray(flags.entities) ? flags.entities : flags.entities ? [flags.entities] : [];
2476
- const cron = flags.cron || "0 0 * * *";
2477
- const args = flags.args || "{}";
2478
- const definition = this.getDefinition(
2479
- jobName,
2480
- entities,
2481
- cron,
2482
- args || "{}"
2407
+ /**
2408
+ * Helper method to add queries with simplified parameters
2409
+ * @param featureName The name of the feature
2410
+ * @param name The query name
2411
+ * @param options Query configuration options
2412
+ */
2413
+ addQuery(featureName, name, options) {
2414
+ const importPath = this.getFeatureImportPath(
2415
+ featureName,
2416
+ "server",
2417
+ "queries",
2418
+ name
2483
2419
  );
2484
- this.updateConfigWithCheck(
2485
- configPath,
2486
- "job",
2487
- jobName,
2488
- definition,
2489
- featurePath,
2490
- flags.force || false
2420
+ const config = this.getOperationConfig(
2421
+ name,
2422
+ importPath,
2423
+ options.entities,
2424
+ options.auth
2491
2425
  );
2426
+ super.query(name, config);
2427
+ return this;
2492
2428
  }
2493
2429
  /**
2494
- * Generates a job definition for the feature configuration.
2430
+ * Helper method to add background jobs with simplified parameters
2431
+ * @param featureName The name of the feature
2432
+ * @param name Job name
2433
+ * @param options Job configuration options
2495
2434
  */
2496
- getDefinition(jobName, entities, cron, args) {
2497
- const templatePath = this.getDefaultTemplatePath("config/job.eta");
2498
- return this.templateUtility.processTemplate(templatePath, {
2499
- jobName,
2500
- entities: entities.map((e) => `"${e}"`).join(", "),
2501
- cron,
2502
- args
2435
+ addJob(featureName, name, options) {
2436
+ const importPath = this.getFeatureImportPath(
2437
+ featureName,
2438
+ "server",
2439
+ "jobs",
2440
+ name
2441
+ );
2442
+ super.job(name, {
2443
+ executor: "PgBoss",
2444
+ perform: {
2445
+ fn: {
2446
+ import: name,
2447
+ from: `@src/${importPath}`
2448
+ }
2449
+ },
2450
+ entities: options.entities,
2451
+ ...options.cron && {
2452
+ schedule: {
2453
+ cron: options.cron,
2454
+ args: options.args || {}
2455
+ }
2456
+ }
2503
2457
  });
2458
+ return this;
2504
2459
  }
2505
- };
2506
-
2507
- // src/generators/query/schema.ts
2508
- import { extend as extend6 } from "@ingenyus/swarm";
2509
- import { z as z8 } from "zod";
2510
- var validQueries = Object.values(QUERY_OPERATIONS);
2511
- var querySchema = extend6(
2512
- z8.string().min(1, "Query type is required").refine(getTypedValueValidator(validQueries), {
2513
- message: `Invalid query. Must be one of: ${validQueries.join(", ")}`
2514
- }).transform(getTypedValueTransformer(validQueries)),
2515
- {
2516
- description: "The query operation to generate",
2517
- friendlyName: "Query Operation",
2518
- shortName: "o",
2519
- examples: validQueries,
2520
- helpText: "Available queries: get, getAll, getFiltered"
2521
- }
2522
- );
2523
- var dataTypeSchema2 = extend6(z8.string().min(1, "Data type is required"), {
2524
- description: "The data type/model name for this query",
2525
- friendlyName: "Data Type",
2526
- shortName: "d",
2527
- examples: ["User", "Product", "Task"],
2528
- helpText: "The Wasp entity or model name this query will work with"
2529
- });
2530
- var schema7 = z8.object({
2531
- feature: commonSchemas.feature,
2532
- operation: querySchema,
2533
- dataType: dataTypeSchema2,
2534
- name: extend6(commonSchemas.name.optional(), commonSchemas.name._metadata),
2535
- entities: commonSchemas.entities,
2536
- force: commonSchemas.force,
2537
- auth: commonSchemas.auth
2538
- });
2539
-
2540
- // src/generators/query/query-generator.ts
2541
- var QueryGenerator = class extends OperationGeneratorBase {
2542
- get entityType() {
2543
- return CONFIG_TYPES.QUERY;
2544
- }
2545
- description = "Generate queries (data fetching) for Wasp applications";
2546
- schema = schema7;
2547
- async generate(flags) {
2548
- const { dataType, feature, name } = flags;
2549
- const operation = flags.operation;
2550
- const operationType = "query";
2551
- const entities = flags.entities ? Array.isArray(flags.entities) ? flags.entities : flags.entities.split(",").map((e) => e.trim()).filter(Boolean) : [];
2552
- if (dataType && !entities.includes(dataType)) {
2553
- entities.unshift(dataType);
2554
- }
2555
- const { operationCode, operationName } = await this.generateOperationComponents(
2556
- dataType,
2557
- operation,
2558
- flags.auth,
2559
- entities,
2560
- false,
2561
- null,
2460
+ /**
2461
+ * Helper method to add API namespaces with simplified parameters
2462
+ * @param featureName The name of the feature
2463
+ * @param name Namespace name
2464
+ * @param options API namespace configuration options
2465
+ */
2466
+ addApiNamespace(featureName, name, options) {
2467
+ const importPath = this.getFeatureImportPath(
2468
+ featureName,
2469
+ "server",
2470
+ "middleware",
2562
2471
  name
2563
2472
  );
2564
- return this.handleGeneratorError(
2565
- this.entityType,
2566
- operationName,
2567
- async () => {
2568
- const configPath = this.validateFeatureConfig(feature);
2569
- const { targetDirectory: operationsDir, importDirectory } = this.ensureTargetDirectory(feature, operationType);
2570
- const importPath = `${importDirectory}/${operationName}`;
2571
- this.generateOperationFile(
2572
- operationsDir,
2573
- operationName,
2574
- operationCode,
2575
- flags.force || false
2576
- );
2577
- const definition = this.getDefinition(
2578
- operationName,
2579
- feature,
2580
- entities,
2581
- "query",
2582
- importPath,
2583
- flags.auth
2584
- );
2585
- this.updateConfigWithCheck(
2586
- configPath,
2587
- "addQuery",
2588
- operationName,
2589
- definition,
2590
- feature,
2591
- flags.force || false
2592
- );
2473
+ super.apiNamespace(name, {
2474
+ path: options.path,
2475
+ middlewareConfigFn: {
2476
+ import: name,
2477
+ from: `@src/${importPath}`
2593
2478
  }
2594
- );
2479
+ });
2480
+ return this;
2595
2481
  }
2596
- };
2597
-
2598
- // src/generators/route/route-generator.ts
2599
- import { formatDisplayName, toCamelCase as toCamelCase6, toPascalCase as toPascalCase5 } from "@ingenyus/swarm";
2600
-
2601
- // src/generators/route/schema.ts
2602
- import { z as z9 } from "zod";
2603
- var schema8 = z9.object({
2604
- feature: commonSchemas.feature,
2605
- name: commonSchemas.name,
2606
- path: commonSchemas.path,
2607
- auth: commonSchemas.auth,
2608
- force: commonSchemas.force
2609
- });
2610
-
2611
- // src/generators/route/route-generator.ts
2612
- var RouteGenerator = class extends EntityGeneratorBase {
2613
- get entityType() {
2614
- return CONFIG_TYPES.ROUTE;
2482
+ /**
2483
+ * Calculates the import path for a feature component
2484
+ * @param featureName The name of the feature
2485
+ * @param type The type of component (client, server, etc.)
2486
+ * @param subPath The sub-path within the feature directory
2487
+ * @param fileName The name of the file (optional, defaults to featureName)
2488
+ * @returns The calculated import path
2489
+ */
2490
+ getFeatureImportPath(featureName, type, subPath, fileName) {
2491
+ const file = fileName || featureName;
2492
+ return `features/${featureName}/${type}/${subPath}/${file}`;
2615
2493
  }
2616
- description = "Generate route handlers for Wasp applications";
2617
- schema = schema8;
2618
- async generate(flags) {
2619
- const { path: routePath, name, feature } = flags;
2620
- const routeName = toCamelCase6(name || getRouteNameFromPath(routePath));
2621
- const componentName = toPascalCase5(routeName);
2622
- const fileName = `${componentName}.tsx`;
2623
- return this.handleGeneratorError(this.entityType, routeName, async () => {
2624
- const configPath = this.validateFeatureConfig(feature);
2625
- const { targetDirectory } = this.ensureTargetDirectory(feature, "page");
2626
- const targetFile = `${targetDirectory}/${fileName}`;
2627
- await this.generatePageFile(targetFile, componentName, flags);
2628
- this.updateConfigFile(feature, routeName, routePath, flags, configPath);
2629
- });
2494
+ /**
2495
+ * Converts a singular word to its plural form
2496
+ * @param word The singular word to pluralize
2497
+ * @returns The plural form of the word
2498
+ */
2499
+ getPlural(word) {
2500
+ if (word.endsWith("y")) {
2501
+ return word.slice(0, -1) + "ies";
2502
+ } else if (word.endsWith("s") || word.endsWith("sh") || word.endsWith("ch") || word.endsWith("x") || word.endsWith("z")) {
2503
+ return word + "es";
2504
+ } else {
2505
+ return word + "s";
2506
+ }
2630
2507
  }
2631
- async generatePageFile(targetFile, componentName, flags) {
2632
- const templatePath = "files/client/page.eta";
2633
- const replacements = {
2634
- componentName,
2635
- displayName: formatDisplayName(componentName)
2508
+ /**
2509
+ * Configures all feature modules by scanning the features directory
2510
+ */
2511
+ async configureFeatures() {
2512
+ const featuresDir = path8.join(process.cwd(), "src", "features");
2513
+ if (!fs3.existsSync(featuresDir)) {
2514
+ console.warn(
2515
+ "Features directory not found, skipping feature configuration"
2516
+ );
2517
+ return this;
2518
+ }
2519
+ const getAllFeatureFiles = (dir) => {
2520
+ let results = [];
2521
+ const list = fs3.readdirSync(dir, { withFileTypes: true });
2522
+ for (const entry of list) {
2523
+ const fullPath = path8.join(dir, entry.name);
2524
+ if (entry.isDirectory()) {
2525
+ results = results.concat(getAllFeatureFiles(fullPath));
2526
+ } else if (entry.isFile() && entry.name.endsWith(".wasp.ts")) {
2527
+ results.push(path8.relative(featuresDir, fullPath));
2528
+ }
2529
+ }
2530
+ return results;
2636
2531
  };
2637
- await this.renderTemplateToFile(
2638
- "page.eta",
2639
- replacements,
2640
- targetFile,
2641
- "Page file",
2642
- flags.force || false
2643
- );
2644
- }
2645
- updateConfigFile(featurePath, routeName, routePath, flags, configPath) {
2646
- const definition = this.getDefinition(
2647
- routeName,
2648
- routePath,
2649
- featurePath,
2650
- flags.auth
2651
- );
2652
- this.updateConfigWithCheck(
2653
- configPath,
2654
- "addRoute",
2655
- routeName,
2656
- definition,
2657
- featurePath,
2658
- flags.force || false
2659
- );
2532
+ const featureFiles = getAllFeatureFiles(featuresDir);
2533
+ for (const file of featureFiles) {
2534
+ try {
2535
+ const featureName = path8.dirname(file);
2536
+ const modulePath = path8.join(
2537
+ process.cwd(),
2538
+ ".wasp",
2539
+ "src",
2540
+ "features",
2541
+ file.replace(".ts", ".js")
2542
+ );
2543
+ const module = await import(modulePath);
2544
+ if (module.default) {
2545
+ module.default(this, featureName);
2546
+ }
2547
+ } catch (error) {
2548
+ console.error(`Failed to load feature module ${file}:`, error);
2549
+ }
2550
+ }
2551
+ return this;
2660
2552
  }
2661
2553
  /**
2662
- * Generates a route definition for the feature configuration.
2554
+ * Helper method to get the configuration for an action or query
2555
+ * @param name The operation name
2556
+ * @param importPath Import path (excluding `@src/` prefix), e.g. "features/dashboard/server/queries/getTasks"
2557
+ * @param entities Comma-separated list of entities (optional, defaults to datatype)
2558
+ * @param auth Require authentication (optional)
2663
2559
  */
2664
- getDefinition(routeName, routePath, featurePath, auth = false) {
2665
- const templatePath = this.getDefaultTemplatePath("config/route.eta");
2666
- return this.templateUtility.processTemplate(templatePath, {
2667
- featureName: featurePath.split("/").pop() || featurePath,
2668
- routeName,
2669
- routePath,
2670
- auth: String(auth)
2671
- });
2560
+ getOperationConfig(name, importPath, entities, auth) {
2561
+ return {
2562
+ fn: {
2563
+ import: name,
2564
+ from: `@src/${importPath}`
2565
+ },
2566
+ entities,
2567
+ auth: auth || false
2568
+ };
2672
2569
  }
2673
2570
  };
2674
2571
  export {
@@ -2677,42 +2574,10 @@ export {
2677
2574
  ApiNamespaceGenerator,
2678
2575
  App,
2679
2576
  CrudGenerator,
2680
- FeatureDirectoryGenerator,
2577
+ FeatureGenerator,
2681
2578
  JobGenerator,
2682
2579
  QueryGenerator,
2683
2580
  RouteGenerator,
2684
- TemplateUtility,
2685
2581
  WaspConfigGenerator,
2686
- commonSchemas,
2687
- copyDirectory,
2688
- createWaspPlugin,
2689
- ensureDirectoryExists,
2690
- featureExists,
2691
- findWaspRoot,
2692
- generateIntersectionType,
2693
- generateJsonTypeHandling,
2694
- generateOmitType,
2695
- generatePartialType,
2696
- generatePickType,
2697
- getAppRootDir,
2698
- getConfigDir,
2699
- getEntityMetadata,
2700
- getFeatureDir,
2701
- getFeatureImportPath,
2702
- getFeatureTargetDir,
2703
- getIdFields,
2704
- getJsonFields,
2705
- getOptionalFields,
2706
- getPluginVersion,
2707
- getRequiredFields,
2708
- getRouteNameFromPath,
2709
- getTemplatesDir,
2710
- getTypedArrayTransformer,
2711
- getTypedArrayValidator,
2712
- getTypedValueTransformer,
2713
- getTypedValueValidator,
2714
- needsPrismaImport,
2715
- normaliseFeaturePath,
2716
- realFileSystem,
2717
2582
  wasp
2718
2583
  };