yantr-js 0.1.0-beta.2
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/dist/index.d.ts +2 -0
- package/dist/index.js +1023 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/registry/registry.json +58 -0
- package/registry/templates/auth/auth.controller.ts +125 -0
- package/registry/templates/auth/auth.middleware.ts +116 -0
- package/registry/templates/auth/auth.routes.ts +34 -0
- package/registry/templates/auth/auth.service.ts +140 -0
- package/registry/templates/base/error-handler.ts +127 -0
- package/registry/templates/base/zod-middleware.ts +104 -0
- package/registry/templates/database/db.ts +83 -0
- package/registry/templates/database/prisma.ts +47 -0
- package/registry/templates/logger/http-logger.ts +61 -0
- package/registry/templates/logger/logger.ts +60 -0
- package/registry/templates/security/helmet.ts +88 -0
- package/registry/templates/security/rate-limiter.ts +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import path4 from "path";
|
|
10
|
+
import fs4 from "fs-extra";
|
|
11
|
+
|
|
12
|
+
// src/lib/installer.ts
|
|
13
|
+
import { execa } from "execa";
|
|
14
|
+
import fs from "fs-extra";
|
|
15
|
+
import path from "path";
|
|
16
|
+
async function detectPackageManager(cwd) {
|
|
17
|
+
const lockFiles = {
|
|
18
|
+
"pnpm-lock.yaml": "pnpm",
|
|
19
|
+
"yarn.lock": "yarn",
|
|
20
|
+
"package-lock.json": "npm",
|
|
21
|
+
"bun.lockb": "bun"
|
|
22
|
+
};
|
|
23
|
+
for (const [lockFile, pm] of Object.entries(lockFiles)) {
|
|
24
|
+
if (await fs.pathExists(path.join(cwd, lockFile))) {
|
|
25
|
+
return pm;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
async function runPackageManager(pm, args, cwd) {
|
|
31
|
+
await execa(pm, args, { cwd, stdio: "inherit" });
|
|
32
|
+
}
|
|
33
|
+
async function installDependencies(pm, packages, cwd, isDev = false) {
|
|
34
|
+
const args = [];
|
|
35
|
+
switch (pm) {
|
|
36
|
+
case "npm":
|
|
37
|
+
args.push("install", isDev ? "-D" : "", ...packages);
|
|
38
|
+
break;
|
|
39
|
+
case "pnpm":
|
|
40
|
+
args.push("add", isDev ? "-D" : "", ...packages);
|
|
41
|
+
break;
|
|
42
|
+
case "yarn":
|
|
43
|
+
args.push("add", isDev ? "-D" : "", ...packages);
|
|
44
|
+
break;
|
|
45
|
+
case "bun":
|
|
46
|
+
args.push("add", isDev ? "-d" : "", ...packages);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const filteredArgs = args.filter(Boolean);
|
|
50
|
+
await runPackageManager(pm, filteredArgs, cwd);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/lib/config.ts
|
|
54
|
+
import fs2 from "fs-extra";
|
|
55
|
+
import path2 from "path";
|
|
56
|
+
import { z } from "zod";
|
|
57
|
+
var SetuConfigSchema = z.object({
|
|
58
|
+
$schema: z.string().optional(),
|
|
59
|
+
projectName: z.string(),
|
|
60
|
+
srcDir: z.string().default("./src"),
|
|
61
|
+
packageManager: z.enum(["npm", "pnpm", "yarn", "bun"]),
|
|
62
|
+
installedComponents: z.array(z.string()).default([])
|
|
63
|
+
});
|
|
64
|
+
var CONFIG_FILE = "yantr.json";
|
|
65
|
+
async function configExists(cwd) {
|
|
66
|
+
return fs2.pathExists(path2.join(cwd, CONFIG_FILE));
|
|
67
|
+
}
|
|
68
|
+
async function readConfig(cwd) {
|
|
69
|
+
const configPath = path2.join(cwd, CONFIG_FILE);
|
|
70
|
+
if (!await fs2.pathExists(configPath)) {
|
|
71
|
+
throw new Error('yantr.json not found. Run "setu init" first.');
|
|
72
|
+
}
|
|
73
|
+
const content = await fs2.readJson(configPath);
|
|
74
|
+
return SetuConfigSchema.parse(content);
|
|
75
|
+
}
|
|
76
|
+
async function writeConfig(cwd, config) {
|
|
77
|
+
const configPath = path2.join(cwd, CONFIG_FILE);
|
|
78
|
+
await fs2.writeJson(configPath, config, { spaces: 2 });
|
|
79
|
+
}
|
|
80
|
+
function createConfig(projectName, srcDir, packageManager) {
|
|
81
|
+
return {
|
|
82
|
+
$schema: "https://raw.githubusercontent.com/SibilSoren/setu-js/main/cli/schema.json",
|
|
83
|
+
projectName,
|
|
84
|
+
srcDir,
|
|
85
|
+
packageManager,
|
|
86
|
+
installedComponents: []
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function addInstalledComponent(cwd, component) {
|
|
90
|
+
const config = await readConfig(cwd);
|
|
91
|
+
if (!config.installedComponents.includes(component)) {
|
|
92
|
+
config.installedComponents.push(component);
|
|
93
|
+
await writeConfig(cwd, config);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function isComponentInstalled(cwd, component) {
|
|
97
|
+
const config = await readConfig(cwd);
|
|
98
|
+
return config.installedComponents.includes(component);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/utils/fs.ts
|
|
102
|
+
import fs3 from "fs-extra";
|
|
103
|
+
import path3 from "path";
|
|
104
|
+
async function ensureDir(dirPath) {
|
|
105
|
+
await fs3.ensureDir(dirPath);
|
|
106
|
+
}
|
|
107
|
+
async function writeFile(filePath, content) {
|
|
108
|
+
await fs3.ensureDir(path3.dirname(filePath));
|
|
109
|
+
await fs3.writeFile(filePath, content, "utf-8");
|
|
110
|
+
}
|
|
111
|
+
async function fileExists(filePath) {
|
|
112
|
+
return fs3.pathExists(filePath);
|
|
113
|
+
}
|
|
114
|
+
async function isNodeProject(cwd) {
|
|
115
|
+
return fs3.pathExists(path3.join(cwd, "package.json"));
|
|
116
|
+
}
|
|
117
|
+
async function getProjectName(cwd) {
|
|
118
|
+
const pkgPath = path3.join(cwd, "package.json");
|
|
119
|
+
if (await fs3.pathExists(pkgPath)) {
|
|
120
|
+
const pkg = await fs3.readJson(pkgPath);
|
|
121
|
+
return pkg.name || null;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
async function hasSrcDirectory(cwd) {
|
|
126
|
+
return fs3.pathExists(path3.join(cwd, "src"));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/commands/init.ts
|
|
130
|
+
async function init(options) {
|
|
131
|
+
console.clear();
|
|
132
|
+
p.intro(chalk.bgCyan.black(" \u{1FA9B} yantr init "));
|
|
133
|
+
const cwd = process.cwd();
|
|
134
|
+
const isNode = await isNodeProject(cwd);
|
|
135
|
+
if (!isNode) {
|
|
136
|
+
p.log.error("No package.json found in current directory.");
|
|
137
|
+
p.log.info("Please run this command in an existing Node.js project, or run:");
|
|
138
|
+
p.log.info(chalk.cyan(" npm init -y"));
|
|
139
|
+
p.outro(chalk.red("Initialization cancelled."));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const hasConfig = await configExists(cwd);
|
|
143
|
+
if (hasConfig && !options.yes) {
|
|
144
|
+
const overwrite = await p.confirm({
|
|
145
|
+
message: "yantr.json already exists. Overwrite?",
|
|
146
|
+
initialValue: false
|
|
147
|
+
});
|
|
148
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
149
|
+
p.outro(chalk.yellow("Initialization cancelled."));
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let projectName;
|
|
154
|
+
let srcDir;
|
|
155
|
+
let packageManager;
|
|
156
|
+
if (options.yes) {
|
|
157
|
+
projectName = await getProjectName(cwd) || path4.basename(cwd);
|
|
158
|
+
srcDir = await hasSrcDirectory(cwd) ? "./src" : ".";
|
|
159
|
+
packageManager = await detectPackageManager(cwd) || "npm";
|
|
160
|
+
} else {
|
|
161
|
+
const detectedPm = await detectPackageManager(cwd);
|
|
162
|
+
const detectedName = await getProjectName(cwd) || path4.basename(cwd);
|
|
163
|
+
const hasSrc = await hasSrcDirectory(cwd);
|
|
164
|
+
const responses = await p.group(
|
|
165
|
+
{
|
|
166
|
+
projectName: () => p.text({
|
|
167
|
+
message: "Project name:",
|
|
168
|
+
defaultValue: detectedName,
|
|
169
|
+
placeholder: detectedName
|
|
170
|
+
}),
|
|
171
|
+
srcDir: () => p.select({
|
|
172
|
+
message: "Where should Yantr put generated files?",
|
|
173
|
+
initialValue: hasSrc ? "./src" : ".",
|
|
174
|
+
options: [
|
|
175
|
+
{ value: "./src", label: "./src (recommended)" },
|
|
176
|
+
{ value: ".", label: ". (project root)" },
|
|
177
|
+
{ value: "./lib", label: "./lib" }
|
|
178
|
+
]
|
|
179
|
+
}),
|
|
180
|
+
packageManager: () => p.select({
|
|
181
|
+
message: "Package manager:",
|
|
182
|
+
initialValue: detectedPm || "npm",
|
|
183
|
+
options: [
|
|
184
|
+
{ value: "npm", label: "npm" },
|
|
185
|
+
{ value: "pnpm", label: "pnpm" },
|
|
186
|
+
{ value: "yarn", label: "yarn" },
|
|
187
|
+
{ value: "bun", label: "bun" }
|
|
188
|
+
]
|
|
189
|
+
})
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
onCancel: () => {
|
|
193
|
+
p.cancel("Initialization cancelled.");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
projectName = responses.projectName;
|
|
199
|
+
srcDir = responses.srcDir;
|
|
200
|
+
packageManager = responses.packageManager;
|
|
201
|
+
}
|
|
202
|
+
const spinner4 = p.spinner();
|
|
203
|
+
spinner4.start("Creating configuration...");
|
|
204
|
+
const config = createConfig(projectName, srcDir, packageManager);
|
|
205
|
+
await writeConfig(cwd, config);
|
|
206
|
+
spinner4.stop("Created yantr.json");
|
|
207
|
+
spinner4.start("Setting up base templates...");
|
|
208
|
+
const templatesDir = path4.join(srcDir, "lib", "yantr");
|
|
209
|
+
await ensureDir(path4.join(cwd, templatesDir));
|
|
210
|
+
const cliDir = path4.dirname(new URL(import.meta.url).pathname);
|
|
211
|
+
const registryPath = path4.join(cliDir, "..", "..", "registry", "templates", "base");
|
|
212
|
+
const baseTemplatesPath = await fs4.pathExists(registryPath) ? registryPath : path4.join(cliDir, "registry", "templates", "base");
|
|
213
|
+
const errorHandlerContent = `import type { Request, Response, NextFunction } from 'express';
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Custom error class for application errors
|
|
217
|
+
*/
|
|
218
|
+
export class AppError extends Error {
|
|
219
|
+
public readonly statusCode: number;
|
|
220
|
+
public readonly isOperational: boolean;
|
|
221
|
+
public readonly code?: string;
|
|
222
|
+
|
|
223
|
+
constructor(
|
|
224
|
+
message: string,
|
|
225
|
+
statusCode: number = 500,
|
|
226
|
+
isOperational: boolean = true,
|
|
227
|
+
code?: string
|
|
228
|
+
) {
|
|
229
|
+
super(message);
|
|
230
|
+
this.statusCode = statusCode;
|
|
231
|
+
this.isOperational = isOperational;
|
|
232
|
+
this.code = code;
|
|
233
|
+
|
|
234
|
+
Object.setPrototypeOf(this, AppError.prototype);
|
|
235
|
+
Error.captureStackTrace(this, this.constructor);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Common HTTP error classes
|
|
241
|
+
*/
|
|
242
|
+
export class NotFoundError extends AppError {
|
|
243
|
+
constructor(message: string = 'Resource not found') {
|
|
244
|
+
super(message, 404, true, 'NOT_FOUND');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export class BadRequestError extends AppError {
|
|
249
|
+
constructor(message: string = 'Bad request') {
|
|
250
|
+
super(message, 400, true, 'BAD_REQUEST');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export class UnauthorizedError extends AppError {
|
|
255
|
+
constructor(message: string = 'Unauthorized') {
|
|
256
|
+
super(message, 401, true, 'UNAUTHORIZED');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export class ForbiddenError extends AppError {
|
|
261
|
+
constructor(message: string = 'Forbidden') {
|
|
262
|
+
super(message, 403, true, 'FORBIDDEN');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export class ConflictError extends AppError {
|
|
267
|
+
constructor(message: string = 'Conflict') {
|
|
268
|
+
super(message, 409, true, 'CONFLICT');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export class ValidationError extends AppError {
|
|
273
|
+
public readonly errors: Record<string, string[]>;
|
|
274
|
+
|
|
275
|
+
constructor(errors: Record<string, string[]>) {
|
|
276
|
+
super('Validation failed', 422, true, 'VALIDATION_ERROR');
|
|
277
|
+
this.errors = errors;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Global error handler middleware
|
|
283
|
+
*/
|
|
284
|
+
export function errorHandler(
|
|
285
|
+
err: Error,
|
|
286
|
+
_req: Request,
|
|
287
|
+
res: Response,
|
|
288
|
+
_next: NextFunction
|
|
289
|
+
): void {
|
|
290
|
+
console.error('[Error]', err);
|
|
291
|
+
|
|
292
|
+
if (err instanceof AppError) {
|
|
293
|
+
const response: Record<string, unknown> = {
|
|
294
|
+
success: false,
|
|
295
|
+
error: {
|
|
296
|
+
message: err.message,
|
|
297
|
+
code: err.code,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (err instanceof ValidationError) {
|
|
302
|
+
response.error = {
|
|
303
|
+
...response.error as object,
|
|
304
|
+
details: err.errors,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
res.status(err.statusCode).json(response);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const statusCode = 500;
|
|
313
|
+
const message = process.env.NODE_ENV === 'production'
|
|
314
|
+
? 'Internal server error'
|
|
315
|
+
: err.message;
|
|
316
|
+
|
|
317
|
+
res.status(statusCode).json({
|
|
318
|
+
success: false,
|
|
319
|
+
error: {
|
|
320
|
+
message,
|
|
321
|
+
code: 'INTERNAL_ERROR',
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Async handler wrapper to catch errors in async route handlers
|
|
328
|
+
*/
|
|
329
|
+
export function asyncHandler(
|
|
330
|
+
fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
|
|
331
|
+
) {
|
|
332
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
333
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
`;
|
|
337
|
+
const zodMiddlewareContent = `import type { Request, Response, NextFunction } from 'express';
|
|
338
|
+
import { z, ZodError, ZodSchema } from 'zod';
|
|
339
|
+
import { ValidationError } from './error-handler';
|
|
340
|
+
|
|
341
|
+
type RequestLocation = 'body' | 'query' | 'params';
|
|
342
|
+
|
|
343
|
+
interface ValidateOptions {
|
|
344
|
+
body?: ZodSchema;
|
|
345
|
+
query?: ZodSchema;
|
|
346
|
+
params?: ZodSchema;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Format Zod errors into a readable format
|
|
351
|
+
*/
|
|
352
|
+
function formatZodErrors(error: ZodError): Record<string, string[]> {
|
|
353
|
+
const errors: Record<string, string[]> = {};
|
|
354
|
+
|
|
355
|
+
for (const issue of error.issues) {
|
|
356
|
+
const path = issue.path.join('.');
|
|
357
|
+
const key = path || 'root';
|
|
358
|
+
|
|
359
|
+
if (!errors[key]) {
|
|
360
|
+
errors[key] = [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
errors[key].push(issue.message);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return errors;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Validate request data against Zod schemas
|
|
371
|
+
*/
|
|
372
|
+
export function validate(schemas: ValidateOptions) {
|
|
373
|
+
return async (req: Request, _res: Response, next: NextFunction) => {
|
|
374
|
+
const allErrors: Record<string, string[]> = {};
|
|
375
|
+
const locations: RequestLocation[] = ['body', 'query', 'params'];
|
|
376
|
+
|
|
377
|
+
for (const location of locations) {
|
|
378
|
+
const schema = schemas[location];
|
|
379
|
+
|
|
380
|
+
if (schema) {
|
|
381
|
+
const result = await schema.safeParseAsync(req[location]);
|
|
382
|
+
|
|
383
|
+
if (!result.success) {
|
|
384
|
+
const errors = formatZodErrors(result.error);
|
|
385
|
+
|
|
386
|
+
for (const [key, messages] of Object.entries(errors)) {
|
|
387
|
+
const prefixedKey = \`\${location}.\${key}\`;
|
|
388
|
+
allErrors[prefixedKey] = messages;
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
req[location] = result.data;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (Object.keys(allErrors).length > 0) {
|
|
397
|
+
return next(new ValidationError(allErrors));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
next();
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function validateBody<T extends ZodSchema>(schema: T) {
|
|
405
|
+
return validate({ body: schema });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function validateQuery<T extends ZodSchema>(schema: T) {
|
|
409
|
+
return validate({ query: schema });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function validateParams<T extends ZodSchema>(schema: T) {
|
|
413
|
+
return validate({ params: schema });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export { z };
|
|
417
|
+
`;
|
|
418
|
+
await writeFile(
|
|
419
|
+
path4.join(cwd, templatesDir, "error-handler.ts"),
|
|
420
|
+
errorHandlerContent
|
|
421
|
+
);
|
|
422
|
+
await writeFile(
|
|
423
|
+
path4.join(cwd, templatesDir, "zod-middleware.ts"),
|
|
424
|
+
zodMiddlewareContent
|
|
425
|
+
);
|
|
426
|
+
spinner4.stop("Base templates created");
|
|
427
|
+
spinner4.start("Installing dependencies...");
|
|
428
|
+
try {
|
|
429
|
+
await installDependencies(packageManager, ["zod"], cwd, false);
|
|
430
|
+
spinner4.stop("Dependencies installed");
|
|
431
|
+
} catch (error) {
|
|
432
|
+
spinner4.stop("Could not install dependencies automatically");
|
|
433
|
+
p.log.warning(`Please run: ${chalk.cyan(`${packageManager} add zod`)}`);
|
|
434
|
+
}
|
|
435
|
+
p.note(
|
|
436
|
+
`${chalk.green("\u2713")} yantr.json created
|
|
437
|
+
${chalk.green("\u2713")} ${templatesDir}/error-handler.ts
|
|
438
|
+
${chalk.green("\u2713")} ${templatesDir}/zod-middleware.ts`,
|
|
439
|
+
"Files created"
|
|
440
|
+
);
|
|
441
|
+
p.log.info("Next steps:");
|
|
442
|
+
p.log.step(1, `Add error handler to your Express app:`);
|
|
443
|
+
console.log(chalk.gray(` import { errorHandler } from '${templatesDir}/error-handler';`));
|
|
444
|
+
console.log(chalk.gray(` app.use(errorHandler);`));
|
|
445
|
+
p.log.step(2, `Add components: ${chalk.cyan("yantr add auth")}`);
|
|
446
|
+
p.log.step(3, `Generate routes: ${chalk.cyan("yantr generate route users")}`);
|
|
447
|
+
p.outro(chalk.green("Yantr initialized successfully! \u{1FA9B}"));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/commands/add.ts
|
|
451
|
+
import * as p2 from "@clack/prompts";
|
|
452
|
+
import chalk2 from "chalk";
|
|
453
|
+
import path6 from "path";
|
|
454
|
+
|
|
455
|
+
// src/lib/registry.ts
|
|
456
|
+
import { z as z2 } from "zod";
|
|
457
|
+
import fs5 from "fs-extra";
|
|
458
|
+
import path5 from "path";
|
|
459
|
+
import { fileURLToPath } from "url";
|
|
460
|
+
var ComponentSchema = z2.object({
|
|
461
|
+
name: z2.string(),
|
|
462
|
+
description: z2.string(),
|
|
463
|
+
files: z2.array(z2.string()),
|
|
464
|
+
dependencies: z2.array(z2.string()).default([]),
|
|
465
|
+
devDependencies: z2.array(z2.string()).default([])
|
|
466
|
+
});
|
|
467
|
+
var RegistrySchema = z2.object({
|
|
468
|
+
version: z2.string(),
|
|
469
|
+
baseUrl: z2.string(),
|
|
470
|
+
components: z2.record(z2.string(), ComponentSchema)
|
|
471
|
+
});
|
|
472
|
+
var REGISTRY_URL = "https://raw.githubusercontent.com/SibilSoren/setu-js/main/cli/registry/registry.json";
|
|
473
|
+
var CACHE_DIR = ".yantr-cache";
|
|
474
|
+
var CACHE_TTL_MS = 1e3 * 60 * 60;
|
|
475
|
+
function getCacheDir() {
|
|
476
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
|
|
477
|
+
return path5.join(homeDir, CACHE_DIR);
|
|
478
|
+
}
|
|
479
|
+
async function getCachedRegistry() {
|
|
480
|
+
const cacheDir = getCacheDir();
|
|
481
|
+
const cachePath = path5.join(cacheDir, "registry.json");
|
|
482
|
+
try {
|
|
483
|
+
if (!await fs5.pathExists(cachePath)) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
const stats = await fs5.stat(cachePath);
|
|
487
|
+
const age = Date.now() - stats.mtimeMs;
|
|
488
|
+
if (age > CACHE_TTL_MS) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const content = await fs5.readJson(cachePath);
|
|
492
|
+
return RegistrySchema.parse(content);
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async function cacheRegistry(registry) {
|
|
498
|
+
const cacheDir = getCacheDir();
|
|
499
|
+
const cachePath = path5.join(cacheDir, "registry.json");
|
|
500
|
+
try {
|
|
501
|
+
await fs5.ensureDir(cacheDir);
|
|
502
|
+
await fs5.writeJson(cachePath, registry);
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function loadLocalRegistry() {
|
|
507
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
508
|
+
const __dirname2 = path5.dirname(__filename2);
|
|
509
|
+
const possiblePaths = [
|
|
510
|
+
// From source: cli/src/lib -> cli/registry
|
|
511
|
+
path5.join(__dirname2, "..", "..", "registry", "registry.json"),
|
|
512
|
+
// From dist: cli/dist -> cli/registry
|
|
513
|
+
path5.join(__dirname2, "..", "registry", "registry.json"),
|
|
514
|
+
// Direct path from built bundle
|
|
515
|
+
path5.resolve(__dirname2, "..", "registry", "registry.json")
|
|
516
|
+
];
|
|
517
|
+
for (const registryPath of possiblePaths) {
|
|
518
|
+
if (await fs5.pathExists(registryPath)) {
|
|
519
|
+
const content = await fs5.readJson(registryPath);
|
|
520
|
+
return RegistrySchema.parse(content);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
throw new Error(`Local registry not found. Tried: ${possiblePaths.join(", ")}`);
|
|
524
|
+
}
|
|
525
|
+
async function loadLocalTemplateFile(filePath) {
|
|
526
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
527
|
+
const __dirname2 = path5.dirname(__filename2);
|
|
528
|
+
const possiblePaths = [
|
|
529
|
+
path5.join(__dirname2, "..", "..", "registry", "templates", filePath),
|
|
530
|
+
path5.join(__dirname2, "..", "registry", "templates", filePath)
|
|
531
|
+
];
|
|
532
|
+
for (const templatePath of possiblePaths) {
|
|
533
|
+
if (await fs5.pathExists(templatePath)) {
|
|
534
|
+
return fs5.readFile(templatePath, "utf-8");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
async function getRegistry() {
|
|
540
|
+
const cached = await getCachedRegistry();
|
|
541
|
+
if (cached) {
|
|
542
|
+
return cached;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const local = await loadLocalRegistry();
|
|
546
|
+
return local;
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const controller = new AbortController();
|
|
551
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
552
|
+
const response = await fetch(REGISTRY_URL, { signal: controller.signal });
|
|
553
|
+
clearTimeout(timeoutId);
|
|
554
|
+
if (!response.ok) {
|
|
555
|
+
throw new Error(`HTTP ${response.status}`);
|
|
556
|
+
}
|
|
557
|
+
const data = await response.json();
|
|
558
|
+
const remote = RegistrySchema.parse(data);
|
|
559
|
+
await cacheRegistry(remote);
|
|
560
|
+
return remote;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
throw new Error(`Failed to load registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function getComponent(name) {
|
|
566
|
+
const registry = await getRegistry();
|
|
567
|
+
return registry.components[name] || null;
|
|
568
|
+
}
|
|
569
|
+
async function listComponents() {
|
|
570
|
+
const registry = await getRegistry();
|
|
571
|
+
return Object.keys(registry.components);
|
|
572
|
+
}
|
|
573
|
+
async function fetchTemplateFile(filePath) {
|
|
574
|
+
const local = await loadLocalTemplateFile(filePath);
|
|
575
|
+
if (local) {
|
|
576
|
+
return local;
|
|
577
|
+
}
|
|
578
|
+
const registry = await getRegistry();
|
|
579
|
+
const url = `${registry.baseUrl}/${filePath}`;
|
|
580
|
+
const controller = new AbortController();
|
|
581
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
582
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
583
|
+
clearTimeout(timeoutId);
|
|
584
|
+
if (!response.ok) {
|
|
585
|
+
throw new Error(`Failed to fetch template: ${url} (${response.status})`);
|
|
586
|
+
}
|
|
587
|
+
return response.text();
|
|
588
|
+
}
|
|
589
|
+
async function fetchComponentFiles(componentName) {
|
|
590
|
+
const component = await getComponent(componentName);
|
|
591
|
+
if (!component) {
|
|
592
|
+
throw new Error(`Component not found: ${componentName}`);
|
|
593
|
+
}
|
|
594
|
+
const files = /* @__PURE__ */ new Map();
|
|
595
|
+
for (const filePath of component.files) {
|
|
596
|
+
const content = await fetchTemplateFile(filePath);
|
|
597
|
+
files.set(filePath, content);
|
|
598
|
+
}
|
|
599
|
+
return files;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/commands/add.ts
|
|
603
|
+
async function add(componentName, options) {
|
|
604
|
+
console.clear();
|
|
605
|
+
p2.intro(chalk2.bgCyan.black(` \u{1FA9B} yantr add ${componentName} `));
|
|
606
|
+
try {
|
|
607
|
+
const cwd = process.cwd();
|
|
608
|
+
if (!await configExists(cwd)) {
|
|
609
|
+
p2.log.error("yantr.json not found.");
|
|
610
|
+
p2.log.info("Please run " + chalk2.cyan("yantr init") + " first.");
|
|
611
|
+
p2.outro(chalk2.red("Add cancelled."));
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
const config = await readConfig(cwd);
|
|
615
|
+
const spinner4 = p2.spinner();
|
|
616
|
+
spinner4.start("Fetching component registry...");
|
|
617
|
+
const availableComponents = await listComponents();
|
|
618
|
+
if (!availableComponents.includes(componentName)) {
|
|
619
|
+
spinner4.stop("Component not found");
|
|
620
|
+
p2.log.error(`Unknown component: ${chalk2.red(componentName)}`);
|
|
621
|
+
p2.log.info(`Available components: ${chalk2.cyan(availableComponents.join(", "))}`);
|
|
622
|
+
p2.outro(chalk2.red("Add cancelled."));
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
const alreadyInstalled = await isComponentInstalled(cwd, componentName);
|
|
626
|
+
if (alreadyInstalled && !options.overwrite) {
|
|
627
|
+
spinner4.stop("Component already installed");
|
|
628
|
+
const overwrite = await p2.confirm({
|
|
629
|
+
message: `${componentName} is already installed. Overwrite?`,
|
|
630
|
+
initialValue: false
|
|
631
|
+
});
|
|
632
|
+
if (p2.isCancel(overwrite) || !overwrite) {
|
|
633
|
+
p2.outro(chalk2.yellow("Add cancelled."));
|
|
634
|
+
process.exit(0);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
spinner4.start(`Downloading ${componentName}...`);
|
|
638
|
+
const component = await getComponent(componentName);
|
|
639
|
+
if (!component) {
|
|
640
|
+
spinner4.stop("Failed to fetch component");
|
|
641
|
+
p2.log.error("Failed to get component metadata");
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
const files = await fetchComponentFiles(componentName);
|
|
645
|
+
spinner4.stop(`Downloaded ${files.size} files`);
|
|
646
|
+
spinner4.start("Installing files...");
|
|
647
|
+
const installedFiles = [];
|
|
648
|
+
const baseDir = path6.join(cwd, config.srcDir, "lib", "yantr");
|
|
649
|
+
for (const [filePath, content] of files) {
|
|
650
|
+
const fileName = path6.basename(filePath);
|
|
651
|
+
const targetPath = path6.join(baseDir, componentName, fileName);
|
|
652
|
+
if (await fileExists(targetPath) && !options.overwrite && !alreadyInstalled) {
|
|
653
|
+
const shouldOverwrite = await p2.confirm({
|
|
654
|
+
message: `File ${fileName} already exists. Overwrite?`,
|
|
655
|
+
initialValue: false
|
|
656
|
+
});
|
|
657
|
+
if (p2.isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
await writeFile(targetPath, content);
|
|
662
|
+
installedFiles.push(path6.relative(cwd, targetPath));
|
|
663
|
+
}
|
|
664
|
+
spinner4.stop("Files installed");
|
|
665
|
+
if (component.dependencies.length > 0 || component.devDependencies.length > 0) {
|
|
666
|
+
spinner4.start("Installing dependencies...");
|
|
667
|
+
try {
|
|
668
|
+
if (component.dependencies.length > 0) {
|
|
669
|
+
await installDependencies(
|
|
670
|
+
config.packageManager,
|
|
671
|
+
component.dependencies,
|
|
672
|
+
cwd,
|
|
673
|
+
false
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (component.devDependencies.length > 0) {
|
|
677
|
+
await installDependencies(
|
|
678
|
+
config.packageManager,
|
|
679
|
+
component.devDependencies,
|
|
680
|
+
cwd,
|
|
681
|
+
true
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
spinner4.stop("Dependencies installed");
|
|
685
|
+
} catch (error) {
|
|
686
|
+
spinner4.stop("Could not install dependencies automatically");
|
|
687
|
+
if (component.dependencies.length > 0) {
|
|
688
|
+
p2.log.warning(
|
|
689
|
+
`Please run: ${chalk2.cyan(`${config.packageManager} add ${component.dependencies.join(" ")}`)}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if (component.devDependencies.length > 0) {
|
|
693
|
+
p2.log.warning(
|
|
694
|
+
`Please run: ${chalk2.cyan(`${config.packageManager} add -D ${component.devDependencies.join(" ")}`)}`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
await addInstalledComponent(cwd, componentName);
|
|
700
|
+
const filesNote = installedFiles.map((f) => `${chalk2.green("\u2713")} ${f}`).join("\n");
|
|
701
|
+
p2.note(filesNote, "Files created");
|
|
702
|
+
if (component.dependencies.length > 0) {
|
|
703
|
+
p2.log.info(`Dependencies: ${chalk2.cyan(component.dependencies.join(", "))}`);
|
|
704
|
+
}
|
|
705
|
+
const usageHints = {
|
|
706
|
+
auth: [
|
|
707
|
+
`Import auth routes: ${chalk2.gray(`import authRoutes from '${config.srcDir}/lib/yantr/auth/auth.routes';`)}`,
|
|
708
|
+
`Add to app: ${chalk2.gray('app.use("/api/auth", authRoutes);')}`
|
|
709
|
+
],
|
|
710
|
+
logger: [
|
|
711
|
+
`Import logger: ${chalk2.gray(`import { logger } from '${config.srcDir}/lib/yantr/logger/logger';`)}`,
|
|
712
|
+
`Use HTTP logging: ${chalk2.gray("app.use(httpLogger);")}`
|
|
713
|
+
],
|
|
714
|
+
database: [
|
|
715
|
+
`Import prisma: ${chalk2.gray(`import { prisma } from '${config.srcDir}/lib/yantr/database/prisma';`)}`,
|
|
716
|
+
`Initialize: ${chalk2.gray("npx prisma init")}`
|
|
717
|
+
],
|
|
718
|
+
security: [
|
|
719
|
+
`Import security: ${chalk2.gray(`import { rateLimiter, helmetConfig } from '${config.srcDir}/lib/yantr/security';`)}`,
|
|
720
|
+
`Add to app: ${chalk2.gray("app.use(helmetConfig); app.use(rateLimiter);")}`
|
|
721
|
+
]
|
|
722
|
+
};
|
|
723
|
+
if (usageHints[componentName]) {
|
|
724
|
+
p2.log.info("Usage:");
|
|
725
|
+
usageHints[componentName].forEach((hint, i) => {
|
|
726
|
+
console.log(` ${i + 1}. ${hint}`);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
p2.outro(chalk2.green(`${component.name} added successfully! \u{1F309}`));
|
|
730
|
+
} catch (error) {
|
|
731
|
+
p2.log.error(`Failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
732
|
+
console.error(error);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/commands/generate.ts
|
|
738
|
+
import * as p3 from "@clack/prompts";
|
|
739
|
+
import chalk3 from "chalk";
|
|
740
|
+
import path7 from "path";
|
|
741
|
+
async function generate(type, name, _options) {
|
|
742
|
+
console.clear();
|
|
743
|
+
p3.intro(chalk3.bgCyan.black(` \u{1FA9B} yantr generate ${type} ${name} `));
|
|
744
|
+
try {
|
|
745
|
+
const cwd = process.cwd();
|
|
746
|
+
if (!await configExists(cwd)) {
|
|
747
|
+
p3.log.error("yantr.json not found.");
|
|
748
|
+
p3.log.info("Please run " + chalk3.cyan("yantr init") + " first.");
|
|
749
|
+
p3.outro(chalk3.red("Generate cancelled."));
|
|
750
|
+
process.exit(1);
|
|
751
|
+
}
|
|
752
|
+
const config = await readConfig(cwd);
|
|
753
|
+
const validTypes = ["route"];
|
|
754
|
+
if (!validTypes.includes(type)) {
|
|
755
|
+
p3.log.error(`Unknown type: ${chalk3.red(type)}`);
|
|
756
|
+
p3.log.info(`Available types: ${chalk3.cyan(validTypes.join(", "))}`);
|
|
757
|
+
p3.outro(chalk3.red("Generate cancelled."));
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
if (type === "route") {
|
|
761
|
+
await generateRoute(cwd, config.srcDir, name);
|
|
762
|
+
}
|
|
763
|
+
p3.outro(chalk3.green(`Generated ${type} "${name}" successfully! \u{1FA9B}`));
|
|
764
|
+
} catch (error) {
|
|
765
|
+
p3.log.error(`Failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function generateRoute(cwd, srcDir, name) {
|
|
770
|
+
const spinner4 = p3.spinner();
|
|
771
|
+
const normalizedName = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
772
|
+
const pascalName = toPascalCase(normalizedName);
|
|
773
|
+
const camelName = toCamelCase(normalizedName);
|
|
774
|
+
const baseDir = path7.join(cwd, srcDir);
|
|
775
|
+
const routesDir = path7.join(baseDir, "routes");
|
|
776
|
+
const controllersDir = path7.join(baseDir, "controllers");
|
|
777
|
+
const servicesDir = path7.join(baseDir, "services");
|
|
778
|
+
const files = {
|
|
779
|
+
route: path7.join(routesDir, `${normalizedName}.routes.ts`),
|
|
780
|
+
controller: path7.join(controllersDir, `${normalizedName}.controller.ts`),
|
|
781
|
+
service: path7.join(servicesDir, `${normalizedName}.service.ts`)
|
|
782
|
+
};
|
|
783
|
+
for (const [type, filePath] of Object.entries(files)) {
|
|
784
|
+
if (await fileExists(filePath)) {
|
|
785
|
+
const overwrite = await p3.confirm({
|
|
786
|
+
message: `${type} file already exists. Overwrite?`,
|
|
787
|
+
initialValue: false
|
|
788
|
+
});
|
|
789
|
+
if (p3.isCancel(overwrite) || !overwrite) {
|
|
790
|
+
p3.outro(chalk3.yellow("Generate cancelled."));
|
|
791
|
+
process.exit(0);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
spinner4.start("Generating files...");
|
|
796
|
+
const serviceContent = generateServiceTemplate(pascalName, camelName);
|
|
797
|
+
await writeFile(files.service, serviceContent);
|
|
798
|
+
const controllerContent = generateControllerTemplate(pascalName, camelName, normalizedName);
|
|
799
|
+
await writeFile(files.controller, controllerContent);
|
|
800
|
+
const routeContent = generateRouteTemplate(pascalName, camelName, normalizedName);
|
|
801
|
+
await writeFile(files.route, routeContent);
|
|
802
|
+
spinner4.stop("Files generated");
|
|
803
|
+
const createdFiles = Object.values(files).map(
|
|
804
|
+
(f) => `${chalk3.green("\u2713")} ${path7.relative(cwd, f)}`
|
|
805
|
+
).join("\n");
|
|
806
|
+
p3.note(createdFiles, "Files created");
|
|
807
|
+
p3.log.info("Add to your app:");
|
|
808
|
+
console.log(chalk3.gray(` import ${camelName}Routes from './${srcDir}/routes/${normalizedName}.routes';`));
|
|
809
|
+
console.log(chalk3.gray(` app.use('/api/${normalizedName}', ${camelName}Routes);`));
|
|
810
|
+
}
|
|
811
|
+
function generateServiceTemplate(pascalName, _camelName) {
|
|
812
|
+
return `/**
|
|
813
|
+
* ${pascalName} Service
|
|
814
|
+
*
|
|
815
|
+
* Business logic for ${pascalName.toLowerCase()} operations.
|
|
816
|
+
*/
|
|
817
|
+
|
|
818
|
+
export interface ${pascalName} {
|
|
819
|
+
id: string;
|
|
820
|
+
// Add your fields here
|
|
821
|
+
createdAt: Date;
|
|
822
|
+
updatedAt: Date;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export interface Create${pascalName}Input {
|
|
826
|
+
// Add your input fields here
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export interface Update${pascalName}Input {
|
|
830
|
+
// Add your update fields here
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Get all ${pascalName.toLowerCase()}s
|
|
835
|
+
*/
|
|
836
|
+
export async function getAll(): Promise<${pascalName}[]> {
|
|
837
|
+
// TODO: Implement database query
|
|
838
|
+
// Example: return prisma.${_camelName}.findMany();
|
|
839
|
+
return [];
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Get ${pascalName.toLowerCase()} by ID
|
|
844
|
+
*/
|
|
845
|
+
export async function getById(id: string): Promise<${pascalName} | null> {
|
|
846
|
+
// TODO: Implement database query
|
|
847
|
+
// Example: return prisma.${_camelName}.findUnique({ where: { id } });
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Create a new ${pascalName.toLowerCase()}
|
|
853
|
+
*/
|
|
854
|
+
export async function create(input: Create${pascalName}Input): Promise<${pascalName}> {
|
|
855
|
+
// TODO: Implement database insert
|
|
856
|
+
// Example: return prisma.${_camelName}.create({ data: input });
|
|
857
|
+
throw new Error('Not implemented');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Update ${pascalName.toLowerCase()} by ID
|
|
862
|
+
*/
|
|
863
|
+
export async function update(id: string, input: Update${pascalName}Input): Promise<${pascalName}> {
|
|
864
|
+
// TODO: Implement database update
|
|
865
|
+
// Example: return prisma.${_camelName}.update({ where: { id }, data: input });
|
|
866
|
+
throw new Error('Not implemented');
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Delete ${pascalName.toLowerCase()} by ID
|
|
871
|
+
*/
|
|
872
|
+
export async function remove(id: string): Promise<void> {
|
|
873
|
+
// TODO: Implement database delete
|
|
874
|
+
// Example: await prisma.${_camelName}.delete({ where: { id } });
|
|
875
|
+
throw new Error('Not implemented');
|
|
876
|
+
}
|
|
877
|
+
`;
|
|
878
|
+
}
|
|
879
|
+
function generateControllerTemplate(pascalName, camelName, normalizedName) {
|
|
880
|
+
return `import type { Request, Response } from 'express';
|
|
881
|
+
import { z } from 'zod';
|
|
882
|
+
import * as ${camelName}Service from '../services/${normalizedName}.service';
|
|
883
|
+
import { asyncHandler } from '../lib/yantr/error-handler';
|
|
884
|
+
import { NotFoundError } from '../lib/yantr/error-handler';
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* ${pascalName} Controller
|
|
888
|
+
*
|
|
889
|
+
* Request handlers for ${pascalName.toLowerCase()} endpoints.
|
|
890
|
+
*/
|
|
891
|
+
|
|
892
|
+
// Validation schemas
|
|
893
|
+
export const create${pascalName}Schema = z.object({
|
|
894
|
+
// Add your validation rules here
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
export const update${pascalName}Schema = z.object({
|
|
898
|
+
// Add your validation rules here
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* GET /api/${normalizedName}
|
|
903
|
+
* Get all ${pascalName.toLowerCase()}s
|
|
904
|
+
*/
|
|
905
|
+
export const getAll = asyncHandler(async (_req: Request, res: Response) => {
|
|
906
|
+
const items = await ${camelName}Service.getAll();
|
|
907
|
+
|
|
908
|
+
res.json({
|
|
909
|
+
success: true,
|
|
910
|
+
data: items,
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* GET /api/${normalizedName}/:id
|
|
916
|
+
* Get ${pascalName.toLowerCase()} by ID
|
|
917
|
+
*/
|
|
918
|
+
export const getById = asyncHandler(async (req: Request, res: Response) => {
|
|
919
|
+
const { id } = req.params;
|
|
920
|
+
const item = await ${camelName}Service.getById(id);
|
|
921
|
+
|
|
922
|
+
if (!item) {
|
|
923
|
+
throw new NotFoundError('${pascalName} not found');
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
res.json({
|
|
927
|
+
success: true,
|
|
928
|
+
data: item,
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* POST /api/${normalizedName}
|
|
934
|
+
* Create a new ${pascalName.toLowerCase()}
|
|
935
|
+
*/
|
|
936
|
+
export const create = asyncHandler(async (req: Request, res: Response) => {
|
|
937
|
+
const input = create${pascalName}Schema.parse(req.body);
|
|
938
|
+
const item = await ${camelName}Service.create(input);
|
|
939
|
+
|
|
940
|
+
res.status(201).json({
|
|
941
|
+
success: true,
|
|
942
|
+
data: item,
|
|
943
|
+
message: '${pascalName} created successfully',
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* PUT /api/${normalizedName}/:id
|
|
949
|
+
* Update ${pascalName.toLowerCase()} by ID
|
|
950
|
+
*/
|
|
951
|
+
export const update = asyncHandler(async (req: Request, res: Response) => {
|
|
952
|
+
const { id } = req.params;
|
|
953
|
+
const input = update${pascalName}Schema.parse(req.body);
|
|
954
|
+
const item = await ${camelName}Service.update(id, input);
|
|
955
|
+
|
|
956
|
+
res.json({
|
|
957
|
+
success: true,
|
|
958
|
+
data: item,
|
|
959
|
+
message: '${pascalName} updated successfully',
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* DELETE /api/${normalizedName}/:id
|
|
965
|
+
* Delete ${pascalName.toLowerCase()} by ID
|
|
966
|
+
*/
|
|
967
|
+
export const remove = asyncHandler(async (req: Request, res: Response) => {
|
|
968
|
+
const { id } = req.params;
|
|
969
|
+
await ${camelName}Service.remove(id);
|
|
970
|
+
|
|
971
|
+
res.json({
|
|
972
|
+
success: true,
|
|
973
|
+
message: '${pascalName} deleted successfully',
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
`;
|
|
977
|
+
}
|
|
978
|
+
function generateRouteTemplate(pascalName, camelName, normalizedName) {
|
|
979
|
+
return `import { Router } from 'express';
|
|
980
|
+
import * as ${camelName}Controller from '../controllers/${normalizedName}.controller';
|
|
981
|
+
import { validateBody } from '../lib/yantr/zod-middleware';
|
|
982
|
+
import {
|
|
983
|
+
create${pascalName}Schema,
|
|
984
|
+
update${pascalName}Schema
|
|
985
|
+
} from '../controllers/${normalizedName}.controller';
|
|
986
|
+
|
|
987
|
+
const router = Router();
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* ${pascalName} Routes
|
|
991
|
+
*
|
|
992
|
+
* GET /api/${normalizedName} - Get all ${pascalName.toLowerCase()}s
|
|
993
|
+
* GET /api/${normalizedName}/:id - Get ${pascalName.toLowerCase()} by ID
|
|
994
|
+
* POST /api/${normalizedName} - Create ${pascalName.toLowerCase()}
|
|
995
|
+
* PUT /api/${normalizedName}/:id - Update ${pascalName.toLowerCase()}
|
|
996
|
+
* DELETE /api/${normalizedName}/:id - Delete ${pascalName.toLowerCase()}
|
|
997
|
+
*/
|
|
998
|
+
|
|
999
|
+
router.get('/', ${camelName}Controller.getAll);
|
|
1000
|
+
router.get('/:id', ${camelName}Controller.getById);
|
|
1001
|
+
router.post('/', validateBody(create${pascalName}Schema), ${camelName}Controller.create);
|
|
1002
|
+
router.put('/:id', validateBody(update${pascalName}Schema), ${camelName}Controller.update);
|
|
1003
|
+
router.delete('/:id', ${camelName}Controller.remove);
|
|
1004
|
+
|
|
1005
|
+
export default router;
|
|
1006
|
+
`;
|
|
1007
|
+
}
|
|
1008
|
+
function toPascalCase(str) {
|
|
1009
|
+
return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1010
|
+
}
|
|
1011
|
+
function toCamelCase(str) {
|
|
1012
|
+
const pascal = toPascalCase(str);
|
|
1013
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/index.ts
|
|
1017
|
+
var program = new Command();
|
|
1018
|
+
program.name("yantr").description("A Shadcn for Backend - Production-grade backend scaffolding CLI").version("0.1.0-beta.2");
|
|
1019
|
+
program.command("init").description("Initialize Setu in your project").option("-y, --yes", "Skip prompts and use defaults").action(init);
|
|
1020
|
+
program.command("add").description("Add a component to your project").argument("<component>", "Component to add (auth, logger, database, security)").option("-o, --overwrite", "Overwrite existing files").action(add);
|
|
1021
|
+
program.command("generate").alias("g").description("Generate boilerplate code").argument("<type>", "Type of code to generate (route)").argument("<name>", "Name of the resource").action(generate);
|
|
1022
|
+
program.parse();
|
|
1023
|
+
//# sourceMappingURL=index.js.map
|