@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
@@ -0,0 +1,2211 @@
1
+ // src/types/constants.ts
2
+ var PLUGIN_NAME = "wasp";
3
+ var OPERATION_TYPES = ["query", "action"];
4
+ var API_HTTP_METHODS = [
5
+ "ALL",
6
+ "GET",
7
+ "POST",
8
+ "PUT",
9
+ "DELETE"
10
+ ];
11
+ var OPERATIONS = {
12
+ CREATE: "create",
13
+ UPDATE: "update",
14
+ DELETE: "delete",
15
+ GET: "get",
16
+ GETALL: "getAll",
17
+ GETFILTERED: "getFiltered"
18
+ };
19
+ var CRUD_OPERATIONS = {
20
+ CREATE: "create",
21
+ GET: "get",
22
+ GETALL: "getAll",
23
+ UPDATE: "update",
24
+ DELETE: "delete"
25
+ };
26
+ var ACTION_OPERATIONS = {
27
+ CREATE: "create",
28
+ UPDATE: "update",
29
+ DELETE: "delete"
30
+ };
31
+ var QUERY_OPERATIONS = {
32
+ GET: "get",
33
+ GETALL: "getAll",
34
+ GETFILTERED: "getFiltered"
35
+ };
36
+ var TYPE_DIRECTORIES = {
37
+ component: "client/components",
38
+ hook: "client/hooks",
39
+ layout: "client/layouts",
40
+ page: "client/pages",
41
+ util: "client/utils",
42
+ action: "server/actions",
43
+ query: "server/queries",
44
+ middleware: "server/middleware",
45
+ job: "server/jobs",
46
+ api: "server/apis",
47
+ crud: "server/cruds",
48
+ type: "types"
49
+ };
50
+ var CONFIG_TYPES = {
51
+ ROUTE: "Route",
52
+ QUERY: "Query",
53
+ ACTION: "Action",
54
+ JOB: "Job",
55
+ API: "Api",
56
+ API_NAMESPACE: "ApiNamespace",
57
+ CRUD: "Crud"
58
+ };
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
+
70
+ // src/common/filesystem.ts
71
+ import { toPascalCase, validateFeaturePath } from "@ingenyus/swarm";
72
+ import fs from "fs";
73
+ import path from "path";
74
+ var realFileSystem = {
75
+ readFileSync: fs.readFileSync,
76
+ writeFileSync: fs.writeFileSync,
77
+ existsSync: fs.existsSync,
78
+ copyFileSync: fs.copyFileSync,
79
+ mkdirSync: fs.mkdirSync,
80
+ readdirSync: fs.readdirSync,
81
+ statSync: fs.statSync
82
+ };
83
+ function findWaspRoot(fileSystem, startDir = process.cwd()) {
84
+ const startDirPath = path.resolve(startDir);
85
+ let currentDirPath = startDirPath;
86
+ const root = path.parse(currentDirPath).root;
87
+ while (currentDirPath !== root) {
88
+ const waspRootPath = path.join(currentDirPath, ".wasproot");
89
+ if (fileSystem.existsSync(waspRootPath)) {
90
+ return currentDirPath;
91
+ }
92
+ currentDirPath = path.dirname(currentDirPath);
93
+ }
94
+ throw new Error(
95
+ `Couldn't find Wasp application root from ${startDirPath}. Make sure you are running this command from within a Wasp project directory.`
96
+ );
97
+ }
98
+ function copyDirectory(fileSystem, src, dest) {
99
+ if (!fileSystem.existsSync(dest)) {
100
+ fileSystem.mkdirSync(dest, { recursive: true });
101
+ }
102
+ const entries = fileSystem.readdirSync(src, { withFileTypes: true });
103
+ for (const entry of entries) {
104
+ const srcPath = path.join(src, entry.name);
105
+ const destPath = path.join(dest, entry.name);
106
+ if (entry.isDirectory()) {
107
+ copyDirectory(fileSystem, srcPath, destPath);
108
+ } else {
109
+ fileSystem.copyFileSync(srcPath, destPath);
110
+ }
111
+ }
112
+ }
113
+ function ensureDirectoryExists(fileSystem, dir) {
114
+ if (!fileSystem.existsSync(dir)) {
115
+ fileSystem.mkdirSync(dir, { recursive: true });
116
+ }
117
+ }
118
+ function normaliseFeaturePath(featurePath) {
119
+ const segments = validateFeaturePath(featurePath);
120
+ const normalisedSegments = [];
121
+ for (let i = 0; i < segments.length; i++) {
122
+ const segment = segments[i];
123
+ const previousSegment = normalisedSegments[normalisedSegments.length - 1];
124
+ if (previousSegment !== "features" && segment !== "features") {
125
+ normalisedSegments.push("features");
126
+ }
127
+ normalisedSegments.push(segment);
128
+ }
129
+ return normalisedSegments.join("/");
130
+ }
131
+ function getFeatureDir(fileSystem, featureName) {
132
+ const waspRoot = findWaspRoot(fileSystem);
133
+ const normalisedPath = normaliseFeaturePath(featureName);
134
+ return path.join(waspRoot, "src", normalisedPath);
135
+ }
136
+ function getFeatureImportPath(featurePath) {
137
+ const segments = validateFeaturePath(featurePath);
138
+ return segments.join("/");
139
+ }
140
+ function getRouteNameFromPath(routePath) {
141
+ const lastSegment = routePath.split("/").filter(Boolean).pop() || "index";
142
+ const cleanSegment = lastSegment.replace(/[:*]/g, "");
143
+ return `${toPascalCase(cleanSegment)}Page`;
144
+ }
145
+
146
+ // src/common/prisma.ts
147
+ import {
148
+ getSchema
149
+ } from "@mrleebo/prisma-ast";
150
+ import fs2 from "fs";
151
+ import path2 from "path";
152
+ async function getEntityMetadata(modelName) {
153
+ try {
154
+ const schemaPath = path2.join(process.cwd(), "schema.prisma");
155
+ const schemaContent = fs2.readFileSync(schemaPath, "utf8");
156
+ const schema9 = getSchema(schemaContent);
157
+ const model = schema9.list?.find(
158
+ (m) => m.type === "model" && m.name === modelName
159
+ );
160
+ if (!model || model.type !== "model") {
161
+ throw new Error(`Model ${modelName} not found in schema`);
162
+ }
163
+ const compositeIdAttr = (model.properties || []).find(
164
+ (item) => item.type === "attribute" && item.kind === "object" && item.name === "id"
165
+ );
166
+ let compositeIdFields = [];
167
+ if (compositeIdAttr?.args?.[0]) {
168
+ const arg = compositeIdAttr.args[0];
169
+ if (typeof arg.value === "object" && arg.value !== null && "type" in arg.value && arg.value.type === "array" && "args" in arg.value) {
170
+ compositeIdFields = arg.value.args;
171
+ }
172
+ }
173
+ const fields = (model.properties || []).filter(
174
+ (item) => item.type === "field" && !item.array && !item.attributes?.some((attr) => attr.name === "relation")
175
+ ).map((field) => {
176
+ const fieldType = typeof field.fieldType === "string" ? field.fieldType : field.fieldType.name;
177
+ const tsType = getPrismaToTsType(fieldType);
178
+ const isRequired = !field.optional;
179
+ const isId = field.attributes?.some((attr) => attr.name === "id") || compositeIdFields.includes(field.name);
180
+ const isUnique = field.attributes?.some((attr) => attr.name === "unique") || false;
181
+ const hasDefaultValue = field.attributes?.some((attr) => attr.name === "default") || false;
182
+ const isUpdatedAt = field.attributes?.some((attr) => attr.name === "updatedAt") || false;
183
+ const isGenerated = field.attributes?.some((attr) => attr.name === "map") || false;
184
+ return {
185
+ name: field.name,
186
+ type: fieldType,
187
+ tsType,
188
+ isRequired,
189
+ isId,
190
+ isUnique,
191
+ hasDefaultValue,
192
+ isGenerated,
193
+ isUpdatedAt
194
+ };
195
+ });
196
+ return {
197
+ name: modelName,
198
+ fields
199
+ };
200
+ } catch (error) {
201
+ throw new Error(
202
+ `Failed to get entity metadata for ${modelName}: ${error instanceof Error ? error.message : String(error)}`
203
+ );
204
+ }
205
+ }
206
+ function getIdFields(model) {
207
+ const idFields = model.fields.filter((f) => f.isId).map((f) => f.name);
208
+ if (idFields.length === 0) {
209
+ throw new Error(`No ID field found for model ${model.name}`);
210
+ }
211
+ return idFields;
212
+ }
213
+ function getRequiredFields(model) {
214
+ return model.fields.filter(
215
+ (f) => f.isRequired && !f.hasDefaultValue && !f.isGenerated && !f.isUpdatedAt
216
+ ).map((f) => f.name);
217
+ }
218
+ function getOptionalFields(model) {
219
+ return model.fields.filter(
220
+ (field) => (field.hasDefaultValue && field.type !== "DateTime" || !field.isRequired) && !field.isId && !field.isGenerated && !field.isUpdatedAt
221
+ ).map((field) => field.name);
222
+ }
223
+ function getJsonFields(model) {
224
+ return model.fields.filter((f) => f.type === "Json").map((f) => f.name);
225
+ }
226
+ function generateJsonTypeHandling(jsonFields) {
227
+ if (jsonFields.length === 0) return "";
228
+ const assignments = jsonFields.map(
229
+ (field) => ` ${field}: (data.${field} as Prisma.JsonValue) || Prisma.JsonNull`
230
+ ).join(",\n");
231
+ return `,
232
+ ${assignments}`;
233
+ }
234
+ function needsPrismaImport(model) {
235
+ return model.fields.some((f) => f.type === "Json" || f.type === "Decimal");
236
+ }
237
+ function generatePickType(modelName, fields, allFields) {
238
+ if (fields.length === 0) return "";
239
+ if (fields.length === allFields.length) return modelName;
240
+ const fieldUnion = fields.map((f) => `"${f}"`).join(" | ");
241
+ return `Pick<${modelName}, ${fieldUnion}>`;
242
+ }
243
+ function generateOmitType(modelName, fields, allFields) {
244
+ if (fields.length === 0) return modelName;
245
+ if (fields.length === allFields.length) return "";
246
+ const fieldUnion = fields.map((f) => `"${f}"`).join(" | ");
247
+ return `Omit<${modelName}, ${fieldUnion}>`;
248
+ }
249
+ function generatePartialType(typeString) {
250
+ if (!typeString) return "";
251
+ return `Partial<${typeString}>`;
252
+ }
253
+ function generateIntersectionType(type1, type2) {
254
+ if (!type1 && !type2) return "";
255
+ if (!type1) return type2;
256
+ if (!type2) return type1;
257
+ return `${type1} & ${type2}`;
258
+ }
259
+ function getPrismaToTsType(type) {
260
+ const typeMap = {
261
+ String: "string",
262
+ Int: "number",
263
+ Float: "number",
264
+ Boolean: "boolean",
265
+ DateTime: "Date",
266
+ Json: "Prisma.JsonValue",
267
+ BigInt: "bigint",
268
+ Decimal: "Prisma.Decimal",
269
+ Bytes: "Buffer"
270
+ };
271
+ return typeMap[type] || type;
272
+ }
273
+
274
+ // src/common/schemas.ts
275
+ import { commandRegistry } from "@ingenyus/swarm";
276
+ import { z } from "zod";
277
+ var commonSchemas = {
278
+ feature: z.string().min(1, "Feature is required").meta({
279
+ description: "The feature directory this component will be generated in"
280
+ }).register(commandRegistry, {
281
+ shortName: "f",
282
+ examples: ["root", "auth", "dashboard/users"],
283
+ helpText: "Can be nested as a logical or relative path, e.g. 'dashboard/users' or 'features/dashboard/features/users'"
284
+ }),
285
+ name: z.string().min(1, "Name is required").meta({ description: "The name of the generated component" }).register(commandRegistry, {
286
+ shortName: "n",
287
+ examples: ["users", "task"],
288
+ helpText: "Will be used for generated files and configuration entries"
289
+ }),
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, {
296
+ shortName: "p",
297
+ examples: ["/api/users/:id", "/api/products"],
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"
304
+ }),
305
+ entities: z.array(z.string()).optional().meta({
306
+ description: "The Wasp entities that will be available to this component (optional)"
307
+ }).register(commandRegistry, {
308
+ shortName: "e",
309
+ examples: ["User", "User Task"],
310
+ helpText: "An array of Wasp entity names"
311
+ }),
312
+ force: z.boolean().optional().meta({
313
+ description: "Force overwrite of existing files and configuration entries (optional)"
314
+ }).register(commandRegistry, {
315
+ shortName: "F",
316
+ helpText: "CAUTION: Will overwrite existing files and configuration entries with current parameters"
317
+ }),
318
+ auth: z.boolean().optional().meta({
319
+ description: "Require authentication for this component (optional)"
320
+ }).register(commandRegistry, {
321
+ shortName: "a",
322
+ helpText: "Will generate authentication checks"
323
+ })
324
+ };
325
+
326
+ // src/common/templates.ts
327
+ import { toKebabCase } from "@ingenyus/swarm";
328
+ import { Eta } from "eta";
329
+ import path3 from "path";
330
+ var TemplateUtility = class {
331
+ constructor(fileSystem) {
332
+ this.fileSystem = fileSystem;
333
+ }
334
+ processTemplate(templatePath, replacements) {
335
+ const declarations = Object.keys(replacements).map((key) => `${key}=it.${key}`).join(", ");
336
+ const functionHeader = declarations ? `const ${declarations};` : void 0;
337
+ const templateDir = path3.dirname(templatePath);
338
+ const eta = new Eta({
339
+ autoTrim: false,
340
+ autoEscape: false,
341
+ views: templateDir,
342
+ functionHeader
343
+ });
344
+ const templateName = path3.basename(templatePath).replace(/\.eta$/, "");
345
+ if (this.fileSystem.existsSync(templatePath)) {
346
+ return eta.render(templateName, replacements);
347
+ } else {
348
+ const template = this.fileSystem.readFileSync(templatePath, "utf8");
349
+ return eta.renderString(template, replacements);
350
+ }
351
+ }
352
+ /**
353
+ * Helper method to resolve template paths for concrete generators
354
+ * @param relativePath - The relative path to the template file
355
+ * @param generatorName - The name of the generator (e.g., 'api', 'job')
356
+ * @param currentFileUrl - The import.meta.url from the concrete generator class
357
+ * @returns The full path to the template file
358
+ */
359
+ resolveTemplatePath(relativePath, generatorName, currentFileUrl) {
360
+ const generatorDirName = toKebabCase(generatorName);
361
+ const currentFilePath = new URL(currentFileUrl).pathname;
362
+ const currentFileDir = path3.dirname(currentFilePath);
363
+ const currentFileName = path3.basename(currentFilePath);
364
+ const isInstalledPackage = currentFileDir.includes("node_modules") && currentFileDir.endsWith("/dist") && currentFileName === "index.js";
365
+ const startDir = isInstalledPackage ? currentFileDir : path3.dirname(path3.dirname(currentFileDir));
366
+ return path3.join(
367
+ startDir,
368
+ "generators",
369
+ generatorDirName,
370
+ "templates",
371
+ relativePath
372
+ );
373
+ }
374
+ };
375
+
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";
382
+ import path5 from "path";
383
+
384
+ // src/generators/base/wasp-generator.base.ts
385
+ import {
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);
404
+ }
405
+ path = path4;
406
+ templateUtility;
407
+ /**
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
412
+ */
413
+ getTemplatePath(templateName) {
414
+ return this.templateUtility.resolveTemplatePath(
415
+ templateName,
416
+ "feature",
417
+ import.meta.url
418
+ );
419
+ }
420
+ /**
421
+ * Generate a TypeScript Wasp config file in a feature directory
422
+ * @param featurePath - The feature directory path
423
+ */
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}`);
441
+ }
442
+ /**
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
447
+ */
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
469
+ );
470
+ content = this.removeExistingDefinition(content, declaration);
471
+ const hasExistingDefinitions = this.hasExistingDefinitions(
472
+ content,
473
+ methodName
474
+ );
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
520
+ */
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*['"`]([^'"`]+)['"`]/
579
+ );
580
+ if (match) {
581
+ methodCalls.push({
582
+ lineIndex: i,
583
+ endLineIndex: j,
584
+ methodName: match[1],
585
+ itemName: match[2]
586
+ });
587
+ }
588
+ }
589
+ }
590
+ const groups = {};
591
+ methodCalls.forEach((call) => {
592
+ if (!groups[call.methodName]) {
593
+ groups[call.methodName] = [];
594
+ }
595
+ groups[call.methodName].push({
596
+ lineIndex: call.lineIndex,
597
+ endLineIndex: call.endLineIndex,
598
+ itemName: call.itemName
599
+ });
600
+ });
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 };
654
+ }
655
+ /**
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
663
+ */
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;
673
+ }
674
+ /**
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
679
+ */
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;
688
+ }
689
+ /**
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
695
+ */
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;
710
+ }
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--;
736
+ }
737
+ }
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");
753
+ }
754
+ /**
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
759
+ */
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")
784
+ );
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
+ }
807
+ }
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;
854
+ }
855
+ /**
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
878
+ */
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;
892
+ }
893
+ /**
894
+ * Generic existence check with force flag handling
895
+ * Consolidates the pattern used in both file and config checks
896
+ */
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);
901
+ }
902
+ return exists;
903
+ }
904
+ /**
905
+ * Checks if a file exists and handles force flag logic
906
+ */
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
+ );
915
+ }
916
+ /**
917
+ * Safely writes a file with proper error handling and logging
918
+ */
919
+ writeFile(filePath, content, fileType, fileExists) {
920
+ this.fileSystem.writeFileSync(filePath, content);
921
+ this.logger.success(
922
+ `${fileExists ? "Overwrote" : "Generated"} ${fileType}: ${filePath}`
923
+ );
924
+ }
925
+ };
926
+
927
+ // src/generators/feature/schema.ts
928
+ import { z as z2 } from "zod";
929
+ var schema = z2.object({
930
+ target: commonSchemas.target
931
+ });
932
+
933
+ // src/generators/feature/feature-generator.ts
934
+ var FeatureGenerator = class extends WaspGeneratorBase {
935
+ constructor(logger = singletonLogger3, fileSystem = realFileSystem) {
936
+ super(fileSystem, logger);
937
+ this.logger = logger;
938
+ this.fileSystem = fileSystem;
939
+ this.name = "feature";
940
+ this.description = "Generates a feature directory containing a Wasp configuration file";
941
+ }
942
+ name;
943
+ description;
944
+ schema = schema;
945
+ getDefaultTemplatePath(templateName) {
946
+ return this.templateUtility.resolveTemplatePath(
947
+ templateName,
948
+ this.name,
949
+ import.meta.url
950
+ );
951
+ }
952
+ /**
953
+ * Generates a feature directory containing a Wasp configuration file
954
+ * @param target - The target path of the generated directory
955
+ */
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
+ );
969
+ }
970
+ }
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
992
+ );
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
+ }
1012
+ }
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");
1020
+ }
1021
+ /**
1022
+ * Checks if a config item already exists in the feature config
1023
+ */
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`
1036
+ );
1037
+ }
1038
+ /**
1039
+ * Updates the feature config with a new definition
1040
+ */
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
+ );
1046
+ }
1047
+ /**
1048
+ * Consolidated helper for updating config files with existence check
1049
+ * This replaces the duplicated updateConfigFile pattern in concrete generators
1050
+ */
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,
1064
+ methodName
1065
+ );
1066
+ }
1067
+ }
1068
+ /**
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
1074
+ */
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}`;
1137
+ }
1138
+ }
1139
+ /**
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.
1143
+ */
1144
+ getOperationTemplatePath(templateName) {
1145
+ return this.templateUtility.resolveTemplatePath(
1146
+ templateName,
1147
+ "operation",
1148
+ import.meta.url
1149
+ );
1150
+ }
1151
+ /**
1152
+ * Gets the TypeScript type name for an operation.
1153
+ */
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";');
1165
+ }
1166
+ imports.push(`import { ${modelName} } from "wasp/entities";`);
1167
+ }
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";`
1174
+ );
1175
+ return imports.join("\n");
1176
+ }
1177
+ /**
1178
+ * Gets the operation type ("query" or "action") for a given operation.
1179
+ */
1180
+ getOperationType(operation) {
1181
+ return operation === OPERATIONS.GETALL || operation === OPERATIONS.GET || operation === OPERATIONS.GETFILTERED ? "query" : "action";
1182
+ }
1183
+ /**
1184
+ * Generates the operation components needed for file and config generation.
1185
+ */
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
1193
+ );
1194
+ const operationCode = await this.generateOperationCode(
1195
+ model,
1196
+ operation,
1197
+ auth,
1198
+ isCrudOverride,
1199
+ crudName
1200
+ );
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;
1243
+ }
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)}>`;
1249
+ break;
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;
1261
+ }
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 = "";
1291
+ }
1292
+ }
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);
1314
+ }
1315
+ /**
1316
+ * Generates an operation file for a given operation.
1317
+ */
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"
1324
+ );
1325
+ this.writeFile(operationFile, operationCode, "operation file", fileExists);
1326
+ }
1327
+ /**
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
1331
+ */
1332
+ copyOperationTemplates(templateDir, targetDir) {
1333
+ copyDirectory(this.fileSystem, templateDir, targetDir);
1334
+ this.logger.debug(
1335
+ `Copied operation templates from ${templateDir} to ${targetDir}`
1336
+ );
1337
+ }
1338
+ /**
1339
+ * Generates an operation definition for the feature configuration.
1340
+ */
1341
+ getDefinition(operationName, featurePath, entities, operationType, importPath, auth = false) {
1342
+ if (!OPERATION_TYPES.includes(operationType)) {
1343
+ handleFatalError3(`Unknown operation type: ${operationType}`);
1344
+ }
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
+ });
1361
+ }
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
1521
+ );
1522
+ }
1523
+ async updateConfigFile(apiName, apiFile, importDirectory, args, configFilePath) {
1524
+ const {
1525
+ feature,
1526
+ force = false,
1527
+ entities,
1528
+ method,
1529
+ path: path8,
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
+ path8,
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
1552
+ );
1553
+ }
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
+ }
1574
+
1575
+ ` : "";
1576
+ const methodCheck = method !== "ALL" ? ` if (req.method !== '${method}') {
1577
+ throw new HttpError(405);
1578
+ }
1579
+
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
+ };
1590
+ }
1591
+ };
1592
+
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;
1610
+ }
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
+ );
1638
+ }
1639
+ );
1640
+ }
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
1648
+ );
1649
+ this.updateConfigWithCheck(
1650
+ configFilePath,
1651
+ "addApiNamespace",
1652
+ namespaceName,
1653
+ definition,
1654
+ args.feature,
1655
+ force
1656
+ );
1657
+ }
1658
+ /**
1659
+ * Generates an apiNamespace definition for the feature configuration.
1660
+ */
1661
+ async getDefinition(namespaceName, middlewareImportPath, pathValue) {
1662
+ const templatePath = this.templateUtility.resolveTemplatePath(
1663
+ "config/api-namespace.eta",
1664
+ "api-namespace",
1665
+ import.meta.url
1666
+ );
1667
+ return this.templateUtility.processTemplate(templatePath, {
1668
+ namespaceName,
1669
+ middlewareImportPath,
1670
+ pathValue
1671
+ });
1672
+ }
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;
1731
+ }
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
+ });
1767
+ }
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
1776
+ );
1777
+ const replacements = {
1778
+ imports,
1779
+ operations
1780
+ };
1781
+ await this.renderTemplateToFile(
1782
+ "crud.eta",
1783
+ replacements,
1784
+ targetFile,
1785
+ "CRUD file",
1786
+ force
1787
+ );
1788
+ }
1789
+ /**
1790
+ * Generates import statements for an operation.
1791
+ */
1792
+ generateCrudImports(model, modelName, crudName, operations) {
1793
+ const imports = [];
1794
+ if (operations.some((operation) => operation !== "getAll")) {
1795
+ if (needsPrismaImport(model)) {
1796
+ imports.push('import { Prisma } from "@prisma/client";');
1797
+ }
1798
+ imports.push(`import { type ${modelName} } from "wasp/entities";`);
1799
+ }
1800
+ imports.push('import { HttpError } from "wasp/server";');
1801
+ imports.push(`import { type ${crudName} } from "wasp/server/crud";`);
1802
+ return imports.join("\n");
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
+ }
1840
+ /**
1841
+ * Generates operation code for overridden CRUD operations and returns as a single string.
1842
+ */
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("");
1861
+ }
1862
+ /**
1863
+ * Generates a CRUD definition for the feature configuration.
1864
+ */
1865
+ getDefinition(crudName, dataType, operations) {
1866
+ const templatePath = this.templateUtility.resolveTemplatePath(
1867
+ "config/crud.eta",
1868
+ "crud",
1869
+ import.meta.url
1870
+ );
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
+ });
1877
+ }
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;
1936
+ }
1937
+ },
1938
+ {
1939
+ message: "Args must be a valid JSON object string"
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
+ });
1954
+
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
+ `;
1983
+ }
1984
+ const replacements = {
1985
+ imports,
1986
+ jobType,
1987
+ jobName
1988
+ };
1989
+ await this.renderTemplateToFile(
1990
+ "job.eta",
1991
+ replacements,
1992
+ targetFile,
1993
+ "job worker",
1994
+ args.force || false
1995
+ );
1996
+ }
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
2017
+ );
2018
+ }
2019
+ /**
2020
+ * Generates a job definition for the feature configuration.
2021
+ */
2022
+ getDefinition(jobName, entities, cron, args) {
2023
+ const templatePath = this.getDefaultTemplatePath("config/job.eta");
2024
+ return this.templateUtility.processTemplate(templatePath, {
2025
+ jobName,
2026
+ entities: entities.map((e) => `"${e}"`).join(", "),
2027
+ cron,
2028
+ args
2029
+ });
2030
+ }
2031
+ };
2032
+
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(", ")}`
2045
+ });
2046
+ var schema7 = z8.object({
2047
+ feature: commonSchemas.feature,
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) ?? {}),
2054
+ entities: commonSchemas.entities,
2055
+ force: commonSchemas.force,
2056
+ auth: commonSchemas.auth
2057
+ });
2058
+
2059
+ // src/generators/query/query-generator.ts
2060
+ var QueryGenerator = class extends OperationGeneratorBase {
2061
+ get componentType() {
2062
+ return CONFIG_TYPES.QUERY;
2063
+ }
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 ?? [];
2071
+ if (dataType && !entities.includes(dataType)) {
2072
+ entities.unshift(dataType);
2073
+ }
2074
+ const { operationCode, operationName } = await this.generateOperationComponents(
2075
+ dataType,
2076
+ operation,
2077
+ args.auth,
2078
+ entities,
2079
+ false,
2080
+ null,
2081
+ name
2082
+ );
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
+ }
2113
+ );
2114
+ }
2115
+ };
2116
+
2117
+ // src/generators/route/route-generator.ts
2118
+ import {
2119
+ formatDisplayName,
2120
+ toCamelCase as toCamelCase6,
2121
+ toPascalCase as toPascalCase5
2122
+ } from "@ingenyus/swarm";
2123
+
2124
+ // src/generators/route/schema.ts
2125
+ import { z as z9 } from "zod";
2126
+ var schema8 = z9.object({
2127
+ feature: commonSchemas.feature,
2128
+ name: commonSchemas.name,
2129
+ path: commonSchemas.path,
2130
+ auth: commonSchemas.auth,
2131
+ force: commonSchemas.force
2132
+ });
2133
+
2134
+ // src/generators/route/route-generator.ts
2135
+ var RouteGenerator = class extends ComponentGeneratorBase {
2136
+ get componentType() {
2137
+ return CONFIG_TYPES.ROUTE;
2138
+ }
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`;
2146
+ return this.handleGeneratorError(
2147
+ this.componentType,
2148
+ routeName,
2149
+ async () => {
2150
+ const configPath = this.validateFeatureConfig(feature);
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);
2155
+ }
2156
+ );
2157
+ }
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
2178
+ );
2179
+ this.updateConfigWithCheck(
2180
+ configPath,
2181
+ "addRoute",
2182
+ routeName,
2183
+ definition,
2184
+ featurePath,
2185
+ args.force || false
2186
+ );
2187
+ }
2188
+ /**
2189
+ * Generates a route definition for the feature configuration.
2190
+ */
2191
+ getDefinition(routeName, routePath, featurePath, auth = false) {
2192
+ const templatePath = this.getDefaultTemplatePath("config/route.eta");
2193
+ return this.templateUtility.processTemplate(templatePath, {
2194
+ featureName: featurePath.split("/").pop() || featurePath,
2195
+ routeName,
2196
+ routePath,
2197
+ auth: String(auth)
2198
+ });
2199
+ }
2200
+ };
2201
+ export {
2202
+ ActionGenerator,
2203
+ ApiGenerator,
2204
+ ApiNamespaceGenerator,
2205
+ CrudGenerator,
2206
+ FeatureGenerator,
2207
+ JobGenerator,
2208
+ QueryGenerator,
2209
+ RouteGenerator,
2210
+ WaspConfigGenerator
2211
+ };