@appswave/rq-codegen 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1788 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/generate.ts
7
+ import chalk from "chalk";
8
+ import inquirer from "inquirer";
9
+
10
+ // src/config/loader.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { pathToFileURL } from "url";
14
+
15
+ // src/config/defaults.ts
16
+ var DEFAULT_CONFIG = {
17
+ srcDir: "./src",
18
+ aliases: {
19
+ api: "@api",
20
+ components: "@components",
21
+ hooks: "@hooks",
22
+ types: "@app-types",
23
+ utils: "@utils",
24
+ contexts: "@contexts",
25
+ constants: "@constants",
26
+ views: "@views",
27
+ pages: "@pages",
28
+ validations: "@validations",
29
+ assets: "@assets",
30
+ routes: "@routes",
31
+ hoc: "@hoc",
32
+ appConfig: "@app-config"
33
+ },
34
+ features: {
35
+ i18n: true,
36
+ toast: true,
37
+ barrel: true,
38
+ routeRegistration: false
39
+ },
40
+ naming: {
41
+ dtoSuffixes: {
42
+ read: "ForReadDto",
43
+ create: "ForCreateDto",
44
+ update: "ForUpdateDto",
45
+ list: "ListDto",
46
+ listResponse: "ListResponseDto",
47
+ params: "ParamsDto"
48
+ },
49
+ validationSuffix: ".schema.ts",
50
+ pageSuffix: "Page",
51
+ hookPrefix: "use"
52
+ },
53
+ paths: {
54
+ handlers: "api/handlers",
55
+ apiConfig: "api/config",
56
+ types: "types/api",
57
+ queries: "lib/hooks/queries",
58
+ mutations: "lib/hooks/mutations",
59
+ sharedHooks: "lib/hooks/shared",
60
+ hookUtils: "lib/hooks/utils",
61
+ uiComponents: "components/ui",
62
+ sharedComponents: "components/shared",
63
+ formComponents: "components/forms",
64
+ pages: "pages",
65
+ views: "views",
66
+ validations: "validations"
67
+ },
68
+ router: {
69
+ routerFile: "routes/router.tsx",
70
+ routesFile: "routes/routes.ts",
71
+ layouts: ["MainLayout", "DashboardLayout"]
72
+ },
73
+ hooks: {
74
+ toast: { import: "useToast", from: "@hooks/shared" },
75
+ translation: { import: "useAppTranslation", from: "@hooks/shared" },
76
+ paginatedQuery: { import: "usePaginatedDataTableQuery", from: "@hooks/utils" }
77
+ }
78
+ };
79
+
80
+ // src/config/schema.ts
81
+ import { z } from "zod";
82
+ var hookImportSchema = z.object({
83
+ import: z.string(),
84
+ from: z.string()
85
+ });
86
+ var configSchema = z.object({
87
+ srcDir: z.string().optional(),
88
+ aliases: z.object({
89
+ api: z.string().optional(),
90
+ components: z.string().optional(),
91
+ hooks: z.string().optional(),
92
+ types: z.string().optional(),
93
+ utils: z.string().optional(),
94
+ contexts: z.string().optional(),
95
+ constants: z.string().optional(),
96
+ views: z.string().optional(),
97
+ pages: z.string().optional(),
98
+ validations: z.string().optional(),
99
+ assets: z.string().optional(),
100
+ routes: z.string().optional(),
101
+ hoc: z.string().optional(),
102
+ appConfig: z.string().optional()
103
+ }).optional(),
104
+ features: z.object({
105
+ i18n: z.boolean().optional(),
106
+ toast: z.boolean().optional(),
107
+ barrel: z.boolean().optional(),
108
+ routeRegistration: z.boolean().optional()
109
+ }).optional(),
110
+ naming: z.object({
111
+ dtoSuffixes: z.object({
112
+ read: z.string().optional(),
113
+ create: z.string().optional(),
114
+ update: z.string().optional(),
115
+ list: z.string().optional(),
116
+ listResponse: z.string().optional(),
117
+ params: z.string().optional()
118
+ }).optional(),
119
+ validationSuffix: z.string().optional(),
120
+ pageSuffix: z.string().optional(),
121
+ hookPrefix: z.string().optional()
122
+ }).optional(),
123
+ paths: z.object({
124
+ handlers: z.string().optional(),
125
+ apiConfig: z.string().optional(),
126
+ types: z.string().optional(),
127
+ queries: z.string().optional(),
128
+ mutations: z.string().optional(),
129
+ sharedHooks: z.string().optional(),
130
+ hookUtils: z.string().optional(),
131
+ uiComponents: z.string().optional(),
132
+ sharedComponents: z.string().optional(),
133
+ formComponents: z.string().optional(),
134
+ pages: z.string().optional(),
135
+ views: z.string().optional(),
136
+ validations: z.string().optional()
137
+ }).optional(),
138
+ router: z.object({
139
+ routerFile: z.string().optional(),
140
+ routesFile: z.string().optional(),
141
+ layouts: z.array(z.string()).optional()
142
+ }).optional(),
143
+ hooks: z.object({
144
+ toast: hookImportSchema.optional(),
145
+ translation: hookImportSchema.optional(),
146
+ paginatedQuery: hookImportSchema.optional()
147
+ }).optional(),
148
+ templatesDir: z.string().optional()
149
+ });
150
+
151
+ // src/config/loader.ts
152
+ function deepMerge(target, source) {
153
+ const result = { ...target };
154
+ for (const key of Object.keys(source)) {
155
+ const sourceVal = source[key];
156
+ const targetVal = target[key];
157
+ if (sourceVal === void 0) continue;
158
+ if (sourceVal !== null && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
159
+ result[key] = deepMerge(
160
+ targetVal,
161
+ sourceVal
162
+ );
163
+ } else {
164
+ result[key] = sourceVal;
165
+ }
166
+ }
167
+ return result;
168
+ }
169
+ function detectAliasesFromTsConfig(projectRoot) {
170
+ const configFiles = ["tsconfig.app.json", "tsconfig.json"];
171
+ for (const configFile of configFiles) {
172
+ const fullPath = path.resolve(projectRoot, configFile);
173
+ if (!fs.existsSync(fullPath)) continue;
174
+ try {
175
+ const raw = JSON.parse(fs.readFileSync(fullPath, "utf-8"));
176
+ const paths = raw.compilerOptions?.paths ?? {};
177
+ return mapTsPathsToAliases(paths);
178
+ } catch {
179
+ continue;
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+ function mapTsPathsToAliases(paths) {
185
+ const aliases = {};
186
+ const aliasMap = {
187
+ "api": "api",
188
+ "components": "components",
189
+ "hooks": "hooks",
190
+ "app-types": "types",
191
+ "types": "types",
192
+ "utils": "utils",
193
+ "contexts": "contexts",
194
+ "constants": "constants",
195
+ "views": "views",
196
+ "pages": "pages",
197
+ "validations": "validations",
198
+ "assets": "assets",
199
+ "routes": "routes",
200
+ "hoc": "hoc",
201
+ "app-config": "appConfig"
202
+ };
203
+ for (const tsAlias of Object.keys(paths)) {
204
+ const match = tsAlias.match(/^@([\w-]+)/);
205
+ if (!match) continue;
206
+ const aliasName = match[1];
207
+ const configKey = aliasMap[aliasName];
208
+ if (configKey) {
209
+ aliases[configKey] = tsAlias.replace("/*", "");
210
+ }
211
+ }
212
+ return aliases;
213
+ }
214
+ async function findConfigFile(projectRoot) {
215
+ const candidates = [
216
+ "rqgen.config.ts",
217
+ "rqgen.config.mts",
218
+ "rqgen.config.js",
219
+ "rqgen.config.mjs"
220
+ ];
221
+ for (const candidate of candidates) {
222
+ const fullPath = path.resolve(projectRoot, candidate);
223
+ if (fs.existsSync(fullPath)) return fullPath;
224
+ }
225
+ const pkgPath = path.resolve(projectRoot, "package.json");
226
+ if (fs.existsSync(pkgPath)) {
227
+ try {
228
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
229
+ if (pkg.rqgen) return pkgPath;
230
+ } catch {
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+ async function loadConfigFromFile(configPath) {
236
+ const ext = path.extname(configPath);
237
+ const basename = path.basename(configPath);
238
+ if (basename === "package.json") {
239
+ const pkg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
240
+ return pkg.rqgen ?? {};
241
+ }
242
+ if (ext === ".ts" || ext === ".mts") {
243
+ try {
244
+ const fileUrl2 = pathToFileURL(configPath).href;
245
+ const mod2 = await import(fileUrl2);
246
+ return mod2.default ?? mod2;
247
+ } catch {
248
+ throw new Error(
249
+ `Failed to load TypeScript config: ${configPath}
250
+ Make sure you have "tsx" installed: npm i -D tsx
251
+ And run with: NODE_OPTIONS="--import tsx" rq-codegen`
252
+ );
253
+ }
254
+ }
255
+ const fileUrl = pathToFileURL(configPath).href;
256
+ const mod = await import(fileUrl);
257
+ return mod.default ?? mod;
258
+ }
259
+ async function loadConfig(projectRoot) {
260
+ const root = projectRoot ?? process.cwd();
261
+ const configPath = await findConfigFile(root);
262
+ let userConfig = {};
263
+ if (configPath) {
264
+ const rawConfig = await loadConfigFromFile(configPath);
265
+ const parsed = configSchema.safeParse(rawConfig);
266
+ if (!parsed.success) {
267
+ const errors = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`);
268
+ throw new Error(`Invalid rqgen config:
269
+ ${errors.join("\n")}`);
270
+ }
271
+ userConfig = parsed.data;
272
+ }
273
+ if (!userConfig.aliases) {
274
+ const detected = detectAliasesFromTsConfig(root);
275
+ if (detected) {
276
+ userConfig.aliases = detected;
277
+ }
278
+ }
279
+ const config = deepMerge(DEFAULT_CONFIG, userConfig);
280
+ return config;
281
+ }
282
+
283
+ // src/core/engine.ts
284
+ import fs5 from "fs";
285
+ import path5 from "path";
286
+ import Handlebars2 from "handlebars";
287
+
288
+ // src/core/helpers.ts
289
+ function registerHelpers(handlebars, config) {
290
+ handlebars.registerHelper("constantCase", (text) => {
291
+ return text.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toUpperCase();
292
+ });
293
+ handlebars.registerHelper("eq", (a, b) => a === b);
294
+ handlebars.registerHelper("neq", (a, b) => a !== b);
295
+ handlebars.registerHelper("plural", (text) => {
296
+ if (text.endsWith("s")) return text;
297
+ if (text.endsWith("y") && !/[aeiou]y$/i.test(text)) return text.slice(0, -1) + "ies";
298
+ return text + "s";
299
+ });
300
+ handlebars.registerHelper("includes", (arr, val) => {
301
+ return Array.isArray(arr) && arr.includes(val);
302
+ });
303
+ handlebars.registerHelper("join", (arr, separator) => {
304
+ return Array.isArray(arr) ? arr.join(separator) : "";
305
+ });
306
+ handlebars.registerHelper("configAlias", (key) => {
307
+ return config.aliases[key] ?? `@${key}`;
308
+ });
309
+ handlebars.registerHelper("configPath", (key) => {
310
+ return config.paths[key] ?? key;
311
+ });
312
+ handlebars.registerHelper("dtoSuffix", (key) => {
313
+ return config.naming.dtoSuffixes[key] ?? "";
314
+ });
315
+ handlebars.registerHelper("ifFeature", function(feature, options) {
316
+ const isEnabled = config.features[feature];
317
+ return isEnabled ? options.fn(this) : options.inverse(this);
318
+ });
319
+ handlebars.registerHelper("pascalCase", (text) => {
320
+ return text.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (_, c) => c.toUpperCase());
321
+ });
322
+ handlebars.registerHelper("camelCase", (text) => {
323
+ return text.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (_, c) => c.toLowerCase());
324
+ });
325
+ handlebars.registerHelper("kebabCase", (text) => {
326
+ return text.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
327
+ });
328
+ }
329
+
330
+ // src/core/actions.ts
331
+ import fs3 from "fs";
332
+ import path3 from "path";
333
+ import Handlebars from "handlebars";
334
+
335
+ // src/utils/string.ts
336
+ function toPascalCase(str) {
337
+ return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (_, c) => c.toUpperCase());
338
+ }
339
+ function toKebabCase(str) {
340
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
341
+ }
342
+ function toConstantCase(str) {
343
+ return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toUpperCase();
344
+ }
345
+
346
+ // src/utils/fs.ts
347
+ import fs2 from "fs";
348
+ import path2 from "path";
349
+ function dirExists(dirPath) {
350
+ const resolved = path2.resolve(process.cwd(), dirPath);
351
+ return fs2.existsSync(resolved) && fs2.statSync(resolved).isDirectory();
352
+ }
353
+ function getDirectories(dirPath) {
354
+ const resolved = path2.resolve(process.cwd(), dirPath);
355
+ if (!fs2.existsSync(resolved)) return [];
356
+ return fs2.readdirSync(resolved, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
357
+ }
358
+ function ensureDirectoryExists(filePath) {
359
+ const dir = path2.dirname(filePath);
360
+ if (!fs2.existsSync(dir)) {
361
+ fs2.mkdirSync(dir, { recursive: true });
362
+ }
363
+ }
364
+
365
+ // src/core/actions.ts
366
+ function barrelAppend(barrelRelativePath, exportLine, config, data) {
367
+ if (!config.features.barrel) {
368
+ return { type: "skipped", path: barrelRelativePath, message: "Barrel updates disabled" };
369
+ }
370
+ const handlebars = Handlebars.create();
371
+ handlebars.registerHelper(
372
+ "kebabCase",
373
+ (text) => text.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase()
374
+ );
375
+ handlebars.registerHelper(
376
+ "pascalCase",
377
+ (text) => text.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (_, c) => c.toUpperCase())
378
+ );
379
+ handlebars.registerHelper(
380
+ "camelCase",
381
+ (text) => text.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (_, c) => c.toLowerCase())
382
+ );
383
+ const renderedPath = handlebars.compile(barrelRelativePath)(data);
384
+ const barrelPath = path3.resolve(process.cwd(), config.srcDir, renderedPath);
385
+ const renderedExport = handlebars.compile(exportLine)(data);
386
+ ensureDirectoryExists(barrelPath);
387
+ let content = "";
388
+ if (fs3.existsSync(barrelPath)) {
389
+ content = fs3.readFileSync(barrelPath, "utf-8");
390
+ }
391
+ if (content.includes(renderedExport)) {
392
+ return { type: "skipped", path: barrelPath, message: "Export already exists" };
393
+ }
394
+ const trimmedContent = content.trimEnd();
395
+ const newContent = trimmedContent ? `${trimmedContent}
396
+ ${renderedExport}
397
+ ` : `${renderedExport}
398
+ `;
399
+ fs3.writeFileSync(barrelPath, newContent, "utf-8");
400
+ return { type: "updated", path: barrelPath, message: `Updated barrel: ${renderedPath}` };
401
+ }
402
+ function routeRegister(config, data) {
403
+ const results = [];
404
+ const routerFilePath = path3.resolve(process.cwd(), config.srcDir, config.router.routerFile);
405
+ const routesFilePath = path3.resolve(process.cwd(), config.srcDir, config.router.routesFile);
406
+ if (!fs3.existsSync(routerFilePath)) {
407
+ return [{ type: "failed", path: routerFilePath, message: "Router file not found" }];
408
+ }
409
+ if (!fs3.existsSync(routesFilePath)) {
410
+ return [{ type: "failed", path: routesFilePath, message: "Routes file not found" }];
411
+ }
412
+ const pascalName = toPascalCase(data.pageName);
413
+ const kebabCategory = toKebabCase(data.category);
414
+ const constantCategory = toConstantCase(data.category);
415
+ const constantPage = toConstantCase(data.pageName);
416
+ const pageSuffix = config.naming.pageSuffix;
417
+ const pagesAlias = config.aliases.pages;
418
+ let routerContent = fs3.readFileSync(routerFilePath, "utf-8");
419
+ const lazyImportLine = `const ${pascalName}${pageSuffix} = lazy(() => import('${pagesAlias}/${kebabCategory}/${pascalName}${pageSuffix}'));`;
420
+ if (!routerContent.includes(lazyImportLine)) {
421
+ const lazyImportRegex = /^const \w+ = lazy\(\(\) => import\([^)]+\)\);$/gm;
422
+ let lastMatch = null;
423
+ let match;
424
+ while ((match = lazyImportRegex.exec(routerContent)) !== null) {
425
+ lastMatch = match;
426
+ }
427
+ if (lastMatch) {
428
+ const insertPos = lastMatch.index + lastMatch[0].length;
429
+ routerContent = routerContent.slice(0, insertPos) + "\n" + lazyImportLine + routerContent.slice(insertPos);
430
+ fs3.writeFileSync(routerFilePath, routerContent, "utf-8");
431
+ results.push({ type: "updated", path: routerFilePath, message: `Added lazy import for ${pascalName}${pageSuffix}` });
432
+ } else {
433
+ results.push({
434
+ type: "failed",
435
+ path: routerFilePath,
436
+ message: `Could not find lazy import insertion point. Add manually:
437
+ ${lazyImportLine}`
438
+ });
439
+ }
440
+ } else {
441
+ results.push({ type: "skipped", path: routerFilePath, message: "Lazy import already exists" });
442
+ }
443
+ let routesContent = fs3.readFileSync(routesFilePath, "utf-8");
444
+ const routeConstant = ` ${constantPage}: '${data.routePath}',`;
445
+ const categoryRegex = new RegExp(`${constantCategory}:\\s*\\{([^}]*)\\}`, "s");
446
+ const categoryMatch = categoryRegex.exec(routesContent);
447
+ if (categoryMatch) {
448
+ if (!routesContent.includes(routeConstant)) {
449
+ const closingBrace = routesContent.indexOf("}", categoryMatch.index + categoryMatch[0].indexOf("{"));
450
+ routesContent = routesContent.slice(0, closingBrace) + routeConstant + "\n " + routesContent.slice(closingBrace);
451
+ fs3.writeFileSync(routesFilePath, routesContent, "utf-8");
452
+ results.push({ type: "updated", path: routesFilePath, message: `Added route constant ${constantCategory}.${constantPage}` });
453
+ } else {
454
+ results.push({ type: "skipped", path: routesFilePath, message: "Route constant already exists" });
455
+ }
456
+ } else {
457
+ const newSection = ` ${constantCategory}: {
458
+ ${routeConstant}
459
+ },`;
460
+ const fullRoutesEnd = routesContent.lastIndexOf("} as const");
461
+ if (fullRoutesEnd !== -1) {
462
+ routesContent = routesContent.slice(0, fullRoutesEnd) + newSection + "\n" + routesContent.slice(fullRoutesEnd);
463
+ fs3.writeFileSync(routesFilePath, routesContent, "utf-8");
464
+ results.push({ type: "updated", path: routesFilePath, message: `Added new route category ${constantCategory}` });
465
+ } else {
466
+ results.push({
467
+ type: "failed",
468
+ path: routesFilePath,
469
+ message: `Could not find FULL_ROUTES_PATH. Add manually:
470
+ ${constantCategory}: { ${constantPage}: '${data.routePath}' }`
471
+ });
472
+ }
473
+ }
474
+ return results;
475
+ }
476
+
477
+ // src/core/template-resolver.ts
478
+ import fs4 from "fs";
479
+ import path4 from "path";
480
+ import { fileURLToPath } from "url";
481
+ var __filename = fileURLToPath(import.meta.url);
482
+ var __dirname = path4.dirname(__filename);
483
+ var BUNDLED_TEMPLATES_DIR = path4.resolve(__dirname, "../../templates");
484
+ function resolveTemplatePath(templateRelative, config) {
485
+ if (config.templatesDir) {
486
+ const localPath = path4.resolve(process.cwd(), config.templatesDir, templateRelative);
487
+ if (fs4.existsSync(localPath)) return localPath;
488
+ }
489
+ const bundledPath = path4.resolve(BUNDLED_TEMPLATES_DIR, templateRelative);
490
+ if (fs4.existsSync(bundledPath)) return bundledPath;
491
+ throw new Error(
492
+ `Template not found: ${templateRelative}
493
+ Searched:
494
+ ` + (config.templatesDir ? ` - ${path4.resolve(process.cwd(), config.templatesDir, templateRelative)}
495
+ ` : "") + ` - ${bundledPath}`
496
+ );
497
+ }
498
+
499
+ // src/core/engine.ts
500
+ var handlebarsInstance = null;
501
+ function getHandlebars(config) {
502
+ if (!handlebarsInstance) {
503
+ handlebarsInstance = Handlebars2.create();
504
+ registerHelpers(handlebarsInstance, config);
505
+ }
506
+ return handlebarsInstance;
507
+ }
508
+ function resetHandlebars() {
509
+ handlebarsInstance = null;
510
+ }
511
+ function renderString(template, data, config) {
512
+ const hbs = getHandlebars(config);
513
+ return hbs.compile(template)(data);
514
+ }
515
+ async function executeActions(actions, answers, config) {
516
+ const results = [];
517
+ const hbs = getHandlebars(config);
518
+ for (const action of actions) {
519
+ switch (action.type) {
520
+ case "add": {
521
+ try {
522
+ const mergedData = { ...answers, ...action.data ?? {}, config };
523
+ const resolvedPath = renderString(action.path, mergedData, config);
524
+ const outputPath = path5.resolve(process.cwd(), config.srcDir, resolvedPath);
525
+ const templatePath = resolveTemplatePath(action.templateFile, config);
526
+ const templateContent = fs5.readFileSync(templatePath, "utf-8");
527
+ const compiled = hbs.compile(templateContent);
528
+ const rendered = compiled(mergedData);
529
+ ensureDirectoryExists(outputPath);
530
+ if (fs5.existsSync(outputPath)) {
531
+ results.push({
532
+ type: "skipped",
533
+ path: outputPath,
534
+ message: "File already exists"
535
+ });
536
+ } else {
537
+ fs5.writeFileSync(outputPath, rendered, "utf-8");
538
+ results.push({
539
+ type: "created",
540
+ path: outputPath,
541
+ message: `Created ${resolvedPath}`
542
+ });
543
+ }
544
+ } catch (error) {
545
+ results.push({
546
+ type: "failed",
547
+ path: action.path,
548
+ message: error instanceof Error ? error.message : String(error)
549
+ });
550
+ }
551
+ break;
552
+ }
553
+ case "barrel-append": {
554
+ const mergedData = { ...answers, config };
555
+ const result = barrelAppend(action.path, action.exportLine, config, mergedData);
556
+ results.push(result);
557
+ break;
558
+ }
559
+ case "route-register": {
560
+ const routeResults = routeRegister(config, action.data);
561
+ results.push(...routeResults);
562
+ break;
563
+ }
564
+ }
565
+ }
566
+ return results;
567
+ }
568
+
569
+ // src/utils/validation.ts
570
+ function validateName(value) {
571
+ if (!value || value.trim() === "") return "Name is required";
572
+ if (/[^a-zA-Z0-9-]/.test(value)) return "Name must be alphanumeric (hyphens allowed)";
573
+ return true;
574
+ }
575
+
576
+ // src/generators/component-ui.ts
577
+ function componentUiPrompts() {
578
+ return [
579
+ {
580
+ type: "input",
581
+ name: "name",
582
+ message: "Component name (e.g., StatusIndicator):",
583
+ validate: validateName
584
+ }
585
+ ];
586
+ }
587
+ function componentUiActions(_answers, config) {
588
+ const basePath = config.paths.uiComponents;
589
+ return [
590
+ {
591
+ type: "add",
592
+ path: `${basePath}/{{kebabCase name}}/{{pascalCase name}}.tsx`,
593
+ templateFile: "component-ui/Component.tsx.hbs"
594
+ },
595
+ {
596
+ type: "add",
597
+ path: `${basePath}/{{kebabCase name}}/index.ts`,
598
+ templateFile: "component-ui/index.ts.hbs"
599
+ },
600
+ {
601
+ type: "barrel-append",
602
+ path: `${basePath}/index.ts`,
603
+ exportLine: "export * from './{{kebabCase name}}';"
604
+ }
605
+ ];
606
+ }
607
+
608
+ // src/generators/component-shared.ts
609
+ function componentSharedPrompts() {
610
+ return [
611
+ {
612
+ type: "input",
613
+ name: "name",
614
+ message: "Component name (e.g., InfoCard):",
615
+ validate: validateName
616
+ },
617
+ {
618
+ type: "input",
619
+ name: "subComponentsRaw",
620
+ message: "Sub-component names (comma-separated, e.g., Header,Body,Footer):",
621
+ default: "Header,Body,Footer"
622
+ }
623
+ ];
624
+ }
625
+ function preprocessComponentSharedAnswers(answers) {
626
+ return {
627
+ ...answers,
628
+ subComponents: answers.subComponentsRaw.split(",").map((s) => s.trim()).filter(Boolean)
629
+ };
630
+ }
631
+ function componentSharedActions(_answers, config) {
632
+ const basePath = config.paths.sharedComponents;
633
+ return [
634
+ {
635
+ type: "add",
636
+ path: `${basePath}/{{kebabCase name}}/{{pascalCase name}}.tsx`,
637
+ templateFile: "component-shared/Component.tsx.hbs"
638
+ },
639
+ {
640
+ type: "add",
641
+ path: `${basePath}/{{kebabCase name}}/index.ts`,
642
+ templateFile: "component-shared/index.ts.hbs"
643
+ },
644
+ {
645
+ type: "barrel-append",
646
+ path: `${basePath}/index.ts`,
647
+ exportLine: "export * from './{{kebabCase name}}';"
648
+ }
649
+ ];
650
+ }
651
+
652
+ // src/generators/component-form.ts
653
+ function componentFormPrompts() {
654
+ return [
655
+ {
656
+ type: "input",
657
+ name: "name",
658
+ message: "Field name (e.g., DateRange, Slider):",
659
+ validate: validateName
660
+ }
661
+ ];
662
+ }
663
+ function componentFormActions(_answers, config) {
664
+ const basePath = config.paths.formComponents;
665
+ return [
666
+ {
667
+ type: "add",
668
+ path: `${basePath}/form-{{kebabCase name}}/Form{{pascalCase name}}.tsx`,
669
+ templateFile: "component-form/FormComponent.tsx.hbs"
670
+ },
671
+ {
672
+ type: "add",
673
+ path: `${basePath}/form-{{kebabCase name}}/index.ts`,
674
+ templateFile: "component-form/index.ts.hbs"
675
+ },
676
+ {
677
+ type: "barrel-append",
678
+ path: `${basePath}/index.ts`,
679
+ exportLine: "export * from './form-{{kebabCase name}}';"
680
+ }
681
+ ];
682
+ }
683
+
684
+ // src/generators/page.ts
685
+ function pagePrompts(config) {
686
+ const prompts = [
687
+ {
688
+ type: "input",
689
+ name: "name",
690
+ message: "Page name (e.g., Communities, EventDetails):",
691
+ validate: validateName
692
+ },
693
+ {
694
+ type: "list",
695
+ name: "category",
696
+ message: "Page category (folder):",
697
+ choices: () => {
698
+ const dirs = getDirectories(`${config.srcDir}/${config.paths.pages}`);
699
+ return [...dirs, "\u2500\u2500 Create new category \u2500\u2500"];
700
+ }
701
+ },
702
+ {
703
+ type: "input",
704
+ name: "newCategory",
705
+ message: "New category name (kebab-case):",
706
+ when: (answers) => answers.category === "\u2500\u2500 Create new category \u2500\u2500",
707
+ validate: validateName
708
+ }
709
+ ];
710
+ if (config.features.routeRegistration) {
711
+ prompts.push(
712
+ {
713
+ type: "confirm",
714
+ name: "registerRoute",
715
+ message: "Auto-register route in router?",
716
+ default: true
717
+ },
718
+ {
719
+ type: "list",
720
+ name: "layout",
721
+ message: "Which layout?",
722
+ choices: () => config.router.layouts,
723
+ when: (answers) => !!answers.registerRoute
724
+ },
725
+ {
726
+ type: "confirm",
727
+ name: "isProtected",
728
+ message: "Is this a protected route (requires auth)?",
729
+ default: true,
730
+ when: (answers) => !!answers.registerRoute
731
+ },
732
+ {
733
+ type: "input",
734
+ name: "routePath",
735
+ message: "Route path (e.g., communities, events/details):",
736
+ when: (answers) => !!answers.registerRoute,
737
+ validate: validateName
738
+ }
739
+ );
740
+ }
741
+ return prompts;
742
+ }
743
+ function preprocessPageAnswers(answers) {
744
+ if (answers.newCategory) {
745
+ return { ...answers, category: answers.newCategory };
746
+ }
747
+ return answers;
748
+ }
749
+ function pageActions(answers, config) {
750
+ const basePath = config.paths.pages;
751
+ const pageSuffix = config.naming.pageSuffix;
752
+ const actions = [
753
+ {
754
+ type: "add",
755
+ path: `${basePath}/{{kebabCase category}}/{{pascalCase name}}${pageSuffix}.tsx`,
756
+ templateFile: "page/Page.tsx.hbs",
757
+ data: { pageSuffix }
758
+ }
759
+ ];
760
+ if (config.features.routeRegistration && answers.registerRoute) {
761
+ actions.push({
762
+ type: "route-register",
763
+ data: {
764
+ pageName: answers.name,
765
+ category: answers.category,
766
+ layout: answers.layout ?? config.router.layouts[0],
767
+ isProtected: answers.isProtected ?? true,
768
+ routePath: answers.routePath ?? answers.name.toLowerCase()
769
+ }
770
+ });
771
+ }
772
+ return actions;
773
+ }
774
+
775
+ // src/generators/view.ts
776
+ function viewPrompts(config) {
777
+ return [
778
+ {
779
+ type: "list",
780
+ name: "feature",
781
+ message: "Feature (parent folder):",
782
+ choices: () => {
783
+ const dirs = getDirectories(`${config.srcDir}/${config.paths.views}`);
784
+ return [...dirs, "\u2500\u2500 Create new feature \u2500\u2500"];
785
+ }
786
+ },
787
+ {
788
+ type: "input",
789
+ name: "newFeature",
790
+ message: "New feature name (kebab-case):",
791
+ when: (answers) => answers.feature === "\u2500\u2500 Create new feature \u2500\u2500",
792
+ validate: validateName
793
+ },
794
+ {
795
+ type: "input",
796
+ name: "name",
797
+ message: "View component name (e.g., CommunityCard):",
798
+ validate: validateName
799
+ }
800
+ ];
801
+ }
802
+ function preprocessViewAnswers(answers) {
803
+ if (answers.newFeature) {
804
+ return { ...answers, feature: answers.newFeature };
805
+ }
806
+ return answers;
807
+ }
808
+ function viewActions(answers, config) {
809
+ const basePath = config.paths.views;
810
+ const isNewFeature = !!answers.newFeature;
811
+ const actions = [
812
+ {
813
+ type: "add",
814
+ path: `${basePath}/{{kebabCase feature}}/{{kebabCase name}}/{{pascalCase name}}.tsx`,
815
+ templateFile: "view/View.tsx.hbs"
816
+ },
817
+ {
818
+ type: "add",
819
+ path: `${basePath}/{{kebabCase feature}}/{{kebabCase name}}/index.ts`,
820
+ templateFile: "view/index.ts.hbs"
821
+ },
822
+ {
823
+ type: "barrel-append",
824
+ path: `${basePath}/{{kebabCase feature}}/index.ts`,
825
+ exportLine: "export * from './{{kebabCase name}}';"
826
+ }
827
+ ];
828
+ if (isNewFeature || !dirExists(`${config.srcDir}/${config.paths.views}/${answers.feature}`)) {
829
+ actions.push({
830
+ type: "barrel-append",
831
+ path: `${basePath}/index.ts`,
832
+ exportLine: "export * from './{{kebabCase feature}}';"
833
+ });
834
+ }
835
+ return actions;
836
+ }
837
+
838
+ // src/generators/handler.ts
839
+ function handlerPrompts() {
840
+ return [
841
+ {
842
+ type: "input",
843
+ name: "name",
844
+ message: "Entity name (e.g., products):",
845
+ validate: validateName
846
+ },
847
+ {
848
+ type: "input",
849
+ name: "singularName",
850
+ message: "Singular entity name for mutations (e.g., product):",
851
+ validate: validateName
852
+ },
853
+ {
854
+ type: "input",
855
+ name: "endpointKey",
856
+ message: "ApiEndpoints key (e.g., PRODUCTS):"
857
+ },
858
+ {
859
+ type: "checkbox",
860
+ name: "operations",
861
+ message: "Which operations?",
862
+ choices: [
863
+ { name: "list", value: "list", checked: true },
864
+ { name: "details", value: "details", checked: true },
865
+ { name: "create", value: "create" },
866
+ { name: "update", value: "update" },
867
+ { name: "delete", value: "delete" }
868
+ ]
869
+ },
870
+ {
871
+ type: "confirm",
872
+ name: "chainTypes",
873
+ message: "Also generate DTO types?",
874
+ default: true
875
+ },
876
+ {
877
+ type: "confirm",
878
+ name: "chainQueryHook",
879
+ message: "Also generate query hook(s)?",
880
+ default: true
881
+ },
882
+ {
883
+ type: "confirm",
884
+ name: "isPaginated",
885
+ message: "Is the list endpoint paginated?",
886
+ default: false,
887
+ when: (answers) => {
888
+ const ops = answers.operations;
889
+ return !!answers.chainQueryHook && ops?.includes("list");
890
+ }
891
+ },
892
+ {
893
+ type: "confirm",
894
+ name: "chainMutationHook",
895
+ message: "Also generate mutation hook(s)?",
896
+ default: true
897
+ }
898
+ ];
899
+ }
900
+ function handlerActions(answers, config) {
901
+ const operations = answers.operations || [];
902
+ const entityName = answers.name;
903
+ const singularName = answers.singularName;
904
+ const pascalSingular = toPascalCase(singularName);
905
+ const actions = [
906
+ {
907
+ type: "add",
908
+ path: `${config.paths.handlers}/{{camelCase name}}.ts`,
909
+ templateFile: "handler/handler.ts.hbs",
910
+ data: { operations }
911
+ },
912
+ {
913
+ type: "barrel-append",
914
+ path: `${config.paths.handlers}/index.ts`,
915
+ exportLine: "export * from './{{camelCase name}}';"
916
+ }
917
+ ];
918
+ if (answers.chainTypes) {
919
+ actions.push(
920
+ {
921
+ type: "add",
922
+ path: `${config.paths.types}/{{pascalCase name}}Dto.ts`,
923
+ templateFile: "types-dto/dto.ts.hbs",
924
+ data: {
925
+ includeCreateDto: operations.includes("create"),
926
+ includeUpdateDto: operations.includes("update"),
927
+ includeParamsDto: true
928
+ }
929
+ },
930
+ {
931
+ type: "barrel-append",
932
+ path: `${config.paths.types}/index.ts`,
933
+ exportLine: "export * from './{{pascalCase name}}Dto';"
934
+ }
935
+ );
936
+ }
937
+ if (answers.chainQueryHook && operations.includes("list")) {
938
+ if (answers.isPaginated) {
939
+ actions.push(
940
+ {
941
+ type: "add",
942
+ path: `${config.paths.queries}/use{{pascalCase name}}PaginatedQuery.ts`,
943
+ templateFile: "query-hook/hook-paginated.ts.hbs",
944
+ data: { hookName: `${entityName}Paginated`, handlerName: entityName, handlerKey: "list" }
945
+ },
946
+ {
947
+ type: "barrel-append",
948
+ path: `${config.paths.queries}/index.ts`,
949
+ exportLine: "export * from './use{{pascalCase name}}PaginatedQuery';"
950
+ }
951
+ );
952
+ } else {
953
+ actions.push(
954
+ {
955
+ type: "add",
956
+ path: `${config.paths.queries}/use{{pascalCase name}}ListQuery.ts`,
957
+ templateFile: "query-hook/hook.ts.hbs",
958
+ data: { hookName: `${entityName}List`, handlerName: entityName, handlerKey: "list" }
959
+ },
960
+ {
961
+ type: "barrel-append",
962
+ path: `${config.paths.queries}/index.ts`,
963
+ exportLine: "export * from './use{{pascalCase name}}ListQuery';"
964
+ }
965
+ );
966
+ }
967
+ }
968
+ if (answers.chainQueryHook && operations.includes("details")) {
969
+ actions.push(
970
+ {
971
+ type: "add",
972
+ path: `${config.paths.queries}/use{{pascalCase name}}DetailsQuery.ts`,
973
+ templateFile: "query-hook/hook-details.ts.hbs",
974
+ data: { hookName: `${entityName}Details`, handlerName: entityName, handlerKey: "details" }
975
+ },
976
+ {
977
+ type: "barrel-append",
978
+ path: `${config.paths.queries}/index.ts`,
979
+ exportLine: "export * from './use{{pascalCase name}}DetailsQuery';"
980
+ }
981
+ );
982
+ }
983
+ if (answers.chainMutationHook && operations.includes("create")) {
984
+ actions.push(
985
+ {
986
+ type: "add",
987
+ path: `${config.paths.mutations}/useCreate${pascalSingular}Mutation.ts`,
988
+ templateFile: "mutation-hook/hook.ts.hbs",
989
+ data: { handlerName: entityName, handlerKey: "create", invalidateKey: "list", mutationName: `Create${pascalSingular}` }
990
+ },
991
+ {
992
+ type: "barrel-append",
993
+ path: `${config.paths.mutations}/index.ts`,
994
+ exportLine: `export * from './useCreate${pascalSingular}Mutation';`
995
+ }
996
+ );
997
+ }
998
+ if (answers.chainMutationHook && operations.includes("update")) {
999
+ actions.push(
1000
+ {
1001
+ type: "add",
1002
+ path: `${config.paths.mutations}/useUpdate${pascalSingular}Mutation.ts`,
1003
+ templateFile: "mutation-hook/hook.ts.hbs",
1004
+ data: { handlerName: entityName, handlerKey: "update", invalidateKey: "list", mutationName: `Update${pascalSingular}` }
1005
+ },
1006
+ {
1007
+ type: "barrel-append",
1008
+ path: `${config.paths.mutations}/index.ts`,
1009
+ exportLine: `export * from './useUpdate${pascalSingular}Mutation';`
1010
+ }
1011
+ );
1012
+ }
1013
+ if (answers.chainMutationHook && operations.includes("delete")) {
1014
+ actions.push(
1015
+ {
1016
+ type: "add",
1017
+ path: `${config.paths.mutations}/useDelete${pascalSingular}Mutation.ts`,
1018
+ templateFile: "mutation-hook/hook.ts.hbs",
1019
+ data: { handlerName: entityName, handlerKey: "remove", invalidateKey: "list", mutationName: `Delete${pascalSingular}` }
1020
+ },
1021
+ {
1022
+ type: "barrel-append",
1023
+ path: `${config.paths.mutations}/index.ts`,
1024
+ exportLine: `export * from './useDelete${pascalSingular}Mutation';`
1025
+ }
1026
+ );
1027
+ }
1028
+ return actions;
1029
+ }
1030
+
1031
+ // src/generators/query-hook.ts
1032
+ function queryHookPrompts() {
1033
+ return [
1034
+ {
1035
+ type: "input",
1036
+ name: "name",
1037
+ message: "Hook name suffix (e.g., CommunityList, ProductDetails):",
1038
+ validate: validateName
1039
+ },
1040
+ {
1041
+ type: "input",
1042
+ name: "handlerName",
1043
+ message: "Handler name to import (e.g., Community):",
1044
+ validate: validateName
1045
+ },
1046
+ {
1047
+ type: "input",
1048
+ name: "handlerKey",
1049
+ message: "Handler key (e.g., list, details):",
1050
+ default: "list"
1051
+ },
1052
+ {
1053
+ type: "confirm",
1054
+ name: "isDetailsQuery",
1055
+ message: "Is this a details/by-id query (takes an id parameter)?",
1056
+ default: false
1057
+ },
1058
+ {
1059
+ type: "confirm",
1060
+ name: "isPaginated",
1061
+ message: "Is this a paginated list query?",
1062
+ default: false,
1063
+ when: (answers) => !answers.isDetailsQuery
1064
+ }
1065
+ ];
1066
+ }
1067
+ function queryHookActions(answers, config) {
1068
+ let templateFile;
1069
+ if (answers.isDetailsQuery) {
1070
+ templateFile = "query-hook/hook-details.ts.hbs";
1071
+ } else if (answers.isPaginated) {
1072
+ templateFile = "query-hook/hook-paginated.ts.hbs";
1073
+ } else {
1074
+ templateFile = "query-hook/hook.ts.hbs";
1075
+ }
1076
+ return [
1077
+ {
1078
+ type: "add",
1079
+ path: `${config.paths.queries}/use{{pascalCase name}}Query.ts`,
1080
+ templateFile,
1081
+ data: { hookName: answers.name }
1082
+ },
1083
+ {
1084
+ type: "barrel-append",
1085
+ path: `${config.paths.queries}/index.ts`,
1086
+ exportLine: "export * from './use{{pascalCase name}}Query';"
1087
+ }
1088
+ ];
1089
+ }
1090
+
1091
+ // src/generators/mutation-hook.ts
1092
+ function mutationHookPrompts() {
1093
+ return [
1094
+ {
1095
+ type: "input",
1096
+ name: "mutationName",
1097
+ message: "Mutation name (e.g., CreateCommunity, UpdateUser):",
1098
+ validate: validateName
1099
+ },
1100
+ {
1101
+ type: "input",
1102
+ name: "handlerName",
1103
+ message: "Handler name to import (e.g., Community):",
1104
+ validate: validateName
1105
+ },
1106
+ {
1107
+ type: "input",
1108
+ name: "handlerKey",
1109
+ message: "Handler mutation key (e.g., create, update, remove):",
1110
+ default: "create"
1111
+ },
1112
+ {
1113
+ type: "input",
1114
+ name: "invalidateKey",
1115
+ message: "Handler query key to invalidate on success (e.g., list):",
1116
+ default: "list"
1117
+ }
1118
+ ];
1119
+ }
1120
+ function mutationHookActions(_answers, config) {
1121
+ return [
1122
+ {
1123
+ type: "add",
1124
+ path: `${config.paths.mutations}/use{{pascalCase mutationName}}Mutation.ts`,
1125
+ templateFile: "mutation-hook/hook.ts.hbs"
1126
+ },
1127
+ {
1128
+ type: "barrel-append",
1129
+ path: `${config.paths.mutations}/index.ts`,
1130
+ exportLine: "export * from './use{{pascalCase mutationName}}Mutation';"
1131
+ }
1132
+ ];
1133
+ }
1134
+
1135
+ // src/generators/types-dto.ts
1136
+ function typesDtoPrompts() {
1137
+ return [
1138
+ {
1139
+ type: "input",
1140
+ name: "name",
1141
+ message: "Entity name (e.g., Community, Event):",
1142
+ validate: validateName
1143
+ },
1144
+ {
1145
+ type: "confirm",
1146
+ name: "includeCreateDto",
1147
+ message: "Include ForCreateDto?",
1148
+ default: true
1149
+ },
1150
+ {
1151
+ type: "confirm",
1152
+ name: "includeUpdateDto",
1153
+ message: "Include ForUpdateDto?",
1154
+ default: true
1155
+ },
1156
+ {
1157
+ type: "confirm",
1158
+ name: "includeParamsDto",
1159
+ message: "Include ParamsDto?",
1160
+ default: true
1161
+ }
1162
+ ];
1163
+ }
1164
+ function typesDtoActions(_answers, config) {
1165
+ return [
1166
+ {
1167
+ type: "add",
1168
+ path: `${config.paths.types}/{{pascalCase name}}Dto.ts`,
1169
+ templateFile: "types-dto/dto.ts.hbs"
1170
+ },
1171
+ {
1172
+ type: "barrel-append",
1173
+ path: `${config.paths.types}/index.ts`,
1174
+ exportLine: "export * from './{{pascalCase name}}Dto';"
1175
+ }
1176
+ ];
1177
+ }
1178
+
1179
+ // src/generators/shared-hook.ts
1180
+ function sharedHookPrompts() {
1181
+ return [
1182
+ {
1183
+ type: "input",
1184
+ name: "name",
1185
+ message: "Hook name (e.g., WindowSize, Clipboard):",
1186
+ validate: validateName
1187
+ },
1188
+ {
1189
+ type: "checkbox",
1190
+ name: "reactImports",
1191
+ message: "Which React imports?",
1192
+ choices: [
1193
+ { name: "useState", value: "useState", checked: true },
1194
+ { name: "useEffect", value: "useEffect", checked: true },
1195
+ { name: "useCallback", value: "useCallback" },
1196
+ { name: "useMemo", value: "useMemo" },
1197
+ { name: "useRef", value: "useRef" }
1198
+ ]
1199
+ }
1200
+ ];
1201
+ }
1202
+ function sharedHookActions(_answers, config) {
1203
+ return [
1204
+ {
1205
+ type: "add",
1206
+ path: `${config.paths.sharedHooks}/use{{pascalCase name}}.ts`,
1207
+ templateFile: "shared-hook/hook.ts.hbs"
1208
+ },
1209
+ {
1210
+ type: "barrel-append",
1211
+ path: `${config.paths.sharedHooks}/index.ts`,
1212
+ exportLine: "export * from './use{{pascalCase name}}';"
1213
+ }
1214
+ ];
1215
+ }
1216
+
1217
+ // src/generators/validation.ts
1218
+ function validationPrompts() {
1219
+ return [
1220
+ {
1221
+ type: "input",
1222
+ name: "name",
1223
+ message: "Schema name (e.g., communitySettings, eventCreate):",
1224
+ validate: validateName
1225
+ }
1226
+ ];
1227
+ }
1228
+ function validationActions(_answers, config) {
1229
+ const suffix = config.naming.validationSuffix;
1230
+ const fileExt = suffix;
1231
+ const barrelExt = suffix.replace(/\.ts$/, "");
1232
+ return [
1233
+ {
1234
+ type: "add",
1235
+ path: `${config.paths.validations}/{{camelCase name}}${fileExt}`,
1236
+ templateFile: "validation/validation.ts.hbs"
1237
+ },
1238
+ {
1239
+ type: "barrel-append",
1240
+ path: `${config.paths.validations}/index.ts`,
1241
+ exportLine: `export * from './{{camelCase name}}${barrelExt}';`
1242
+ }
1243
+ ];
1244
+ }
1245
+
1246
+ // src/generators/feature.ts
1247
+ function featurePrompts() {
1248
+ return [
1249
+ {
1250
+ type: "input",
1251
+ name: "name",
1252
+ message: "Feature/entity name (e.g., products):",
1253
+ validate: validateName
1254
+ },
1255
+ {
1256
+ type: "input",
1257
+ name: "singularName",
1258
+ message: "Singular entity name for mutations (e.g., product):",
1259
+ validate: validateName
1260
+ },
1261
+ {
1262
+ type: "input",
1263
+ name: "endpointKey",
1264
+ message: "ApiEndpoints key (e.g., PRODUCTS):"
1265
+ },
1266
+ {
1267
+ type: "checkbox",
1268
+ name: "artifacts",
1269
+ message: "Which artifacts to generate?",
1270
+ choices: [
1271
+ { name: "API Handler", value: "handler", checked: true },
1272
+ { name: "DTO Types", value: "types", checked: true },
1273
+ { name: "Query Hook (list)", value: "queryList", checked: true },
1274
+ { name: "Query Hook (details)", value: "queryDetails", checked: true },
1275
+ { name: "Mutation Hook (create)", value: "mutationCreate" },
1276
+ { name: "Mutation Hook (update)", value: "mutationUpdate" },
1277
+ { name: "Mutation Hook (delete)", value: "mutationDelete" },
1278
+ { name: "View Component", value: "view" },
1279
+ { name: "Page Component", value: "page" },
1280
+ { name: "Validation Schema", value: "validation" }
1281
+ ]
1282
+ },
1283
+ {
1284
+ type: "confirm",
1285
+ name: "isPaginated",
1286
+ message: "Is the list endpoint paginated?",
1287
+ default: false,
1288
+ when: (answers) => {
1289
+ return answers.artifacts?.includes("queryList");
1290
+ }
1291
+ }
1292
+ ];
1293
+ }
1294
+ function featureActions(answers, config) {
1295
+ const artifacts = answers.artifacts || [];
1296
+ const actions = [];
1297
+ const entityName = answers.name;
1298
+ const singularName = answers.singularName;
1299
+ const pascalSingular = toPascalCase(singularName);
1300
+ const validationSuffix = config.naming.validationSuffix;
1301
+ const validationBarrelExt = validationSuffix.replace(/\.ts$/, "");
1302
+ if (artifacts.includes("types")) {
1303
+ actions.push(
1304
+ {
1305
+ type: "add",
1306
+ path: `${config.paths.types}/{{pascalCase name}}Dto.ts`,
1307
+ templateFile: "types-dto/dto.ts.hbs",
1308
+ data: {
1309
+ includeCreateDto: artifacts.includes("mutationCreate"),
1310
+ includeUpdateDto: artifacts.includes("mutationUpdate"),
1311
+ includeParamsDto: true
1312
+ }
1313
+ },
1314
+ {
1315
+ type: "barrel-append",
1316
+ path: `${config.paths.types}/index.ts`,
1317
+ exportLine: "export * from './{{pascalCase name}}Dto';"
1318
+ }
1319
+ );
1320
+ }
1321
+ if (artifacts.includes("handler")) {
1322
+ const operations = ["list", "details"];
1323
+ if (artifacts.includes("mutationCreate")) operations.push("create");
1324
+ if (artifacts.includes("mutationUpdate")) operations.push("update");
1325
+ if (artifacts.includes("mutationDelete")) operations.push("delete");
1326
+ actions.push(
1327
+ {
1328
+ type: "add",
1329
+ path: `${config.paths.handlers}/{{camelCase name}}.ts`,
1330
+ templateFile: "handler/handler.ts.hbs",
1331
+ data: { operations, endpointKey: answers.endpointKey }
1332
+ },
1333
+ {
1334
+ type: "barrel-append",
1335
+ path: `${config.paths.handlers}/index.ts`,
1336
+ exportLine: "export * from './{{camelCase name}}';"
1337
+ }
1338
+ );
1339
+ }
1340
+ if (artifacts.includes("queryList")) {
1341
+ if (answers.isPaginated) {
1342
+ actions.push(
1343
+ {
1344
+ type: "add",
1345
+ path: `${config.paths.queries}/use{{pascalCase name}}PaginatedQuery.ts`,
1346
+ templateFile: "query-hook/hook-paginated.ts.hbs",
1347
+ data: { hookName: `${entityName}Paginated`, handlerName: entityName, handlerKey: "list" }
1348
+ },
1349
+ {
1350
+ type: "barrel-append",
1351
+ path: `${config.paths.queries}/index.ts`,
1352
+ exportLine: "export * from './use{{pascalCase name}}PaginatedQuery';"
1353
+ }
1354
+ );
1355
+ } else {
1356
+ actions.push(
1357
+ {
1358
+ type: "add",
1359
+ path: `${config.paths.queries}/use{{pascalCase name}}ListQuery.ts`,
1360
+ templateFile: "query-hook/hook.ts.hbs",
1361
+ data: { hookName: `${entityName}List`, handlerName: entityName, handlerKey: "list" }
1362
+ },
1363
+ {
1364
+ type: "barrel-append",
1365
+ path: `${config.paths.queries}/index.ts`,
1366
+ exportLine: "export * from './use{{pascalCase name}}ListQuery';"
1367
+ }
1368
+ );
1369
+ }
1370
+ }
1371
+ if (artifacts.includes("queryDetails")) {
1372
+ actions.push(
1373
+ {
1374
+ type: "add",
1375
+ path: `${config.paths.queries}/use{{pascalCase name}}DetailsQuery.ts`,
1376
+ templateFile: "query-hook/hook-details.ts.hbs",
1377
+ data: { hookName: `${entityName}Details`, handlerName: entityName, handlerKey: "details" }
1378
+ },
1379
+ {
1380
+ type: "barrel-append",
1381
+ path: `${config.paths.queries}/index.ts`,
1382
+ exportLine: "export * from './use{{pascalCase name}}DetailsQuery';"
1383
+ }
1384
+ );
1385
+ }
1386
+ if (artifacts.includes("mutationCreate")) {
1387
+ actions.push(
1388
+ {
1389
+ type: "add",
1390
+ path: `${config.paths.mutations}/useCreate${pascalSingular}Mutation.ts`,
1391
+ templateFile: "mutation-hook/hook.ts.hbs",
1392
+ data: { handlerName: entityName, handlerKey: "create", invalidateKey: "list", mutationName: `Create${pascalSingular}` }
1393
+ },
1394
+ {
1395
+ type: "barrel-append",
1396
+ path: `${config.paths.mutations}/index.ts`,
1397
+ exportLine: `export * from './useCreate${pascalSingular}Mutation';`
1398
+ }
1399
+ );
1400
+ }
1401
+ if (artifacts.includes("mutationUpdate")) {
1402
+ actions.push(
1403
+ {
1404
+ type: "add",
1405
+ path: `${config.paths.mutations}/useUpdate${pascalSingular}Mutation.ts`,
1406
+ templateFile: "mutation-hook/hook.ts.hbs",
1407
+ data: { handlerName: entityName, handlerKey: "update", invalidateKey: "list", mutationName: `Update${pascalSingular}` }
1408
+ },
1409
+ {
1410
+ type: "barrel-append",
1411
+ path: `${config.paths.mutations}/index.ts`,
1412
+ exportLine: `export * from './useUpdate${pascalSingular}Mutation';`
1413
+ }
1414
+ );
1415
+ }
1416
+ if (artifacts.includes("mutationDelete")) {
1417
+ actions.push(
1418
+ {
1419
+ type: "add",
1420
+ path: `${config.paths.mutations}/useDelete${pascalSingular}Mutation.ts`,
1421
+ templateFile: "mutation-hook/hook.ts.hbs",
1422
+ data: { handlerName: entityName, handlerKey: "remove", invalidateKey: "list", mutationName: `Delete${pascalSingular}` }
1423
+ },
1424
+ {
1425
+ type: "barrel-append",
1426
+ path: `${config.paths.mutations}/index.ts`,
1427
+ exportLine: `export * from './useDelete${pascalSingular}Mutation';`
1428
+ }
1429
+ );
1430
+ }
1431
+ if (artifacts.includes("view")) {
1432
+ actions.push(
1433
+ {
1434
+ type: "add",
1435
+ path: `${config.paths.views}/{{kebabCase name}}/{{kebabCase name}}-list/{{pascalCase name}}List.tsx`,
1436
+ templateFile: "view/View.tsx.hbs",
1437
+ data: { name: `${entityName}List` }
1438
+ },
1439
+ {
1440
+ type: "add",
1441
+ path: `${config.paths.views}/{{kebabCase name}}/{{kebabCase name}}-list/index.ts`,
1442
+ templateFile: "view/index.ts.hbs",
1443
+ data: { name: `${entityName}List` }
1444
+ },
1445
+ {
1446
+ type: "barrel-append",
1447
+ path: `${config.paths.views}/{{kebabCase name}}/index.ts`,
1448
+ exportLine: "export * from './{{kebabCase name}}-list';"
1449
+ },
1450
+ {
1451
+ type: "barrel-append",
1452
+ path: `${config.paths.views}/index.ts`,
1453
+ exportLine: "export * from './{{kebabCase name}}';"
1454
+ }
1455
+ );
1456
+ }
1457
+ if (artifacts.includes("page")) {
1458
+ const pageSuffix = config.naming.pageSuffix;
1459
+ actions.push({
1460
+ type: "add",
1461
+ path: `${config.paths.pages}/{{kebabCase name}}/{{pascalCase name}}${pageSuffix}.tsx`,
1462
+ templateFile: "page/Page.tsx.hbs",
1463
+ data: { pageSuffix }
1464
+ });
1465
+ }
1466
+ if (artifacts.includes("validation")) {
1467
+ actions.push(
1468
+ {
1469
+ type: "add",
1470
+ path: `${config.paths.validations}/{{camelCase name}}${validationSuffix}`,
1471
+ templateFile: "validation/validation.ts.hbs"
1472
+ },
1473
+ {
1474
+ type: "barrel-append",
1475
+ path: `${config.paths.validations}/index.ts`,
1476
+ exportLine: `export * from './{{camelCase name}}${validationBarrelExt}';`
1477
+ }
1478
+ );
1479
+ }
1480
+ return actions;
1481
+ }
1482
+
1483
+ // src/generators/index.ts
1484
+ function getGenerators(_config) {
1485
+ return [
1486
+ {
1487
+ name: "component-ui",
1488
+ description: "CVA-based UI component (shadcn/ui style)",
1489
+ prompts: () => componentUiPrompts(),
1490
+ actions: (answers, cfg) => componentUiActions(answers, cfg)
1491
+ },
1492
+ {
1493
+ name: "component-shared",
1494
+ description: "Compound shared component",
1495
+ prompts: () => componentSharedPrompts(),
1496
+ actions: (answers, cfg) => componentSharedActions(answers, cfg),
1497
+ preprocess: (answers) => preprocessComponentSharedAnswers(answers)
1498
+ },
1499
+ {
1500
+ name: "component-form",
1501
+ description: "React Hook Form component (useController-based)",
1502
+ prompts: () => componentFormPrompts(),
1503
+ actions: (answers, cfg) => componentFormActions(answers, cfg)
1504
+ },
1505
+ {
1506
+ name: "page",
1507
+ description: "Route-level page component",
1508
+ prompts: (cfg) => pagePrompts(cfg),
1509
+ actions: (answers, cfg) => pageActions(answers, cfg),
1510
+ preprocess: (answers) => preprocessPageAnswers(answers)
1511
+ },
1512
+ {
1513
+ name: "view",
1514
+ description: "Feature view component",
1515
+ prompts: (cfg) => viewPrompts(cfg),
1516
+ actions: (answers, cfg) => viewActions(answers, cfg),
1517
+ preprocess: (answers) => preprocessViewAnswers(answers)
1518
+ },
1519
+ {
1520
+ name: "handler",
1521
+ description: "API handler (+ types + hooks)",
1522
+ prompts: () => handlerPrompts(),
1523
+ actions: (answers, cfg) => handlerActions(answers, cfg)
1524
+ },
1525
+ {
1526
+ name: "query-hook",
1527
+ description: "React Query hook",
1528
+ prompts: () => queryHookPrompts(),
1529
+ actions: (answers, cfg) => queryHookActions(answers, cfg)
1530
+ },
1531
+ {
1532
+ name: "mutation-hook",
1533
+ description: "React Query mutation hook",
1534
+ prompts: () => mutationHookPrompts(),
1535
+ actions: (answers, cfg) => mutationHookActions(answers, cfg)
1536
+ },
1537
+ {
1538
+ name: "types-dto",
1539
+ description: "DTO type definitions",
1540
+ prompts: () => typesDtoPrompts(),
1541
+ actions: (answers, cfg) => typesDtoActions(answers, cfg)
1542
+ },
1543
+ {
1544
+ name: "shared-hook",
1545
+ description: "Custom utility hook",
1546
+ prompts: () => sharedHookPrompts(),
1547
+ actions: (answers, cfg) => sharedHookActions(answers, cfg)
1548
+ },
1549
+ {
1550
+ name: "validation",
1551
+ description: "Zod validation schema",
1552
+ prompts: () => validationPrompts(),
1553
+ actions: (answers, cfg) => validationActions(answers, cfg)
1554
+ },
1555
+ {
1556
+ name: "feature",
1557
+ description: "Full feature scaffold (handler + types + hooks + view + page)",
1558
+ prompts: () => featurePrompts(),
1559
+ actions: (answers, cfg) => featureActions(answers, cfg)
1560
+ }
1561
+ ];
1562
+ }
1563
+
1564
+ // src/commands/generate.ts
1565
+ async function runGenerate(generatorName) {
1566
+ let config;
1567
+ try {
1568
+ config = await loadConfig();
1569
+ } catch (error) {
1570
+ console.error(
1571
+ chalk.red("Error loading config:"),
1572
+ error instanceof Error ? error.message : error
1573
+ );
1574
+ console.log(chalk.yellow('Run "rq-codegen init" to create a config file.'));
1575
+ process.exit(1);
1576
+ }
1577
+ resetHandlebars();
1578
+ const generators = getGenerators(config);
1579
+ if (!generatorName) {
1580
+ const choices = generators.map((g) => ({
1581
+ name: `${g.name.padEnd(18)} ${chalk.dim("\u2014")} ${g.description}`,
1582
+ value: g.name
1583
+ }));
1584
+ const { selected } = await inquirer.prompt([
1585
+ {
1586
+ type: "list",
1587
+ name: "selected",
1588
+ message: "What would you like to generate?",
1589
+ choices
1590
+ }
1591
+ ]);
1592
+ generatorName = selected;
1593
+ }
1594
+ const generator = generators.find((g) => g.name === generatorName);
1595
+ if (!generator) {
1596
+ console.error(chalk.red(`Unknown generator: ${generatorName}`));
1597
+ console.log(
1598
+ chalk.yellow("Available generators:"),
1599
+ generators.map((g) => g.name).join(", ")
1600
+ );
1601
+ process.exit(1);
1602
+ }
1603
+ console.log(chalk.cyan(`
1604
+ Running: ${generator.name}
1605
+ `));
1606
+ const prompts = generator.prompts(config);
1607
+ const answers = await inquirer.prompt(prompts);
1608
+ const processedAnswers = generator.preprocess ? generator.preprocess(answers) : answers;
1609
+ const actions = generator.actions(processedAnswers, config);
1610
+ const results = await executeActions(actions, processedAnswers, config);
1611
+ console.log("");
1612
+ for (const result of results) {
1613
+ switch (result.type) {
1614
+ case "created":
1615
+ console.log(chalk.green(" CREATED"), result.message);
1616
+ break;
1617
+ case "updated":
1618
+ console.log(chalk.blue(" UPDATED"), result.message);
1619
+ break;
1620
+ case "skipped":
1621
+ console.log(chalk.yellow(" SKIPPED"), result.message);
1622
+ break;
1623
+ case "failed":
1624
+ console.error(chalk.red(" FAILED"), result.message);
1625
+ break;
1626
+ }
1627
+ }
1628
+ const created = results.filter((r) => r.type === "created").length;
1629
+ const updated = results.filter((r) => r.type === "updated").length;
1630
+ const failed = results.filter((r) => r.type === "failed").length;
1631
+ console.log("");
1632
+ if (failed > 0) {
1633
+ console.log(chalk.red(` ${failed} action(s) failed.`));
1634
+ }
1635
+ console.log(
1636
+ chalk.green(` Done! ${created} file(s) created, ${updated} barrel(s) updated.
1637
+ `)
1638
+ );
1639
+ }
1640
+
1641
+ // src/commands/init.ts
1642
+ import fs6 from "fs";
1643
+ import path6 from "path";
1644
+ import chalk2 from "chalk";
1645
+ import inquirer2 from "inquirer";
1646
+ async function runInit(options) {
1647
+ const configPath = path6.resolve(process.cwd(), "rqgen.config.ts");
1648
+ if (fs6.existsSync(configPath) && !options.force) {
1649
+ console.error(
1650
+ chalk2.red("Config file already exists: rqgen.config.ts")
1651
+ );
1652
+ console.log(chalk2.yellow("Use --force to overwrite."));
1653
+ process.exit(1);
1654
+ }
1655
+ console.log(chalk2.cyan("\n Initializing @appswave/rq-codegen config...\n"));
1656
+ const detectedAliases = detectAliasesFromTsConfig(process.cwd());
1657
+ const srcDirExists = fs6.existsSync(path6.resolve(process.cwd(), "src"));
1658
+ const srcDir = srcDirExists ? "./src" : ".";
1659
+ const hasRouter = fs6.existsSync(
1660
+ path6.resolve(process.cwd(), srcDir, "routes/router.tsx")
1661
+ );
1662
+ const hasI18n = fs6.existsSync(path6.resolve(process.cwd(), srcDir, "locales")) || fs6.existsSync(path6.resolve(process.cwd(), "i18next.config.ts"));
1663
+ const answers = await inquirer2.prompt([
1664
+ {
1665
+ type: "confirm",
1666
+ name: "i18n",
1667
+ message: "Does your project use i18n (i18next)?",
1668
+ default: hasI18n
1669
+ },
1670
+ {
1671
+ type: "confirm",
1672
+ name: "toast",
1673
+ message: "Does your project use toast notifications?",
1674
+ default: true
1675
+ },
1676
+ {
1677
+ type: "confirm",
1678
+ name: "routeRegistration",
1679
+ message: "Enable automatic route registration when generating pages?",
1680
+ default: hasRouter
1681
+ }
1682
+ ]);
1683
+ const aliasesStr = detectedAliases ? formatAliases(detectedAliases) : " // Auto-detected from tsconfig.json \u2014 customize if needed";
1684
+ const configContent = `import { defineConfig } from '@appswave/rq-codegen';
1685
+
1686
+ export default defineConfig({
1687
+ srcDir: '${srcDir}',
1688
+
1689
+ aliases: {
1690
+ ${aliasesStr}
1691
+ },
1692
+
1693
+ features: {
1694
+ i18n: ${answers.i18n},
1695
+ toast: ${answers.toast},
1696
+ barrel: true,
1697
+ routeRegistration: ${answers.routeRegistration},
1698
+ },
1699
+
1700
+ naming: {
1701
+ dtoSuffixes: {
1702
+ read: 'ForReadDto',
1703
+ create: 'ForCreateDto',
1704
+ update: 'ForUpdateDto',
1705
+ list: 'ListDto',
1706
+ listResponse: 'ListResponseDto',
1707
+ params: 'ParamsDto',
1708
+ },
1709
+ validationSuffix: '.schema.ts',
1710
+ pageSuffix: 'Page',
1711
+ hookPrefix: 'use',
1712
+ },
1713
+
1714
+ paths: {
1715
+ handlers: 'api/handlers',
1716
+ apiConfig: 'api/config',
1717
+ types: 'types/api',
1718
+ queries: 'lib/hooks/queries',
1719
+ mutations: 'lib/hooks/mutations',
1720
+ sharedHooks: 'lib/hooks/shared',
1721
+ hookUtils: 'lib/hooks/utils',
1722
+ uiComponents: 'components/ui',
1723
+ sharedComponents: 'components/shared',
1724
+ formComponents: 'components/forms',
1725
+ pages: 'pages',
1726
+ views: 'views',
1727
+ validations: 'validations',
1728
+ },
1729
+ ${answers.routeRegistration ? `
1730
+ router: {
1731
+ routerFile: 'routes/router.tsx',
1732
+ routesFile: 'routes/routes.ts',
1733
+ layouts: ['MainLayout', 'DashboardLayout'],
1734
+ },
1735
+ ` : ""}
1736
+ hooks: {
1737
+ toast: { import: 'useToast', from: '@hooks/shared' },
1738
+ translation: { import: 'useAppTranslation', from: '@hooks/shared' },
1739
+ paginatedQuery: { import: 'usePaginatedDataTableQuery', from: '@hooks/utils' },
1740
+ },
1741
+ });
1742
+ `;
1743
+ fs6.writeFileSync(configPath, configContent, "utf-8");
1744
+ console.log(chalk2.green(" Created rqgen.config.ts"));
1745
+ console.log(chalk2.dim(" Edit the config to customize paths, aliases, and features.\n"));
1746
+ }
1747
+ function formatAliases(aliases) {
1748
+ const lines = [];
1749
+ const keyMap = {
1750
+ api: "api",
1751
+ components: "components",
1752
+ hooks: "hooks",
1753
+ types: "types",
1754
+ utils: "utils",
1755
+ contexts: "contexts",
1756
+ constants: "constants",
1757
+ views: "views",
1758
+ pages: "pages",
1759
+ validations: "validations",
1760
+ assets: "assets",
1761
+ routes: "routes",
1762
+ hoc: "hoc",
1763
+ appConfig: "appConfig"
1764
+ };
1765
+ for (const [key, configKey] of Object.entries(keyMap)) {
1766
+ const value = aliases[key];
1767
+ if (value) {
1768
+ lines.push(` ${configKey}: '${value}',`);
1769
+ }
1770
+ }
1771
+ return lines.length > 0 ? lines.join("\n") : " // No aliases detected \u2014 add your path aliases here";
1772
+ }
1773
+
1774
+ // src/cli.ts
1775
+ function createCli() {
1776
+ const program2 = new Command();
1777
+ program2.name("rq-codegen").description(
1778
+ "Config-driven code generator for React + TypeScript + React Query projects"
1779
+ ).version("0.1.0");
1780
+ program2.command("init").description("Initialize rqgen.config.ts with auto-detected settings").option("--force", "Overwrite existing config file").action(runInit);
1781
+ program2.argument("[generator]", "Generator to run (e.g., handler, page, feature)").action(runGenerate);
1782
+ return program2;
1783
+ }
1784
+
1785
+ // bin/cli.ts
1786
+ var program = createCli();
1787
+ program.parse(process.argv);
1788
+ //# sourceMappingURL=cli.js.map