@shakil-dev/shakil-stack 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/index.js +433 -0
  2. package/package.json +22 -0
package/bin/index.js ADDED
@@ -0,0 +1,433 @@
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 inquirer = require('inquirer');
7
+ const chalk = require('chalk');
8
+ const ora = require('ora');
9
+
10
+ async function main() {
11
+ console.log(chalk.cyan('\nšŸš€ Initializing Shakil-Stack Project Generator...\n'));
12
+
13
+ const answers = await inquirer.prompt([
14
+ {
15
+ type: 'input',
16
+ name: 'projectName',
17
+ message: 'What is your project name?',
18
+ default: 'my-new-project',
19
+ validate: (input) => (input ? true : 'Project name is required'),
20
+ },
21
+ {
22
+ type: 'list',
23
+ name: 'packageManager',
24
+ message: 'Which package manager would you like to use?',
25
+ choices: ['pnpm', 'npm', 'yarn'],
26
+ default: 'pnpm',
27
+ },
28
+ {
29
+ type: 'confirm',
30
+ name: 'installDeps',
31
+ message: 'Would you like to install dependencies now?',
32
+ default: true,
33
+ },
34
+ ]);
35
+
36
+ const { projectName, packageManager, installDeps } = answers;
37
+ const projectPath = path.resolve(process.cwd(), projectName);
38
+
39
+ if (fs.existsSync(projectPath)) {
40
+ console.log(chalk.red(`\nāŒ Error: Directory ${projectName} already exists.\n`));
41
+ process.exit(1);
42
+ }
43
+
44
+ try {
45
+ // 1. Create Root Directory
46
+ await fs.ensureDir(projectPath);
47
+
48
+ const spinner = ora(`šŸš€ Creating project: ${chalk.cyan(projectName)}...`).start();
49
+
50
+ // 2. Define Backend Folder Structure
51
+ const backendDirs = [
52
+ 'prisma',
53
+ 'src/app/config',
54
+ 'src/app/errorHelpers',
55
+ 'src/app/interfaces',
56
+ 'src/app/lib',
57
+ 'src/app/middleware',
58
+ 'src/app/module',
59
+ 'src/app/routes',
60
+ 'src/app/utils',
61
+ ];
62
+
63
+ spinner.text = `šŸ“‚ Creating backend folder structure...`;
64
+ for (const dir of backendDirs) {
65
+ await fs.ensureDir(path.join(projectPath, 'backend', dir));
66
+ }
67
+
68
+ // 3. Generate Frontend using create-next-app FIRST
69
+ spinner.text = `šŸ“¦ Running create-next-app for frontend...`;
70
+ spinner.stop();
71
+
72
+ const nextAppCmd = `npx create-next-app@latest frontend --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${packageManager} --no-git`;
73
+ try {
74
+ execSync(nextAppCmd, { cwd: projectPath, stdio: 'inherit' });
75
+ } catch (err) {
76
+ console.log(chalk.yellow('\nāš ļø Warning: Failed to generate frontend via create-next-app. Building manually...'));
77
+ await fs.ensureDir(path.join(projectPath, 'frontend'));
78
+ }
79
+
80
+ // 4. Create additional frontend folders AFTER create-next-app
81
+ const frontendExtraFolders = ['config', 'hooks', 'lib', 'services', 'types'];
82
+ for (const folder of frontendExtraFolders) {
83
+ await fs.ensureDir(path.join(projectPath, 'frontend', 'src', folder));
84
+ }
85
+
86
+ spinner.start(`šŸ“‚ Finalizing root files and backend code...`);
87
+
88
+ // 5. Root Files
89
+ 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"');
90
+ await fs.outputFile(path.join(projectPath, '.gitignore'), 'node_modules\n.env\ndist\n*.db\n.next\n.DS_Store');
91
+ await fs.outputFile(path.join(projectPath, 'README.md'), `# ${projectName}\n\nGenerated with Full EchoNet-style CLI.`);
92
+
93
+ // 6. Backend Files (Refined)
94
+
95
+ const serverTs = `import { Server } from 'http';
96
+ import app from './app.js';
97
+ import config from './app/config/index.js';
98
+
99
+ let server: Server;
100
+
101
+ async function bootstrap() {
102
+ try {
103
+ server = app.listen(config.port, () => {
104
+ console.log(\`${projectName} server is listening on port \${config.port}\`);
105
+ });
106
+ } catch (error) {
107
+ console.error('Failed to start server:', error);
108
+ }
109
+ }
110
+
111
+ bootstrap();
112
+ `;
113
+
114
+ const appTs = `import cors from 'cors';
115
+ import express, { Application, Request, Response } from 'express';
116
+ import httpStatus from 'http-status';
117
+ import globalErrorHandler from './app/middleware/globalErrorHandler.js';
118
+ import notFound from './app/middleware/notFound.js';
119
+ import router from './app/routes/index.js';
120
+ import cookieParser from 'cookie-parser';
121
+ import morgan from 'morgan';
122
+ import helmet from 'helmet';
123
+ import { rateLimit } from 'express-rate-limit';
124
+ import { sanitizeRequest } from './app/middleware/sanitizeRequest.js';
125
+
126
+ const app: Application = express();
127
+
128
+ app.use(helmet());
129
+ app.use(cors({
130
+ origin: ["http://localhost:3000", "http://127.0.0.1:3000"],
131
+ credentials: true,
132
+ methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
133
+ allowedHeaders: ["Content-Type", "Authorization", "Cookie"]
134
+ }));
135
+
136
+ const authLimiter = rateLimit({
137
+ windowMs: 15 * 60 * 1000,
138
+ limit: 100,
139
+ standardHeaders: 'draft-7',
140
+ legacyHeaders: false,
141
+ message: "Too many requests from this IP, please try again after 15 minutes"
142
+ });
143
+
144
+ app.use('/api/v1/auth', authLimiter);
145
+
146
+ app.use(cookieParser());
147
+ app.use(express.json({ limit: '10mb' }));
148
+ app.use(express.urlencoded({ extended: true, limit: '10mb' }));
149
+ app.use(sanitizeRequest);
150
+ app.use(morgan("dev"));
151
+
152
+ app.use('/api/v1', router);
153
+
154
+ app.get('/', (req: Request, res: Response) => {
155
+ res.status(httpStatus.OK).json({
156
+ success: true,
157
+ message: 'Welcome to ${projectName} API',
158
+ });
159
+ });
160
+
161
+ app.use(globalErrorHandler);
162
+ app.use(notFound);
163
+
164
+ export default app;
165
+ `;
166
+
167
+ const configTs = `import dotenv from 'dotenv';
168
+ import path from 'path';
169
+
170
+ dotenv.config({ path: path.join(process.cwd(), '.env') });
171
+
172
+ export default {
173
+ env: process.env.NODE_ENV,
174
+ port: process.env.PORT || 8000,
175
+ database_url: process.env.DATABASE_URL,
176
+ jwt_secret: process.env.JWT_SECRET,
177
+ };
178
+ `;
179
+
180
+ const prismaTs = `import "dotenv/config";
181
+ import { PrismaClient } from "@prisma/client";
182
+ import pkg from 'pg';
183
+ import { PrismaPg } from '@prisma/adapter-pg';
184
+ import config from '../config/index.js';
185
+
186
+ const { Pool } = pkg;
187
+ const connectionString = config.database_url as string;
188
+ const pool = new Pool({ connectionString });
189
+ const adapter = new PrismaPg(pool as any);
190
+ const prisma = new PrismaClient({ adapter });
191
+
192
+ export default prisma;
193
+ export { prisma };
194
+ `;
195
+
196
+ const authTs = `import { betterAuth } from "better-auth";
197
+ import { prismaAdapter } from "better-auth/adapters/prisma";
198
+ import config from "../config/index.js";
199
+ import { prisma } from "./prisma.js";
200
+
201
+ export const auth = betterAuth({
202
+ database: prismaAdapter(prisma, {
203
+ provider: "postgresql",
204
+ }),
205
+ secret: config.jwt_secret,
206
+ baseURL: "http://localhost:8000",
207
+ trustedOrigins: ["http://localhost:3000"],
208
+ emailAndPassword: {
209
+ enabled: true,
210
+ },
211
+ });
212
+ `;
213
+
214
+ const routesTs = `import { Router } from 'express';
215
+ const router = Router();
216
+ export default router;
217
+ `;
218
+
219
+ const globalErrorHandlerTs = `import { ErrorRequestHandler } from 'express';
220
+ import config from '../config/index.js';
221
+
222
+ const globalErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
223
+ res.status(500).json({
224
+ success: false,
225
+ message: error.message || 'Something went wrong!',
226
+ stack: config.env !== 'production' ? error?.stack : undefined,
227
+ });
228
+ };
229
+
230
+ export default globalErrorHandler;
231
+ `;
232
+
233
+ const notFoundTs = `import { Request, Response, NextFunction } from 'express';
234
+ import httpStatus from 'http-status';
235
+
236
+ const notFound = (req: Request, res: Response, next: NextFunction) => {
237
+ res.status(httpStatus.NOT_FOUND).json({
238
+ success: false,
239
+ message: 'API Not Found',
240
+ });
241
+ };
242
+
243
+ export default notFound;
244
+ `;
245
+
246
+ const catchAsyncTs = `import { NextFunction, Request, RequestHandler, Response } from 'express';
247
+
248
+ const catchAsync = (fn: RequestHandler) => {
249
+ return async (req: Request, res: Response, next: NextFunction) => {
250
+ try {
251
+ await fn(req, res, next);
252
+ } catch (error) {
253
+ next(error);
254
+ }
255
+ };
256
+ };
257
+
258
+ export default catchAsync;
259
+ `;
260
+
261
+ const apiErrorTs = `class ApiError extends Error {
262
+ statusCode: number;
263
+ constructor(statusCode: number, message: string | undefined, stack = '') {
264
+ super(message);
265
+ this.statusCode = statusCode;
266
+ if (stack) this.stack = stack;
267
+ else Error.captureStackTrace(this, this.constructor);
268
+ }
269
+ }
270
+ export default ApiError;
271
+ `;
272
+
273
+ const sanitizerTs = `import { JSDOM } from 'jsdom';
274
+ import createDOMPurify from 'dompurify';
275
+
276
+ const window = new JSDOM('').window;
277
+ const DOMPurify = createDOMPurify(window as any);
278
+
279
+ export const sanitize = (data: any): any => {
280
+ if (typeof data === 'string') return DOMPurify.sanitize(data);
281
+ if (typeof data === 'object' && data !== null) {
282
+ for (const key in data) {
283
+ if (Object.prototype.hasOwnProperty.call(data, key)) data[key] = sanitize(data[key]);
284
+ }
285
+ }
286
+ return data;
287
+ };
288
+ `;
289
+
290
+ const sanitizeRequestTs = `import { Request, Response, NextFunction } from 'express';
291
+ import { sanitize } from '../utils/sanitizer.js';
292
+
293
+ export const sanitizeRequest = (req: Request, res: Response, next: NextFunction) => {
294
+ if (req.body) sanitize(req.body);
295
+ if (req.query) sanitize(req.query);
296
+ if (req.params) sanitize(req.params);
297
+ next();
298
+ };
299
+ `;
300
+
301
+ const schemaPrisma = `generator client {
302
+ provider = "prisma-client-js"
303
+ }
304
+
305
+ datasource db {
306
+ provider = "postgresql"
307
+ }
308
+
309
+ model User {
310
+ id String @id @default(uuid())
311
+ email String @unique
312
+ name String
313
+ createdAt DateTime @default(now())
314
+ updatedAt DateTime @updatedAt
315
+ accounts Account[]
316
+ sessions Session[]
317
+ }
318
+
319
+ model Session {
320
+ id String @id @default(uuid())
321
+ userId String
322
+ token String @unique
323
+ expiresAt DateTime
324
+ createdAt DateTime @default(now())
325
+ updatedAt DateTime @updatedAt
326
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
327
+ }
328
+
329
+ model Account {
330
+ id String @id @default(uuid())
331
+ userId String
332
+ accountId String
333
+ providerId String
334
+ accessToken String?
335
+ refreshToken String?
336
+ idToken String?
337
+ accessTokenExpiresAt DateTime?
338
+ refreshTokenExpiresAt DateTime?
339
+ scope String?
340
+ password String?
341
+ createdAt DateTime @default(now())
342
+ updatedAt DateTime @updatedAt
343
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
344
+ }
345
+ `;
346
+
347
+ // Write backend files
348
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'server.ts'), serverTs);
349
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app.ts'), appTs);
350
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'config', 'index.ts'), configTs);
351
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'lib', 'prisma.ts'), prismaTs);
352
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'lib', 'auth.ts'), authTs);
353
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'routes', 'index.ts'), routesTs);
354
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'middleware', 'globalErrorHandler.ts'), globalErrorHandlerTs);
355
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'middleware', 'notFound.ts'), notFoundTs);
356
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'middleware', 'sanitizeRequest.ts'), sanitizeRequestTs);
357
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'utils', 'catchAsync.ts'), catchAsyncTs);
358
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'utils', 'sanitizer.ts'), sanitizerTs);
359
+ await fs.outputFile(path.join(projectPath, 'backend', 'src', 'app', 'errorHelpers', 'ApiError.ts'), apiErrorTs);
360
+ await fs.outputFile(path.join(projectPath, 'backend', 'prisma', 'schema.prisma'), schemaPrisma);
361
+ await fs.outputFile(path.join(projectPath, 'backend', '.env'), 'DATABASE_URL="postgresql://user:password@localhost:5432/mydb"\nJWT_SECRET="your-secret-key"');
362
+
363
+ // Backend package.json
364
+ const backendPkg = {
365
+ name: `${projectName}-backend`,
366
+ version: '1.0.0',
367
+ type: "module",
368
+ scripts: {
369
+ "dev": "nodemon --exec tsx src/server.ts",
370
+ "build": "tsup src/server.ts --format esm --platform node --target node20 --outDir dist",
371
+ "start": "node dist/server.js",
372
+ },
373
+ dependencies: {
374
+ "@prisma/adapter-pg": "^7.5.0",
375
+ "@prisma/client": "^7.5.0",
376
+ "better-auth": "^1.5.6",
377
+ "cookie-parser": "^1.4.7",
378
+ "cors": "^2.8.6",
379
+ "dompurify": "^3.3.3",
380
+ "dotenv": "^17.3.1",
381
+ "express": "^5.2.1",
382
+ "express-rate-limit": "^8.3.1",
383
+ "helmet": "^8.1.0",
384
+ "http-status": "^2.1.0",
385
+ "jsdom": "^29.0.1",
386
+ "jsonwebtoken": "^9.0.3",
387
+ "morgan": "^1.10.1",
388
+ "pg": "^8.20.0",
389
+ "winston": "^3.19.0",
390
+ "zod": "^4.3.6"
391
+ },
392
+ devDependencies: {
393
+ "@types/cookie-parser": "^1.4.10",
394
+ "@types/cors": "^2.8.19",
395
+ "@types/express": "^5.0.6",
396
+ "@types/node": "^20.19.37",
397
+ "@types/pg": "^8.20.0",
398
+ "@types/morgan": "^1.9.10",
399
+ "@types/jsdom": "^21.1.7",
400
+ "prisma": "^7.5.0",
401
+ "tsx": "^4.21.0",
402
+ "nodemon": "^3.1.14",
403
+ "tsup": "^8.5.1",
404
+ "typescript": "^5.9.3"
405
+ }
406
+ };
407
+ await fs.writeJson(path.join(projectPath, 'backend', 'package.json'), backendPkg, { spaces: 2 });
408
+
409
+ spinner.succeed(chalk.green(`āœ… Project structure created! ✨`));
410
+
411
+ // 7. Dependency Installation
412
+ if (installDeps) {
413
+ console.log(chalk.yellow(`\nšŸ“¦ Finalizing dependencies with ${packageManager}...\n`));
414
+ try {
415
+ execSync(`cd "${path.join(projectPath, 'backend')}" && ${packageManager} install`, { stdio: 'inherit' });
416
+ console.log(chalk.green(`\nāœ… Backend dependencies installed! ✨\n`));
417
+ } catch (err) {
418
+ console.log(chalk.red(`\nāŒ Step failed. You can manually run '${packageManager} install' in the backend folder.\n`));
419
+ }
420
+ }
421
+
422
+ console.log(chalk.cyan(`To get started:`));
423
+ console.log(chalk.white(` cd ${projectName}`));
424
+ console.log(chalk.white(` cd backend && ${packageManager} dev\n`));
425
+ console.log(chalk.white(` cd frontend && ${packageManager} dev\n`));
426
+
427
+ } catch (error) {
428
+ console.error(error);
429
+ process.exit(1);
430
+ }
431
+ }
432
+
433
+ main();
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@shakil-dev/shakil-stack",
3
+ "version": "1.0.0",
4
+ "description": "Full-stack EchoNet-style project generator CLI",
5
+ "main": "./bin/index.js",
6
+ "bin": {
7
+ "shakil-stack": "bin/index.js"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "scripts": {
13
+ "start": "node ./bin/index.js"
14
+ },
15
+ "dependencies": {
16
+ "chalk": "^4.1.2",
17
+ "commander": "^11.1.0",
18
+ "fs-extra": "^11.2.0",
19
+ "ora": "^5.4.1",
20
+ "inquirer": "^8.2.6"
21
+ }
22
+ }