@shakil-dev/shakil-stack 2.1.2 → 2.2.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.js +350 -0
- package/package.json +21 -9
- package/.github/workflows/publish.yml +0 -44
- package/bin/index.js +0 -729
package/dist/index.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{Command as ae}from"commander";import Q from"fs-extra";import w from"path";import{fileURLToPath as ie}from"url";import o from"fs-extra";import s from"path";import n from"chalk";import ee from"ora";import M from"inquirer";import{execSync as X}from"child_process";import ue from"fs-extra";var i=(e,r=process.cwd())=>{try{return X(e,{stdio:"inherit",cwd:r}),!0}catch{return!1}},f=()=>{let e=process.env.npm_config_user_agent||"";return e.includes("pnpm")?"pnpm":e.includes("yarn")?"yarn":"npm"};var j=e=>`import { Server } from 'http';
|
|
3
|
+
import app from './app.js';
|
|
4
|
+
import config from './app/config/index.js';
|
|
5
|
+
|
|
6
|
+
async function bootstrap() {
|
|
7
|
+
try {
|
|
8
|
+
const server: Server = app.listen(config.port, () => {
|
|
9
|
+
console.log(\`\u{1F680} Server is running on http://localhost:\${config.port}\`);
|
|
10
|
+
});
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('Failed to start server:', error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
bootstrap();
|
|
17
|
+
`,T=e=>`import cors from 'cors';
|
|
18
|
+
import express, { Application, Request, Response } from 'express';
|
|
19
|
+
import httpStatus from 'http-status';
|
|
20
|
+
import globalErrorHandler from './app/middleware/globalErrorHandler.js';
|
|
21
|
+
import notFound from './app/middleware/notFound.js';
|
|
22
|
+
import router from './app/routes/index.js';
|
|
23
|
+
import cookieParser from 'cookie-parser';
|
|
24
|
+
import morgan from 'morgan';
|
|
25
|
+
import helmet from 'helmet';
|
|
26
|
+
import { rateLimit } from 'express-rate-limit';
|
|
27
|
+
import { sanitizeRequest } from './app/middleware/sanitizeRequest.js';
|
|
28
|
+
|
|
29
|
+
const app: Application = express();
|
|
30
|
+
|
|
31
|
+
app.use(helmet());
|
|
32
|
+
app.use(cors({
|
|
33
|
+
origin: ["http://localhost:3000", "http://127.0.0.1:3000"],
|
|
34
|
+
credentials: true,
|
|
35
|
+
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
36
|
+
allowedHeaders: ["Content-Type", "Authorization", "Cookie"]
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const authLimiter = rateLimit({
|
|
40
|
+
windowMs: 15 * 60 * 1000,
|
|
41
|
+
limit: 100,
|
|
42
|
+
standardHeaders: 'draft-7',
|
|
43
|
+
legacyHeaders: false,
|
|
44
|
+
message: "Too many requests from this IP, please try again after 15 minutes"
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.use('/api/v1/auth', authLimiter);
|
|
48
|
+
|
|
49
|
+
app.use(cookieParser());
|
|
50
|
+
app.use(express.json({ limit: '10mb' }));
|
|
51
|
+
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
52
|
+
app.use(sanitizeRequest);
|
|
53
|
+
app.use(morgan("dev"));
|
|
54
|
+
|
|
55
|
+
app.use('/api/v1', router);
|
|
56
|
+
|
|
57
|
+
app.get('/', (req: Request, res: Response) => {
|
|
58
|
+
res.status(httpStatus.OK).json({
|
|
59
|
+
success: true,
|
|
60
|
+
message: 'Welcome to ${e} API',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
app.use(globalErrorHandler);
|
|
65
|
+
app.use(notFound);
|
|
66
|
+
|
|
67
|
+
export default app;
|
|
68
|
+
`,S=`import dotenv from 'dotenv';
|
|
69
|
+
import path from 'path';
|
|
70
|
+
|
|
71
|
+
dotenv.config({ path: path.join(process.cwd(), '.env') });
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
env: process.env.NODE_ENV,
|
|
75
|
+
port: process.env.PORT || 8000,
|
|
76
|
+
database_url: process.env.DATABASE_URL,
|
|
77
|
+
jwt_secret: process.env.JWT_SECRET,
|
|
78
|
+
};
|
|
79
|
+
`,R=`import "dotenv/config";
|
|
80
|
+
import { PrismaClient } from "@prisma/client";
|
|
81
|
+
import pkg from 'pg';
|
|
82
|
+
import { PrismaPg } from '@prisma/adapter-pg';
|
|
83
|
+
import config from '../config/index.js';
|
|
84
|
+
|
|
85
|
+
const { Pool } = pkg;
|
|
86
|
+
const connectionString = config.database_url as string;
|
|
87
|
+
const pool = new Pool({ connectionString });
|
|
88
|
+
const adapter = new PrismaPg(pool as any);
|
|
89
|
+
const prisma = new PrismaClient({ adapter });
|
|
90
|
+
|
|
91
|
+
export default prisma;
|
|
92
|
+
export { prisma };
|
|
93
|
+
`,v=`import { betterAuth } from "better-auth";
|
|
94
|
+
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
95
|
+
import config from "../config/index.js";
|
|
96
|
+
import { prisma } from "./prisma.js";
|
|
97
|
+
|
|
98
|
+
export const auth = betterAuth({
|
|
99
|
+
database: prismaAdapter(prisma, {
|
|
100
|
+
provider: "postgresql",
|
|
101
|
+
}),
|
|
102
|
+
secret: config.jwt_secret,
|
|
103
|
+
baseURL: "http://localhost:8000",
|
|
104
|
+
trustedOrigins: ["http://localhost:3000"],
|
|
105
|
+
emailAndPassword: {
|
|
106
|
+
enabled: true,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
`,D=`import { Router } from 'express';
|
|
110
|
+
const router = Router();
|
|
111
|
+
export default router;
|
|
112
|
+
`,A=`import { ErrorRequestHandler } from 'express';
|
|
113
|
+
import config from '../config/index.js';
|
|
114
|
+
|
|
115
|
+
const globalErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
|
|
116
|
+
res.status(500).json({
|
|
117
|
+
success: false,
|
|
118
|
+
message: error.message || 'Something went wrong!',
|
|
119
|
+
stack: config.env !== 'production' ? error?.stack : undefined,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default globalErrorHandler;
|
|
124
|
+
`,F=`import { Request, Response, NextFunction } from 'express';
|
|
125
|
+
import httpStatus from 'http-status';
|
|
126
|
+
|
|
127
|
+
const notFound = (req: Request, res: Response, next: NextFunction) => {
|
|
128
|
+
res.status(httpStatus.NOT_FOUND).json({
|
|
129
|
+
success: false,
|
|
130
|
+
message: 'API Not Found',
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default notFound;
|
|
135
|
+
`,P=`import { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
136
|
+
|
|
137
|
+
const catchAsync = (fn: RequestHandler) => {
|
|
138
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
139
|
+
try {
|
|
140
|
+
await fn(req, res, next);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
next(error);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default catchAsync;
|
|
148
|
+
`,$=`class ApiError extends Error {
|
|
149
|
+
statusCode: number;
|
|
150
|
+
constructor(statusCode: number, message: string | undefined, stack = '') {
|
|
151
|
+
super(message);
|
|
152
|
+
this.statusCode = statusCode;
|
|
153
|
+
if (stack) this.stack = stack;
|
|
154
|
+
else Error.captureStackTrace(this, this.constructor);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export default ApiError;
|
|
158
|
+
`,q=`import { JSDOM } from 'jsdom';
|
|
159
|
+
import createDOMPurify from 'dompurify';
|
|
160
|
+
|
|
161
|
+
const window = new JSDOM('').window;
|
|
162
|
+
const DOMPurify = createDOMPurify(window as any);
|
|
163
|
+
|
|
164
|
+
export const sanitize = (data: any): any => {
|
|
165
|
+
if (typeof data === 'string') return DOMPurify.sanitize(data);
|
|
166
|
+
if (typeof data === 'object' && data !== null) {
|
|
167
|
+
for (const key in data) {
|
|
168
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) data[key] = sanitize(data[key]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return data;
|
|
172
|
+
};
|
|
173
|
+
`,E=`import { Request, Response, NextFunction } from 'express';
|
|
174
|
+
import { sanitize } from '../utils/sanitizer.js';
|
|
175
|
+
|
|
176
|
+
export const sanitizeRequest = (req: Request, res: Response, next: NextFunction) => {
|
|
177
|
+
if (req.body) sanitize(req.body);
|
|
178
|
+
if (req.query) sanitize(req.query);
|
|
179
|
+
if (req.params) sanitize(req.params);
|
|
180
|
+
next();
|
|
181
|
+
};
|
|
182
|
+
`,C=`import { Response } from 'express';
|
|
183
|
+
|
|
184
|
+
type IResponse<T> = {
|
|
185
|
+
statusCode: number;
|
|
186
|
+
success: boolean;
|
|
187
|
+
message?: string | null;
|
|
188
|
+
meta?: {
|
|
189
|
+
limit: number;
|
|
190
|
+
page: number;
|
|
191
|
+
total: number;
|
|
192
|
+
};
|
|
193
|
+
data: T;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const sendResponse = <T>(res: Response, data: IResponse<T>) => {
|
|
197
|
+
res.status(data.statusCode).json({
|
|
198
|
+
success: data.success,
|
|
199
|
+
message: data.message || null,
|
|
200
|
+
meta: data.meta || null,
|
|
201
|
+
data: data.data || null,
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export default sendResponse;
|
|
206
|
+
`,z=`generator client {
|
|
207
|
+
provider = "prisma-client-js"
|
|
208
|
+
previewFeatures = ["prismaSchemaFolder"]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
datasource db {
|
|
212
|
+
provider = "postgresql"
|
|
213
|
+
}
|
|
214
|
+
`,I=`model User {
|
|
215
|
+
id String @id @default(uuid())
|
|
216
|
+
email String @unique
|
|
217
|
+
name String
|
|
218
|
+
createdAt DateTime @default(now())
|
|
219
|
+
updatedAt DateTime @updatedAt
|
|
220
|
+
accounts Account[]
|
|
221
|
+
sessions Session[]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
model Session {
|
|
225
|
+
id String @id @default(uuid())
|
|
226
|
+
userId String
|
|
227
|
+
token String @unique
|
|
228
|
+
expiresAt DateTime
|
|
229
|
+
createdAt DateTime @default(now())
|
|
230
|
+
updatedAt DateTime @updatedAt
|
|
231
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
model Account {
|
|
235
|
+
id String @id @default(uuid())
|
|
236
|
+
userId String
|
|
237
|
+
accountId String
|
|
238
|
+
providerId String
|
|
239
|
+
accessToken String?
|
|
240
|
+
refreshToken String?
|
|
241
|
+
idToken String?
|
|
242
|
+
accessTokenExpiresAt DateTime?
|
|
243
|
+
refreshTokenExpiresAt DateTime?
|
|
244
|
+
scope String?
|
|
245
|
+
password String?
|
|
246
|
+
createdAt DateTime @default(now())
|
|
247
|
+
updatedAt DateTime @updatedAt
|
|
248
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
249
|
+
}
|
|
250
|
+
`,O=`import "dotenv/config";
|
|
251
|
+
import { defineConfig } from "prisma/config";
|
|
252
|
+
import process from "process";
|
|
253
|
+
|
|
254
|
+
export default defineConfig({
|
|
255
|
+
schema: "prisma/schema",
|
|
256
|
+
datasource: {
|
|
257
|
+
url: process.env.DATABASE_URL,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
`,_=`{
|
|
261
|
+
"compilerOptions": {
|
|
262
|
+
"target": "ES2022",
|
|
263
|
+
"module": "NodeNext",
|
|
264
|
+
"moduleResolution": "NodeNext",
|
|
265
|
+
"outDir": "./dist",
|
|
266
|
+
"rootDir": "./src",
|
|
267
|
+
"strict": true,
|
|
268
|
+
"esModuleInterop": true,
|
|
269
|
+
"skipLibCheck": true,
|
|
270
|
+
"forceConsistentCasingInFileNames": true,
|
|
271
|
+
"baseUrl": ".",
|
|
272
|
+
"paths": {
|
|
273
|
+
"@/*": ["src/*"]
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
"include": ["src/**/*"],
|
|
277
|
+
"exclude": ["node_modules", "dist"]
|
|
278
|
+
}
|
|
279
|
+
`;var b=async e=>{let r=e;r||(r=(await M.prompt([{type:"input",name:"projectName",message:"What is your project name?",default:"shakil-stack-app"}])).projectName),r||(console.log(n.red("\u274C Error: Project name is required.")),process.exit(1));let{packageManager:a,useShadcn:g,installDeps:c}=await M.prompt([{type:"list",name:"packageManager",message:"Which package manager do you want to use?",choices:["pnpm","npm","yarn"],default:f()},{type:"confirm",name:"useShadcn",message:"Would you like to use shadcn/ui?",default:!0},{type:"confirm",name:"installDeps",message:"Do you want to install dependencies automatically?",default:!0}]),t=s.join(process.cwd(),r);o.existsSync(t)&&(console.log(n.red(`\u274C Error: Directory ${r} already exists.`)),process.exit(1)),console.log(n.cyan(`
|
|
280
|
+
\u{1F680} Initializing ${n.bold(r)}...
|
|
281
|
+
`));let m=ee("\u{1F6E0}\uFE0F Creating project structure...").start();try{await o.ensureDir(t),await o.ensureDir(s.join(t,"backend","src","app","config")),await o.ensureDir(s.join(t,"backend","src","app","lib")),await o.ensureDir(s.join(t,"backend","src","app","module")),await o.ensureDir(s.join(t,"backend","src","app","routes")),await o.ensureDir(s.join(t,"backend","src","app","middleware")),await o.ensureDir(s.join(t,"backend","src","app","utils")),await o.ensureDir(s.join(t,"backend","src","app","errorHelpers")),await o.ensureDir(s.join(t,"backend","prisma","schema")),console.log(n.cyan(`
|
|
282
|
+
\u{1F5BC}\uFE0F Scaffolding Next.js frontend...`)),i(`npx create-next-app@latest frontend --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${a}`,t);let d=["config","hooks","lib","services","types"];for(let y of d)await o.ensureDir(s.join(t,"frontend","src",y));if(g){console.log(n.cyan(`
|
|
283
|
+
\u{1F3A8} Setting up shadcn/ui...`));try{i("npx shadcn@latest init -d",s.join(t,"frontend")),console.log(n.cyan("\u{1F4E6} Adding common shadcn/ui components...")),i(`npx shadcn@latest add ${["button","card","input","label","textarea","dialog","dropdown-menu","table","tabs","checkbox"].join(" ")} -y`,s.join(t,"frontend")),console.log(n.green("\u2705 shadcn/ui and common components initialized successfully!\u2728"))}catch{console.log(n.yellow(`
|
|
284
|
+
\u26A0\uFE0F Warning: Failed to automate shadcn/ui init. You can run "npx shadcn@latest init" in the frontend folder.`))}}await o.outputFile(s.join(t,"backend","src","server.ts"),j(r)),await o.outputFile(s.join(t,"backend","src","app.ts"),T(r)),await o.outputFile(s.join(t,"backend","src","app","config","index.ts"),S),await o.outputFile(s.join(t,"backend","src","app","lib","prisma.ts"),R),await o.outputFile(s.join(t,"backend","src","app","lib","auth.ts"),v),await o.outputFile(s.join(t,"backend","src","app","routes","index.ts"),D),await o.outputFile(s.join(t,"backend","src","app","middleware","globalErrorHandler.ts"),A),await o.outputFile(s.join(t,"backend","src","app","middleware","notFound.ts"),F),await o.outputFile(s.join(t,"backend","src","app","middleware","sanitizeRequest.ts"),E),await o.outputFile(s.join(t,"backend","src","app","utils","catchAsync.ts"),P),await o.outputFile(s.join(t,"backend","src","app","utils","sendResponse.ts"),C),await o.outputFile(s.join(t,"backend","src","app","utils","sanitizer.ts"),q),await o.outputFile(s.join(t,"backend","src","app","errorHelpers","ApiError.ts"),$),await o.outputFile(s.join(t,"backend","prisma","schema","base.prisma"),z),await o.outputFile(s.join(t,"backend","prisma","schema","user.prisma"),I),await o.outputFile(s.join(t,"backend","prisma.config.ts"),O),await o.outputFile(s.join(t,"backend","tsconfig.json"),_),await o.outputFile(s.join(t,"backend",".gitignore"),`node_modules
|
|
285
|
+
dist
|
|
286
|
+
.env`),await o.outputFile(s.join(t,"backend",".env"),`DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
|
|
287
|
+
JWT_SECRET="your-secret-key"`);let h={name:`${r}-backend`,version:"1.0.0",type:"module",scripts:{test:'echo "Error: no test specified" && exit 1',dev:"nodemon --exec tsx src/server.ts",build:"prisma generate && tsup src/server.ts --format esm --platform node --target node20 --outDir dist --external pg-native",postinstall:"prisma generate",start:"node dist/server.js","prisma:generate":"prisma generate","prisma:migrate":"prisma migrate dev","prisma:studio":"prisma studio",seed:"tsx prisma/seed.ts",setup:"pnpm install && pnpm add @prisma/adapter-pg pg && pnpm add -D @types/pg && pnpm prisma:generate",predev:"pnpm run prisma:generate",init:"pnpm run prisma:generate && pnpm run prisma:migrate --name init",lint:"eslint src/**/*.ts","lint:fix":"eslint src/**/*.ts --fix",format:"prettier --write .",push:"prisma db push",pull:"prisma db pull"},dependencies:{"@prisma/adapter-pg":"^7.5.0","@prisma/client":"^7.5.0","better-auth":"^1.5.6","cookie-parser":"^1.4.7",cors:"^2.8.6",dompurify:"^3.3.3",dotenv:"^17.3.1",express:"^5.2.1","express-rate-limit":"^8.3.1",helmet:"^8.1.0","http-status":"^2.1.0",jsdom:"^29.0.1",jsonwebtoken:"^9.0.3",morgan:"^1.10.1",pg:"^8.20.0",winston:"^3.19.0",zod:"^4.3.6"},devDependencies:{"@types/cookie-parser":"^1.4.10","@types/cors":"^2.8.19","@types/express":"^5.0.6","@types/node":"^20.19.37","@types/pg":"^8.20.0","@types/morgan":"^1.9.10","@types/jsdom":"^21.1.7",prisma:"^7.5.0",tsx:"^4.21.0",nodemon:"^3.1.14",tsup:"^8.5.1",typescript:"^5.9.3",eslint:"^9.21.0",prettier:"^3.5.2"}};await o.writeJson(s.join(t,"backend","package.json"),h,{spaces:2}),m.succeed(n.green("\u2705 Project structure created! \u2728")),c&&(console.log(n.yellow(`
|
|
288
|
+
\u{1F4E6} Finalizing dependencies with ${a}...
|
|
289
|
+
`)),i(`${a} install`,s.join(t,"backend"))),console.log(n.cyan("To get started:")),console.log(n.white(` cd ${r}`)),console.log(n.white(` cd backend && ${a} dev
|
|
290
|
+
`)),console.log(n.white(` cd frontend && ${a} dev
|
|
291
|
+
`))}catch(d){m.fail(n.red("\u274C Failed to initialize project.")),console.error(d),process.exit(1)}};import l from"fs-extra";import x from"path";import p from"chalk";import re from"ora";var H=(e,r)=>`import { Request, Response } from 'express';
|
|
292
|
+
import httpStatus from 'http-status';
|
|
293
|
+
import catchAsync from '../../utils/catchAsync.js';
|
|
294
|
+
import sendResponse from '../../utils/sendResponse.js';
|
|
295
|
+
import { ${e}Service } from './${r}.service.js';
|
|
296
|
+
|
|
297
|
+
const create${e} = catchAsync(async (req: Request, res: Response) => {
|
|
298
|
+
const result = await ${e}Service.create${e}IntoDB(req.body);
|
|
299
|
+
sendResponse(res, {
|
|
300
|
+
statusCode: httpStatus.OK,
|
|
301
|
+
success: true,
|
|
302
|
+
message: '${e} created successfully',
|
|
303
|
+
data: result,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
export const ${e}Controller = {
|
|
308
|
+
create${e},
|
|
309
|
+
};
|
|
310
|
+
`,L=(e,r)=>`import { ${e} } from '@prisma/client';
|
|
311
|
+
import prisma from '../../lib/prisma.js';
|
|
312
|
+
|
|
313
|
+
const create${e}IntoDB = async (payload: any) => {
|
|
314
|
+
// Logic here
|
|
315
|
+
return payload;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export const ${e}Service = {
|
|
319
|
+
create${e}IntoDB,
|
|
320
|
+
};
|
|
321
|
+
`,N=(e,r)=>`import { Router } from 'express';
|
|
322
|
+
import { ${e}Controller } from './${r}.controller.js';
|
|
323
|
+
|
|
324
|
+
const router = Router();
|
|
325
|
+
|
|
326
|
+
router.post('/create-${r}', ${e}Controller.create${e});
|
|
327
|
+
|
|
328
|
+
export const ${e}Routes = router;
|
|
329
|
+
`,U=e=>`export type I${e} = {
|
|
330
|
+
// Define interface
|
|
331
|
+
};
|
|
332
|
+
`,B=e=>`import { z } from 'zod';
|
|
333
|
+
|
|
334
|
+
const create${e}ValidationSchema = z.object({
|
|
335
|
+
body: z.object({
|
|
336
|
+
// Define schema
|
|
337
|
+
}),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
export const ${e}Validations = {
|
|
341
|
+
create${e}ValidationSchema,
|
|
342
|
+
};
|
|
343
|
+
`,J=e=>`export const ${e}SearchableFields = [];
|
|
344
|
+
`,W=e=>`model ${e} {
|
|
345
|
+
id String @id @default(uuid())
|
|
346
|
+
name String
|
|
347
|
+
createdAt DateTime @default(now())
|
|
348
|
+
updatedAt DateTime @updatedAt
|
|
349
|
+
}
|
|
350
|
+
`;var V=async e=>{e||(console.log(p.red("\u274C Error: Module name is required.")),process.exit(1));let r=e.charAt(0).toUpperCase()+e.slice(1),a=e.toLowerCase(),g=l.existsSync("backend")?"backend":".",c=x.join(g,"src","app","module",r);l.existsSync(x.join(g,"src","app","module"))||(console.log(p.red("\u274C Error: This command must be run inside your shakil-stack project root or backend directory.")),process.exit(1)),l.existsSync(c)&&(console.log(p.red(`\u274C Error: Module ${r} already exists.`)),process.exit(1));let t=re(`\u{1F6E0}\uFE0F Generating module: ${p.cyan(r)}...`).start();try{await l.ensureDir(c);let m={"controller.ts":H(r,a),"service.ts":L(r,a),"route.ts":N(r,a),"interface.ts":U(r),"validation.ts":B(r),"constant.ts":J(r)};await l.outputFile(x.join(g,"prisma","schema",`${a}.prisma`),W(r));for(let[d,h]of Object.entries(m))await l.outputFile(x.join(c,`${a}.${d}`),h);t.succeed(p.green(`\u2705 Module ${r} generated successfully! \u2728`)),console.log(p.gray(`Created at: ${c}`))}catch(m){t.fail(p.red("\u274C Failed to generate module.")),console.error(m)}};import se from"fs-extra";import oe from"chalk";var G=async()=>{let e=f(),r=se.existsSync("backend")?"backend":".";console.log(oe.cyan(`\u{1F3D7}\uFE0F Building backend with ${e}...`)),i(`${e} run build`,r)};import ne from"fs-extra";import k from"chalk";var K=async e=>{let r=ne.existsSync("backend")?"backend":".";e==="generate"?(console.log(k.cyan("\u{1F504} Generating Prisma client...")),i("npx prisma generate",r)):e==="migrate"?(console.log(k.cyan("\u{1F680} Running Prisma migrations...")),i("npx prisma migrate dev",r)):console.log(k.red(`\u274C Error: Unknown prisma subcommand: ${e}`))};var pe=ie(import.meta.url),Y=w.dirname(pe),ce=w.resolve(Y,Q.existsSync(w.resolve(Y,"../../package.json"))?"../../package.json":"../package.json"),me=Q.readJsonSync(ce),u=new ae;u.name("shakil-stack").description("Full-stack EchoNet-style project generator CLI").version(me.version);u.command("init").description("Initialize a new full-stack project").argument("[projectName]","Name of the project").action(e=>{b(e)});u.command("generate").alias("g").description("Generate a new module").argument("<type>","Type of generation (module)").argument("<name>","Name of the module").action((e,r)=>{e==="module"?V(r):console.log(`\u274C Error: Unknown generation type: ${e}`)});u.command("build").description("Build the backend for production").action(()=>{G()});u.command("prisma").description("Prisma utilities").argument("<subcommand>","generate | migrate").action(e=>{K(e)});process.argv.slice(2).length?u.parse(process.argv):b();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shakil-dev/shakil-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Full-stack EchoNet-style project generator CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shakil-stack",
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
"generator",
|
|
14
14
|
"cli"
|
|
15
15
|
],
|
|
16
|
-
"main": "
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"type": "module",
|
|
17
18
|
"bin": {
|
|
18
|
-
"shakil-stack": "
|
|
19
|
+
"shakil-stack": "dist/index.js"
|
|
19
20
|
},
|
|
20
21
|
"license": "MIT",
|
|
21
22
|
"repository": {
|
|
@@ -29,14 +30,25 @@
|
|
|
29
30
|
"publishConfig": {
|
|
30
31
|
"access": "public"
|
|
31
32
|
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"start": "node ./bin/index.js"
|
|
34
|
-
},
|
|
35
33
|
"dependencies": {
|
|
36
34
|
"chalk": "^4.1.2",
|
|
37
35
|
"commander": "^11.1.0",
|
|
38
36
|
"fs-extra": "^11.2.0",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
37
|
+
"inquirer": "^8.2.6",
|
|
38
|
+
"ora": "^5.4.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/fs-extra": "^11.0.4",
|
|
42
|
+
"@types/inquirer": "^9.0.9",
|
|
43
|
+
"@types/node": "^25.5.0",
|
|
44
|
+
"@types/ora": "^3.2.0",
|
|
45
|
+
"tsup": "^8.5.1",
|
|
46
|
+
"tsx": "^4.21.0",
|
|
47
|
+
"typescript": "^6.0.2"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup src/bin/index.ts --format esm --outDir dist --clean --minify --target node20",
|
|
51
|
+
"dev": "tsx src/bin/index.ts",
|
|
52
|
+
"start": "node dist/bin/index.js"
|
|
41
53
|
}
|
|
42
|
-
}
|
|
54
|
+
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: write
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
publish:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout Code
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
with:
|
|
18
|
-
fetch-depth: 0
|
|
19
|
-
|
|
20
|
-
- name: Setup Node.js
|
|
21
|
-
uses: actions/setup-node@v4
|
|
22
|
-
with:
|
|
23
|
-
node-version: '20'
|
|
24
|
-
registry-url: 'https://registry.npmjs.org'
|
|
25
|
-
|
|
26
|
-
- name: Install Dependencies
|
|
27
|
-
run: npm ci
|
|
28
|
-
|
|
29
|
-
- name: Set Git Identity
|
|
30
|
-
run: |
|
|
31
|
-
git config --global user.name "github-actions[bot]"
|
|
32
|
-
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
33
|
-
git add . # Ensure any changes like package-lock.json are staged
|
|
34
|
-
|
|
35
|
-
- name: Bump Version
|
|
36
|
-
run: npm version patch
|
|
37
|
-
|
|
38
|
-
- name: Publish to NPM
|
|
39
|
-
run: npm publish --access public
|
|
40
|
-
env:
|
|
41
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
42
|
-
|
|
43
|
-
- name: Push Version Change to GitHub
|
|
44
|
-
run: git push origin main --tags
|
package/bin/index.js
DELETED
|
@@ -1,729 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs-extra');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { execSync } = require('child_process');
|
|
6
|
-
const { Command } = require('commander');
|
|
7
|
-
const inquirer = require('inquirer');
|
|
8
|
-
const chalk = require('chalk');
|
|
9
|
-
const ora = require('ora');
|
|
10
|
-
|
|
11
|
-
const program = new Command();
|
|
12
|
-
|
|
13
|
-
// --- Utils ---
|
|
14
|
-
const getPackageManager = () => {
|
|
15
|
-
if (fs.existsSync('pnpm-lock.yaml')) return 'pnpm';
|
|
16
|
-
if (fs.existsSync('yarn.lock')) return 'yarn';
|
|
17
|
-
return 'npm';
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const runCommand = (command, cwd = process.cwd()) => {
|
|
21
|
-
try {
|
|
22
|
-
execSync(command, { stdio: 'inherit', cwd });
|
|
23
|
-
} catch (err) {
|
|
24
|
-
// console.error(chalk.red(`Failed to execute: ${command}`));
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// --- Command: Init ---
|
|
29
|
-
const initProject = async (name) => {
|
|
30
|
-
console.log(chalk.cyan('\n🚀 Initializing Shakil-Stack Project Generator...\n'));
|
|
31
|
-
|
|
32
|
-
const answers = await inquirer.prompt([
|
|
33
|
-
{
|
|
34
|
-
type: 'input',
|
|
35
|
-
name: 'projectName',
|
|
36
|
-
message: 'What is your project name?',
|
|
37
|
-
default: name || 'my-new-project',
|
|
38
|
-
validate: (input) => (input ? true : 'Project name is required'),
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
type: 'list',
|
|
42
|
-
name: 'packageManager',
|
|
43
|
-
message: 'Which package manager would you like to use?',
|
|
44
|
-
choices: ['pnpm', 'npm', 'yarn'],
|
|
45
|
-
default: 'pnpm',
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: 'confirm',
|
|
49
|
-
name: 'useShadcn',
|
|
50
|
-
message: 'Would you like to use shadcn/ui for components?',
|
|
51
|
-
default: true,
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
type: 'confirm',
|
|
55
|
-
name: 'installDeps',
|
|
56
|
-
message: 'Would you like to install dependencies now?',
|
|
57
|
-
default: true,
|
|
58
|
-
},
|
|
59
|
-
]);
|
|
60
|
-
|
|
61
|
-
const { projectName, packageManager, installDeps, useShadcn } = answers;
|
|
62
|
-
const projectPath = path.resolve(process.cwd(), projectName);
|
|
63
|
-
|
|
64
|
-
if (fs.existsSync(projectPath)) {
|
|
65
|
-
console.log(chalk.red(`\n❌ Error: Directory ${projectName} already exists.\n`));
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
await fs.ensureDir(projectPath);
|
|
71
|
-
const spinner = ora(`🚀 Creating project: ${chalk.cyan(projectName)}...`).start();
|
|
72
|
-
|
|
73
|
-
// Backend Folder Structure
|
|
74
|
-
const backendDirs = [
|
|
75
|
-
'prisma',
|
|
76
|
-
'src/app/config',
|
|
77
|
-
'src/app/errorHelpers',
|
|
78
|
-
'src/app/interfaces',
|
|
79
|
-
'src/app/lib',
|
|
80
|
-
'src/app/middleware',
|
|
81
|
-
'src/app/module',
|
|
82
|
-
'src/app/routes',
|
|
83
|
-
'src/app/utils',
|
|
84
|
-
];
|
|
85
|
-
|
|
86
|
-
spinner.text = `📂 Creating backend folder structure...`;
|
|
87
|
-
for (const dir of backendDirs) {
|
|
88
|
-
await fs.ensureDir(path.join(projectPath, 'backend', dir));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Frontend (create-next-app)
|
|
92
|
-
spinner.text = `📦 Running create-next-app for frontend...`;
|
|
93
|
-
spinner.stop();
|
|
94
|
-
const nextAppCmd = `npx create-next-app@latest frontend --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${packageManager} --no-git`;
|
|
95
|
-
try {
|
|
96
|
-
execSync(nextAppCmd, { cwd: projectPath, stdio: 'inherit' });
|
|
97
|
-
} catch (err) {
|
|
98
|
-
console.log(chalk.yellow('\n⚠️ Warning: Failed to generate frontend via create-next-app. Building manually...'));
|
|
99
|
-
await fs.ensureDir(path.join(projectPath, 'frontend'));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Frontend extra folders
|
|
103
|
-
const frontendExtraFolders = ['config', 'hooks', 'lib', 'services', 'types'];
|
|
104
|
-
for (const folder of frontendExtraFolders) {
|
|
105
|
-
await fs.ensureDir(path.join(projectPath, 'frontend', 'src', folder));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Shadcn/UI initialization
|
|
109
|
-
if (useShadcn) {
|
|
110
|
-
console.log(chalk.cyan('\n🎨 Setting up shadcn/ui...'));
|
|
111
|
-
try {
|
|
112
|
-
// Using non-interactive init with default settings
|
|
113
|
-
// -d flag uses default options: TypeScript, Tailwind, Lucide Icons, Slate color, css variables
|
|
114
|
-
execSync(`npx shadcn@latest init -d`, {
|
|
115
|
-
cwd: path.join(projectPath, 'frontend'),
|
|
116
|
-
stdio: 'inherit'
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
console.log(chalk.cyan('📦 Adding common shadcn/ui components...'));
|
|
120
|
-
const commonComponents = ['button', 'card', 'input', 'label', 'textarea', 'dialog', 'dropdown-menu', 'table', 'tabs', 'checkbox'];
|
|
121
|
-
execSync(`npx shadcn@latest add ${commonComponents.join(' ')} -y`, {
|
|
122
|
-
cwd: path.join(projectPath, 'frontend'),
|
|
123
|
-
stdio: 'inherit'
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
console.log(chalk.green('✅ shadcn/ui and common components initialized successfully!✨'));
|
|
127
|
-
} catch (err) {
|
|
128
|
-
console.log(chalk.yellow('\n⚠️ Warning: Failed to automate shadcn/ui init. You can run "npx shadcn@latest init" in the frontend folder.'));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
spinner.start(`📂 Finalizing root files and backend code...`);
|
|
133
|
-
|
|
134
|
-
// Root Files
|
|
135
|
-
await fs.outputFile(path.join(projectPath, '.env'), 'DATABASE_URL="postgresql://user:password@localhost:5432/mydb"\nPORT=8000\nNODE_ENV=development\nJWT_SECRET="your-secret-key"');
|
|
136
|
-
await fs.outputFile(path.join(projectPath, '.gitignore'), 'node_modules\n.env\ndist\n*.db\n.next\n.DS_Store');
|
|
137
|
-
await fs.outputFile(path.join(projectPath, 'README.md'), `# ${projectName}\n\nGenerated with Full Shakil-Stack CLI.`);
|
|
138
|
-
|
|
139
|
-
// Backend Files Templates
|
|
140
|
-
const serverTs = `import { Server } from 'http';
|
|
141
|
-
import app from './app.js';
|
|
142
|
-
import config from './app/config/index.js';
|
|
143
|
-
|
|
144
|
-
let server: Server;
|
|
145
|
-
|
|
146
|
-
async function bootstrap() {
|
|
147
|
-
try {
|
|
148
|
-
server = app.listen(config.port, () => {
|
|
149
|
-
console.log(\`${projectName} server is listening on port \${config.port}\`);
|
|
150
|
-
});
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error('Failed to start server:', error);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
bootstrap();
|
|
157
|
-
`;
|
|
158
|
-
|
|
159
|
-
const appTs = `import cors from 'cors';
|
|
160
|
-
import express, { Application, Request, Response } from 'express';
|
|
161
|
-
import httpStatus from 'http-status';
|
|
162
|
-
import globalErrorHandler from './app/middleware/globalErrorHandler.js';
|
|
163
|
-
import notFound from './app/middleware/notFound.js';
|
|
164
|
-
import router from './app/routes/index.js';
|
|
165
|
-
import cookieParser from 'cookie-parser';
|
|
166
|
-
import morgan from 'morgan';
|
|
167
|
-
import helmet from 'helmet';
|
|
168
|
-
import { rateLimit } from 'express-rate-limit';
|
|
169
|
-
import { sanitizeRequest } from './app/middleware/sanitizeRequest.js';
|
|
170
|
-
|
|
171
|
-
const app: Application = express();
|
|
172
|
-
|
|
173
|
-
app.use(helmet());
|
|
174
|
-
app.use(cors({
|
|
175
|
-
origin: ["http://localhost:3000", "http://127.0.0.1:3000"],
|
|
176
|
-
credentials: true,
|
|
177
|
-
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
178
|
-
allowedHeaders: ["Content-Type", "Authorization", "Cookie"]
|
|
179
|
-
}));
|
|
180
|
-
|
|
181
|
-
const authLimiter = rateLimit({
|
|
182
|
-
windowMs: 15 * 60 * 1000,
|
|
183
|
-
limit: 100,
|
|
184
|
-
standardHeaders: 'draft-7',
|
|
185
|
-
legacyHeaders: false,
|
|
186
|
-
message: "Too many requests from this IP, please try again after 15 minutes"
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
app.use('/api/v1/auth', authLimiter);
|
|
190
|
-
|
|
191
|
-
app.use(cookieParser());
|
|
192
|
-
app.use(express.json({ limit: '10mb' }));
|
|
193
|
-
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
194
|
-
app.use(sanitizeRequest);
|
|
195
|
-
app.use(morgan("dev"));
|
|
196
|
-
|
|
197
|
-
app.use('/api/v1', router);
|
|
198
|
-
|
|
199
|
-
app.get('/', (req: Request, res: Response) => {
|
|
200
|
-
res.status(httpStatus.OK).json({
|
|
201
|
-
success: true,
|
|
202
|
-
message: 'Welcome to ${projectName} API',
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
app.use(globalErrorHandler);
|
|
207
|
-
app.use(notFound);
|
|
208
|
-
|
|
209
|
-
export default app;
|
|
210
|
-
`;
|
|
211
|
-
|
|
212
|
-
const configTs = `import dotenv from 'dotenv';
|
|
213
|
-
import path from 'path';
|
|
214
|
-
|
|
215
|
-
dotenv.config({ path: path.join(process.cwd(), '.env') });
|
|
216
|
-
|
|
217
|
-
export default {
|
|
218
|
-
env: process.env.NODE_ENV,
|
|
219
|
-
port: process.env.PORT || 8000,
|
|
220
|
-
database_url: process.env.DATABASE_URL,
|
|
221
|
-
jwt_secret: process.env.JWT_SECRET,
|
|
222
|
-
};
|
|
223
|
-
`;
|
|
224
|
-
|
|
225
|
-
const prismaTs = `import "dotenv/config";
|
|
226
|
-
import { PrismaClient } from "@prisma/client";
|
|
227
|
-
import pkg from 'pg';
|
|
228
|
-
import { PrismaPg } from '@prisma/adapter-pg';
|
|
229
|
-
import config from '../config/index.js';
|
|
230
|
-
|
|
231
|
-
const { Pool } = pkg;
|
|
232
|
-
const connectionString = config.database_url as string;
|
|
233
|
-
const pool = new Pool({ connectionString });
|
|
234
|
-
const adapter = new PrismaPg(pool as any);
|
|
235
|
-
const prisma = new PrismaClient({ adapter });
|
|
236
|
-
|
|
237
|
-
export default prisma;
|
|
238
|
-
export { prisma };
|
|
239
|
-
`;
|
|
240
|
-
|
|
241
|
-
const authTs = `import { betterAuth } from "better-auth";
|
|
242
|
-
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
243
|
-
import config from "../config/index.js";
|
|
244
|
-
import { prisma } from "./prisma.js";
|
|
245
|
-
|
|
246
|
-
export const auth = betterAuth({
|
|
247
|
-
database: prismaAdapter(prisma, {
|
|
248
|
-
provider: "postgresql",
|
|
249
|
-
}),
|
|
250
|
-
secret: config.jwt_secret,
|
|
251
|
-
baseURL: "http://localhost:8000",
|
|
252
|
-
trustedOrigins: ["http://localhost:3000"],
|
|
253
|
-
emailAndPassword: {
|
|
254
|
-
enabled: true,
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
`;
|
|
258
|
-
|
|
259
|
-
const routesTs = `import { Router } from 'express';
|
|
260
|
-
const router = Router();
|
|
261
|
-
export default router;
|
|
262
|
-
`;
|
|
263
|
-
|
|
264
|
-
const globalErrorHandlerTs = `import { ErrorRequestHandler } from 'express';
|
|
265
|
-
import config from '../config/index.js';
|
|
266
|
-
|
|
267
|
-
const globalErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
|
|
268
|
-
res.status(500).json({
|
|
269
|
-
success: false,
|
|
270
|
-
message: error.message || 'Something went wrong!',
|
|
271
|
-
stack: config.env !== 'production' ? error?.stack : undefined,
|
|
272
|
-
});
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
export default globalErrorHandler;
|
|
276
|
-
`;
|
|
277
|
-
|
|
278
|
-
const notFoundTs = `import { Request, Response, NextFunction } from 'express';
|
|
279
|
-
import httpStatus from 'http-status';
|
|
280
|
-
|
|
281
|
-
const notFound = (req: Request, res: Response, next: NextFunction) => {
|
|
282
|
-
res.status(httpStatus.NOT_FOUND).json({
|
|
283
|
-
success: false,
|
|
284
|
-
message: 'API Not Found',
|
|
285
|
-
});
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
export default notFound;
|
|
289
|
-
`;
|
|
290
|
-
|
|
291
|
-
const catchAsyncTs = `import { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
292
|
-
|
|
293
|
-
const catchAsync = (fn: RequestHandler) => {
|
|
294
|
-
return async (req: Request, res: Response, next: NextFunction) => {
|
|
295
|
-
try {
|
|
296
|
-
await fn(req, res, next);
|
|
297
|
-
} catch (error) {
|
|
298
|
-
next(error);
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
export default catchAsync;
|
|
304
|
-
`;
|
|
305
|
-
|
|
306
|
-
const apiErrorTs = `class ApiError extends Error {
|
|
307
|
-
statusCode: number;
|
|
308
|
-
constructor(statusCode: number, message: string | undefined, stack = '') {
|
|
309
|
-
super(message);
|
|
310
|
-
this.statusCode = statusCode;
|
|
311
|
-
if (stack) this.stack = stack;
|
|
312
|
-
else Error.captureStackTrace(this, this.constructor);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
export default ApiError;
|
|
316
|
-
`;
|
|
317
|
-
|
|
318
|
-
const sanitizerTs = `import { JSDOM } from 'jsdom';
|
|
319
|
-
import createDOMPurify from 'dompurify';
|
|
320
|
-
|
|
321
|
-
const window = new JSDOM('').window;
|
|
322
|
-
const DOMPurify = createDOMPurify(window as any);
|
|
323
|
-
|
|
324
|
-
export const sanitize = (data: any): any => {
|
|
325
|
-
if (typeof data === 'string') return DOMPurify.sanitize(data);
|
|
326
|
-
if (typeof data === 'object' && data !== null) {
|
|
327
|
-
for (const key in data) {
|
|
328
|
-
if (Object.prototype.hasOwnProperty.call(data, key)) data[key] = sanitize(data[key]);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return data;
|
|
332
|
-
};
|
|
333
|
-
`;
|
|
334
|
-
|
|
335
|
-
const sanitizeRequestTs = `import { Request, Response, NextFunction } from 'express';
|
|
336
|
-
import { sanitize } from '../utils/sanitizer.js';
|
|
337
|
-
|
|
338
|
-
export const sanitizeRequest = (req: Request, res: Response, next: NextFunction) => {
|
|
339
|
-
if (req.body) sanitize(req.body);
|
|
340
|
-
if (req.query) sanitize(req.query);
|
|
341
|
-
if (req.params) sanitize(req.params);
|
|
342
|
-
next();
|
|
343
|
-
};
|
|
344
|
-
`;
|
|
345
|
-
|
|
346
|
-
const basePrisma = `generator client {
|
|
347
|
-
provider = "prisma-client-js"
|
|
348
|
-
previewFeatures = ["prismaSchemaFolder"]
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
datasource db {
|
|
352
|
-
provider = "postgresql"
|
|
353
|
-
}
|
|
354
|
-
`;
|
|
355
|
-
|
|
356
|
-
const userPrisma = `model User {
|
|
357
|
-
id String @id @default(uuid())
|
|
358
|
-
email String @unique
|
|
359
|
-
name String
|
|
360
|
-
createdAt DateTime @default(now())
|
|
361
|
-
updatedAt DateTime @updatedAt
|
|
362
|
-
accounts Account[]
|
|
363
|
-
sessions Session[]
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
model Session {
|
|
367
|
-
id String @id @default(uuid())
|
|
368
|
-
userId String
|
|
369
|
-
token String @unique
|
|
370
|
-
expiresAt DateTime
|
|
371
|
-
createdAt DateTime @default(now())
|
|
372
|
-
updatedAt DateTime @updatedAt
|
|
373
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
model Account {
|
|
377
|
-
id String @id @default(uuid())
|
|
378
|
-
userId String
|
|
379
|
-
accountId String
|
|
380
|
-
providerId String
|
|
381
|
-
accessToken String?
|
|
382
|
-
refreshToken String?
|
|
383
|
-
idToken String?
|
|
384
|
-
accessTokenExpiresAt DateTime?
|
|
385
|
-
refreshTokenExpiresAt DateTime?
|
|
386
|
-
scope String?
|
|
387
|
-
password String?
|
|
388
|
-
createdAt DateTime @default(now())
|
|
389
|
-
updatedAt DateTime @updatedAt
|
|
390
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
391
|
-
}
|
|
392
|
-
`;
|
|
393
|
-
|
|
394
|
-
const prismaConfigTs = `import "dotenv/config";
|
|
395
|
-
import { defineConfig } from "prisma/config";
|
|
396
|
-
import process from "process";
|
|
397
|
-
|
|
398
|
-
export default defineConfig({
|
|
399
|
-
schema: "prisma/schema",
|
|
400
|
-
datasource: {
|
|
401
|
-
url: process.env.DATABASE_URL,
|
|
402
|
-
},
|
|
403
|
-
});
|
|
404
|
-
`;
|
|
405
|
-
|
|
406
|
-
const tsconfigTs = `{
|
|
407
|
-
"compilerOptions": {
|
|
408
|
-
"target": "ES2022",
|
|
409
|
-
"module": "NodeNext",
|
|
410
|
-
"moduleResolution": "NodeNext",
|
|
411
|
-
"outDir": "./dist",
|
|
412
|
-
"rootDir": "./src",
|
|
413
|
-
"strict": true,
|
|
414
|
-
"esModuleInterop": true,
|
|
415
|
-
"skipLibCheck": true,
|
|
416
|
-
"forceConsistentCasingInFileNames": true,
|
|
417
|
-
"baseUrl": ".",
|
|
418
|
-
"paths": {
|
|
419
|
-
"@/*": ["src/*"]
|
|
420
|
-
}
|
|
421
|
-
},
|
|
422
|
-
"include": ["src/**/*"],
|
|
423
|
-
"exclude": ["node_modules", "dist"]
|
|
424
|
-
}
|
|
425
|
-
`;
|
|
426
|
-
|
|
427
|
-
const sendResponseTs = `import { Response } from 'express';
|
|
428
|
-
|
|
429
|
-
type IResponse<T> = {
|
|
430
|
-
statusCode: number;
|
|
431
|
-
success: boolean;
|
|
432
|
-
message?: string | null;
|
|
433
|
-
meta?: {
|
|
434
|
-
limit: number;
|
|
435
|
-
page: number;
|
|
436
|
-
total: number;
|
|
437
|
-
};
|
|
438
|
-
data: T;
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const sendResponse = <T>(res: Response, data: IResponse<T>) => {
|
|
442
|
-
res.status(data.statusCode).json({
|
|
443
|
-
success: data.success,
|
|
444
|
-
message: data.message || null,
|
|
445
|
-
meta: data.meta || null,
|
|
446
|
-
data: data.data || null,
|
|
447
|
-
});
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
export default sendResponse;
|
|
451
|
-
`;
|
|
452
|
-
|
|
453
|
-
// Writing Backend Files
|
|
454
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'server.ts'), serverTs);
|
|
455
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app.ts'), appTs);
|
|
456
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'config', 'index.ts'), configTs);
|
|
457
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'lib', 'prisma.ts'), prismaTs);
|
|
458
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'lib', 'auth.ts'), authTs);
|
|
459
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'routes', 'index.ts'), routesTs);
|
|
460
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'middleware', 'globalErrorHandler.ts'), globalErrorHandlerTs);
|
|
461
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'middleware', 'notFound.ts'), notFoundTs);
|
|
462
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'middleware', 'sanitizeRequest.ts'), sanitizeRequestTs);
|
|
463
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'utils', 'catchAsync.ts'), catchAsyncTs);
|
|
464
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'utils', 'sendResponse.ts'), sendResponseTs);
|
|
465
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'utils', 'sanitizer.ts'), sanitizerTs);
|
|
466
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'errorHelpers', 'ApiError.ts'), apiErrorTs);
|
|
467
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'prisma', 'schema', 'base.prisma'), basePrisma);
|
|
468
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'prisma', 'schema', 'user.prisma'), userPrisma);
|
|
469
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'prisma.config.ts'), prismaConfigTs);
|
|
470
|
-
await fs.outputFile(path.join(projectPath, 'backend', 'tsconfig.json'), tsconfigTs);
|
|
471
|
-
await fs.outputFile(path.join(projectPath, 'backend', '.gitignore'), 'node_modules\ndist\n.env');
|
|
472
|
-
await fs.outputFile(path.join(projectPath, 'backend', '.env'), 'DATABASE_URL="postgresql://user:password@localhost:5432/mydb"\nJWT_SECRET="your-secret-key"');
|
|
473
|
-
|
|
474
|
-
const backendPkg = {
|
|
475
|
-
name: `${projectName}-backend`,
|
|
476
|
-
version: '1.0.0',
|
|
477
|
-
type: "module",
|
|
478
|
-
scripts: {
|
|
479
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
480
|
-
"dev": "nodemon --exec tsx src/server.ts",
|
|
481
|
-
"build": "prisma generate && tsup src/server.ts --format esm --platform node --target node20 --outDir dist --external pg-native",
|
|
482
|
-
"postinstall": "prisma generate",
|
|
483
|
-
"start": "node dist/server.js",
|
|
484
|
-
"prisma:generate": "prisma generate",
|
|
485
|
-
"prisma:migrate": "prisma migrate dev",
|
|
486
|
-
"prisma:studio": "prisma studio",
|
|
487
|
-
"seed": "tsx prisma/seed.ts",
|
|
488
|
-
"setup": "pnpm install && pnpm add @prisma/adapter-pg pg && pnpm add -D @types/pg && pnpm prisma:generate",
|
|
489
|
-
"predev": "pnpm run prisma:generate",
|
|
490
|
-
"init": "pnpm run prisma:generate && pnpm run prisma:migrate --name init",
|
|
491
|
-
"lint": "eslint src/**/*.ts",
|
|
492
|
-
"lint:fix": "eslint src/**/*.ts --fix",
|
|
493
|
-
"format": "prettier --write .",
|
|
494
|
-
"push": "prisma db push",
|
|
495
|
-
"pull": "prisma db pull"
|
|
496
|
-
},
|
|
497
|
-
dependencies: {
|
|
498
|
-
"@prisma/adapter-pg": "^7.5.0",
|
|
499
|
-
"@prisma/client": "^7.5.0",
|
|
500
|
-
"better-auth": "^1.5.6",
|
|
501
|
-
"cookie-parser": "^1.4.7",
|
|
502
|
-
"cors": "^2.8.6",
|
|
503
|
-
"dompurify": "^3.3.3",
|
|
504
|
-
"dotenv": "^17.3.1",
|
|
505
|
-
"express": "^5.2.1",
|
|
506
|
-
"express-rate-limit": "^8.3.1",
|
|
507
|
-
"helmet": "^8.1.0",
|
|
508
|
-
"http-status": "^2.1.0",
|
|
509
|
-
"jsdom": "^29.0.1",
|
|
510
|
-
"jsonwebtoken": "^9.0.3",
|
|
511
|
-
"morgan": "^1.10.1",
|
|
512
|
-
"pg": "^8.20.0",
|
|
513
|
-
"winston": "^3.19.0",
|
|
514
|
-
"zod": "^4.3.6"
|
|
515
|
-
},
|
|
516
|
-
devDependencies: {
|
|
517
|
-
"@types/cookie-parser": "^1.4.10",
|
|
518
|
-
"@types/cors": "^2.8.19",
|
|
519
|
-
"@types/express": "^5.0.6",
|
|
520
|
-
"@types/node": "^20.19.37",
|
|
521
|
-
"@types/pg": "^8.20.0",
|
|
522
|
-
"@types/morgan": "^1.9.10",
|
|
523
|
-
"@types/jsdom": "^21.1.7",
|
|
524
|
-
"prisma": "^7.5.0",
|
|
525
|
-
"tsx": "^4.21.0",
|
|
526
|
-
"nodemon": "^3.1.14",
|
|
527
|
-
"tsup": "^8.5.1",
|
|
528
|
-
"typescript": "^5.9.3",
|
|
529
|
-
"eslint": "^9.21.0",
|
|
530
|
-
"prettier": "^3.5.2"
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
await fs.writeJson(path.join(projectPath, 'backend', 'package.json'), backendPkg, { spaces: 2 });
|
|
534
|
-
|
|
535
|
-
spinner.succeed(chalk.green(`✅ Project structure created! ✨`));
|
|
536
|
-
|
|
537
|
-
if (installDeps) {
|
|
538
|
-
console.log(chalk.yellow(`\n📦 Finalizing dependencies with ${packageManager}...\n`));
|
|
539
|
-
runCommand(`cd "${path.join(projectPath, 'backend')}" && ${packageManager} install`);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
console.log(chalk.cyan(`To get started:`));
|
|
543
|
-
console.log(chalk.white(` cd ${projectName}`));
|
|
544
|
-
console.log(chalk.white(` cd backend && ${packageManager} dev\n`));
|
|
545
|
-
console.log(chalk.white(` cd frontend && ${packageManager} dev\n`));
|
|
546
|
-
|
|
547
|
-
} catch (error) {
|
|
548
|
-
console.error(error);
|
|
549
|
-
process.exit(1);
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
// --- Command: Generate Module ---
|
|
554
|
-
const generateModule = async (name) => {
|
|
555
|
-
if (!name) {
|
|
556
|
-
console.log(chalk.red('❌ Error: Module name is required.'));
|
|
557
|
-
process.exit(1);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const moduleName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
561
|
-
const lowercaseName = name.toLowerCase();
|
|
562
|
-
|
|
563
|
-
// Check if inside a shakil-stack project
|
|
564
|
-
const backendRoot = fs.existsSync('backend') ? 'backend' : '.';
|
|
565
|
-
const moduleDir = path.join(backendRoot, 'src', 'app', 'module', moduleName);
|
|
566
|
-
|
|
567
|
-
if (!fs.existsSync(path.join(backendRoot, 'src', 'app', 'module'))) {
|
|
568
|
-
console.log(chalk.red('❌ Error: This command must be run inside your shakil-stack project root or backend directory.'));
|
|
569
|
-
process.exit(1);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (fs.existsSync(moduleDir)) {
|
|
573
|
-
console.log(chalk.red(`❌ Error: Module ${moduleName} already exists.`));
|
|
574
|
-
process.exit(1);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const spinner = ora(`🛠️ Generating module: ${chalk.cyan(moduleName)}...`).start();
|
|
578
|
-
|
|
579
|
-
try {
|
|
580
|
-
await fs.ensureDir(moduleDir);
|
|
581
|
-
|
|
582
|
-
const files = {
|
|
583
|
-
'controller.ts': `import { Request, Response } from 'express';
|
|
584
|
-
import httpStatus from 'http-status';
|
|
585
|
-
import catchAsync from '../../utils/catchAsync.js';
|
|
586
|
-
import sendResponse from '../../utils/sendResponse.js';
|
|
587
|
-
import { ${moduleName}Service } from './${lowercaseName}.service.js';
|
|
588
|
-
|
|
589
|
-
const create${moduleName} = catchAsync(async (req: Request, res: Response) => {
|
|
590
|
-
const result = await ${moduleName}Service.create${moduleName}IntoDB(req.body);
|
|
591
|
-
sendResponse(res, {
|
|
592
|
-
statusCode: httpStatus.OK,
|
|
593
|
-
success: true,
|
|
594
|
-
message: '${moduleName} created successfully',
|
|
595
|
-
data: result,
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
export const ${moduleName}Controller = {
|
|
600
|
-
create${moduleName},
|
|
601
|
-
};
|
|
602
|
-
`,
|
|
603
|
-
'service.ts': `import { ${moduleName} } from '@prisma/client';
|
|
604
|
-
import prisma from '../../lib/prisma.js';
|
|
605
|
-
|
|
606
|
-
const create${moduleName}IntoDB = async (payload: any) => {
|
|
607
|
-
// Logic here
|
|
608
|
-
return payload;
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
export const ${moduleName}Service = {
|
|
612
|
-
create${moduleName}IntoDB,
|
|
613
|
-
};
|
|
614
|
-
`,
|
|
615
|
-
'route.ts': `import { Router } from 'express';
|
|
616
|
-
import { ${moduleName}Controller } from './${lowercaseName}.controller.js';
|
|
617
|
-
|
|
618
|
-
const router = Router();
|
|
619
|
-
|
|
620
|
-
router.post('/create-${lowercaseName}', ${moduleName}Controller.create${moduleName});
|
|
621
|
-
|
|
622
|
-
export const ${moduleName}Routes = router;
|
|
623
|
-
`,
|
|
624
|
-
'interface.ts': `export type I${moduleName} = {
|
|
625
|
-
// Define interface
|
|
626
|
-
};
|
|
627
|
-
`,
|
|
628
|
-
'validation.ts': `import { z } from 'zod';
|
|
629
|
-
|
|
630
|
-
const create${moduleName}ValidationSchema = z.object({
|
|
631
|
-
body: z.object({
|
|
632
|
-
// Define schema
|
|
633
|
-
}),
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
export const ${moduleName}Validations = {
|
|
637
|
-
create${moduleName}ValidationSchema,
|
|
638
|
-
};
|
|
639
|
-
`,
|
|
640
|
-
'constant.ts': `export const ${moduleName}SearchableFields = [];
|
|
641
|
-
`,
|
|
642
|
-
};
|
|
643
|
-
|
|
644
|
-
// Add Prisma schema for the module
|
|
645
|
-
const modulePrisma = `model ${moduleName} {
|
|
646
|
-
id String @id @default(uuid())
|
|
647
|
-
name String
|
|
648
|
-
createdAt DateTime @default(now())
|
|
649
|
-
updatedAt DateTime @updatedAt
|
|
650
|
-
}
|
|
651
|
-
`;
|
|
652
|
-
await fs.outputFile(path.join(backendRoot, 'prisma', 'schema', `${lowercaseName}.prisma`), modulePrisma);
|
|
653
|
-
|
|
654
|
-
for (const [ext, content] of Object.entries(files)) {
|
|
655
|
-
await fs.outputFile(path.join(moduleDir, `${lowercaseName}.${ext}`), content);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
spinner.succeed(chalk.green(`✅ Module ${moduleName} generated successfully! ✨`));
|
|
659
|
-
console.log(chalk.gray(`Created at: ${moduleDir}`));
|
|
660
|
-
|
|
661
|
-
} catch (error) {
|
|
662
|
-
spinner.fail(chalk.red('❌ Failed to generate module.'));
|
|
663
|
-
console.error(error);
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
|
|
667
|
-
// --- CLI Structure ---
|
|
668
|
-
const packageJson = require('../package.json');
|
|
669
|
-
|
|
670
|
-
program
|
|
671
|
-
.name('shakil-stack')
|
|
672
|
-
.description('Full-stack EchoNet-style project generator CLI')
|
|
673
|
-
.version(packageJson.version);
|
|
674
|
-
|
|
675
|
-
program
|
|
676
|
-
.command('init')
|
|
677
|
-
.description('Initialize a new full-stack project')
|
|
678
|
-
.argument('[projectName]', 'Name of the project')
|
|
679
|
-
.action((projectName) => {
|
|
680
|
-
initProject(projectName);
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
program
|
|
684
|
-
.command('generate')
|
|
685
|
-
.alias('g')
|
|
686
|
-
.description('Generate a new module')
|
|
687
|
-
.argument('<type>', 'Type of generation (module)')
|
|
688
|
-
.argument('<name>', 'Name of the module')
|
|
689
|
-
.action((type, name) => {
|
|
690
|
-
if (type === 'module') {
|
|
691
|
-
generateModule(name);
|
|
692
|
-
} else {
|
|
693
|
-
console.log(chalk.red(`❌ Error: Unknown generation type: ${type}`));
|
|
694
|
-
}
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
program
|
|
698
|
-
.command('build')
|
|
699
|
-
.description('Build the backend for production')
|
|
700
|
-
.action(() => {
|
|
701
|
-
const pm = getPackageManager();
|
|
702
|
-
const backendRoot = fs.existsSync('backend') ? 'backend' : '.';
|
|
703
|
-
console.log(chalk.cyan(`🏗️ Building backend with ${pm}...`));
|
|
704
|
-
runCommand(`${pm} run build`, backendRoot);
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
program
|
|
708
|
-
.command('prisma')
|
|
709
|
-
.description('Prisma utilities')
|
|
710
|
-
.argument('<subcommand>', 'generate | migrate')
|
|
711
|
-
.action((subcommand) => {
|
|
712
|
-
const backendRoot = fs.existsSync('backend') ? 'backend' : '.';
|
|
713
|
-
if (subcommand === 'generate') {
|
|
714
|
-
console.log(chalk.cyan('🔄 Generating Prisma client...'));
|
|
715
|
-
runCommand('npx prisma generate', backendRoot);
|
|
716
|
-
} else if (subcommand === 'migrate') {
|
|
717
|
-
console.log(chalk.cyan('🚀 Running Prisma migrations...'));
|
|
718
|
-
runCommand('npx prisma migrate dev', backendRoot);
|
|
719
|
-
} else {
|
|
720
|
-
console.log(chalk.red(`❌ Error: Unknown prisma subcommand: ${subcommand}`));
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
// Handle default action (no command)
|
|
725
|
-
if (!process.argv.slice(2).length) {
|
|
726
|
-
initProject();
|
|
727
|
-
} else {
|
|
728
|
-
program.parse(process.argv);
|
|
729
|
-
}
|