@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.
- package/README.md +1279 -0
- package/dist/bin/cli.js +1788 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/templates/component-form/FormComponent.tsx.hbs +60 -0
- package/templates/component-form/index.ts.hbs +1 -0
- package/templates/component-shared/Component.tsx.hbs +37 -0
- package/templates/component-shared/index.ts.hbs +1 -0
- package/templates/component-ui/Component.tsx.hbs +41 -0
- package/templates/component-ui/index.ts.hbs +1 -0
- package/templates/handler/handler.ts.hbs +80 -0
- package/templates/mutation-hook/hook.ts.hbs +53 -0
- package/templates/page/Page.tsx.hbs +7 -0
- package/templates/query-hook/hook-details.ts.hbs +12 -0
- package/templates/query-hook/hook-paginated.ts.hbs +12 -0
- package/templates/query-hook/hook.ts.hbs +12 -0
- package/templates/shared-hook/hook.ts.hbs +9 -0
- package/templates/types-dto/dto.ts.hbs +40 -0
- package/templates/validation/validation.ts.hbs +22 -0
- package/templates/view/View.tsx.hbs +13 -0
- package/templates/view/index.ts.hbs +1 -0
package/dist/bin/cli.js
ADDED
|
@@ -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
|