@msishamim/create-next-monorepo 1.0.0 → 2.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.
- package/dist/cli.js +3399 -414
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +97 -3
- package/dist/index.js +3414 -415
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,16 @@ var ProjectConfig = class {
|
|
|
43
43
|
packageManager;
|
|
44
44
|
gitInit;
|
|
45
45
|
githubFiles;
|
|
46
|
+
docker;
|
|
47
|
+
i18n;
|
|
48
|
+
payments;
|
|
49
|
+
email;
|
|
50
|
+
apiDocs;
|
|
51
|
+
storage;
|
|
52
|
+
e2e;
|
|
53
|
+
storybook;
|
|
54
|
+
cache;
|
|
55
|
+
logging;
|
|
46
56
|
/** Resolved npm package versions — set by Generator before template rendering */
|
|
47
57
|
versions = {};
|
|
48
58
|
constructor(options) {
|
|
@@ -58,6 +68,16 @@ var ProjectConfig = class {
|
|
|
58
68
|
this.packageManager = options.packageManager ?? "pnpm";
|
|
59
69
|
this.gitInit = options.gitInit ?? true;
|
|
60
70
|
this.githubFiles = options.githubFiles ?? false;
|
|
71
|
+
this.docker = options.docker ?? "none";
|
|
72
|
+
this.i18n = options.i18n ?? "none";
|
|
73
|
+
this.payments = options.payments ?? "none";
|
|
74
|
+
this.email = options.email ?? "none";
|
|
75
|
+
this.apiDocs = options.apiDocs ?? "none";
|
|
76
|
+
this.storage = options.storage ?? "none";
|
|
77
|
+
this.e2e = options.e2e ?? "none";
|
|
78
|
+
this.storybook = options.storybook ?? false;
|
|
79
|
+
this.cache = options.cache ?? "none";
|
|
80
|
+
this.logging = options.logging ?? "default";
|
|
61
81
|
}
|
|
62
82
|
// ── Derived name variants ──────────────────────────────────────
|
|
63
83
|
/** PascalCase (e.g. "my-app" → "MyApp") */
|
|
@@ -86,6 +106,33 @@ var ProjectConfig = class {
|
|
|
86
106
|
get usesTailwind() {
|
|
87
107
|
return this.styling === "tailwind";
|
|
88
108
|
}
|
|
109
|
+
get hasDocker() {
|
|
110
|
+
return this.docker !== "none";
|
|
111
|
+
}
|
|
112
|
+
get hasI18n() {
|
|
113
|
+
return this.i18n !== "none";
|
|
114
|
+
}
|
|
115
|
+
get hasPayments() {
|
|
116
|
+
return this.payments !== "none";
|
|
117
|
+
}
|
|
118
|
+
get hasEmail() {
|
|
119
|
+
return this.email !== "none";
|
|
120
|
+
}
|
|
121
|
+
get hasApiDocs() {
|
|
122
|
+
return this.apiDocs !== "none";
|
|
123
|
+
}
|
|
124
|
+
get hasStorage() {
|
|
125
|
+
return this.storage !== "none";
|
|
126
|
+
}
|
|
127
|
+
get hasE2e() {
|
|
128
|
+
return this.e2e !== "none";
|
|
129
|
+
}
|
|
130
|
+
get hasCache() {
|
|
131
|
+
return this.cache !== "none";
|
|
132
|
+
}
|
|
133
|
+
get hasLogging() {
|
|
134
|
+
return this.logging !== "default";
|
|
135
|
+
}
|
|
89
136
|
/** Install command for the selected package manager */
|
|
90
137
|
get installCommand() {
|
|
91
138
|
switch (this.packageManager) {
|
|
@@ -172,6 +219,32 @@ var ProjectConfig = class {
|
|
|
172
219
|
packages.push("jest", "@types/jest", "ts-jest");
|
|
173
220
|
}
|
|
174
221
|
packages.push("zod");
|
|
222
|
+
if (this.i18n === "next-intl") packages.push("next-intl");
|
|
223
|
+
if (this.payments === "stripe") packages.push("stripe");
|
|
224
|
+
if (this.payments === "lemonsqueezy") packages.push("@lemonsqueezy/lemonsqueezy.js");
|
|
225
|
+
if (this.payments === "paddle") packages.push("@paddle/paddle-node-sdk");
|
|
226
|
+
if (this.email === "resend") packages.push("resend", "@react-email/components");
|
|
227
|
+
if (this.email === "nodemailer") packages.push("nodemailer", "@types/nodemailer");
|
|
228
|
+
if (this.email === "sendgrid") packages.push("@sendgrid/mail");
|
|
229
|
+
if (this.apiDocs === "swagger" || this.apiDocs === "redoc") {
|
|
230
|
+
if (this.backend === "nestjs") {
|
|
231
|
+
packages.push("@nestjs/swagger");
|
|
232
|
+
} else {
|
|
233
|
+
packages.push("swagger-jsdoc", "swagger-ui-express", "@types/swagger-jsdoc", "@types/swagger-ui-express");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (this.apiDocs === "redoc") packages.push("redoc-express");
|
|
237
|
+
if (this.storage === "s3") packages.push("@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner");
|
|
238
|
+
if (this.storage === "uploadthing") packages.push("uploadthing", "@uploadthing/react");
|
|
239
|
+
if (this.storage === "cloudinary") packages.push("cloudinary");
|
|
240
|
+
if (this.e2e === "playwright") packages.push("@playwright/test");
|
|
241
|
+
if (this.e2e === "cypress") packages.push("cypress");
|
|
242
|
+
if (this.storybook) {
|
|
243
|
+
packages.push("storybook", "@storybook/react", "@storybook/react-vite", "@storybook/addon-essentials");
|
|
244
|
+
}
|
|
245
|
+
if (this.cache === "redis") packages.push("ioredis");
|
|
246
|
+
if (this.logging === "pino") packages.push("pino", "pino-pretty");
|
|
247
|
+
if (this.logging === "winston") packages.push("winston");
|
|
175
248
|
return packages;
|
|
176
249
|
}
|
|
177
250
|
};
|
|
@@ -246,7 +319,47 @@ var FALLBACK_VERSIONS = {
|
|
|
246
319
|
"@types/jest": "^29.5.14",
|
|
247
320
|
"ts-jest": "^29.3.2",
|
|
248
321
|
// Validation
|
|
249
|
-
zod: "^3.24.3"
|
|
322
|
+
zod: "^3.24.3",
|
|
323
|
+
// ── v2.0 options ──
|
|
324
|
+
// i18n
|
|
325
|
+
"next-intl": "^4.1.0",
|
|
326
|
+
// Payments
|
|
327
|
+
stripe: "^17.7.0",
|
|
328
|
+
"@lemonsqueezy/lemonsqueezy.js": "^4.0.0",
|
|
329
|
+
"@paddle/paddle-node-sdk": "^1.7.0",
|
|
330
|
+
// Email
|
|
331
|
+
resend: "^4.1.0",
|
|
332
|
+
"@react-email/components": "^0.0.36",
|
|
333
|
+
nodemailer: "^6.10.0",
|
|
334
|
+
"@types/nodemailer": "^6.4.17",
|
|
335
|
+
"@sendgrid/mail": "^8.1.0",
|
|
336
|
+
// API Docs
|
|
337
|
+
"@nestjs/swagger": "^8.1.0",
|
|
338
|
+
"swagger-jsdoc": "^6.2.8",
|
|
339
|
+
"swagger-ui-express": "^5.0.1",
|
|
340
|
+
"@types/swagger-jsdoc": "^6.0.4",
|
|
341
|
+
"@types/swagger-ui-express": "^4.1.8",
|
|
342
|
+
"redoc-express": "^2.1.0",
|
|
343
|
+
// Storage
|
|
344
|
+
"@aws-sdk/client-s3": "^3.750.0",
|
|
345
|
+
"@aws-sdk/s3-request-presigner": "^3.750.0",
|
|
346
|
+
uploadthing: "^7.6.0",
|
|
347
|
+
"@uploadthing/react": "^7.3.0",
|
|
348
|
+
cloudinary: "^2.6.0",
|
|
349
|
+
// E2E
|
|
350
|
+
"@playwright/test": "^1.50.0",
|
|
351
|
+
cypress: "^14.0.0",
|
|
352
|
+
// Storybook
|
|
353
|
+
storybook: "^8.5.0",
|
|
354
|
+
"@storybook/react": "^8.5.0",
|
|
355
|
+
"@storybook/react-vite": "^8.5.0",
|
|
356
|
+
"@storybook/addon-essentials": "^8.5.0",
|
|
357
|
+
// Cache
|
|
358
|
+
ioredis: "^5.6.0",
|
|
359
|
+
// Logging
|
|
360
|
+
pino: "^9.6.0",
|
|
361
|
+
"pino-pretty": "^13.0.0",
|
|
362
|
+
winston: "^3.17.0"
|
|
250
363
|
};
|
|
251
364
|
var VersionResolver = class {
|
|
252
365
|
resolved = {};
|
|
@@ -436,11 +549,27 @@ function getExpectedDirectories(config) {
|
|
|
436
549
|
dirs.push(stateDir);
|
|
437
550
|
}
|
|
438
551
|
if (config.githubFiles) {
|
|
439
|
-
dirs.push(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
552
|
+
dirs.push(".github/ISSUE_TEMPLATE", ".github/workflows");
|
|
553
|
+
}
|
|
554
|
+
if (config.docker === "full") dirs.push("nginx");
|
|
555
|
+
if (config.hasI18n) dirs.push("apps/web/i18n", "apps/web/messages", "apps/web/app/[locale]");
|
|
556
|
+
if (config.hasPayments) {
|
|
557
|
+
dirs.push("packages/payments/src");
|
|
558
|
+
dirs.push("apps/web/app/pricing");
|
|
559
|
+
if (config.backend === "nestjs") dirs.push("apps/api/src/payments");
|
|
560
|
+
}
|
|
561
|
+
if (config.hasEmail) dirs.push("packages/email/src", "packages/email/src/templates");
|
|
562
|
+
if (config.hasApiDocs) dirs.push("apps/api/src/docs");
|
|
563
|
+
if (config.hasStorage) {
|
|
564
|
+
dirs.push("packages/storage/src");
|
|
565
|
+
if (config.storage === "uploadthing") dirs.push("apps/web/app/api/uploadthing");
|
|
566
|
+
}
|
|
567
|
+
if (config.hasE2e) {
|
|
568
|
+
dirs.push(config.e2e === "playwright" ? "apps/web/e2e" : "apps/web/cypress/e2e");
|
|
569
|
+
}
|
|
570
|
+
if (config.storybook) dirs.push("packages/ui/.storybook");
|
|
571
|
+
if (config.hasCache) dirs.push("packages/cache/src");
|
|
572
|
+
if (config.hasLogging) dirs.push("packages/lib/src/logger");
|
|
444
573
|
return dirs;
|
|
445
574
|
}
|
|
446
575
|
function getExpectedFiles(config) {
|
|
@@ -601,6 +730,94 @@ function getExpectedFiles(config) {
|
|
|
601
730
|
".github/workflows/ci.yml"
|
|
602
731
|
);
|
|
603
732
|
}
|
|
733
|
+
if (config.hasDocker) {
|
|
734
|
+
files.push("apps/web/Dockerfile", "apps/api/Dockerfile", "docker-compose.prod.yml", ".dockerignore");
|
|
735
|
+
if (config.docker === "full") files.push("nginx/nginx.conf", "nginx/Dockerfile");
|
|
736
|
+
}
|
|
737
|
+
if (config.hasI18n) {
|
|
738
|
+
files.push(
|
|
739
|
+
"apps/web/i18n/request.ts",
|
|
740
|
+
"apps/web/i18n/routing.ts",
|
|
741
|
+
"apps/web/i18n/navigation.ts",
|
|
742
|
+
"apps/web/messages/en.json",
|
|
743
|
+
"apps/web/messages/ar.json",
|
|
744
|
+
"apps/web/app/[locale]/layout.tsx",
|
|
745
|
+
"apps/web/app/[locale]/page.tsx",
|
|
746
|
+
"apps/web/components/language-switcher.tsx"
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (config.hasPayments) {
|
|
750
|
+
files.push(
|
|
751
|
+
"packages/payments/package.json",
|
|
752
|
+
"packages/payments/tsconfig.json",
|
|
753
|
+
"packages/payments/src/index.ts",
|
|
754
|
+
"packages/payments/src/client.ts",
|
|
755
|
+
"packages/payments/src/webhook.ts",
|
|
756
|
+
"packages/payments/src/checkout.ts",
|
|
757
|
+
"packages/payments/src/subscription.ts",
|
|
758
|
+
"apps/web/app/pricing/page.tsx"
|
|
759
|
+
);
|
|
760
|
+
if (config.backend === "nestjs") {
|
|
761
|
+
files.push("apps/api/src/payments/payments.controller.ts");
|
|
762
|
+
} else {
|
|
763
|
+
files.push("apps/api/src/routes/payments.ts");
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (config.hasEmail) {
|
|
767
|
+
files.push(
|
|
768
|
+
"packages/email/package.json",
|
|
769
|
+
"packages/email/tsconfig.json",
|
|
770
|
+
"packages/email/src/index.ts",
|
|
771
|
+
"packages/email/src/client.ts",
|
|
772
|
+
"packages/email/src/send.ts",
|
|
773
|
+
"packages/email/src/templates/welcome.tsx",
|
|
774
|
+
"packages/email/src/templates/reset-password.tsx"
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
if (config.hasApiDocs) {
|
|
778
|
+
files.push("apps/api/src/docs/swagger-config.ts", "apps/api/src/docs/setup.ts");
|
|
779
|
+
}
|
|
780
|
+
if (config.hasStorage) {
|
|
781
|
+
files.push(
|
|
782
|
+
"packages/storage/package.json",
|
|
783
|
+
"packages/storage/tsconfig.json",
|
|
784
|
+
"packages/storage/src/index.ts",
|
|
785
|
+
"packages/storage/src/client.ts",
|
|
786
|
+
"packages/storage/src/upload.ts",
|
|
787
|
+
"packages/storage/src/download.ts"
|
|
788
|
+
);
|
|
789
|
+
if (config.storage === "uploadthing") {
|
|
790
|
+
files.push("apps/web/app/api/uploadthing/core.ts", "apps/web/app/api/uploadthing/route.ts");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (config.hasE2e) {
|
|
794
|
+
if (config.e2e === "playwright") {
|
|
795
|
+
files.push("apps/web/playwright.config.ts", "apps/web/e2e/example.spec.ts");
|
|
796
|
+
} else {
|
|
797
|
+
files.push("apps/web/cypress.config.ts", "apps/web/cypress/e2e/example.cy.ts");
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (config.storybook) {
|
|
801
|
+
files.push(
|
|
802
|
+
"packages/ui/.storybook/main.ts",
|
|
803
|
+
"packages/ui/.storybook/preview.ts",
|
|
804
|
+
"packages/ui/src/components/button.stories.tsx",
|
|
805
|
+
"packages/ui/src/components/card.stories.tsx",
|
|
806
|
+
"packages/ui/src/components/input.stories.tsx"
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if (config.hasCache) {
|
|
810
|
+
files.push(
|
|
811
|
+
"packages/cache/package.json",
|
|
812
|
+
"packages/cache/tsconfig.json",
|
|
813
|
+
"packages/cache/src/index.ts",
|
|
814
|
+
"packages/cache/src/client.ts",
|
|
815
|
+
"packages/cache/src/service.ts"
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
if (config.hasLogging) {
|
|
819
|
+
files.push("packages/lib/src/logger/index.ts", "packages/lib/src/logger/logger.ts");
|
|
820
|
+
}
|
|
604
821
|
return files;
|
|
605
822
|
}
|
|
606
823
|
|
|
@@ -3127,287 +3344,511 @@ ${setupPm}
|
|
|
3127
3344
|
`;
|
|
3128
3345
|
}
|
|
3129
3346
|
|
|
3130
|
-
// src/templates/
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
"
|
|
3135
|
-
"version": "0.0.0",
|
|
3347
|
+
// src/templates/cache-templates.ts
|
|
3348
|
+
function cachePackageJson(config) {
|
|
3349
|
+
return `{
|
|
3350
|
+
"name": "@${config.name}/cache",
|
|
3351
|
+
"version": "1.0.0",
|
|
3136
3352
|
"private": true,
|
|
3137
|
-
"
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
"
|
|
3142
|
-
"
|
|
3143
|
-
"
|
|
3353
|
+
"type": "module",
|
|
3354
|
+
"main": "./src/index.ts",
|
|
3355
|
+
"types": "./src/index.ts",
|
|
3356
|
+
"exports": {
|
|
3357
|
+
".": "./src/index.ts",
|
|
3358
|
+
"./client": "./src/client.ts",
|
|
3359
|
+
"./service": "./src/service.ts"
|
|
3144
3360
|
},
|
|
3145
3361
|
"dependencies": {
|
|
3146
|
-
"
|
|
3147
|
-
"@nestjs/core": "${config.versions["@nestjs/core"] ?? "^11.0.20"}",
|
|
3148
|
-
"@nestjs/platform-express": "${config.versions["@nestjs/platform-express"] ?? "^11.0.20"}",
|
|
3149
|
-
"reflect-metadata": "${config.versions["reflect-metadata"] ?? "^0.2.2"}",
|
|
3150
|
-
"rxjs": "${config.versions["rxjs"] ?? "^7.8.2"}",
|
|
3151
|
-
"@${config.name}/lib": "workspace:*"${config.hasDatabase ? `,
|
|
3152
|
-
"@${config.name}/database": "workspace:*"` : ""}
|
|
3362
|
+
"ioredis": "${config.versions["ioredis"] ?? "^5.6.0"}"
|
|
3153
3363
|
},
|
|
3154
3364
|
"devDependencies": {
|
|
3155
|
-
"@nestjs/cli": "${config.versions["@nestjs/cli"] ?? "^11.0.5"}",
|
|
3156
|
-
"@nestjs/testing": "${config.versions["@nestjs/testing"] ?? "^11.0.20"}",
|
|
3157
|
-
"@types/node": "${config.versions["@types/node"] ?? "^22.15.3"}",
|
|
3158
3365
|
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
3159
3366
|
}
|
|
3160
3367
|
}
|
|
3161
3368
|
`;
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
"extends": "@${config.name}/config/typescript/
|
|
3369
|
+
}
|
|
3370
|
+
function cacheTsConfig(config) {
|
|
3371
|
+
return `{
|
|
3372
|
+
"extends": "@${config.name}/config/typescript/base",
|
|
3166
3373
|
"compilerOptions": {
|
|
3167
3374
|
"outDir": "./dist",
|
|
3168
|
-
"rootDir": "./src"
|
|
3375
|
+
"rootDir": "./src",
|
|
3376
|
+
"verbatimModuleSyntax": false
|
|
3169
3377
|
},
|
|
3170
|
-
"include": ["src/**/*"]
|
|
3171
|
-
"exclude": ["node_modules", "dist", "test"]
|
|
3378
|
+
"include": ["src/**/*"]
|
|
3172
3379
|
}
|
|
3173
3380
|
`;
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3381
|
+
}
|
|
3382
|
+
function cacheIndex() {
|
|
3383
|
+
return `export { redis } from './client.js';
|
|
3384
|
+
export { CacheService } from './service.js';
|
|
3385
|
+
`;
|
|
3386
|
+
}
|
|
3387
|
+
function cacheClient() {
|
|
3388
|
+
return `import Redis from 'ioredis';
|
|
3181
3389
|
|
|
3182
|
-
|
|
3183
|
-
const app = await NestFactory.create(AppModule);
|
|
3390
|
+
const redisUrl = process.env.REDIS_URL ?? 'redis://localhost:6379';
|
|
3184
3391
|
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
});
|
|
3392
|
+
const globalForRedis = globalThis as unknown as {
|
|
3393
|
+
redis: Redis | undefined;
|
|
3394
|
+
};
|
|
3189
3395
|
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3396
|
+
export const redis = globalForRedis.redis ?? new Redis(redisUrl, {
|
|
3397
|
+
maxRetriesPerRequest: 3,
|
|
3398
|
+
retryStrategy(times) {
|
|
3399
|
+
if (times > 3) return null;
|
|
3400
|
+
return Math.min(times * 200, 2000);
|
|
3401
|
+
},
|
|
3402
|
+
});
|
|
3194
3403
|
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
console.log(\`\u{1F680} ${config.pascalCase} API running on http://localhost:\${port}/api\`);
|
|
3404
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
3405
|
+
globalForRedis.redis = redis;
|
|
3198
3406
|
}
|
|
3199
|
-
|
|
3200
|
-
bootstrap();
|
|
3201
3407
|
`;
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
import { AppController } from './app.controller.js';
|
|
3206
|
-
import { AppService } from './app.service.js';
|
|
3408
|
+
}
|
|
3409
|
+
function cacheService() {
|
|
3410
|
+
return `import { redis } from './client.js';
|
|
3207
3411
|
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
providers: [AppService],
|
|
3212
|
-
})
|
|
3213
|
-
export class AppModule {}
|
|
3214
|
-
`;
|
|
3215
|
-
}
|
|
3216
|
-
appController(_config) {
|
|
3217
|
-
return `import { Controller, Get } from '@nestjs/common';
|
|
3218
|
-
import { AppService } from './app.service.js';
|
|
3412
|
+
/** Simple cache wrapper around Redis with JSON serialization and TTL support */
|
|
3413
|
+
export class CacheService {
|
|
3414
|
+
private defaultTtl: number;
|
|
3219
3415
|
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3416
|
+
constructor(defaultTtl = 3600) {
|
|
3417
|
+
this.defaultTtl = defaultTtl;
|
|
3418
|
+
}
|
|
3223
3419
|
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3420
|
+
/** Get a cached value by key */
|
|
3421
|
+
async get<T>(key: string): Promise<T | null> {
|
|
3422
|
+
const value = await redis.get(key);
|
|
3423
|
+
if (!value) return null;
|
|
3424
|
+
try {
|
|
3425
|
+
return JSON.parse(value) as T;
|
|
3426
|
+
} catch {
|
|
3427
|
+
return value as unknown as T;
|
|
3428
|
+
}
|
|
3227
3429
|
}
|
|
3228
3430
|
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3431
|
+
/** Set a cached value with optional TTL (seconds) */
|
|
3432
|
+
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
3433
|
+
const serialized = JSON.stringify(value);
|
|
3434
|
+
const expiry = ttl ?? this.defaultTtl;
|
|
3435
|
+
await redis.set(key, serialized, 'EX', expiry);
|
|
3232
3436
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3437
|
+
|
|
3438
|
+
/** Delete a cached value */
|
|
3439
|
+
async del(key: string): Promise<void> {
|
|
3440
|
+
await redis.del(key);
|
|
3235
3441
|
}
|
|
3236
|
-
appService(config) {
|
|
3237
|
-
return `import { Injectable } from '@nestjs/common';
|
|
3238
3442
|
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
uptime: process.uptime(),
|
|
3246
|
-
};
|
|
3443
|
+
/** Delete all keys matching a pattern */
|
|
3444
|
+
async delPattern(pattern: string): Promise<void> {
|
|
3445
|
+
const keys = await redis.keys(pattern);
|
|
3446
|
+
if (keys.length > 0) {
|
|
3447
|
+
await redis.del(...keys);
|
|
3448
|
+
}
|
|
3247
3449
|
}
|
|
3248
3450
|
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
version: '1.0.0',
|
|
3253
|
-
environment: process.env.NODE_ENV ?? 'development',
|
|
3254
|
-
};
|
|
3451
|
+
/** Check if a key exists */
|
|
3452
|
+
async exists(key: string): Promise<boolean> {
|
|
3453
|
+
return (await redis.exists(key)) === 1;
|
|
3255
3454
|
}
|
|
3256
3455
|
}
|
|
3257
3456
|
`;
|
|
3258
|
-
|
|
3259
|
-
exceptionFilter() {
|
|
3260
|
-
return `import {
|
|
3261
|
-
ExceptionFilter,
|
|
3262
|
-
Catch,
|
|
3263
|
-
ArgumentsHost,
|
|
3264
|
-
HttpException,
|
|
3265
|
-
HttpStatus,
|
|
3266
|
-
} from '@nestjs/common';
|
|
3267
|
-
import type { Response } from 'express';
|
|
3268
|
-
|
|
3269
|
-
@Catch()
|
|
3270
|
-
export class HttpExceptionFilter implements ExceptionFilter {
|
|
3271
|
-
catch(exception: unknown, host: ArgumentsHost) {
|
|
3272
|
-
const ctx = host.switchToHttp();
|
|
3273
|
-
const response = ctx.getResponse<Response>();
|
|
3457
|
+
}
|
|
3274
3458
|
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3459
|
+
// src/templates/i18n-templates.ts
|
|
3460
|
+
function i18nRequest() {
|
|
3461
|
+
return `import { getRequestConfig } from 'next-intl/server';
|
|
3462
|
+
import { routing } from './routing';
|
|
3279
3463
|
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
? exception.message
|
|
3283
|
-
: 'Internal server error';
|
|
3464
|
+
export default getRequestConfig(async ({ requestLocale }) => {
|
|
3465
|
+
let locale = await requestLocale;
|
|
3284
3466
|
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
error: {
|
|
3288
|
-
code: HttpStatus[status] ?? 'UNKNOWN_ERROR',
|
|
3289
|
-
message,
|
|
3290
|
-
},
|
|
3291
|
-
timestamp: new Date().toISOString(),
|
|
3292
|
-
});
|
|
3467
|
+
if (!locale || !routing.locales.includes(locale as any)) {
|
|
3468
|
+
locale = routing.defaultLocale;
|
|
3293
3469
|
}
|
|
3470
|
+
|
|
3471
|
+
return {
|
|
3472
|
+
locale,
|
|
3473
|
+
messages: (await import(\`../messages/\${locale}.json\`)).default,
|
|
3474
|
+
};
|
|
3475
|
+
});
|
|
3476
|
+
`;
|
|
3294
3477
|
}
|
|
3478
|
+
function i18nRouting() {
|
|
3479
|
+
return `import { defineRouting } from 'next-intl/routing';
|
|
3480
|
+
|
|
3481
|
+
export const routing = defineRouting({
|
|
3482
|
+
locales: ['en', 'ar'],
|
|
3483
|
+
defaultLocale: 'en',
|
|
3484
|
+
});
|
|
3295
3485
|
`;
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
NestInterceptor,
|
|
3301
|
-
ExecutionContext,
|
|
3302
|
-
CallHandler,
|
|
3303
|
-
} from '@nestjs/common';
|
|
3304
|
-
import { Observable, tap } from 'rxjs';
|
|
3486
|
+
}
|
|
3487
|
+
function i18nNavigation() {
|
|
3488
|
+
return `import { createNavigation } from 'next-intl/navigation';
|
|
3489
|
+
import { routing } from './routing';
|
|
3305
3490
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3491
|
+
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
|
3492
|
+
createNavigation(routing);
|
|
3493
|
+
`;
|
|
3494
|
+
}
|
|
3495
|
+
function messagesEn(config) {
|
|
3496
|
+
return JSON.stringify(
|
|
3497
|
+
{
|
|
3498
|
+
common: {
|
|
3499
|
+
appName: config.pascalCase,
|
|
3500
|
+
home: "Home",
|
|
3501
|
+
about: "About",
|
|
3502
|
+
contact: "Contact",
|
|
3503
|
+
getStarted: "Get Started",
|
|
3504
|
+
documentation: "Documentation",
|
|
3505
|
+
language: "Language"
|
|
3506
|
+
},
|
|
3507
|
+
home: {
|
|
3508
|
+
title: config.pascalCase,
|
|
3509
|
+
description: `Production-ready monorepo with Next.js 15 and Turborepo.`,
|
|
3510
|
+
frontend: "Frontend",
|
|
3511
|
+
frontendDesc: "Next.js 15 with App Router",
|
|
3512
|
+
backend: "Backend",
|
|
3513
|
+
backendDesc: "REST API server"
|
|
3514
|
+
},
|
|
3515
|
+
notFound: {
|
|
3516
|
+
title: "Page not found",
|
|
3517
|
+
goHome: "Go Home"
|
|
3518
|
+
}
|
|
3519
|
+
},
|
|
3520
|
+
null,
|
|
3521
|
+
2
|
|
3522
|
+
) + "\n";
|
|
3523
|
+
}
|
|
3524
|
+
function messagesAr(config) {
|
|
3525
|
+
return JSON.stringify(
|
|
3526
|
+
{
|
|
3527
|
+
common: {
|
|
3528
|
+
appName: config.pascalCase,
|
|
3529
|
+
home: "\u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629",
|
|
3530
|
+
about: "\u062D\u0648\u0644",
|
|
3531
|
+
contact: "\u0627\u062A\u0635\u0644 \u0628\u0646\u0627",
|
|
3532
|
+
getStarted: "\u0627\u0628\u062F\u0623 \u0627\u0644\u0622\u0646",
|
|
3533
|
+
documentation: "\u0627\u0644\u062A\u0648\u062B\u064A\u0642",
|
|
3534
|
+
language: "\u0627\u0644\u0644\u063A\u0629"
|
|
3535
|
+
},
|
|
3536
|
+
home: {
|
|
3537
|
+
title: config.pascalCase,
|
|
3538
|
+
description: "\u0645\u0633\u062A\u0648\u062F\u0639 \u0625\u0646\u062A\u0627\u062C\u064A \u0645\u062A\u0643\u0627\u0645\u0644 \u0645\u0639 Next.js 15 \u0648 Turborepo.",
|
|
3539
|
+
frontend: "\u0627\u0644\u0648\u0627\u062C\u0647\u0629 \u0627\u0644\u0623\u0645\u0627\u0645\u064A\u0629",
|
|
3540
|
+
frontendDesc: "Next.js 15 \u0645\u0639 App Router",
|
|
3541
|
+
backend: "\u0627\u0644\u062E\u0627\u062F\u0645",
|
|
3542
|
+
backendDesc: "\u062E\u0627\u062F\u0645 REST API"
|
|
3543
|
+
},
|
|
3544
|
+
notFound: {
|
|
3545
|
+
title: "\u0627\u0644\u0635\u0641\u062D\u0629 \u063A\u064A\u0631 \u0645\u0648\u062C\u0648\u062F\u0629",
|
|
3546
|
+
goHome: "\u0627\u0644\u0639\u0648\u062F\u0629 \u0644\u0644\u0631\u0626\u064A\u0633\u064A\u0629"
|
|
3547
|
+
}
|
|
3548
|
+
},
|
|
3549
|
+
null,
|
|
3550
|
+
2
|
|
3551
|
+
) + "\n";
|
|
3552
|
+
}
|
|
3553
|
+
function i18nLayout(config) {
|
|
3554
|
+
return `import type { Metadata } from 'next';
|
|
3555
|
+
import { NextIntlClientProvider } from 'next-intl';
|
|
3556
|
+
import { getMessages } from 'next-intl/server';
|
|
3557
|
+
import { notFound } from 'next/navigation';
|
|
3558
|
+
import { routing } from '@/i18n/routing';
|
|
3559
|
+
import '../globals.css';
|
|
3313
3560
|
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3561
|
+
export const metadata: Metadata = {
|
|
3562
|
+
title: '${config.pascalCase}',
|
|
3563
|
+
description: 'Built with Next.js 15 and Turborepo',
|
|
3564
|
+
};
|
|
3565
|
+
|
|
3566
|
+
export default async function LocaleLayout({
|
|
3567
|
+
children,
|
|
3568
|
+
params,
|
|
3569
|
+
}: {
|
|
3570
|
+
children: React.ReactNode;
|
|
3571
|
+
params: Promise<{ locale: string }>;
|
|
3572
|
+
}) {
|
|
3573
|
+
const { locale } = await params;
|
|
3574
|
+
|
|
3575
|
+
if (!routing.locales.includes(locale as any)) {
|
|
3576
|
+
notFound();
|
|
3320
3577
|
}
|
|
3578
|
+
|
|
3579
|
+
const messages = await getMessages();
|
|
3580
|
+
const dir = locale === 'ar' ? 'rtl' : 'ltr';
|
|
3581
|
+
|
|
3582
|
+
return (
|
|
3583
|
+
<html lang={locale} dir={dir}>
|
|
3584
|
+
<body>
|
|
3585
|
+
<NextIntlClientProvider messages={messages}>
|
|
3586
|
+
{children}
|
|
3587
|
+
</NextIntlClientProvider>
|
|
3588
|
+
</body>
|
|
3589
|
+
</html>
|
|
3590
|
+
);
|
|
3321
3591
|
}
|
|
3322
3592
|
`;
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
CanActivate,
|
|
3328
|
-
ExecutionContext,
|
|
3329
|
-
UnauthorizedException,
|
|
3330
|
-
} from '@nestjs/common';
|
|
3593
|
+
}
|
|
3594
|
+
function i18nPage(config) {
|
|
3595
|
+
return `import { useTranslations } from 'next-intl';
|
|
3596
|
+
import { Button, Card } from '@${config.name}/ui';
|
|
3331
3597
|
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
const request = context.switchToHttp().getRequest();
|
|
3336
|
-
const authHeader = request.headers.authorization;
|
|
3598
|
+
export default function HomePage() {
|
|
3599
|
+
const t = useTranslations('home');
|
|
3600
|
+
const tc = useTranslations('common');
|
|
3337
3601
|
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3602
|
+
return (
|
|
3603
|
+
<main style={{
|
|
3604
|
+
minHeight: '100vh',
|
|
3605
|
+
display: 'flex',
|
|
3606
|
+
flexDirection: 'column',
|
|
3607
|
+
alignItems: 'center',
|
|
3608
|
+
justifyContent: 'center',
|
|
3609
|
+
padding: '2rem',
|
|
3610
|
+
}}>
|
|
3611
|
+
<div style={{ maxWidth: '42rem', textAlign: 'center' }}>
|
|
3612
|
+
<h1 style={{ fontSize: '3rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
|
3613
|
+
{t('title')}
|
|
3614
|
+
</h1>
|
|
3615
|
+
<p style={{ fontSize: '1.25rem', color: '#64748b', marginBottom: '2rem' }}>
|
|
3616
|
+
{t('description')}
|
|
3617
|
+
</p>
|
|
3341
3618
|
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3619
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginBottom: '2rem' }}>
|
|
3620
|
+
<Card title={t('frontend')} description={t('frontendDesc')} />
|
|
3621
|
+
<Card title={t('backend')} description={t('backendDesc')} />
|
|
3622
|
+
</div>
|
|
3623
|
+
|
|
3624
|
+
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}>
|
|
3625
|
+
<Button variant="primary" size="lg">{tc('getStarted')}</Button>
|
|
3626
|
+
<Button variant="outline" size="lg">{tc('documentation')}</Button>
|
|
3627
|
+
</div>
|
|
3628
|
+
</div>
|
|
3629
|
+
</main>
|
|
3630
|
+
);
|
|
3345
3631
|
}
|
|
3346
3632
|
`;
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
PipeTransform,
|
|
3351
|
-
Injectable,
|
|
3352
|
-
ArgumentMetadata,
|
|
3353
|
-
BadRequestException,
|
|
3354
|
-
} from '@nestjs/common';
|
|
3355
|
-
import { ZodSchema, ZodError } from 'zod';
|
|
3633
|
+
}
|
|
3634
|
+
function languageSwitcher() {
|
|
3635
|
+
return `'use client';
|
|
3356
3636
|
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
constructor(private schema: ZodSchema) {}
|
|
3637
|
+
import { useLocale } from 'next-intl';
|
|
3638
|
+
import { useRouter, usePathname } from '@/i18n/navigation';
|
|
3360
3639
|
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
message: 'Validation failed',
|
|
3374
|
-
details,
|
|
3375
|
-
});
|
|
3376
|
-
}
|
|
3377
|
-
throw new BadRequestException('Validation failed');
|
|
3378
|
-
}
|
|
3640
|
+
const localeLabels: Record<string, string> = {
|
|
3641
|
+
en: 'English',
|
|
3642
|
+
ar: '\u0627\u0644\u0639\u0631\u0628\u064A\u0629',
|
|
3643
|
+
};
|
|
3644
|
+
|
|
3645
|
+
export function LanguageSwitcher() {
|
|
3646
|
+
const locale = useLocale();
|
|
3647
|
+
const router = useRouter();
|
|
3648
|
+
const pathname = usePathname();
|
|
3649
|
+
|
|
3650
|
+
function handleChange(newLocale: string) {
|
|
3651
|
+
router.replace(pathname, { locale: newLocale });
|
|
3379
3652
|
}
|
|
3653
|
+
|
|
3654
|
+
return (
|
|
3655
|
+
<select
|
|
3656
|
+
value={locale}
|
|
3657
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
3658
|
+
style={{
|
|
3659
|
+
padding: '6px 12px',
|
|
3660
|
+
borderRadius: '6px',
|
|
3661
|
+
border: '1px solid #d1d5db',
|
|
3662
|
+
background: 'white',
|
|
3663
|
+
cursor: 'pointer',
|
|
3664
|
+
}}
|
|
3665
|
+
>
|
|
3666
|
+
{Object.entries(localeLabels).map(([code, label]) => (
|
|
3667
|
+
<option key={code} value={code}>
|
|
3668
|
+
{label}
|
|
3669
|
+
</option>
|
|
3670
|
+
))}
|
|
3671
|
+
</select>
|
|
3672
|
+
);
|
|
3380
3673
|
}
|
|
3381
3674
|
`;
|
|
3382
|
-
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// src/templates/storybook-templates.ts
|
|
3678
|
+
function storybookMain(_config) {
|
|
3679
|
+
return `import type { StorybookConfig } from '@storybook/react-vite';
|
|
3680
|
+
|
|
3681
|
+
const config: StorybookConfig = {
|
|
3682
|
+
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
|
3683
|
+
addons: ['@storybook/addon-essentials'],
|
|
3684
|
+
framework: {
|
|
3685
|
+
name: '@storybook/react-vite',
|
|
3686
|
+
options: {},
|
|
3687
|
+
},
|
|
3383
3688
|
};
|
|
3384
3689
|
|
|
3385
|
-
|
|
3386
|
-
|
|
3690
|
+
export default config;
|
|
3691
|
+
`;
|
|
3692
|
+
}
|
|
3693
|
+
function storybookPreview(_config) {
|
|
3694
|
+
return `import type { Preview } from '@storybook/react';
|
|
3695
|
+
|
|
3696
|
+
const preview: Preview = {
|
|
3697
|
+
parameters: {
|
|
3698
|
+
controls: {
|
|
3699
|
+
matchers: {
|
|
3700
|
+
color: /(background|color)$/i,
|
|
3701
|
+
date: /Date$/i,
|
|
3702
|
+
},
|
|
3703
|
+
},
|
|
3704
|
+
},
|
|
3705
|
+
};
|
|
3706
|
+
|
|
3707
|
+
export default preview;
|
|
3708
|
+
`;
|
|
3709
|
+
}
|
|
3710
|
+
function buttonStories(_config) {
|
|
3711
|
+
return `import type { Meta, StoryObj } from '@storybook/react';
|
|
3712
|
+
import { Button } from './button';
|
|
3713
|
+
|
|
3714
|
+
const meta: Meta<typeof Button> = {
|
|
3715
|
+
title: 'Components/Button',
|
|
3716
|
+
component: Button,
|
|
3717
|
+
tags: ['autodocs'],
|
|
3718
|
+
argTypes: {
|
|
3719
|
+
variant: { control: 'select', options: ['primary', 'secondary', 'outline', 'ghost'] },
|
|
3720
|
+
size: { control: 'select', options: ['sm', 'md', 'lg'] },
|
|
3721
|
+
},
|
|
3722
|
+
};
|
|
3723
|
+
|
|
3724
|
+
export default meta;
|
|
3725
|
+
type Story = StoryObj<typeof Button>;
|
|
3726
|
+
|
|
3727
|
+
export const Primary: Story = {
|
|
3728
|
+
args: { variant: 'primary', children: 'Primary Button' },
|
|
3729
|
+
};
|
|
3730
|
+
|
|
3731
|
+
export const Secondary: Story = {
|
|
3732
|
+
args: { variant: 'secondary', children: 'Secondary Button' },
|
|
3733
|
+
};
|
|
3734
|
+
|
|
3735
|
+
export const Outline: Story = {
|
|
3736
|
+
args: { variant: 'outline', children: 'Outline Button' },
|
|
3737
|
+
};
|
|
3738
|
+
|
|
3739
|
+
export const Ghost: Story = {
|
|
3740
|
+
args: { variant: 'ghost', children: 'Ghost Button' },
|
|
3741
|
+
};
|
|
3742
|
+
|
|
3743
|
+
export const Small: Story = {
|
|
3744
|
+
args: { variant: 'primary', size: 'sm', children: 'Small' },
|
|
3745
|
+
};
|
|
3746
|
+
|
|
3747
|
+
export const Large: Story = {
|
|
3748
|
+
args: { variant: 'primary', size: 'lg', children: 'Large' },
|
|
3749
|
+
};
|
|
3750
|
+
`;
|
|
3751
|
+
}
|
|
3752
|
+
function cardStories(_config) {
|
|
3753
|
+
return `import type { Meta, StoryObj } from '@storybook/react';
|
|
3754
|
+
import { Card } from './card';
|
|
3755
|
+
|
|
3756
|
+
const meta: Meta<typeof Card> = {
|
|
3757
|
+
title: 'Components/Card',
|
|
3758
|
+
component: Card,
|
|
3759
|
+
tags: ['autodocs'],
|
|
3760
|
+
};
|
|
3761
|
+
|
|
3762
|
+
export default meta;
|
|
3763
|
+
type Story = StoryObj<typeof Card>;
|
|
3764
|
+
|
|
3765
|
+
export const Default: Story = {
|
|
3766
|
+
args: {
|
|
3767
|
+
title: 'Card Title',
|
|
3768
|
+
description: 'This is a card description with some example text.',
|
|
3769
|
+
children: 'Card content goes here.',
|
|
3770
|
+
},
|
|
3771
|
+
};
|
|
3772
|
+
|
|
3773
|
+
export const TitleOnly: Story = {
|
|
3774
|
+
args: {
|
|
3775
|
+
title: 'Just a Title',
|
|
3776
|
+
},
|
|
3777
|
+
};
|
|
3778
|
+
|
|
3779
|
+
export const WithContent: Story = {
|
|
3780
|
+
args: {
|
|
3781
|
+
title: 'Interactive Card',
|
|
3782
|
+
description: 'A card with custom content.',
|
|
3783
|
+
children: 'Custom child content rendered inside the card.',
|
|
3784
|
+
},
|
|
3785
|
+
};
|
|
3786
|
+
`;
|
|
3787
|
+
}
|
|
3788
|
+
function inputStories(_config) {
|
|
3789
|
+
return `import type { Meta, StoryObj } from '@storybook/react';
|
|
3790
|
+
import { Input } from './input';
|
|
3791
|
+
|
|
3792
|
+
const meta: Meta<typeof Input> = {
|
|
3793
|
+
title: 'Components/Input',
|
|
3794
|
+
component: Input,
|
|
3795
|
+
tags: ['autodocs'],
|
|
3796
|
+
};
|
|
3797
|
+
|
|
3798
|
+
export default meta;
|
|
3799
|
+
type Story = StoryObj<typeof Input>;
|
|
3800
|
+
|
|
3801
|
+
export const Default: Story = {
|
|
3802
|
+
args: {
|
|
3803
|
+
label: 'Email',
|
|
3804
|
+
placeholder: 'Enter your email',
|
|
3805
|
+
},
|
|
3806
|
+
};
|
|
3807
|
+
|
|
3808
|
+
export const WithError: Story = {
|
|
3809
|
+
args: {
|
|
3810
|
+
label: 'Email',
|
|
3811
|
+
placeholder: 'Enter your email',
|
|
3812
|
+
error: 'Invalid email address',
|
|
3813
|
+
},
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
export const NoLabel: Story = {
|
|
3817
|
+
args: {
|
|
3818
|
+
placeholder: 'Search...',
|
|
3819
|
+
},
|
|
3820
|
+
};
|
|
3821
|
+
`;
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
// src/templates/strategies/nestjs-templates.ts
|
|
3825
|
+
var NestjsTemplateStrategy = class {
|
|
3387
3826
|
packageJson(config) {
|
|
3388
3827
|
return `{
|
|
3389
3828
|
"name": "@${config.name}/api",
|
|
3390
3829
|
"version": "0.0.0",
|
|
3391
3830
|
"private": true,
|
|
3392
|
-
"type": "module",
|
|
3393
3831
|
"scripts": {
|
|
3394
|
-
"build": "
|
|
3395
|
-
"dev": "
|
|
3396
|
-
"start": "
|
|
3832
|
+
"build": "nest build",
|
|
3833
|
+
"dev": "nest start --watch",
|
|
3834
|
+
"start": "nest start",
|
|
3835
|
+
"start:prod": "node dist/main",
|
|
3397
3836
|
"lint": "eslint src --ext .ts",
|
|
3398
3837
|
"test": "${config.testing === "vitest" ? "vitest run" : "jest"}"
|
|
3399
3838
|
},
|
|
3400
3839
|
"dependencies": {
|
|
3401
|
-
"
|
|
3402
|
-
"
|
|
3840
|
+
"@nestjs/common": "${config.versions["@nestjs/common"] ?? "^11.0.20"}",
|
|
3841
|
+
"@nestjs/core": "${config.versions["@nestjs/core"] ?? "^11.0.20"}",
|
|
3842
|
+
"@nestjs/platform-express": "${config.versions["@nestjs/platform-express"] ?? "^11.0.20"}",
|
|
3843
|
+
"reflect-metadata": "${config.versions["reflect-metadata"] ?? "^0.2.2"}",
|
|
3844
|
+
"rxjs": "${config.versions["rxjs"] ?? "^7.8.2"}",
|
|
3403
3845
|
"@${config.name}/lib": "workspace:*"${config.hasDatabase ? `,
|
|
3404
3846
|
"@${config.name}/database": "workspace:*"` : ""}
|
|
3405
3847
|
},
|
|
3406
3848
|
"devDependencies": {
|
|
3407
|
-
"@
|
|
3408
|
-
"@
|
|
3849
|
+
"@nestjs/cli": "${config.versions["@nestjs/cli"] ?? "^11.0.5"}",
|
|
3850
|
+
"@nestjs/testing": "${config.versions["@nestjs/testing"] ?? "^11.0.20"}",
|
|
3409
3851
|
"@types/node": "${config.versions["@types/node"] ?? "^22.15.3"}",
|
|
3410
|
-
"tsx": "^4.19.4",
|
|
3411
3852
|
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
3412
3853
|
}
|
|
3413
3854
|
}
|
|
@@ -3415,13 +3856,10 @@ var ExpressTemplateStrategy = class {
|
|
|
3415
3856
|
}
|
|
3416
3857
|
tsConfig(config) {
|
|
3417
3858
|
return `{
|
|
3418
|
-
"extends": "@${config.name}/config/typescript/
|
|
3859
|
+
"extends": "@${config.name}/config/typescript/node",
|
|
3419
3860
|
"compilerOptions": {
|
|
3420
3861
|
"outDir": "./dist",
|
|
3421
|
-
"rootDir": "./src"
|
|
3422
|
-
"module": "ESNext",
|
|
3423
|
-
"moduleResolution": "bundler",
|
|
3424
|
-
"verbatimModuleSyntax": false
|
|
3862
|
+
"rootDir": "./src"
|
|
3425
3863
|
},
|
|
3426
3864
|
"include": ["src/**/*"],
|
|
3427
3865
|
"exclude": ["node_modules", "dist", "test"]
|
|
@@ -3429,54 +3867,310 @@ var ExpressTemplateStrategy = class {
|
|
|
3429
3867
|
`;
|
|
3430
3868
|
}
|
|
3431
3869
|
mainEntry(config) {
|
|
3432
|
-
return `import {
|
|
3870
|
+
return `import { NestFactory } from '@nestjs/core';
|
|
3871
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
3872
|
+
import { AppModule } from './app.module.js';
|
|
3873
|
+
import { HttpExceptionFilter } from './common/filters/http-exception.filter.js';
|
|
3874
|
+
import { LoggingInterceptor } from './common/interceptors/logging.interceptor.js';
|
|
3433
3875
|
|
|
3434
|
-
|
|
3876
|
+
async function bootstrap() {
|
|
3877
|
+
const app = await NestFactory.create(AppModule);
|
|
3435
3878
|
|
|
3436
|
-
|
|
3879
|
+
app.enableCors({
|
|
3880
|
+
origin: process.env.CORS_ORIGIN ?? 'http://localhost:3000',
|
|
3881
|
+
credentials: true,
|
|
3882
|
+
});
|
|
3437
3883
|
|
|
3438
|
-
app.
|
|
3884
|
+
app.setGlobalPrefix('api');
|
|
3885
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
|
3886
|
+
app.useGlobalFilters(new HttpExceptionFilter());
|
|
3887
|
+
app.useGlobalInterceptors(new LoggingInterceptor());
|
|
3888
|
+
|
|
3889
|
+
const port = process.env.PORT ?? 3001;
|
|
3890
|
+
await app.listen(port);
|
|
3439
3891
|
console.log(\`\u{1F680} ${config.pascalCase} API running on http://localhost:\${port}/api\`);
|
|
3440
|
-
}
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
bootstrap();
|
|
3441
3895
|
`;
|
|
3442
3896
|
}
|
|
3443
3897
|
appSetup(_config) {
|
|
3444
|
-
return `import
|
|
3445
|
-
import
|
|
3446
|
-
import {
|
|
3447
|
-
import { errorHandler } from './common/filters/error-handler.js';
|
|
3448
|
-
import { requestLogger } from './common/interceptors/request-logger.js';
|
|
3449
|
-
|
|
3450
|
-
export function createApp() {
|
|
3451
|
-
const app = express();
|
|
3898
|
+
return `import { Module } from '@nestjs/common';
|
|
3899
|
+
import { AppController } from './app.controller.js';
|
|
3900
|
+
import { AppService } from './app.service.js';
|
|
3452
3901
|
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3902
|
+
@Module({
|
|
3903
|
+
imports: [],
|
|
3904
|
+
controllers: [AppController],
|
|
3905
|
+
providers: [AppService],
|
|
3906
|
+
})
|
|
3907
|
+
export class AppModule {}
|
|
3908
|
+
`;
|
|
3909
|
+
}
|
|
3910
|
+
appController(_config) {
|
|
3911
|
+
return `import { Controller, Get } from '@nestjs/common';
|
|
3912
|
+
import { AppService } from './app.service.js';
|
|
3461
3913
|
|
|
3462
|
-
|
|
3463
|
-
|
|
3914
|
+
@Controller()
|
|
3915
|
+
export class AppController {
|
|
3916
|
+
constructor(private readonly appService: AppService) {}
|
|
3464
3917
|
|
|
3465
|
-
|
|
3466
|
-
|
|
3918
|
+
@Get('health')
|
|
3919
|
+
getHealth() {
|
|
3920
|
+
return this.appService.getHealth();
|
|
3921
|
+
}
|
|
3467
3922
|
|
|
3468
|
-
|
|
3923
|
+
@Get()
|
|
3924
|
+
getInfo() {
|
|
3925
|
+
return this.appService.getInfo();
|
|
3926
|
+
}
|
|
3469
3927
|
}
|
|
3470
3928
|
`;
|
|
3471
3929
|
}
|
|
3472
|
-
|
|
3473
|
-
return `import {
|
|
3474
|
-
import { AppService } from '../services/app.service.js';
|
|
3475
|
-
|
|
3476
|
-
export const healthRouter = Router();
|
|
3477
|
-
const appService = new AppService('${config.name}');
|
|
3930
|
+
appService(config) {
|
|
3931
|
+
return `import { Injectable } from '@nestjs/common';
|
|
3478
3932
|
|
|
3479
|
-
|
|
3933
|
+
@Injectable()
|
|
3934
|
+
export class AppService {
|
|
3935
|
+
getHealth() {
|
|
3936
|
+
return {
|
|
3937
|
+
status: 'ok',
|
|
3938
|
+
timestamp: new Date().toISOString(),
|
|
3939
|
+
uptime: process.uptime(),
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
getInfo() {
|
|
3944
|
+
return {
|
|
3945
|
+
name: '${config.name}',
|
|
3946
|
+
version: '1.0.0',
|
|
3947
|
+
environment: process.env.NODE_ENV ?? 'development',
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
`;
|
|
3952
|
+
}
|
|
3953
|
+
exceptionFilter() {
|
|
3954
|
+
return `import {
|
|
3955
|
+
ExceptionFilter,
|
|
3956
|
+
Catch,
|
|
3957
|
+
ArgumentsHost,
|
|
3958
|
+
HttpException,
|
|
3959
|
+
HttpStatus,
|
|
3960
|
+
} from '@nestjs/common';
|
|
3961
|
+
import type { Response } from 'express';
|
|
3962
|
+
|
|
3963
|
+
@Catch()
|
|
3964
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
3965
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
3966
|
+
const ctx = host.switchToHttp();
|
|
3967
|
+
const response = ctx.getResponse<Response>();
|
|
3968
|
+
|
|
3969
|
+
const status =
|
|
3970
|
+
exception instanceof HttpException
|
|
3971
|
+
? exception.getStatus()
|
|
3972
|
+
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
3973
|
+
|
|
3974
|
+
const message =
|
|
3975
|
+
exception instanceof HttpException
|
|
3976
|
+
? exception.message
|
|
3977
|
+
: 'Internal server error';
|
|
3978
|
+
|
|
3979
|
+
response.status(status).json({
|
|
3980
|
+
success: false,
|
|
3981
|
+
error: {
|
|
3982
|
+
code: HttpStatus[status] ?? 'UNKNOWN_ERROR',
|
|
3983
|
+
message,
|
|
3984
|
+
},
|
|
3985
|
+
timestamp: new Date().toISOString(),
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
`;
|
|
3990
|
+
}
|
|
3991
|
+
loggingInterceptor() {
|
|
3992
|
+
return `import {
|
|
3993
|
+
Injectable,
|
|
3994
|
+
NestInterceptor,
|
|
3995
|
+
ExecutionContext,
|
|
3996
|
+
CallHandler,
|
|
3997
|
+
} from '@nestjs/common';
|
|
3998
|
+
import { Observable, tap } from 'rxjs';
|
|
3999
|
+
|
|
4000
|
+
@Injectable()
|
|
4001
|
+
export class LoggingInterceptor implements NestInterceptor {
|
|
4002
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
|
4003
|
+
const request = context.switchToHttp().getRequest();
|
|
4004
|
+
const method = request.method;
|
|
4005
|
+
const url = request.url;
|
|
4006
|
+
const startTime = Date.now();
|
|
4007
|
+
|
|
4008
|
+
return next.handle().pipe(
|
|
4009
|
+
tap(() => {
|
|
4010
|
+
const duration = Date.now() - startTime;
|
|
4011
|
+
console.log(\`\${method} \${url} \u2014 \${duration}ms\`);
|
|
4012
|
+
}),
|
|
4013
|
+
);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
`;
|
|
4017
|
+
}
|
|
4018
|
+
authGuard() {
|
|
4019
|
+
return `import {
|
|
4020
|
+
Injectable,
|
|
4021
|
+
CanActivate,
|
|
4022
|
+
ExecutionContext,
|
|
4023
|
+
UnauthorizedException,
|
|
4024
|
+
} from '@nestjs/common';
|
|
4025
|
+
|
|
4026
|
+
@Injectable()
|
|
4027
|
+
export class AuthGuard implements CanActivate {
|
|
4028
|
+
canActivate(context: ExecutionContext): boolean {
|
|
4029
|
+
const request = context.switchToHttp().getRequest();
|
|
4030
|
+
const authHeader = request.headers.authorization;
|
|
4031
|
+
|
|
4032
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
4033
|
+
throw new UnauthorizedException('Missing or invalid authorization header');
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
// TODO: Validate the token
|
|
4037
|
+
return true;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
`;
|
|
4041
|
+
}
|
|
4042
|
+
validationPipe() {
|
|
4043
|
+
return `import {
|
|
4044
|
+
PipeTransform,
|
|
4045
|
+
Injectable,
|
|
4046
|
+
ArgumentMetadata,
|
|
4047
|
+
BadRequestException,
|
|
4048
|
+
} from '@nestjs/common';
|
|
4049
|
+
import { ZodSchema, ZodError } from 'zod';
|
|
4050
|
+
|
|
4051
|
+
@Injectable()
|
|
4052
|
+
export class ZodValidationPipe implements PipeTransform {
|
|
4053
|
+
constructor(private schema: ZodSchema) {}
|
|
4054
|
+
|
|
4055
|
+
transform(value: unknown, _metadata: ArgumentMetadata) {
|
|
4056
|
+
try {
|
|
4057
|
+
return this.schema.parse(value);
|
|
4058
|
+
} catch (error) {
|
|
4059
|
+
if (error instanceof ZodError) {
|
|
4060
|
+
const details: Record<string, string[]> = {};
|
|
4061
|
+
for (const issue of error.issues) {
|
|
4062
|
+
const path = issue.path.join('.');
|
|
4063
|
+
if (!details[path]) details[path] = [];
|
|
4064
|
+
details[path].push(issue.message);
|
|
4065
|
+
}
|
|
4066
|
+
throw new BadRequestException({
|
|
4067
|
+
message: 'Validation failed',
|
|
4068
|
+
details,
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
throw new BadRequestException('Validation failed');
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
`;
|
|
4076
|
+
}
|
|
4077
|
+
};
|
|
4078
|
+
|
|
4079
|
+
// src/templates/strategies/express-templates.ts
|
|
4080
|
+
var ExpressTemplateStrategy = class {
|
|
4081
|
+
packageJson(config) {
|
|
4082
|
+
return `{
|
|
4083
|
+
"name": "@${config.name}/api",
|
|
4084
|
+
"version": "0.0.0",
|
|
4085
|
+
"private": true,
|
|
4086
|
+
"type": "module",
|
|
4087
|
+
"scripts": {
|
|
4088
|
+
"build": "tsc",
|
|
4089
|
+
"dev": "tsx watch src/main.ts",
|
|
4090
|
+
"start": "node dist/main.js",
|
|
4091
|
+
"lint": "eslint src --ext .ts",
|
|
4092
|
+
"test": "${config.testing === "vitest" ? "vitest run" : "jest"}"
|
|
4093
|
+
},
|
|
4094
|
+
"dependencies": {
|
|
4095
|
+
"express": "${config.versions["express"] ?? "^5.1.0"}",
|
|
4096
|
+
"cors": "${config.versions["cors"] ?? "^2.8.5"}",
|
|
4097
|
+
"@${config.name}/lib": "workspace:*"${config.hasDatabase ? `,
|
|
4098
|
+
"@${config.name}/database": "workspace:*"` : ""}
|
|
4099
|
+
},
|
|
4100
|
+
"devDependencies": {
|
|
4101
|
+
"@types/express": "${config.versions["@types/express"] ?? "^5.0.2"}",
|
|
4102
|
+
"@types/cors": "${config.versions["@types/cors"] ?? "^2.8.17"}",
|
|
4103
|
+
"@types/node": "${config.versions["@types/node"] ?? "^22.15.3"}",
|
|
4104
|
+
"tsx": "^4.19.4",
|
|
4105
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
`;
|
|
4109
|
+
}
|
|
4110
|
+
tsConfig(config) {
|
|
4111
|
+
return `{
|
|
4112
|
+
"extends": "@${config.name}/config/typescript/base",
|
|
4113
|
+
"compilerOptions": {
|
|
4114
|
+
"outDir": "./dist",
|
|
4115
|
+
"rootDir": "./src",
|
|
4116
|
+
"module": "ESNext",
|
|
4117
|
+
"moduleResolution": "bundler",
|
|
4118
|
+
"verbatimModuleSyntax": false
|
|
4119
|
+
},
|
|
4120
|
+
"include": ["src/**/*"],
|
|
4121
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
4122
|
+
}
|
|
4123
|
+
`;
|
|
4124
|
+
}
|
|
4125
|
+
mainEntry(config) {
|
|
4126
|
+
return `import { createApp } from './app.js';
|
|
4127
|
+
|
|
4128
|
+
const port = process.env.PORT ?? 3001;
|
|
4129
|
+
|
|
4130
|
+
const app = createApp();
|
|
4131
|
+
|
|
4132
|
+
app.listen(port, () => {
|
|
4133
|
+
console.log(\`\u{1F680} ${config.pascalCase} API running on http://localhost:\${port}/api\`);
|
|
4134
|
+
});
|
|
4135
|
+
`;
|
|
4136
|
+
}
|
|
4137
|
+
appSetup(_config) {
|
|
4138
|
+
return `import express from 'express';
|
|
4139
|
+
import cors from 'cors';
|
|
4140
|
+
import { healthRouter } from './routes/health.js';
|
|
4141
|
+
import { errorHandler } from './common/filters/error-handler.js';
|
|
4142
|
+
import { requestLogger } from './common/interceptors/request-logger.js';
|
|
4143
|
+
|
|
4144
|
+
export function createApp() {
|
|
4145
|
+
const app = express();
|
|
4146
|
+
|
|
4147
|
+
// Middleware
|
|
4148
|
+
app.use(cors({
|
|
4149
|
+
origin: process.env.CORS_ORIGIN ?? 'http://localhost:3000',
|
|
4150
|
+
credentials: true,
|
|
4151
|
+
}));
|
|
4152
|
+
app.use(express.json());
|
|
4153
|
+
app.use(express.urlencoded({ extended: true }));
|
|
4154
|
+
app.use(requestLogger);
|
|
4155
|
+
|
|
4156
|
+
// Routes
|
|
4157
|
+
app.use('/api', healthRouter);
|
|
4158
|
+
|
|
4159
|
+
// Error handling (must be last)
|
|
4160
|
+
app.use(errorHandler);
|
|
4161
|
+
|
|
4162
|
+
return app;
|
|
4163
|
+
}
|
|
4164
|
+
`;
|
|
4165
|
+
}
|
|
4166
|
+
appController(config) {
|
|
4167
|
+
return `import { Router } from 'express';
|
|
4168
|
+
import { AppService } from '../services/app.service.js';
|
|
4169
|
+
|
|
4170
|
+
export const healthRouter = Router();
|
|
4171
|
+
const appService = new AppService('${config.name}');
|
|
4172
|
+
|
|
4173
|
+
healthRouter.get('/health', (_req, res) => {
|
|
3480
4174
|
res.json(appService.getHealth());
|
|
3481
4175
|
});
|
|
3482
4176
|
|
|
@@ -4277,175 +4971,2164 @@ type Theme = 'light' | 'dark' | 'system';
|
|
|
4277
4971
|
/** Persisted theme atom */
|
|
4278
4972
|
export const themeAtom = atomWithStorage<Theme>('theme', 'system');
|
|
4279
4973
|
|
|
4280
|
-
/** Derived atom that resolves 'system' to actual theme */
|
|
4281
|
-
export const resolvedThemeAtom = atom((get) => {
|
|
4282
|
-
const theme = get(themeAtom);
|
|
4283
|
-
if (theme !== 'system') return theme;
|
|
4974
|
+
/** Derived atom that resolves 'system' to actual theme */
|
|
4975
|
+
export const resolvedThemeAtom = atom((get) => {
|
|
4976
|
+
const theme = get(themeAtom);
|
|
4977
|
+
if (theme !== 'system') return theme;
|
|
4978
|
+
|
|
4979
|
+
if (typeof window === 'undefined') return 'light';
|
|
4980
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
4981
|
+
});
|
|
4982
|
+
|
|
4983
|
+
/** Convenience hook for theme management */
|
|
4984
|
+
export function useTheme() {
|
|
4985
|
+
const [theme, setTheme] = useAtom(themeAtom);
|
|
4986
|
+
return { theme, setTheme };
|
|
4987
|
+
}
|
|
4988
|
+
`;
|
|
4989
|
+
}
|
|
4990
|
+
/** Jotai works without a provider (uses default store) */
|
|
4991
|
+
providerWrapper() {
|
|
4992
|
+
return "";
|
|
4993
|
+
}
|
|
4994
|
+
};
|
|
4995
|
+
|
|
4996
|
+
// src/templates/strategies/redux-templates.ts
|
|
4997
|
+
var ReduxTemplateStrategy = class {
|
|
4998
|
+
storeSetup() {
|
|
4999
|
+
return `import { configureStore } from '@reduxjs/toolkit';
|
|
5000
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
5001
|
+
import type { TypedUseSelectorHook } from 'react-redux';
|
|
5002
|
+
import { themeReducer } from './theme-slice.js';
|
|
5003
|
+
|
|
5004
|
+
export const store = configureStore({
|
|
5005
|
+
reducer: {
|
|
5006
|
+
theme: themeReducer,
|
|
5007
|
+
},
|
|
5008
|
+
});
|
|
5009
|
+
|
|
5010
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
5011
|
+
export type AppDispatch = typeof store.dispatch;
|
|
5012
|
+
|
|
5013
|
+
/** Typed dispatch hook */
|
|
5014
|
+
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
5015
|
+
|
|
5016
|
+
/** Typed selector hook */
|
|
5017
|
+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
5018
|
+
`;
|
|
5019
|
+
}
|
|
5020
|
+
exampleStore() {
|
|
5021
|
+
return `import { createSlice } from '@reduxjs/toolkit';
|
|
5022
|
+
import type { PayloadAction } from '@reduxjs/toolkit';
|
|
5023
|
+
|
|
5024
|
+
type Theme = 'light' | 'dark' | 'system';
|
|
5025
|
+
|
|
5026
|
+
interface ThemeState {
|
|
5027
|
+
theme: Theme;
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
const initialState: ThemeState = {
|
|
5031
|
+
theme: 'system',
|
|
5032
|
+
};
|
|
5033
|
+
|
|
5034
|
+
const themeSlice = createSlice({
|
|
5035
|
+
name: 'theme',
|
|
5036
|
+
initialState,
|
|
5037
|
+
reducers: {
|
|
5038
|
+
setTheme(state, action: PayloadAction<Theme>) {
|
|
5039
|
+
state.theme = action.payload;
|
|
5040
|
+
},
|
|
5041
|
+
toggleTheme(state) {
|
|
5042
|
+
state.theme = state.theme === 'dark' ? 'light' : 'dark';
|
|
5043
|
+
},
|
|
5044
|
+
},
|
|
5045
|
+
});
|
|
5046
|
+
|
|
5047
|
+
export const { setTheme, toggleTheme } = themeSlice.actions;
|
|
5048
|
+
export const themeReducer = themeSlice.reducer;
|
|
5049
|
+
`;
|
|
5050
|
+
}
|
|
5051
|
+
providerWrapper() {
|
|
5052
|
+
return `'use client';
|
|
5053
|
+
|
|
5054
|
+
import { Provider } from 'react-redux';
|
|
5055
|
+
import { store } from './store';
|
|
5056
|
+
|
|
5057
|
+
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
5058
|
+
return <Provider store={store}>{children}</Provider>;
|
|
5059
|
+
}
|
|
5060
|
+
`;
|
|
5061
|
+
}
|
|
5062
|
+
};
|
|
5063
|
+
|
|
5064
|
+
// src/templates/strategies/tanstack-query-templates.ts
|
|
5065
|
+
var TanstackQueryTemplateStrategy = class {
|
|
5066
|
+
storeSetup() {
|
|
5067
|
+
return `/**
|
|
5068
|
+
* TanStack Query setup and utilities.
|
|
5069
|
+
*/
|
|
5070
|
+
|
|
5071
|
+
export { queryClient } from './query-client.js';
|
|
5072
|
+
export { QueryProvider } from './provider.js';
|
|
5073
|
+
`;
|
|
5074
|
+
}
|
|
5075
|
+
exampleStore(_config) {
|
|
5076
|
+
return `import { QueryClient } from '@tanstack/react-query';
|
|
5077
|
+
|
|
5078
|
+
export const queryClient = new QueryClient({
|
|
5079
|
+
defaultOptions: {
|
|
5080
|
+
queries: {
|
|
5081
|
+
staleTime: 60 * 1000, // 1 minute
|
|
5082
|
+
retry: 1,
|
|
5083
|
+
refetchOnWindowFocus: false,
|
|
5084
|
+
},
|
|
5085
|
+
mutations: {
|
|
5086
|
+
retry: 0,
|
|
5087
|
+
},
|
|
5088
|
+
},
|
|
5089
|
+
});
|
|
5090
|
+
|
|
5091
|
+
/**
|
|
5092
|
+
* Example query hook \u2014 fetches health status from the API.
|
|
5093
|
+
*/
|
|
5094
|
+
export function useHealthQuery() {
|
|
5095
|
+
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001';
|
|
5096
|
+
|
|
5097
|
+
return {
|
|
5098
|
+
queryKey: ['health'],
|
|
5099
|
+
queryFn: async () => {
|
|
5100
|
+
const response = await fetch(\`\${apiUrl}/api/health\`);
|
|
5101
|
+
if (!response.ok) throw new Error('API health check failed');
|
|
5102
|
+
return response.json();
|
|
5103
|
+
},
|
|
5104
|
+
};
|
|
5105
|
+
}
|
|
5106
|
+
`;
|
|
5107
|
+
}
|
|
5108
|
+
providerWrapper() {
|
|
5109
|
+
return `'use client';
|
|
5110
|
+
|
|
5111
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
5112
|
+
import { queryClient } from './query-client';
|
|
5113
|
+
|
|
5114
|
+
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
|
5115
|
+
return (
|
|
5116
|
+
<QueryClientProvider client={queryClient}>
|
|
5117
|
+
{children}
|
|
5118
|
+
</QueryClientProvider>
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
5121
|
+
`;
|
|
5122
|
+
}
|
|
5123
|
+
};
|
|
5124
|
+
|
|
5125
|
+
// src/templates/strategies/state-factory.ts
|
|
5126
|
+
function createStateStrategy(state) {
|
|
5127
|
+
switch (state) {
|
|
5128
|
+
case "zustand":
|
|
5129
|
+
return new ZustandTemplateStrategy();
|
|
5130
|
+
case "jotai":
|
|
5131
|
+
return new JotaiTemplateStrategy();
|
|
5132
|
+
case "redux":
|
|
5133
|
+
return new ReduxTemplateStrategy();
|
|
5134
|
+
case "tanstack-query":
|
|
5135
|
+
return new TanstackQueryTemplateStrategy();
|
|
5136
|
+
case "none":
|
|
5137
|
+
return null;
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
|
|
5141
|
+
// src/templates/strategies/docker-full-templates.ts
|
|
5142
|
+
var DockerFullTemplateStrategy = class {
|
|
5143
|
+
webDockerfile(config) {
|
|
5144
|
+
return `# \u2500\u2500 Stage 1: Dependencies \u2500\u2500
|
|
5145
|
+
FROM node:20-alpine AS deps
|
|
5146
|
+
RUN apk add --no-cache libc6-compat
|
|
5147
|
+
WORKDIR /app
|
|
5148
|
+
COPY package.json ${config.packageManager === "pnpm" ? "pnpm-lock.yaml pnpm-workspace.yaml" : "package-lock.json"} ./
|
|
5149
|
+
COPY apps/web/package.json ./apps/web/
|
|
5150
|
+
COPY packages/ui/package.json ./packages/ui/
|
|
5151
|
+
COPY packages/lib/package.json ./packages/lib/
|
|
5152
|
+
COPY packages/config/package.json ./packages/config/
|
|
5153
|
+
${config.packageManager === "pnpm" ? "RUN corepack enable pnpm && pnpm install --frozen-lockfile" : "RUN npm ci"}
|
|
5154
|
+
|
|
5155
|
+
# \u2500\u2500 Stage 2: Build \u2500\u2500
|
|
5156
|
+
FROM node:20-alpine AS builder
|
|
5157
|
+
WORKDIR /app
|
|
5158
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
5159
|
+
COPY . .
|
|
5160
|
+
RUN ${config.packageManager === "pnpm" ? "corepack enable pnpm && pnpm" : "npm run"} build --filter=@${config.name}/web
|
|
5161
|
+
|
|
5162
|
+
# \u2500\u2500 Stage 3: Runner \u2500\u2500
|
|
5163
|
+
FROM node:20-alpine AS runner
|
|
5164
|
+
WORKDIR /app
|
|
5165
|
+
ENV NODE_ENV=production
|
|
5166
|
+
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
|
5167
|
+
|
|
5168
|
+
COPY --from=builder /app/apps/web/public ./apps/web/public
|
|
5169
|
+
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
|
|
5170
|
+
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
|
|
5171
|
+
|
|
5172
|
+
USER nextjs
|
|
5173
|
+
EXPOSE 3000
|
|
5174
|
+
ENV PORT=3000 HOSTNAME="0.0.0.0"
|
|
5175
|
+
CMD ["node", "apps/web/server.js"]
|
|
5176
|
+
`;
|
|
5177
|
+
}
|
|
5178
|
+
apiDockerfile(config) {
|
|
5179
|
+
return `# \u2500\u2500 Stage 1: Dependencies \u2500\u2500
|
|
5180
|
+
FROM node:20-alpine AS deps
|
|
5181
|
+
WORKDIR /app
|
|
5182
|
+
COPY package.json ${config.packageManager === "pnpm" ? "pnpm-lock.yaml pnpm-workspace.yaml" : "package-lock.json"} ./
|
|
5183
|
+
COPY apps/api/package.json ./apps/api/
|
|
5184
|
+
COPY packages/lib/package.json ./packages/lib/
|
|
5185
|
+
COPY packages/config/package.json ./packages/config/
|
|
5186
|
+
${config.hasDatabase ? `COPY packages/database/package.json ./packages/database/` : ""}
|
|
5187
|
+
${config.packageManager === "pnpm" ? "RUN corepack enable pnpm && pnpm install --frozen-lockfile" : "RUN npm ci"}
|
|
5188
|
+
|
|
5189
|
+
# \u2500\u2500 Stage 2: Build \u2500\u2500
|
|
5190
|
+
FROM node:20-alpine AS builder
|
|
5191
|
+
WORKDIR /app
|
|
5192
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
5193
|
+
COPY . .
|
|
5194
|
+
RUN ${config.packageManager === "pnpm" ? "corepack enable pnpm && pnpm" : "npm run"} build --filter=@${config.name}/api
|
|
5195
|
+
|
|
5196
|
+
# \u2500\u2500 Stage 3: Runner \u2500\u2500
|
|
5197
|
+
FROM node:20-alpine AS runner
|
|
5198
|
+
WORKDIR /app
|
|
5199
|
+
ENV NODE_ENV=production
|
|
5200
|
+
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 appuser
|
|
5201
|
+
|
|
5202
|
+
COPY --from=builder --chown=appuser:nodejs /app/apps/api/dist ./dist
|
|
5203
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
5204
|
+
${config.orm === "prisma" ? "COPY --from=builder /app/packages/database/prisma ./prisma" : ""}
|
|
5205
|
+
|
|
5206
|
+
USER appuser
|
|
5207
|
+
EXPOSE 3001
|
|
5208
|
+
ENV PORT=3001
|
|
5209
|
+
CMD ["node", "dist/main.js"]
|
|
5210
|
+
`;
|
|
5211
|
+
}
|
|
5212
|
+
dockerComposeProd(config) {
|
|
5213
|
+
let services = ` web:
|
|
5214
|
+
build:
|
|
5215
|
+
context: .
|
|
5216
|
+
dockerfile: apps/web/Dockerfile
|
|
5217
|
+
ports:
|
|
5218
|
+
- "3000:3000"
|
|
5219
|
+
environment:
|
|
5220
|
+
- NEXT_PUBLIC_API_URL=http://api:3001
|
|
5221
|
+
depends_on:
|
|
5222
|
+
- api
|
|
5223
|
+
restart: unless-stopped
|
|
5224
|
+
|
|
5225
|
+
api:
|
|
5226
|
+
build:
|
|
5227
|
+
context: .
|
|
5228
|
+
dockerfile: apps/api/Dockerfile
|
|
5229
|
+
ports:
|
|
5230
|
+
- "3001:3001"
|
|
5231
|
+
env_file:
|
|
5232
|
+
- .env
|
|
5233
|
+
restart: unless-stopped
|
|
5234
|
+
`;
|
|
5235
|
+
if (config.hasDatabase && config.db !== "sqlite") {
|
|
5236
|
+
services += ` depends_on:
|
|
5237
|
+
- db
|
|
5238
|
+
`;
|
|
5239
|
+
}
|
|
5240
|
+
if (config.hasCache) {
|
|
5241
|
+
services += `
|
|
5242
|
+
redis:
|
|
5243
|
+
image: redis:7-alpine
|
|
5244
|
+
container_name: ${config.name}-redis
|
|
5245
|
+
ports:
|
|
5246
|
+
- "6379:6379"
|
|
5247
|
+
restart: unless-stopped
|
|
5248
|
+
`;
|
|
5249
|
+
}
|
|
5250
|
+
services += `
|
|
5251
|
+
nginx:
|
|
5252
|
+
build:
|
|
5253
|
+
context: ./nginx
|
|
5254
|
+
dockerfile: Dockerfile
|
|
5255
|
+
ports:
|
|
5256
|
+
- "80:80"
|
|
5257
|
+
depends_on:
|
|
5258
|
+
- web
|
|
5259
|
+
- api
|
|
5260
|
+
restart: unless-stopped
|
|
5261
|
+
`;
|
|
5262
|
+
return `services:
|
|
5263
|
+
${services}`;
|
|
5264
|
+
}
|
|
5265
|
+
dockerignore(_config) {
|
|
5266
|
+
return `node_modules
|
|
5267
|
+
.next
|
|
5268
|
+
dist
|
|
5269
|
+
.git
|
|
5270
|
+
.gitignore
|
|
5271
|
+
*.md
|
|
5272
|
+
.env*
|
|
5273
|
+
!.env.example
|
|
5274
|
+
coverage
|
|
5275
|
+
.turbo
|
|
5276
|
+
`;
|
|
5277
|
+
}
|
|
5278
|
+
extraFiles(_config) {
|
|
5279
|
+
return {
|
|
5280
|
+
"nginx/nginx.conf": `events {
|
|
5281
|
+
worker_connections 1024;
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
http {
|
|
5285
|
+
upstream web {
|
|
5286
|
+
server web:3000;
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
upstream api {
|
|
5290
|
+
server api:3001;
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
server {
|
|
5294
|
+
listen 80;
|
|
5295
|
+
server_name localhost;
|
|
5296
|
+
|
|
5297
|
+
location / {
|
|
5298
|
+
proxy_pass http://web;
|
|
5299
|
+
proxy_set_header Host $host;
|
|
5300
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
5301
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
5302
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
location /api/ {
|
|
5306
|
+
proxy_pass http://api;
|
|
5307
|
+
proxy_set_header Host $host;
|
|
5308
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
5309
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
5310
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
`,
|
|
5315
|
+
"nginx/Dockerfile": `FROM nginx:alpine
|
|
5316
|
+
COPY nginx.conf /etc/nginx/nginx.conf
|
|
5317
|
+
EXPOSE 80
|
|
5318
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
5319
|
+
`
|
|
5320
|
+
};
|
|
5321
|
+
}
|
|
5322
|
+
};
|
|
5323
|
+
|
|
5324
|
+
// src/templates/strategies/docker-minimal-templates.ts
|
|
5325
|
+
var DockerMinimalTemplateStrategy = class {
|
|
5326
|
+
webDockerfile(config) {
|
|
5327
|
+
return `FROM node:20-alpine
|
|
5328
|
+
WORKDIR /app
|
|
5329
|
+
COPY . .
|
|
5330
|
+
${config.packageManager === "pnpm" ? "RUN corepack enable pnpm && pnpm install --frozen-lockfile" : "RUN npm ci"}
|
|
5331
|
+
RUN ${config.packageManager === "pnpm" ? "pnpm" : "npm run"} build --filter=@${config.name}/web
|
|
5332
|
+
EXPOSE 3000
|
|
5333
|
+
CMD ["node", "apps/web/.next/standalone/apps/web/server.js"]
|
|
5334
|
+
`;
|
|
5335
|
+
}
|
|
5336
|
+
apiDockerfile(config) {
|
|
5337
|
+
return `FROM node:20-alpine
|
|
5338
|
+
WORKDIR /app
|
|
5339
|
+
COPY . .
|
|
5340
|
+
${config.packageManager === "pnpm" ? "RUN corepack enable pnpm && pnpm install --frozen-lockfile" : "RUN npm ci"}
|
|
5341
|
+
RUN ${config.packageManager === "pnpm" ? "pnpm" : "npm run"} build --filter=@${config.name}/api
|
|
5342
|
+
EXPOSE 3001
|
|
5343
|
+
CMD ["node", "apps/api/dist/main.js"]
|
|
5344
|
+
`;
|
|
5345
|
+
}
|
|
5346
|
+
dockerComposeProd(_config) {
|
|
5347
|
+
return `services:
|
|
5348
|
+
web:
|
|
5349
|
+
build:
|
|
5350
|
+
context: .
|
|
5351
|
+
dockerfile: apps/web/Dockerfile
|
|
5352
|
+
ports:
|
|
5353
|
+
- "3000:3000"
|
|
5354
|
+
environment:
|
|
5355
|
+
- NEXT_PUBLIC_API_URL=http://api:3001
|
|
5356
|
+
depends_on:
|
|
5357
|
+
- api
|
|
5358
|
+
restart: unless-stopped
|
|
5359
|
+
|
|
5360
|
+
api:
|
|
5361
|
+
build:
|
|
5362
|
+
context: .
|
|
5363
|
+
dockerfile: apps/api/Dockerfile
|
|
5364
|
+
ports:
|
|
5365
|
+
- "3001:3001"
|
|
5366
|
+
env_file:
|
|
5367
|
+
- .env
|
|
5368
|
+
restart: unless-stopped
|
|
5369
|
+
`;
|
|
5370
|
+
}
|
|
5371
|
+
dockerignore(_config) {
|
|
5372
|
+
return `node_modules
|
|
5373
|
+
.next
|
|
5374
|
+
dist
|
|
5375
|
+
.git
|
|
5376
|
+
*.md
|
|
5377
|
+
.env*
|
|
5378
|
+
!.env.example
|
|
5379
|
+
coverage
|
|
5380
|
+
.turbo
|
|
5381
|
+
`;
|
|
5382
|
+
}
|
|
5383
|
+
extraFiles(_config) {
|
|
5384
|
+
return {};
|
|
5385
|
+
}
|
|
5386
|
+
};
|
|
5387
|
+
|
|
5388
|
+
// src/templates/strategies/docker-factory.ts
|
|
5389
|
+
function createDockerStrategy(docker) {
|
|
5390
|
+
switch (docker) {
|
|
5391
|
+
case "full":
|
|
5392
|
+
return new DockerFullTemplateStrategy();
|
|
5393
|
+
case "minimal":
|
|
5394
|
+
return new DockerMinimalTemplateStrategy();
|
|
5395
|
+
case "none":
|
|
5396
|
+
return null;
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
// src/templates/strategies/playwright-templates.ts
|
|
5401
|
+
var PlaywrightTemplateStrategy = class {
|
|
5402
|
+
config(_config) {
|
|
5403
|
+
return `import { defineConfig, devices } from '@playwright/test';
|
|
5404
|
+
|
|
5405
|
+
export default defineConfig({
|
|
5406
|
+
testDir: './e2e',
|
|
5407
|
+
fullyParallel: true,
|
|
5408
|
+
forbidOnly: !!process.env.CI,
|
|
5409
|
+
retries: process.env.CI ? 2 : 0,
|
|
5410
|
+
workers: process.env.CI ? 1 : undefined,
|
|
5411
|
+
reporter: process.env.CI ? 'github' : 'html',
|
|
5412
|
+
use: {
|
|
5413
|
+
baseURL: 'http://localhost:3000',
|
|
5414
|
+
trace: 'on-first-retry',
|
|
5415
|
+
screenshot: 'only-on-failure',
|
|
5416
|
+
},
|
|
5417
|
+
projects: [
|
|
5418
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
5419
|
+
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
5420
|
+
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
5421
|
+
],
|
|
5422
|
+
webServer: {
|
|
5423
|
+
command: 'pnpm dev',
|
|
5424
|
+
url: 'http://localhost:3000',
|
|
5425
|
+
reuseExistingServer: !process.env.CI,
|
|
5426
|
+
timeout: 120_000,
|
|
5427
|
+
},
|
|
5428
|
+
});
|
|
5429
|
+
`;
|
|
5430
|
+
}
|
|
5431
|
+
exampleTest(_config) {
|
|
5432
|
+
return `import { test, expect } from '@playwright/test';
|
|
5433
|
+
|
|
5434
|
+
test.describe('Home Page', () => {
|
|
5435
|
+
test('should display the heading', async ({ page }) => {
|
|
5436
|
+
await page.goto('/');
|
|
5437
|
+
await expect(page.locator('h1')).toBeVisible();
|
|
5438
|
+
});
|
|
5439
|
+
|
|
5440
|
+
test('should have correct title', async ({ page }) => {
|
|
5441
|
+
await page.goto('/');
|
|
5442
|
+
await expect(page).toHaveTitle(/.*$/);
|
|
5443
|
+
});
|
|
5444
|
+
|
|
5445
|
+
test('should navigate without errors', async ({ page }) => {
|
|
5446
|
+
const response = await page.goto('/');
|
|
5447
|
+
expect(response?.status()).toBeLessThan(400);
|
|
5448
|
+
});
|
|
5449
|
+
});
|
|
5450
|
+
`;
|
|
5451
|
+
}
|
|
5452
|
+
ciWorkflow(_config) {
|
|
5453
|
+
return `
|
|
5454
|
+
e2e:
|
|
5455
|
+
name: E2E Tests
|
|
5456
|
+
runs-on: ubuntu-latest
|
|
5457
|
+
needs: ci
|
|
5458
|
+
steps:
|
|
5459
|
+
- name: Checkout
|
|
5460
|
+
uses: actions/checkout@v4
|
|
5461
|
+
|
|
5462
|
+
- name: Setup Node.js
|
|
5463
|
+
uses: actions/setup-node@v4
|
|
5464
|
+
with:
|
|
5465
|
+
node-version: 20
|
|
5466
|
+
|
|
5467
|
+
- name: Install dependencies
|
|
5468
|
+
run: pnpm install --frozen-lockfile
|
|
5469
|
+
|
|
5470
|
+
- name: Install Playwright browsers
|
|
5471
|
+
run: npx playwright install --with-deps chromium
|
|
5472
|
+
|
|
5473
|
+
- name: Build
|
|
5474
|
+
run: pnpm build
|
|
5475
|
+
|
|
5476
|
+
- name: Run E2E tests
|
|
5477
|
+
run: cd apps/web && npx playwright test --project=chromium
|
|
5478
|
+
`;
|
|
5479
|
+
}
|
|
5480
|
+
};
|
|
5481
|
+
|
|
5482
|
+
// src/templates/strategies/cypress-templates.ts
|
|
5483
|
+
var CypressTemplateStrategy = class {
|
|
5484
|
+
config(_config) {
|
|
5485
|
+
return `import { defineConfig } from 'cypress';
|
|
5486
|
+
|
|
5487
|
+
export default defineConfig({
|
|
5488
|
+
e2e: {
|
|
5489
|
+
baseUrl: 'http://localhost:3000',
|
|
5490
|
+
supportFile: false,
|
|
5491
|
+
specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
|
|
5492
|
+
viewportWidth: 1280,
|
|
5493
|
+
viewportHeight: 720,
|
|
5494
|
+
video: false,
|
|
5495
|
+
screenshotOnRunFailure: true,
|
|
5496
|
+
},
|
|
5497
|
+
});
|
|
5498
|
+
`;
|
|
5499
|
+
}
|
|
5500
|
+
exampleTest(_config) {
|
|
5501
|
+
return `describe('Home Page', () => {
|
|
5502
|
+
beforeEach(() => {
|
|
5503
|
+
cy.visit('/');
|
|
5504
|
+
});
|
|
5505
|
+
|
|
5506
|
+
it('should display the heading', () => {
|
|
5507
|
+
cy.get('h1').should('be.visible');
|
|
5508
|
+
});
|
|
5509
|
+
|
|
5510
|
+
it('should load without errors', () => {
|
|
5511
|
+
cy.request('/').its('status').should('be.lessThan', 400);
|
|
5512
|
+
});
|
|
5513
|
+
|
|
5514
|
+
it('should have navigation elements', () => {
|
|
5515
|
+
cy.get('button').should('exist');
|
|
5516
|
+
});
|
|
5517
|
+
});
|
|
5518
|
+
`;
|
|
5519
|
+
}
|
|
5520
|
+
ciWorkflow(_config) {
|
|
5521
|
+
return `
|
|
5522
|
+
e2e:
|
|
5523
|
+
name: E2E Tests
|
|
5524
|
+
runs-on: ubuntu-latest
|
|
5525
|
+
needs: ci
|
|
5526
|
+
steps:
|
|
5527
|
+
- name: Checkout
|
|
5528
|
+
uses: actions/checkout@v4
|
|
5529
|
+
|
|
5530
|
+
- name: Setup Node.js
|
|
5531
|
+
uses: actions/setup-node@v4
|
|
5532
|
+
with:
|
|
5533
|
+
node-version: 20
|
|
5534
|
+
|
|
5535
|
+
- name: Install dependencies
|
|
5536
|
+
run: pnpm install --frozen-lockfile
|
|
5537
|
+
|
|
5538
|
+
- name: Build
|
|
5539
|
+
run: pnpm build
|
|
5540
|
+
|
|
5541
|
+
- name: Run Cypress tests
|
|
5542
|
+
uses: cypress-io/github-action@v6
|
|
5543
|
+
with:
|
|
5544
|
+
working-directory: apps/web
|
|
5545
|
+
start: pnpm start
|
|
5546
|
+
wait-on: 'http://localhost:3000'
|
|
5547
|
+
`;
|
|
5548
|
+
}
|
|
5549
|
+
};
|
|
5550
|
+
|
|
5551
|
+
// src/templates/strategies/e2e-factory.ts
|
|
5552
|
+
function createE2eStrategy(e2e) {
|
|
5553
|
+
switch (e2e) {
|
|
5554
|
+
case "playwright":
|
|
5555
|
+
return new PlaywrightTemplateStrategy();
|
|
5556
|
+
case "cypress":
|
|
5557
|
+
return new CypressTemplateStrategy();
|
|
5558
|
+
case "none":
|
|
5559
|
+
return null;
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
// src/templates/strategies/pino-templates.ts
|
|
5564
|
+
var PinoTemplateStrategy = class {
|
|
5565
|
+
loggerSetup(_config) {
|
|
5566
|
+
return `import pino from 'pino';
|
|
5567
|
+
|
|
5568
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
5569
|
+
|
|
5570
|
+
export const logger = pino({
|
|
5571
|
+
level: process.env.LOG_LEVEL ?? (isProduction ? 'info' : 'debug'),
|
|
5572
|
+
...(isProduction
|
|
5573
|
+
? {}
|
|
5574
|
+
: {
|
|
5575
|
+
transport: {
|
|
5576
|
+
target: 'pino-pretty',
|
|
5577
|
+
options: {
|
|
5578
|
+
colorize: true,
|
|
5579
|
+
translateTime: 'HH:MM:ss',
|
|
5580
|
+
ignore: 'pid,hostname',
|
|
5581
|
+
},
|
|
5582
|
+
},
|
|
5583
|
+
}),
|
|
5584
|
+
});
|
|
5585
|
+
|
|
5586
|
+
/** Create a child logger with a context label */
|
|
5587
|
+
export function createLogger(context: string) {
|
|
5588
|
+
return logger.child({ context });
|
|
5589
|
+
}
|
|
5590
|
+
`;
|
|
5591
|
+
}
|
|
5592
|
+
index(_config) {
|
|
5593
|
+
return `export { logger, createLogger } from './logger.js';
|
|
5594
|
+
`;
|
|
5595
|
+
}
|
|
5596
|
+
};
|
|
5597
|
+
|
|
5598
|
+
// src/templates/strategies/winston-templates.ts
|
|
5599
|
+
var WinstonTemplateStrategy = class {
|
|
5600
|
+
loggerSetup(_config) {
|
|
5601
|
+
return `import winston from 'winston';
|
|
5602
|
+
|
|
5603
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
5604
|
+
|
|
5605
|
+
export const logger = winston.createLogger({
|
|
5606
|
+
level: process.env.LOG_LEVEL ?? (isProduction ? 'info' : 'debug'),
|
|
5607
|
+
format: isProduction
|
|
5608
|
+
? winston.format.combine(winston.format.timestamp(), winston.format.json())
|
|
5609
|
+
: winston.format.combine(
|
|
5610
|
+
winston.format.colorize(),
|
|
5611
|
+
winston.format.timestamp({ format: 'HH:mm:ss' }),
|
|
5612
|
+
winston.format.printf(({ timestamp, level, message, context, ...meta }) => {
|
|
5613
|
+
const ctx = context ? \` [\${context}]\` : '';
|
|
5614
|
+
const metaStr = Object.keys(meta).length ? \` \${JSON.stringify(meta)}\` : '';
|
|
5615
|
+
return \`\${timestamp} \${level}\${ctx}: \${message}\${metaStr}\`;
|
|
5616
|
+
}),
|
|
5617
|
+
),
|
|
5618
|
+
transports: [
|
|
5619
|
+
new winston.transports.Console(),
|
|
5620
|
+
],
|
|
5621
|
+
});
|
|
5622
|
+
|
|
5623
|
+
/** Create a child logger with a context label */
|
|
5624
|
+
export function createLogger(context: string) {
|
|
5625
|
+
return logger.child({ context });
|
|
5626
|
+
}
|
|
5627
|
+
`;
|
|
5628
|
+
}
|
|
5629
|
+
index(_config) {
|
|
5630
|
+
return `export { logger, createLogger } from './logger.js';
|
|
5631
|
+
`;
|
|
5632
|
+
}
|
|
5633
|
+
};
|
|
5634
|
+
|
|
5635
|
+
// src/templates/strategies/logging-factory.ts
|
|
5636
|
+
function createLoggingStrategy(logging) {
|
|
5637
|
+
switch (logging) {
|
|
5638
|
+
case "pino":
|
|
5639
|
+
return new PinoTemplateStrategy();
|
|
5640
|
+
case "winston":
|
|
5641
|
+
return new WinstonTemplateStrategy();
|
|
5642
|
+
case "default":
|
|
5643
|
+
return null;
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
|
|
5647
|
+
// src/templates/strategies/resend-templates.ts
|
|
5648
|
+
var ResendTemplateStrategy = class {
|
|
5649
|
+
packageJson(config) {
|
|
5650
|
+
return `{
|
|
5651
|
+
"name": "@${config.name}/email",
|
|
5652
|
+
"version": "1.0.0",
|
|
5653
|
+
"private": true,
|
|
5654
|
+
"type": "module",
|
|
5655
|
+
"main": "./src/index.ts",
|
|
5656
|
+
"types": "./src/index.ts",
|
|
5657
|
+
"exports": {
|
|
5658
|
+
".": "./src/index.ts"
|
|
5659
|
+
},
|
|
5660
|
+
"dependencies": {
|
|
5661
|
+
"resend": "${config.versions["resend"] ?? "^4.1.0"}",
|
|
5662
|
+
"@react-email/components": "${config.versions["@react-email/components"] ?? "^0.0.36"}",
|
|
5663
|
+
"react": "${config.versions["react"] ?? "^19.1.0"}"
|
|
5664
|
+
},
|
|
5665
|
+
"devDependencies": {
|
|
5666
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
`;
|
|
5670
|
+
}
|
|
5671
|
+
index(_config) {
|
|
5672
|
+
return `export { sendEmail } from './send.js';
|
|
5673
|
+
export { resend } from './client.js';
|
|
5674
|
+
`;
|
|
5675
|
+
}
|
|
5676
|
+
client(_config) {
|
|
5677
|
+
return `import { Resend } from 'resend';
|
|
5678
|
+
|
|
5679
|
+
export const resend = new Resend(process.env.RESEND_API_KEY);
|
|
5680
|
+
`;
|
|
5681
|
+
}
|
|
5682
|
+
sendFunction(_config) {
|
|
5683
|
+
return `import { resend } from './client.js';
|
|
5684
|
+
|
|
5685
|
+
interface SendEmailOptions {
|
|
5686
|
+
to: string | string[];
|
|
5687
|
+
subject: string;
|
|
5688
|
+
react: React.ReactElement;
|
|
5689
|
+
}
|
|
5690
|
+
|
|
5691
|
+
export async function sendEmail({ to, subject, react }: SendEmailOptions) {
|
|
5692
|
+
const { data, error } = await resend.emails.send({
|
|
5693
|
+
from: process.env.EMAIL_FROM ?? 'noreply@example.com',
|
|
5694
|
+
to: Array.isArray(to) ? to : [to],
|
|
5695
|
+
subject,
|
|
5696
|
+
react,
|
|
5697
|
+
});
|
|
5698
|
+
|
|
5699
|
+
if (error) {
|
|
5700
|
+
throw new Error(\`Failed to send email: \${error.message}\`);
|
|
5701
|
+
}
|
|
5702
|
+
|
|
5703
|
+
return data;
|
|
5704
|
+
}
|
|
5705
|
+
`;
|
|
5706
|
+
}
|
|
5707
|
+
welcomeTemplate(config) {
|
|
5708
|
+
return `import { Html, Head, Body, Container, Heading, Text, Button } from '@react-email/components';
|
|
5709
|
+
|
|
5710
|
+
interface WelcomeEmailProps {
|
|
5711
|
+
name: string;
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5714
|
+
export function WelcomeEmail({ name }: WelcomeEmailProps) {
|
|
5715
|
+
return (
|
|
5716
|
+
<Html>
|
|
5717
|
+
<Head />
|
|
5718
|
+
<Body style={{ fontFamily: 'system-ui, sans-serif', background: '#f9fafb' }}>
|
|
5719
|
+
<Container style={{ maxWidth: '600px', margin: '0 auto', padding: '40px 20px' }}>
|
|
5720
|
+
<Heading style={{ fontSize: '24px', color: '#111827' }}>
|
|
5721
|
+
Welcome to ${config.pascalCase}!
|
|
5722
|
+
</Heading>
|
|
5723
|
+
<Text style={{ fontSize: '16px', color: '#4b5563' }}>
|
|
5724
|
+
Hi {name}, thank you for signing up. We're excited to have you on board.
|
|
5725
|
+
</Text>
|
|
5726
|
+
<Button
|
|
5727
|
+
href={process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'}
|
|
5728
|
+
style={{ background: '#2563eb', color: 'white', padding: '12px 24px', borderRadius: '8px', textDecoration: 'none' }}
|
|
5729
|
+
>
|
|
5730
|
+
Get Started
|
|
5731
|
+
</Button>
|
|
5732
|
+
</Container>
|
|
5733
|
+
</Body>
|
|
5734
|
+
</Html>
|
|
5735
|
+
);
|
|
5736
|
+
}
|
|
5737
|
+
`;
|
|
5738
|
+
}
|
|
5739
|
+
resetPasswordTemplate(config) {
|
|
5740
|
+
return `import { Html, Head, Body, Container, Heading, Text, Button } from '@react-email/components';
|
|
5741
|
+
|
|
5742
|
+
interface ResetPasswordEmailProps {
|
|
5743
|
+
resetUrl: string;
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5746
|
+
export function ResetPasswordEmail({ resetUrl }: ResetPasswordEmailProps) {
|
|
5747
|
+
return (
|
|
5748
|
+
<Html>
|
|
5749
|
+
<Head />
|
|
5750
|
+
<Body style={{ fontFamily: 'system-ui, sans-serif', background: '#f9fafb' }}>
|
|
5751
|
+
<Container style={{ maxWidth: '600px', margin: '0 auto', padding: '40px 20px' }}>
|
|
5752
|
+
<Heading style={{ fontSize: '24px', color: '#111827' }}>
|
|
5753
|
+
Reset Your Password
|
|
5754
|
+
</Heading>
|
|
5755
|
+
<Text style={{ fontSize: '16px', color: '#4b5563' }}>
|
|
5756
|
+
Click the button below to reset your ${config.pascalCase} password. This link expires in 1 hour.
|
|
5757
|
+
</Text>
|
|
5758
|
+
<Button
|
|
5759
|
+
href={resetUrl}
|
|
5760
|
+
style={{ background: '#2563eb', color: 'white', padding: '12px 24px', borderRadius: '8px', textDecoration: 'none' }}
|
|
5761
|
+
>
|
|
5762
|
+
Reset Password
|
|
5763
|
+
</Button>
|
|
5764
|
+
<Text style={{ fontSize: '14px', color: '#9ca3af', marginTop: '24px' }}>
|
|
5765
|
+
If you didn't request this, you can safely ignore this email.
|
|
5766
|
+
</Text>
|
|
5767
|
+
</Container>
|
|
5768
|
+
</Body>
|
|
5769
|
+
</Html>
|
|
5770
|
+
);
|
|
5771
|
+
}
|
|
5772
|
+
`;
|
|
5773
|
+
}
|
|
5774
|
+
};
|
|
5775
|
+
|
|
5776
|
+
// src/templates/strategies/nodemailer-templates.ts
|
|
5777
|
+
var NodemailerTemplateStrategy = class {
|
|
5778
|
+
packageJson(config) {
|
|
5779
|
+
return `{
|
|
5780
|
+
"name": "@${config.name}/email",
|
|
5781
|
+
"version": "1.0.0",
|
|
5782
|
+
"private": true,
|
|
5783
|
+
"type": "module",
|
|
5784
|
+
"main": "./src/index.ts",
|
|
5785
|
+
"types": "./src/index.ts",
|
|
5786
|
+
"exports": {
|
|
5787
|
+
".": "./src/index.ts"
|
|
5788
|
+
},
|
|
5789
|
+
"dependencies": {
|
|
5790
|
+
"nodemailer": "${config.versions["nodemailer"] ?? "^6.10.0"}"
|
|
5791
|
+
},
|
|
5792
|
+
"devDependencies": {
|
|
5793
|
+
"@types/nodemailer": "${config.versions["@types/nodemailer"] ?? "^6.4.17"}",
|
|
5794
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
`;
|
|
5798
|
+
}
|
|
5799
|
+
index(_config) {
|
|
5800
|
+
return `export { sendEmail } from './send.js';
|
|
5801
|
+
export { transporter } from './client.js';
|
|
5802
|
+
`;
|
|
5803
|
+
}
|
|
5804
|
+
client(_config) {
|
|
5805
|
+
return `import nodemailer from 'nodemailer';
|
|
5806
|
+
|
|
5807
|
+
export const transporter = nodemailer.createTransport({
|
|
5808
|
+
host: process.env.SMTP_HOST ?? 'smtp.gmail.com',
|
|
5809
|
+
port: Number(process.env.SMTP_PORT ?? 587),
|
|
5810
|
+
secure: process.env.SMTP_SECURE === 'true',
|
|
5811
|
+
auth: {
|
|
5812
|
+
user: process.env.SMTP_USER,
|
|
5813
|
+
pass: process.env.SMTP_PASS,
|
|
5814
|
+
},
|
|
5815
|
+
});
|
|
5816
|
+
`;
|
|
5817
|
+
}
|
|
5818
|
+
sendFunction(_config) {
|
|
5819
|
+
return `import { transporter } from './client.js';
|
|
5820
|
+
|
|
5821
|
+
interface SendEmailOptions {
|
|
5822
|
+
to: string | string[];
|
|
5823
|
+
subject: string;
|
|
5824
|
+
html: string;
|
|
5825
|
+
text?: string;
|
|
5826
|
+
}
|
|
5827
|
+
|
|
5828
|
+
export async function sendEmail({ to, subject, html, text }: SendEmailOptions) {
|
|
5829
|
+
const info = await transporter.sendMail({
|
|
5830
|
+
from: process.env.EMAIL_FROM ?? 'noreply@example.com',
|
|
5831
|
+
to: Array.isArray(to) ? to.join(', ') : to,
|
|
5832
|
+
subject,
|
|
5833
|
+
html,
|
|
5834
|
+
text,
|
|
5835
|
+
});
|
|
5836
|
+
|
|
5837
|
+
return { messageId: info.messageId };
|
|
5838
|
+
}
|
|
5839
|
+
`;
|
|
5840
|
+
}
|
|
5841
|
+
welcomeTemplate(config) {
|
|
5842
|
+
return `/** Generate welcome email HTML */
|
|
5843
|
+
export function welcomeEmailHtml(name: string): string {
|
|
5844
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
|
|
5845
|
+
|
|
5846
|
+
return \`<!DOCTYPE html>
|
|
5847
|
+
<html>
|
|
5848
|
+
<body style="font-family: system-ui, sans-serif; background: #f9fafb; padding: 40px 0;">
|
|
5849
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
|
|
5850
|
+
<h1 style="font-size: 24px; color: #111827;">Welcome to ${config.pascalCase}!</h1>
|
|
5851
|
+
<p style="font-size: 16px; color: #4b5563;">
|
|
5852
|
+
Hi \${name}, thank you for signing up. We're excited to have you on board.
|
|
5853
|
+
</p>
|
|
5854
|
+
<a href="\${appUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none;">
|
|
5855
|
+
Get Started
|
|
5856
|
+
</a>
|
|
5857
|
+
</div>
|
|
5858
|
+
</body>
|
|
5859
|
+
</html>\`;
|
|
5860
|
+
}
|
|
5861
|
+
`;
|
|
5862
|
+
}
|
|
5863
|
+
resetPasswordTemplate(config) {
|
|
5864
|
+
return `/** Generate password reset email HTML */
|
|
5865
|
+
export function resetPasswordEmailHtml(resetUrl: string): string {
|
|
5866
|
+
return \`<!DOCTYPE html>
|
|
5867
|
+
<html>
|
|
5868
|
+
<body style="font-family: system-ui, sans-serif; background: #f9fafb; padding: 40px 0;">
|
|
5869
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
|
|
5870
|
+
<h1 style="font-size: 24px; color: #111827;">Reset Your Password</h1>
|
|
5871
|
+
<p style="font-size: 16px; color: #4b5563;">
|
|
5872
|
+
Click the button below to reset your ${config.pascalCase} password. This link expires in 1 hour.
|
|
5873
|
+
</p>
|
|
5874
|
+
<a href="\${resetUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none;">
|
|
5875
|
+
Reset Password
|
|
5876
|
+
</a>
|
|
5877
|
+
<p style="font-size: 14px; color: #9ca3af; margin-top: 24px;">
|
|
5878
|
+
If you didn't request this, you can safely ignore this email.
|
|
5879
|
+
</p>
|
|
5880
|
+
</div>
|
|
5881
|
+
</body>
|
|
5882
|
+
</html>\`;
|
|
5883
|
+
}
|
|
5884
|
+
`;
|
|
5885
|
+
}
|
|
5886
|
+
};
|
|
5887
|
+
|
|
5888
|
+
// src/templates/strategies/sendgrid-templates.ts
|
|
5889
|
+
var SendGridTemplateStrategy = class {
|
|
5890
|
+
packageJson(config) {
|
|
5891
|
+
return `{
|
|
5892
|
+
"name": "@${config.name}/email",
|
|
5893
|
+
"version": "1.0.0",
|
|
5894
|
+
"private": true,
|
|
5895
|
+
"type": "module",
|
|
5896
|
+
"main": "./src/index.ts",
|
|
5897
|
+
"types": "./src/index.ts",
|
|
5898
|
+
"exports": {
|
|
5899
|
+
".": "./src/index.ts"
|
|
5900
|
+
},
|
|
5901
|
+
"dependencies": {
|
|
5902
|
+
"@sendgrid/mail": "${config.versions["@sendgrid/mail"] ?? "^8.1.0"}"
|
|
5903
|
+
},
|
|
5904
|
+
"devDependencies": {
|
|
5905
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
`;
|
|
5909
|
+
}
|
|
5910
|
+
index(_config) {
|
|
5911
|
+
return `export { sendEmail } from './send.js';
|
|
5912
|
+
export { sgMail } from './client.js';
|
|
5913
|
+
`;
|
|
5914
|
+
}
|
|
5915
|
+
client(_config) {
|
|
5916
|
+
return `import sgMail from '@sendgrid/mail';
|
|
5917
|
+
|
|
5918
|
+
sgMail.setApiKey(process.env.SENDGRID_API_KEY ?? '');
|
|
5919
|
+
|
|
5920
|
+
export { sgMail };
|
|
5921
|
+
`;
|
|
5922
|
+
}
|
|
5923
|
+
sendFunction(_config) {
|
|
5924
|
+
return `import { sgMail } from './client.js';
|
|
5925
|
+
|
|
5926
|
+
interface SendEmailOptions {
|
|
5927
|
+
to: string | string[];
|
|
5928
|
+
subject: string;
|
|
5929
|
+
html: string;
|
|
5930
|
+
text?: string;
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5933
|
+
export async function sendEmail({ to, subject, html, text }: SendEmailOptions) {
|
|
5934
|
+
const msg = {
|
|
5935
|
+
to,
|
|
5936
|
+
from: process.env.EMAIL_FROM ?? 'noreply@example.com',
|
|
5937
|
+
subject,
|
|
5938
|
+
html,
|
|
5939
|
+
text: text ?? '',
|
|
5940
|
+
};
|
|
5941
|
+
|
|
5942
|
+
const [response] = await sgMail.send(msg);
|
|
5943
|
+
return { statusCode: response.statusCode };
|
|
5944
|
+
}
|
|
5945
|
+
`;
|
|
5946
|
+
}
|
|
5947
|
+
welcomeTemplate(config) {
|
|
5948
|
+
return `/** Generate welcome email HTML */
|
|
5949
|
+
export function welcomeEmailHtml(name: string): string {
|
|
5950
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
|
|
5951
|
+
|
|
5952
|
+
return \`<!DOCTYPE html>
|
|
5953
|
+
<html>
|
|
5954
|
+
<body style="font-family: system-ui, sans-serif; background: #f9fafb; padding: 40px 0;">
|
|
5955
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
|
|
5956
|
+
<h1 style="font-size: 24px; color: #111827;">Welcome to ${config.pascalCase}!</h1>
|
|
5957
|
+
<p style="font-size: 16px; color: #4b5563;">Hi \${name}, thank you for signing up.</p>
|
|
5958
|
+
<a href="\${appUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none;">Get Started</a>
|
|
5959
|
+
</div>
|
|
5960
|
+
</body>
|
|
5961
|
+
</html>\`;
|
|
5962
|
+
}
|
|
5963
|
+
`;
|
|
5964
|
+
}
|
|
5965
|
+
resetPasswordTemplate(config) {
|
|
5966
|
+
return `/** Generate password reset email HTML */
|
|
5967
|
+
export function resetPasswordEmailHtml(resetUrl: string): string {
|
|
5968
|
+
return \`<!DOCTYPE html>
|
|
5969
|
+
<html>
|
|
5970
|
+
<body style="font-family: system-ui, sans-serif; background: #f9fafb; padding: 40px 0;">
|
|
5971
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
|
|
5972
|
+
<h1 style="font-size: 24px; color: #111827;">Reset Your Password</h1>
|
|
5973
|
+
<p style="font-size: 16px; color: #4b5563;">Click below to reset your ${config.pascalCase} password.</p>
|
|
5974
|
+
<a href="\${resetUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none;">Reset Password</a>
|
|
5975
|
+
</div>
|
|
5976
|
+
</body>
|
|
5977
|
+
</html>\`;
|
|
5978
|
+
}
|
|
5979
|
+
`;
|
|
5980
|
+
}
|
|
5981
|
+
};
|
|
5982
|
+
|
|
5983
|
+
// src/templates/strategies/email-factory.ts
|
|
5984
|
+
function createEmailStrategy(email) {
|
|
5985
|
+
switch (email) {
|
|
5986
|
+
case "resend":
|
|
5987
|
+
return new ResendTemplateStrategy();
|
|
5988
|
+
case "nodemailer":
|
|
5989
|
+
return new NodemailerTemplateStrategy();
|
|
5990
|
+
case "sendgrid":
|
|
5991
|
+
return new SendGridTemplateStrategy();
|
|
5992
|
+
case "none":
|
|
5993
|
+
return null;
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
|
|
5997
|
+
// src/templates/strategies/s3-templates.ts
|
|
5998
|
+
var S3TemplateStrategy = class {
|
|
5999
|
+
packageJson(config) {
|
|
6000
|
+
return `{
|
|
6001
|
+
"name": "@${config.name}/storage",
|
|
6002
|
+
"version": "1.0.0",
|
|
6003
|
+
"private": true,
|
|
6004
|
+
"type": "module",
|
|
6005
|
+
"main": "./src/index.ts",
|
|
6006
|
+
"types": "./src/index.ts",
|
|
6007
|
+
"exports": {
|
|
6008
|
+
".": "./src/index.ts"
|
|
6009
|
+
},
|
|
6010
|
+
"dependencies": {
|
|
6011
|
+
"@aws-sdk/client-s3": "${config.versions["@aws-sdk/client-s3"] ?? "^3.750.0"}",
|
|
6012
|
+
"@aws-sdk/s3-request-presigner": "${config.versions["@aws-sdk/s3-request-presigner"] ?? "^3.750.0"}"
|
|
6013
|
+
},
|
|
6014
|
+
"devDependencies": {
|
|
6015
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
`;
|
|
6019
|
+
}
|
|
6020
|
+
index(_config) {
|
|
6021
|
+
return `export { s3Client } from './client.js';
|
|
6022
|
+
export { uploadFile, getUploadUrl } from './upload.js';
|
|
6023
|
+
export { getDownloadUrl } from './download.js';
|
|
6024
|
+
`;
|
|
6025
|
+
}
|
|
6026
|
+
client(_config) {
|
|
6027
|
+
return `import { S3Client } from '@aws-sdk/client-s3';
|
|
6028
|
+
|
|
6029
|
+
export const s3Client = new S3Client({
|
|
6030
|
+
region: process.env.AWS_REGION ?? 'us-east-1',
|
|
6031
|
+
credentials: {
|
|
6032
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
|
|
6033
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
|
|
6034
|
+
},
|
|
6035
|
+
...(process.env.S3_ENDPOINT ? { endpoint: process.env.S3_ENDPOINT, forcePathStyle: true } : {}),
|
|
6036
|
+
});
|
|
6037
|
+
|
|
6038
|
+
export const BUCKET_NAME = process.env.S3_BUCKET ?? 'uploads';
|
|
6039
|
+
`;
|
|
6040
|
+
}
|
|
6041
|
+
uploadService(_config) {
|
|
6042
|
+
return `import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
6043
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
6044
|
+
import { s3Client, BUCKET_NAME } from './client.js';
|
|
6045
|
+
|
|
6046
|
+
/** Upload a file buffer directly to S3 */
|
|
6047
|
+
export async function uploadFile(key: string, body: Buffer, contentType: string) {
|
|
6048
|
+
const command = new PutObjectCommand({
|
|
6049
|
+
Bucket: BUCKET_NAME,
|
|
6050
|
+
Key: key,
|
|
6051
|
+
Body: body,
|
|
6052
|
+
ContentType: contentType,
|
|
6053
|
+
});
|
|
6054
|
+
|
|
6055
|
+
await s3Client.send(command);
|
|
6056
|
+
return { key, bucket: BUCKET_NAME };
|
|
6057
|
+
}
|
|
6058
|
+
|
|
6059
|
+
/** Generate a presigned upload URL (client uploads directly to S3) */
|
|
6060
|
+
export async function getUploadUrl(key: string, contentType: string, expiresIn = 3600) {
|
|
6061
|
+
const command = new PutObjectCommand({
|
|
6062
|
+
Bucket: BUCKET_NAME,
|
|
6063
|
+
Key: key,
|
|
6064
|
+
ContentType: contentType,
|
|
6065
|
+
});
|
|
6066
|
+
|
|
6067
|
+
const url = await getSignedUrl(s3Client, command, { expiresIn });
|
|
6068
|
+
return { url, key };
|
|
6069
|
+
}
|
|
6070
|
+
`;
|
|
6071
|
+
}
|
|
6072
|
+
downloadService(_config) {
|
|
6073
|
+
return `import { GetObjectCommand } from '@aws-sdk/client-s3';
|
|
6074
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
6075
|
+
import { s3Client, BUCKET_NAME } from './client.js';
|
|
6076
|
+
|
|
6077
|
+
/** Generate a presigned download URL */
|
|
6078
|
+
export async function getDownloadUrl(key: string, expiresIn = 3600) {
|
|
6079
|
+
const command = new GetObjectCommand({
|
|
6080
|
+
Bucket: BUCKET_NAME,
|
|
6081
|
+
Key: key,
|
|
6082
|
+
});
|
|
6083
|
+
|
|
6084
|
+
return getSignedUrl(s3Client, command, { expiresIn });
|
|
6085
|
+
}
|
|
6086
|
+
`;
|
|
6087
|
+
}
|
|
6088
|
+
apiRoutes(_config) {
|
|
6089
|
+
return {};
|
|
6090
|
+
}
|
|
6091
|
+
};
|
|
6092
|
+
|
|
6093
|
+
// src/templates/strategies/uploadthing-templates.ts
|
|
6094
|
+
var UploadThingTemplateStrategy = class {
|
|
6095
|
+
packageJson(config) {
|
|
6096
|
+
return `{
|
|
6097
|
+
"name": "@${config.name}/storage",
|
|
6098
|
+
"version": "1.0.0",
|
|
6099
|
+
"private": true,
|
|
6100
|
+
"type": "module",
|
|
6101
|
+
"main": "./src/index.ts",
|
|
6102
|
+
"types": "./src/index.ts",
|
|
6103
|
+
"exports": {
|
|
6104
|
+
".": "./src/index.ts"
|
|
6105
|
+
},
|
|
6106
|
+
"dependencies": {
|
|
6107
|
+
"uploadthing": "${config.versions["uploadthing"] ?? "^7.6.0"}",
|
|
6108
|
+
"@uploadthing/react": "${config.versions["@uploadthing/react"] ?? "^7.3.0"}"
|
|
6109
|
+
},
|
|
6110
|
+
"devDependencies": {
|
|
6111
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
`;
|
|
6115
|
+
}
|
|
6116
|
+
index(_config) {
|
|
6117
|
+
return `export { utapi } from './client.js';
|
|
6118
|
+
`;
|
|
6119
|
+
}
|
|
6120
|
+
client(_config) {
|
|
6121
|
+
return `import { UTApi } from 'uploadthing/server';
|
|
6122
|
+
|
|
6123
|
+
export const utapi = new UTApi();
|
|
6124
|
+
`;
|
|
6125
|
+
}
|
|
6126
|
+
uploadService(_config) {
|
|
6127
|
+
return `// UploadThing handles uploads via file router (see apps/web/app/api/uploadthing/)
|
|
6128
|
+
// Use the utapi for server-side file operations
|
|
6129
|
+
|
|
6130
|
+
export { utapi } from './client.js';
|
|
6131
|
+
`;
|
|
6132
|
+
}
|
|
6133
|
+
downloadService(_config) {
|
|
6134
|
+
return `import { utapi } from './client.js';
|
|
6135
|
+
|
|
6136
|
+
/** Get file URLs from UploadThing */
|
|
6137
|
+
export async function getFileUrls(fileKeys: string[]) {
|
|
6138
|
+
const response = await utapi.getFileUrls(fileKeys);
|
|
6139
|
+
return response.data;
|
|
6140
|
+
}
|
|
6141
|
+
|
|
6142
|
+
/** Delete files from UploadThing */
|
|
6143
|
+
export async function deleteFiles(fileKeys: string[]) {
|
|
6144
|
+
await utapi.deleteFiles(fileKeys);
|
|
6145
|
+
}
|
|
6146
|
+
`;
|
|
6147
|
+
}
|
|
6148
|
+
apiRoutes(_config) {
|
|
6149
|
+
return {
|
|
6150
|
+
"apps/web/app/api/uploadthing/core.ts": `import { createUploadthing, type FileRouter } from 'uploadthing/next';
|
|
6151
|
+
|
|
6152
|
+
const f = createUploadthing();
|
|
6153
|
+
|
|
6154
|
+
export const ourFileRouter = {
|
|
6155
|
+
imageUploader: f({ image: { maxFileSize: '4MB', maxFileCount: 4 } })
|
|
6156
|
+
.onUploadComplete(async ({ file }) => {
|
|
6157
|
+
console.log('Upload complete:', file.url);
|
|
6158
|
+
return { url: file.url };
|
|
6159
|
+
}),
|
|
6160
|
+
|
|
6161
|
+
documentUploader: f({ pdf: { maxFileSize: '16MB' }, text: { maxFileSize: '1MB' } })
|
|
6162
|
+
.onUploadComplete(async ({ file }) => {
|
|
6163
|
+
console.log('Document uploaded:', file.name);
|
|
6164
|
+
return { url: file.url };
|
|
6165
|
+
}),
|
|
6166
|
+
} satisfies FileRouter;
|
|
6167
|
+
|
|
6168
|
+
export type OurFileRouter = typeof ourFileRouter;
|
|
6169
|
+
`,
|
|
6170
|
+
"apps/web/app/api/uploadthing/route.ts": `import { createRouteHandler } from 'uploadthing/next';
|
|
6171
|
+
import { ourFileRouter } from './core';
|
|
6172
|
+
|
|
6173
|
+
export const { GET, POST } = createRouteHandler({ router: ourFileRouter });
|
|
6174
|
+
`
|
|
6175
|
+
};
|
|
6176
|
+
}
|
|
6177
|
+
};
|
|
6178
|
+
|
|
6179
|
+
// src/templates/strategies/cloudinary-templates.ts
|
|
6180
|
+
var CloudinaryTemplateStrategy = class {
|
|
6181
|
+
packageJson(config) {
|
|
6182
|
+
return `{
|
|
6183
|
+
"name": "@${config.name}/storage",
|
|
6184
|
+
"version": "1.0.0",
|
|
6185
|
+
"private": true,
|
|
6186
|
+
"type": "module",
|
|
6187
|
+
"main": "./src/index.ts",
|
|
6188
|
+
"types": "./src/index.ts",
|
|
6189
|
+
"exports": {
|
|
6190
|
+
".": "./src/index.ts"
|
|
6191
|
+
},
|
|
6192
|
+
"dependencies": {
|
|
6193
|
+
"cloudinary": "${config.versions["cloudinary"] ?? "^2.6.0"}"
|
|
6194
|
+
},
|
|
6195
|
+
"devDependencies": {
|
|
6196
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
`;
|
|
6200
|
+
}
|
|
6201
|
+
index(_config) {
|
|
6202
|
+
return `export { cloudinary } from './client.js';
|
|
6203
|
+
export { uploadFile } from './upload.js';
|
|
6204
|
+
export { getOptimizedUrl, getTransformUrl } from './download.js';
|
|
6205
|
+
`;
|
|
6206
|
+
}
|
|
6207
|
+
client(_config) {
|
|
6208
|
+
return `import { v2 as cloudinary } from 'cloudinary';
|
|
6209
|
+
|
|
6210
|
+
cloudinary.config({
|
|
6211
|
+
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
|
|
6212
|
+
api_key: process.env.CLOUDINARY_API_KEY,
|
|
6213
|
+
api_secret: process.env.CLOUDINARY_API_SECRET,
|
|
6214
|
+
});
|
|
6215
|
+
|
|
6216
|
+
export { cloudinary };
|
|
6217
|
+
`;
|
|
6218
|
+
}
|
|
6219
|
+
uploadService(_config) {
|
|
6220
|
+
return `import { cloudinary } from './client.js';
|
|
6221
|
+
|
|
6222
|
+
interface UploadOptions {
|
|
6223
|
+
folder?: string;
|
|
6224
|
+
resourceType?: 'image' | 'video' | 'raw' | 'auto';
|
|
6225
|
+
transformation?: Record<string, unknown>;
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
/** Upload a file to Cloudinary */
|
|
6229
|
+
export async function uploadFile(
|
|
6230
|
+
filePath: string,
|
|
6231
|
+
options: UploadOptions = {},
|
|
6232
|
+
) {
|
|
6233
|
+
const result = await cloudinary.uploader.upload(filePath, {
|
|
6234
|
+
folder: options.folder ?? 'uploads',
|
|
6235
|
+
resource_type: options.resourceType ?? 'auto',
|
|
6236
|
+
transformation: options.transformation,
|
|
6237
|
+
});
|
|
6238
|
+
|
|
6239
|
+
return {
|
|
6240
|
+
publicId: result.public_id,
|
|
6241
|
+
url: result.secure_url,
|
|
6242
|
+
format: result.format,
|
|
6243
|
+
width: result.width,
|
|
6244
|
+
height: result.height,
|
|
6245
|
+
bytes: result.bytes,
|
|
6246
|
+
};
|
|
6247
|
+
}
|
|
6248
|
+
|
|
6249
|
+
/** Upload from a buffer */
|
|
6250
|
+
export async function uploadBuffer(
|
|
6251
|
+
buffer: Buffer,
|
|
6252
|
+
options: UploadOptions = {},
|
|
6253
|
+
): Promise<{ publicId: string; url: string }> {
|
|
6254
|
+
return new Promise((resolve, reject) => {
|
|
6255
|
+
cloudinary.uploader
|
|
6256
|
+
.upload_stream(
|
|
6257
|
+
{
|
|
6258
|
+
folder: options.folder ?? 'uploads',
|
|
6259
|
+
resource_type: options.resourceType ?? 'auto',
|
|
6260
|
+
},
|
|
6261
|
+
(error, result) => {
|
|
6262
|
+
if (error || !result) return reject(error ?? new Error('Upload failed'));
|
|
6263
|
+
resolve({ publicId: result.public_id, url: result.secure_url });
|
|
6264
|
+
},
|
|
6265
|
+
)
|
|
6266
|
+
.end(buffer);
|
|
6267
|
+
});
|
|
6268
|
+
}
|
|
6269
|
+
`;
|
|
6270
|
+
}
|
|
6271
|
+
downloadService(_config) {
|
|
6272
|
+
return `import { cloudinary } from './client.js';
|
|
6273
|
+
|
|
6274
|
+
/** Get an optimized URL for an image */
|
|
6275
|
+
export function getOptimizedUrl(publicId: string, options: { width?: number; height?: number; quality?: string } = {}) {
|
|
6276
|
+
return cloudinary.url(publicId, {
|
|
6277
|
+
fetch_format: 'auto',
|
|
6278
|
+
quality: options.quality ?? 'auto',
|
|
6279
|
+
...(options.width ? { width: options.width, crop: 'scale' } : {}),
|
|
6280
|
+
...(options.height ? { height: options.height, crop: 'scale' } : {}),
|
|
6281
|
+
});
|
|
6282
|
+
}
|
|
6283
|
+
|
|
6284
|
+
/** Get a transformed URL */
|
|
6285
|
+
export function getTransformUrl(publicId: string, transformation: Record<string, unknown>) {
|
|
6286
|
+
return cloudinary.url(publicId, transformation);
|
|
6287
|
+
}
|
|
6288
|
+
`;
|
|
6289
|
+
}
|
|
6290
|
+
apiRoutes(_config) {
|
|
6291
|
+
return {};
|
|
6292
|
+
}
|
|
6293
|
+
};
|
|
6294
|
+
|
|
6295
|
+
// src/templates/strategies/storage-factory.ts
|
|
6296
|
+
function createStorageStrategy(storage) {
|
|
6297
|
+
switch (storage) {
|
|
6298
|
+
case "s3":
|
|
6299
|
+
return new S3TemplateStrategy();
|
|
6300
|
+
case "uploadthing":
|
|
6301
|
+
return new UploadThingTemplateStrategy();
|
|
6302
|
+
case "cloudinary":
|
|
6303
|
+
return new CloudinaryTemplateStrategy();
|
|
6304
|
+
case "none":
|
|
6305
|
+
return null;
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
// src/templates/strategies/stripe-templates.ts
|
|
6310
|
+
var StripeTemplateStrategy = class {
|
|
6311
|
+
packageJson(config) {
|
|
6312
|
+
return `{
|
|
6313
|
+
"name": "@${config.name}/payments",
|
|
6314
|
+
"version": "1.0.0",
|
|
6315
|
+
"private": true,
|
|
6316
|
+
"type": "module",
|
|
6317
|
+
"main": "./src/index.ts",
|
|
6318
|
+
"types": "./src/index.ts",
|
|
6319
|
+
"exports": {
|
|
6320
|
+
".": "./src/index.ts"
|
|
6321
|
+
},
|
|
6322
|
+
"dependencies": {
|
|
6323
|
+
"stripe": "${config.versions["stripe"] ?? "^17.7.0"}"
|
|
6324
|
+
},
|
|
6325
|
+
"devDependencies": {
|
|
6326
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
`;
|
|
6330
|
+
}
|
|
6331
|
+
index(_config) {
|
|
6332
|
+
return `export { stripe } from './client.js';
|
|
6333
|
+
export { createCheckoutSession } from './checkout.js';
|
|
6334
|
+
export { handleWebhook } from './webhook.js';
|
|
6335
|
+
export { getSubscription, cancelSubscription } from './subscription.js';
|
|
6336
|
+
`;
|
|
6337
|
+
}
|
|
6338
|
+
client(_config) {
|
|
6339
|
+
return `import Stripe from 'stripe';
|
|
6340
|
+
|
|
6341
|
+
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '', {
|
|
6342
|
+
apiVersion: '2025-03-31.basil',
|
|
6343
|
+
typescript: true,
|
|
6344
|
+
});
|
|
6345
|
+
`;
|
|
6346
|
+
}
|
|
6347
|
+
webhookHandler(_config) {
|
|
6348
|
+
return `import type Stripe from 'stripe';
|
|
6349
|
+
import { stripe } from './client.js';
|
|
6350
|
+
|
|
6351
|
+
/** Verify and handle a Stripe webhook event */
|
|
6352
|
+
export async function handleWebhook(body: string | Buffer, signature: string) {
|
|
6353
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET ?? '';
|
|
6354
|
+
|
|
6355
|
+
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
|
|
6356
|
+
|
|
6357
|
+
switch (event.type) {
|
|
6358
|
+
case 'checkout.session.completed': {
|
|
6359
|
+
const session = event.data.object as Stripe.Checkout.Session;
|
|
6360
|
+
// TODO: Fulfill the order, update database
|
|
6361
|
+
console.log('Checkout completed:', session.id);
|
|
6362
|
+
break;
|
|
6363
|
+
}
|
|
6364
|
+
case 'customer.subscription.updated': {
|
|
6365
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
6366
|
+
// TODO: Update subscription status in database
|
|
6367
|
+
console.log('Subscription updated:', subscription.id);
|
|
6368
|
+
break;
|
|
6369
|
+
}
|
|
6370
|
+
case 'customer.subscription.deleted': {
|
|
6371
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
6372
|
+
// TODO: Handle subscription cancellation
|
|
6373
|
+
console.log('Subscription cancelled:', subscription.id);
|
|
6374
|
+
break;
|
|
6375
|
+
}
|
|
6376
|
+
case 'invoice.payment_failed': {
|
|
6377
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
6378
|
+
// TODO: Notify user of failed payment
|
|
6379
|
+
console.log('Payment failed:', invoice.id);
|
|
6380
|
+
break;
|
|
6381
|
+
}
|
|
6382
|
+
default:
|
|
6383
|
+
console.log('Unhandled event type:', event.type);
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
return { received: true };
|
|
6387
|
+
}
|
|
6388
|
+
`;
|
|
6389
|
+
}
|
|
6390
|
+
checkout(_config) {
|
|
6391
|
+
return `import { stripe } from './client.js';
|
|
6392
|
+
|
|
6393
|
+
interface CreateCheckoutOptions {
|
|
6394
|
+
priceId: string;
|
|
6395
|
+
customerId?: string;
|
|
6396
|
+
customerEmail?: string;
|
|
6397
|
+
mode?: 'payment' | 'subscription';
|
|
6398
|
+
successUrl?: string;
|
|
6399
|
+
cancelUrl?: string;
|
|
6400
|
+
}
|
|
6401
|
+
|
|
6402
|
+
/** Create a Stripe Checkout session */
|
|
6403
|
+
export async function createCheckoutSession({
|
|
6404
|
+
priceId,
|
|
6405
|
+
customerId,
|
|
6406
|
+
customerEmail,
|
|
6407
|
+
mode = 'subscription',
|
|
6408
|
+
successUrl,
|
|
6409
|
+
cancelUrl,
|
|
6410
|
+
}: CreateCheckoutOptions) {
|
|
6411
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
|
|
6412
|
+
|
|
6413
|
+
const session = await stripe.checkout.sessions.create({
|
|
6414
|
+
mode,
|
|
6415
|
+
payment_method_types: ['card'],
|
|
6416
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
6417
|
+
...(customerId ? { customer: customerId } : {}),
|
|
6418
|
+
...(customerEmail && !customerId ? { customer_email: customerEmail } : {}),
|
|
6419
|
+
success_url: successUrl ?? \`\${appUrl}/checkout/success?session_id={CHECKOUT_SESSION_ID}\`,
|
|
6420
|
+
cancel_url: cancelUrl ?? \`\${appUrl}/pricing\`,
|
|
6421
|
+
});
|
|
6422
|
+
|
|
6423
|
+
return { sessionId: session.id, url: session.url };
|
|
6424
|
+
}
|
|
6425
|
+
`;
|
|
6426
|
+
}
|
|
6427
|
+
subscription(_config) {
|
|
6428
|
+
return `import { stripe } from './client.js';
|
|
6429
|
+
|
|
6430
|
+
/** Get a customer's active subscription */
|
|
6431
|
+
export async function getSubscription(customerId: string) {
|
|
6432
|
+
const subscriptions = await stripe.subscriptions.list({
|
|
6433
|
+
customer: customerId,
|
|
6434
|
+
status: 'active',
|
|
6435
|
+
limit: 1,
|
|
6436
|
+
});
|
|
6437
|
+
|
|
6438
|
+
return subscriptions.data[0] ?? null;
|
|
6439
|
+
}
|
|
6440
|
+
|
|
6441
|
+
/** Cancel a subscription at period end */
|
|
6442
|
+
export async function cancelSubscription(subscriptionId: string) {
|
|
6443
|
+
return stripe.subscriptions.update(subscriptionId, {
|
|
6444
|
+
cancel_at_period_end: true,
|
|
6445
|
+
});
|
|
6446
|
+
}
|
|
6447
|
+
|
|
6448
|
+
/** Create a customer portal session */
|
|
6449
|
+
export async function createPortalSession(customerId: string, returnUrl?: string) {
|
|
6450
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
|
|
6451
|
+
|
|
6452
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
6453
|
+
customer: customerId,
|
|
6454
|
+
return_url: returnUrl ?? \`\${appUrl}/dashboard\`,
|
|
6455
|
+
});
|
|
6456
|
+
|
|
6457
|
+
return { url: session.url };
|
|
6458
|
+
}
|
|
6459
|
+
`;
|
|
6460
|
+
}
|
|
6461
|
+
pricingPage(config) {
|
|
6462
|
+
return `import { Button } from '@${config.name}/ui';
|
|
6463
|
+
|
|
6464
|
+
const plans = [
|
|
6465
|
+
{
|
|
6466
|
+
name: 'Free',
|
|
6467
|
+
price: '$0',
|
|
6468
|
+
period: '/month',
|
|
6469
|
+
features: ['1 project', 'Basic support', 'Community access'],
|
|
6470
|
+
priceId: '', // No checkout for free tier
|
|
6471
|
+
highlighted: false,
|
|
6472
|
+
},
|
|
6473
|
+
{
|
|
6474
|
+
name: 'Pro',
|
|
6475
|
+
price: '$19',
|
|
6476
|
+
period: '/month',
|
|
6477
|
+
features: ['Unlimited projects', 'Priority support', 'Advanced features', 'API access'],
|
|
6478
|
+
priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID ?? '',
|
|
6479
|
+
highlighted: true,
|
|
6480
|
+
},
|
|
6481
|
+
{
|
|
6482
|
+
name: 'Enterprise',
|
|
6483
|
+
price: '$99',
|
|
6484
|
+
period: '/month',
|
|
6485
|
+
features: ['Everything in Pro', 'Dedicated support', 'Custom integrations', 'SLA guarantee'],
|
|
6486
|
+
priceId: process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PRICE_ID ?? '',
|
|
6487
|
+
highlighted: false,
|
|
6488
|
+
},
|
|
6489
|
+
];
|
|
6490
|
+
|
|
6491
|
+
export default function PricingPage() {
|
|
6492
|
+
return (
|
|
6493
|
+
<main style={{ minHeight: '100vh', padding: '4rem 2rem' }}>
|
|
6494
|
+
<div style={{ maxWidth: '1024px', margin: '0 auto', textAlign: 'center' }}>
|
|
6495
|
+
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
|
6496
|
+
Simple Pricing
|
|
6497
|
+
</h1>
|
|
6498
|
+
<p style={{ fontSize: '1.25rem', color: '#64748b', marginBottom: '3rem' }}>
|
|
6499
|
+
Choose the plan that fits your needs
|
|
6500
|
+
</p>
|
|
6501
|
+
|
|
6502
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem' }}>
|
|
6503
|
+
{plans.map((plan) => (
|
|
6504
|
+
<div
|
|
6505
|
+
key={plan.name}
|
|
6506
|
+
style={{
|
|
6507
|
+
border: plan.highlighted ? '2px solid #2563eb' : '1px solid #e2e8f0',
|
|
6508
|
+
borderRadius: '12px',
|
|
6509
|
+
padding: '2rem',
|
|
6510
|
+
background: 'white',
|
|
6511
|
+
}}
|
|
6512
|
+
>
|
|
6513
|
+
<h3 style={{ fontSize: '1.25rem', fontWeight: '600' }}>{plan.name}</h3>
|
|
6514
|
+
<div style={{ margin: '1rem 0' }}>
|
|
6515
|
+
<span style={{ fontSize: '2.5rem', fontWeight: 'bold' }}>{plan.price}</span>
|
|
6516
|
+
<span style={{ color: '#64748b' }}>{plan.period}</span>
|
|
6517
|
+
</div>
|
|
6518
|
+
<ul style={{ listStyle: 'none', padding: 0, margin: '1.5rem 0', textAlign: 'left' }}>
|
|
6519
|
+
{plan.features.map((feature) => (
|
|
6520
|
+
<li key={feature} style={{ padding: '0.5rem 0', color: '#374151' }}>
|
|
6521
|
+
\u2713 {feature}
|
|
6522
|
+
</li>
|
|
6523
|
+
))}
|
|
6524
|
+
</ul>
|
|
6525
|
+
<Button
|
|
6526
|
+
variant={plan.highlighted ? 'primary' : 'outline'}
|
|
6527
|
+
style={{ width: '100%' }}
|
|
6528
|
+
>
|
|
6529
|
+
{plan.priceId ? 'Get Started' : 'Start Free'}
|
|
6530
|
+
</Button>
|
|
6531
|
+
</div>
|
|
6532
|
+
))}
|
|
6533
|
+
</div>
|
|
6534
|
+
</div>
|
|
6535
|
+
</main>
|
|
6536
|
+
);
|
|
6537
|
+
}
|
|
6538
|
+
`;
|
|
6539
|
+
}
|
|
6540
|
+
apiRoute(config) {
|
|
6541
|
+
if (config.backend === "nestjs") {
|
|
6542
|
+
return `import { Controller, Post, Req, Headers, RawBodyRequest } from '@nestjs/common';
|
|
6543
|
+
import type { Request } from 'express';
|
|
6544
|
+
import { handleWebhook } from '@${config.name}/payments';
|
|
6545
|
+
|
|
6546
|
+
@Controller('payments')
|
|
6547
|
+
export class PaymentsController {
|
|
6548
|
+
@Post('webhook')
|
|
6549
|
+
async webhook(
|
|
6550
|
+
@Req() req: RawBodyRequest<Request>,
|
|
6551
|
+
@Headers('stripe-signature') signature: string,
|
|
6552
|
+
) {
|
|
6553
|
+
const rawBody = req.rawBody;
|
|
6554
|
+
if (!rawBody) throw new Error('Missing raw body');
|
|
6555
|
+
return handleWebhook(rawBody, signature);
|
|
6556
|
+
}
|
|
6557
|
+
}
|
|
6558
|
+
`;
|
|
6559
|
+
}
|
|
6560
|
+
return `import { Router } from 'express';
|
|
6561
|
+
import { handleWebhook } from '@${config.name}/payments';
|
|
6562
|
+
|
|
6563
|
+
export const paymentsRouter = Router();
|
|
6564
|
+
|
|
6565
|
+
paymentsRouter.post('/payments/webhook', async (req, res, next) => {
|
|
6566
|
+
try {
|
|
6567
|
+
const signature = req.headers['stripe-signature'] as string;
|
|
6568
|
+
const result = await handleWebhook(req.body, signature);
|
|
6569
|
+
res.json(result);
|
|
6570
|
+
} catch (error) {
|
|
6571
|
+
next(error);
|
|
6572
|
+
}
|
|
6573
|
+
});
|
|
6574
|
+
`;
|
|
6575
|
+
}
|
|
6576
|
+
};
|
|
6577
|
+
|
|
6578
|
+
// src/templates/strategies/lemonsqueezy-templates.ts
|
|
6579
|
+
var LemonSqueezyTemplateStrategy = class {
|
|
6580
|
+
packageJson(config) {
|
|
6581
|
+
return `{
|
|
6582
|
+
"name": "@${config.name}/payments",
|
|
6583
|
+
"version": "1.0.0",
|
|
6584
|
+
"private": true,
|
|
6585
|
+
"type": "module",
|
|
6586
|
+
"main": "./src/index.ts",
|
|
6587
|
+
"types": "./src/index.ts",
|
|
6588
|
+
"dependencies": {
|
|
6589
|
+
"@lemonsqueezy/lemonsqueezy.js": "${config.versions["@lemonsqueezy/lemonsqueezy.js"] ?? "^4.0.0"}"
|
|
6590
|
+
},
|
|
6591
|
+
"devDependencies": {
|
|
6592
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
`;
|
|
6596
|
+
}
|
|
6597
|
+
index(_config) {
|
|
6598
|
+
return `export { lemonSqueezySetup } from './client.js';
|
|
6599
|
+
export { createCheckout } from './checkout.js';
|
|
6600
|
+
export { handleWebhook } from './webhook.js';
|
|
6601
|
+
export { getSubscription, cancelSubscription } from './subscription.js';
|
|
6602
|
+
`;
|
|
6603
|
+
}
|
|
6604
|
+
client(_config) {
|
|
6605
|
+
return `import { lemonSqueezySetup as setup } from '@lemonsqueezy/lemonsqueezy.js';
|
|
6606
|
+
|
|
6607
|
+
export function lemonSqueezySetup() {
|
|
6608
|
+
setup({ apiKey: process.env.LEMONSQUEEZY_API_KEY ?? '', onError: (error) => console.error('LemonSqueezy error:', error) });
|
|
6609
|
+
}
|
|
6610
|
+
`;
|
|
6611
|
+
}
|
|
6612
|
+
webhookHandler(_config) {
|
|
6613
|
+
return `import crypto from 'node:crypto';
|
|
6614
|
+
|
|
6615
|
+
/** Verify LemonSqueezy webhook signature and parse event */
|
|
6616
|
+
export async function handleWebhook(body: string, signature: string) {
|
|
6617
|
+
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET ?? '';
|
|
6618
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
6619
|
+
const digest = hmac.update(body).digest('hex');
|
|
6620
|
+
|
|
6621
|
+
if (digest !== signature) {
|
|
6622
|
+
throw new Error('Invalid webhook signature');
|
|
6623
|
+
}
|
|
6624
|
+
|
|
6625
|
+
const event = JSON.parse(body);
|
|
6626
|
+
const eventName = event.meta?.event_name;
|
|
6627
|
+
|
|
6628
|
+
switch (eventName) {
|
|
6629
|
+
case 'order_created':
|
|
6630
|
+
console.log('Order created:', event.data?.id);
|
|
6631
|
+
break;
|
|
6632
|
+
case 'subscription_created':
|
|
6633
|
+
console.log('Subscription created:', event.data?.id);
|
|
6634
|
+
break;
|
|
6635
|
+
case 'subscription_cancelled':
|
|
6636
|
+
console.log('Subscription cancelled:', event.data?.id);
|
|
6637
|
+
break;
|
|
6638
|
+
case 'subscription_payment_failed':
|
|
6639
|
+
console.log('Payment failed:', event.data?.id);
|
|
6640
|
+
break;
|
|
6641
|
+
default:
|
|
6642
|
+
console.log('Unhandled event:', eventName);
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6645
|
+
return { received: true };
|
|
6646
|
+
}
|
|
6647
|
+
`;
|
|
6648
|
+
}
|
|
6649
|
+
checkout(_config) {
|
|
6650
|
+
return `import { createCheckout as lsCreateCheckout } from '@lemonsqueezy/lemonsqueezy.js';
|
|
6651
|
+
import { lemonSqueezySetup } from './client.js';
|
|
6652
|
+
|
|
6653
|
+
interface CreateCheckoutOptions {
|
|
6654
|
+
variantId: number;
|
|
6655
|
+
email?: string;
|
|
6656
|
+
name?: string;
|
|
6657
|
+
}
|
|
6658
|
+
|
|
6659
|
+
/** Create a LemonSqueezy checkout URL */
|
|
6660
|
+
export async function createCheckout({ variantId, email, name }: CreateCheckoutOptions) {
|
|
6661
|
+
lemonSqueezySetup();
|
|
6662
|
+
|
|
6663
|
+
const storeId = process.env.LEMONSQUEEZY_STORE_ID ?? '';
|
|
6664
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
|
|
6665
|
+
|
|
6666
|
+
const { data } = await lsCreateCheckout(storeId, variantId, {
|
|
6667
|
+
checkoutData: {
|
|
6668
|
+
email,
|
|
6669
|
+
name,
|
|
6670
|
+
},
|
|
6671
|
+
productOptions: {
|
|
6672
|
+
redirectUrl: \`\${appUrl}/checkout/success\`,
|
|
6673
|
+
},
|
|
6674
|
+
});
|
|
6675
|
+
|
|
6676
|
+
return { url: data?.data?.attributes?.url ?? null };
|
|
6677
|
+
}
|
|
6678
|
+
`;
|
|
6679
|
+
}
|
|
6680
|
+
subscription(_config) {
|
|
6681
|
+
return `import { getSubscription as lsGetSubscription, cancelSubscription as lsCancelSubscription } from '@lemonsqueezy/lemonsqueezy.js';
|
|
6682
|
+
import { lemonSqueezySetup } from './client.js';
|
|
6683
|
+
|
|
6684
|
+
/** Get subscription details */
|
|
6685
|
+
export async function getSubscription(subscriptionId: string) {
|
|
6686
|
+
lemonSqueezySetup();
|
|
6687
|
+
const { data } = await lsGetSubscription(subscriptionId);
|
|
6688
|
+
return data?.data ?? null;
|
|
6689
|
+
}
|
|
6690
|
+
|
|
6691
|
+
/** Cancel a subscription */
|
|
6692
|
+
export async function cancelSubscription(subscriptionId: string) {
|
|
6693
|
+
lemonSqueezySetup();
|
|
6694
|
+
const { data } = await lsCancelSubscription(subscriptionId);
|
|
6695
|
+
return data?.data ?? null;
|
|
6696
|
+
}
|
|
6697
|
+
`;
|
|
6698
|
+
}
|
|
6699
|
+
pricingPage(config) {
|
|
6700
|
+
return `import { Button } from '@${config.name}/ui';
|
|
6701
|
+
|
|
6702
|
+
const plans = [
|
|
6703
|
+
{ name: 'Free', price: '$0', period: '/month', features: ['1 project', 'Basic support'], variantId: 0, highlighted: false },
|
|
6704
|
+
{ name: 'Pro', price: '$19', period: '/month', features: ['Unlimited projects', 'Priority support', 'Advanced features'], variantId: Number(process.env.NEXT_PUBLIC_LS_PRO_VARIANT_ID ?? 0), highlighted: true },
|
|
6705
|
+
{ name: 'Enterprise', price: '$99', period: '/month', features: ['Everything in Pro', 'Dedicated support', 'SLA'], variantId: Number(process.env.NEXT_PUBLIC_LS_ENTERPRISE_VARIANT_ID ?? 0), highlighted: false },
|
|
6706
|
+
];
|
|
6707
|
+
|
|
6708
|
+
export default function PricingPage() {
|
|
6709
|
+
return (
|
|
6710
|
+
<main style={{ minHeight: '100vh', padding: '4rem 2rem' }}>
|
|
6711
|
+
<div style={{ maxWidth: '1024px', margin: '0 auto', textAlign: 'center' }}>
|
|
6712
|
+
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>Pricing</h1>
|
|
6713
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem' }}>
|
|
6714
|
+
{plans.map((plan) => (
|
|
6715
|
+
<div key={plan.name} style={{ border: plan.highlighted ? '2px solid #2563eb' : '1px solid #e2e8f0', borderRadius: '12px', padding: '2rem', background: 'white' }}>
|
|
6716
|
+
<h3 style={{ fontSize: '1.25rem', fontWeight: '600' }}>{plan.name}</h3>
|
|
6717
|
+
<div style={{ margin: '1rem 0' }}>
|
|
6718
|
+
<span style={{ fontSize: '2.5rem', fontWeight: 'bold' }}>{plan.price}</span>
|
|
6719
|
+
<span style={{ color: '#64748b' }}>{plan.period}</span>
|
|
6720
|
+
</div>
|
|
6721
|
+
<ul style={{ listStyle: 'none', padding: 0, margin: '1.5rem 0', textAlign: 'left' }}>
|
|
6722
|
+
{plan.features.map((f) => <li key={f} style={{ padding: '0.5rem 0' }}>\u2713 {f}</li>)}
|
|
6723
|
+
</ul>
|
|
6724
|
+
<Button variant={plan.highlighted ? 'primary' : 'outline'} style={{ width: '100%' }}>
|
|
6725
|
+
{plan.variantId ? 'Get Started' : 'Start Free'}
|
|
6726
|
+
</Button>
|
|
6727
|
+
</div>
|
|
6728
|
+
))}
|
|
6729
|
+
</div>
|
|
6730
|
+
</div>
|
|
6731
|
+
</main>
|
|
6732
|
+
);
|
|
6733
|
+
}
|
|
6734
|
+
`;
|
|
6735
|
+
}
|
|
6736
|
+
apiRoute(config) {
|
|
6737
|
+
if (config.backend === "nestjs") {
|
|
6738
|
+
return `import { Controller, Post, Req, Headers } from '@nestjs/common';
|
|
6739
|
+
import type { Request } from 'express';
|
|
6740
|
+
import { handleWebhook } from '@${config.name}/payments';
|
|
6741
|
+
|
|
6742
|
+
@Controller('payments')
|
|
6743
|
+
export class PaymentsController {
|
|
6744
|
+
@Post('webhook')
|
|
6745
|
+
async webhook(@Req() req: Request, @Headers('x-signature') signature: string) {
|
|
6746
|
+
const body = JSON.stringify(req.body);
|
|
6747
|
+
return handleWebhook(body, signature);
|
|
6748
|
+
}
|
|
6749
|
+
}
|
|
6750
|
+
`;
|
|
6751
|
+
}
|
|
6752
|
+
return `import { Router } from 'express';
|
|
6753
|
+
import { handleWebhook } from '@${config.name}/payments';
|
|
6754
|
+
|
|
6755
|
+
export const paymentsRouter = Router();
|
|
6756
|
+
|
|
6757
|
+
paymentsRouter.post('/payments/webhook', async (req, res, next) => {
|
|
6758
|
+
try {
|
|
6759
|
+
const signature = req.headers['x-signature'] as string;
|
|
6760
|
+
const result = await handleWebhook(JSON.stringify(req.body), signature);
|
|
6761
|
+
res.json(result);
|
|
6762
|
+
} catch (error) {
|
|
6763
|
+
next(error);
|
|
6764
|
+
}
|
|
6765
|
+
});
|
|
6766
|
+
`;
|
|
6767
|
+
}
|
|
6768
|
+
};
|
|
6769
|
+
|
|
6770
|
+
// src/templates/strategies/paddle-templates.ts
|
|
6771
|
+
var PaddleTemplateStrategy = class {
|
|
6772
|
+
packageJson(config) {
|
|
6773
|
+
return `{
|
|
6774
|
+
"name": "@${config.name}/payments",
|
|
6775
|
+
"version": "1.0.0",
|
|
6776
|
+
"private": true,
|
|
6777
|
+
"type": "module",
|
|
6778
|
+
"main": "./src/index.ts",
|
|
6779
|
+
"types": "./src/index.ts",
|
|
6780
|
+
"dependencies": {
|
|
6781
|
+
"@paddle/paddle-node-sdk": "${config.versions["@paddle/paddle-node-sdk"] ?? "^1.7.0"}"
|
|
6782
|
+
},
|
|
6783
|
+
"devDependencies": {
|
|
6784
|
+
"typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
`;
|
|
6788
|
+
}
|
|
6789
|
+
index(_config) {
|
|
6790
|
+
return `export { paddle } from './client.js';
|
|
6791
|
+
export { createCheckout } from './checkout.js';
|
|
6792
|
+
export { handleWebhook } from './webhook.js';
|
|
6793
|
+
export { getSubscription, cancelSubscription } from './subscription.js';
|
|
6794
|
+
`;
|
|
6795
|
+
}
|
|
6796
|
+
client(_config) {
|
|
6797
|
+
return `import { Paddle, Environment } from '@paddle/paddle-node-sdk';
|
|
6798
|
+
|
|
6799
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
6800
|
+
|
|
6801
|
+
export const paddle = new Paddle(process.env.PADDLE_API_KEY ?? '', {
|
|
6802
|
+
environment: isProd ? Environment.production : Environment.sandbox,
|
|
6803
|
+
});
|
|
6804
|
+
`;
|
|
6805
|
+
}
|
|
6806
|
+
webhookHandler(_config) {
|
|
6807
|
+
return `import { paddle } from './client.js';
|
|
6808
|
+
|
|
6809
|
+
/** Verify and handle a Paddle webhook event */
|
|
6810
|
+
export async function handleWebhook(body: string, signature: string) {
|
|
6811
|
+
const secretKey = process.env.PADDLE_WEBHOOK_SECRET ?? '';
|
|
6812
|
+
|
|
6813
|
+
const eventData = paddle.webhooks.unmarshal(body, secretKey, signature);
|
|
6814
|
+
if (!eventData) throw new Error('Invalid webhook signature');
|
|
6815
|
+
|
|
6816
|
+
switch (eventData.eventType) {
|
|
6817
|
+
case 'transaction.completed':
|
|
6818
|
+
console.log('Transaction completed:', eventData.data?.id);
|
|
6819
|
+
break;
|
|
6820
|
+
case 'subscription.activated':
|
|
6821
|
+
console.log('Subscription activated:', eventData.data?.id);
|
|
6822
|
+
break;
|
|
6823
|
+
case 'subscription.canceled':
|
|
6824
|
+
console.log('Subscription cancelled:', eventData.data?.id);
|
|
6825
|
+
break;
|
|
6826
|
+
case 'subscription.past_due':
|
|
6827
|
+
console.log('Subscription past due:', eventData.data?.id);
|
|
6828
|
+
break;
|
|
6829
|
+
default:
|
|
6830
|
+
console.log('Unhandled event:', eventData.eventType);
|
|
6831
|
+
}
|
|
6832
|
+
|
|
6833
|
+
return { received: true };
|
|
6834
|
+
}
|
|
6835
|
+
`;
|
|
6836
|
+
}
|
|
6837
|
+
checkout(_config) {
|
|
6838
|
+
return `import { paddle } from './client.js';
|
|
6839
|
+
|
|
6840
|
+
interface CreateCheckoutOptions {
|
|
6841
|
+
priceId: string;
|
|
6842
|
+
customerId?: string;
|
|
6843
|
+
customerEmail?: string;
|
|
6844
|
+
}
|
|
4284
6845
|
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
6846
|
+
/** Create a Paddle checkout transaction */
|
|
6847
|
+
export async function createCheckout({ priceId, customerId, customerEmail }: CreateCheckoutOptions) {
|
|
6848
|
+
const transaction = await paddle.transactions.create({
|
|
6849
|
+
items: [{ priceId, quantity: 1 }],
|
|
6850
|
+
...(customerId ? { customerId } : {}),
|
|
6851
|
+
...(customerEmail ? { customerEmail } : {}),
|
|
6852
|
+
});
|
|
4288
6853
|
|
|
4289
|
-
|
|
4290
|
-
export function useTheme() {
|
|
4291
|
-
const [theme, setTheme] = useAtom(themeAtom);
|
|
4292
|
-
return { theme, setTheme };
|
|
6854
|
+
return { transactionId: transaction.id };
|
|
4293
6855
|
}
|
|
4294
6856
|
`;
|
|
4295
6857
|
}
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
6858
|
+
subscription(_config) {
|
|
6859
|
+
return `import { paddle } from './client.js';
|
|
6860
|
+
|
|
6861
|
+
/** Get subscription details */
|
|
6862
|
+
export async function getSubscription(subscriptionId: string) {
|
|
6863
|
+
return paddle.subscriptions.get(subscriptionId);
|
|
6864
|
+
}
|
|
6865
|
+
|
|
6866
|
+
/** Cancel a subscription */
|
|
6867
|
+
export async function cancelSubscription(subscriptionId: string) {
|
|
6868
|
+
return paddle.subscriptions.cancel(subscriptionId, { effectiveFrom: 'next_billing_period' });
|
|
6869
|
+
}
|
|
6870
|
+
`;
|
|
4299
6871
|
}
|
|
4300
|
-
|
|
6872
|
+
pricingPage(config) {
|
|
6873
|
+
return `import { Button } from '@${config.name}/ui';
|
|
4301
6874
|
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
import type { TypedUseSelectorHook } from 'react-redux';
|
|
4308
|
-
import { themeReducer } from './theme-slice.js';
|
|
6875
|
+
const plans = [
|
|
6876
|
+
{ name: 'Free', price: '$0', period: '/month', features: ['1 project', 'Basic support'], priceId: '', highlighted: false },
|
|
6877
|
+
{ name: 'Pro', price: '$19', period: '/month', features: ['Unlimited projects', 'Priority support', 'Advanced features'], priceId: process.env.NEXT_PUBLIC_PADDLE_PRO_PRICE_ID ?? '', highlighted: true },
|
|
6878
|
+
{ name: 'Enterprise', price: '$99', period: '/month', features: ['Everything in Pro', 'Dedicated support', 'SLA'], priceId: process.env.NEXT_PUBLIC_PADDLE_ENTERPRISE_PRICE_ID ?? '', highlighted: false },
|
|
6879
|
+
];
|
|
4309
6880
|
|
|
4310
|
-
export
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
}
|
|
6881
|
+
export default function PricingPage() {
|
|
6882
|
+
return (
|
|
6883
|
+
<main style={{ minHeight: '100vh', padding: '4rem 2rem' }}>
|
|
6884
|
+
<div style={{ maxWidth: '1024px', margin: '0 auto', textAlign: 'center' }}>
|
|
6885
|
+
<h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>Pricing</h1>
|
|
6886
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem' }}>
|
|
6887
|
+
{plans.map((plan) => (
|
|
6888
|
+
<div key={plan.name} style={{ border: plan.highlighted ? '2px solid #2563eb' : '1px solid #e2e8f0', borderRadius: '12px', padding: '2rem', background: 'white' }}>
|
|
6889
|
+
<h3 style={{ fontSize: '1.25rem', fontWeight: '600' }}>{plan.name}</h3>
|
|
6890
|
+
<div style={{ margin: '1rem 0' }}>
|
|
6891
|
+
<span style={{ fontSize: '2.5rem', fontWeight: 'bold' }}>{plan.price}</span>
|
|
6892
|
+
<span style={{ color: '#64748b' }}>{plan.period}</span>
|
|
6893
|
+
</div>
|
|
6894
|
+
<ul style={{ listStyle: 'none', padding: 0, margin: '1.5rem 0', textAlign: 'left' }}>
|
|
6895
|
+
{plan.features.map((f) => <li key={f} style={{ padding: '0.5rem 0' }}>\u2713 {f}</li>)}
|
|
6896
|
+
</ul>
|
|
6897
|
+
<Button variant={plan.highlighted ? 'primary' : 'outline'} style={{ width: '100%' }}>
|
|
6898
|
+
{plan.priceId ? 'Get Started' : 'Start Free'}
|
|
6899
|
+
</Button>
|
|
6900
|
+
</div>
|
|
6901
|
+
))}
|
|
6902
|
+
</div>
|
|
6903
|
+
</div>
|
|
6904
|
+
</main>
|
|
6905
|
+
);
|
|
6906
|
+
}
|
|
6907
|
+
`;
|
|
6908
|
+
}
|
|
6909
|
+
apiRoute(config) {
|
|
6910
|
+
if (config.backend === "nestjs") {
|
|
6911
|
+
return `import { Controller, Post, Req, Headers } from '@nestjs/common';
|
|
6912
|
+
import type { Request } from 'express';
|
|
6913
|
+
import { handleWebhook } from '@${config.name}/payments';
|
|
4315
6914
|
|
|
4316
|
-
|
|
4317
|
-
export
|
|
6915
|
+
@Controller('payments')
|
|
6916
|
+
export class PaymentsController {
|
|
6917
|
+
@Post('webhook')
|
|
6918
|
+
async webhook(@Req() req: Request, @Headers('paddle-signature') signature: string) {
|
|
6919
|
+
const body = JSON.stringify(req.body);
|
|
6920
|
+
return handleWebhook(body, signature);
|
|
6921
|
+
}
|
|
6922
|
+
}
|
|
6923
|
+
`;
|
|
6924
|
+
}
|
|
6925
|
+
return `import { Router } from 'express';
|
|
6926
|
+
import { handleWebhook } from '@${config.name}/payments';
|
|
4318
6927
|
|
|
4319
|
-
|
|
4320
|
-
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
6928
|
+
export const paymentsRouter = Router();
|
|
4321
6929
|
|
|
4322
|
-
|
|
4323
|
-
|
|
6930
|
+
paymentsRouter.post('/payments/webhook', async (req, res, next) => {
|
|
6931
|
+
try {
|
|
6932
|
+
const signature = req.headers['paddle-signature'] as string;
|
|
6933
|
+
const result = await handleWebhook(JSON.stringify(req.body), signature);
|
|
6934
|
+
res.json(result);
|
|
6935
|
+
} catch (error) {
|
|
6936
|
+
next(error);
|
|
6937
|
+
}
|
|
6938
|
+
});
|
|
4324
6939
|
`;
|
|
4325
6940
|
}
|
|
4326
|
-
|
|
4327
|
-
return `import { createSlice } from '@reduxjs/toolkit';
|
|
4328
|
-
import type { PayloadAction } from '@reduxjs/toolkit';
|
|
4329
|
-
|
|
4330
|
-
type Theme = 'light' | 'dark' | 'system';
|
|
6941
|
+
};
|
|
4331
6942
|
|
|
4332
|
-
|
|
4333
|
-
|
|
6943
|
+
// src/templates/strategies/payment-factory.ts
|
|
6944
|
+
function createPaymentStrategy(payments) {
|
|
6945
|
+
switch (payments) {
|
|
6946
|
+
case "stripe":
|
|
6947
|
+
return new StripeTemplateStrategy();
|
|
6948
|
+
case "lemonsqueezy":
|
|
6949
|
+
return new LemonSqueezyTemplateStrategy();
|
|
6950
|
+
case "paddle":
|
|
6951
|
+
return new PaddleTemplateStrategy();
|
|
6952
|
+
case "none":
|
|
6953
|
+
return null;
|
|
6954
|
+
}
|
|
4334
6955
|
}
|
|
4335
6956
|
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
6957
|
+
// src/templates/strategies/swagger-templates.ts
|
|
6958
|
+
var SwaggerTemplateStrategy = class {
|
|
6959
|
+
docsConfig(config) {
|
|
6960
|
+
if (config.backend === "nestjs") {
|
|
6961
|
+
return `import type { INestApplication } from '@nestjs/common';
|
|
6962
|
+
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
4339
6963
|
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
6964
|
+
export function setupSwagger(app: INestApplication) {
|
|
6965
|
+
const config = new DocumentBuilder()
|
|
6966
|
+
.setTitle('${config.pascalCase} API')
|
|
6967
|
+
.setDescription('${config.pascalCase} REST API documentation')
|
|
6968
|
+
.setVersion('1.0.0')
|
|
6969
|
+
.addBearerAuth()
|
|
6970
|
+
.build();
|
|
6971
|
+
|
|
6972
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
6973
|
+
SwaggerModule.setup('api/docs', app, document, {
|
|
6974
|
+
swaggerOptions: {
|
|
6975
|
+
persistAuthorization: true,
|
|
4346
6976
|
},
|
|
4347
|
-
|
|
4348
|
-
|
|
6977
|
+
});
|
|
6978
|
+
}
|
|
6979
|
+
`;
|
|
6980
|
+
}
|
|
6981
|
+
return `import swaggerJsdoc from 'swagger-jsdoc';
|
|
6982
|
+
|
|
6983
|
+
const options: swaggerJsdoc.Options = {
|
|
6984
|
+
definition: {
|
|
6985
|
+
openapi: '3.0.0',
|
|
6986
|
+
info: {
|
|
6987
|
+
title: '${config.pascalCase} API',
|
|
6988
|
+
description: '${config.pascalCase} REST API documentation',
|
|
6989
|
+
version: '1.0.0',
|
|
6990
|
+
},
|
|
6991
|
+
servers: [
|
|
6992
|
+
{ url: 'http://localhost:3001', description: 'Development' },
|
|
6993
|
+
],
|
|
6994
|
+
components: {
|
|
6995
|
+
securitySchemes: {
|
|
6996
|
+
bearerAuth: {
|
|
6997
|
+
type: 'http',
|
|
6998
|
+
scheme: 'bearer',
|
|
6999
|
+
bearerFormat: 'JWT',
|
|
7000
|
+
},
|
|
7001
|
+
},
|
|
4349
7002
|
},
|
|
4350
7003
|
},
|
|
4351
|
-
|
|
7004
|
+
apis: ['./src/routes/*.ts'],
|
|
7005
|
+
};
|
|
4352
7006
|
|
|
4353
|
-
export const
|
|
4354
|
-
export const themeReducer = themeSlice.reducer;
|
|
7007
|
+
export const swaggerSpec = swaggerJsdoc(options);
|
|
4355
7008
|
`;
|
|
4356
7009
|
}
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
import {
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
7010
|
+
docsSetup(config) {
|
|
7011
|
+
if (config.backend === "nestjs") {
|
|
7012
|
+
return `// Add to apps/api/src/main.ts:
|
|
7013
|
+
// import { setupSwagger } from './docs/swagger-config.js';
|
|
7014
|
+
// setupSwagger(app);
|
|
7015
|
+
//
|
|
7016
|
+
// Swagger UI will be available at: http://localhost:3001/api/docs
|
|
7017
|
+
`;
|
|
7018
|
+
}
|
|
7019
|
+
return `import swaggerUi from 'swagger-ui-express';
|
|
7020
|
+
import type { Express } from 'express';
|
|
7021
|
+
import { swaggerSpec } from './swagger-config.js';
|
|
7022
|
+
|
|
7023
|
+
export function setupDocs(app: Express) {
|
|
7024
|
+
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
|
|
7025
|
+
swaggerOptions: {
|
|
7026
|
+
persistAuthorization: true,
|
|
7027
|
+
},
|
|
7028
|
+
}));
|
|
4365
7029
|
}
|
|
4366
7030
|
`;
|
|
4367
7031
|
}
|
|
4368
7032
|
};
|
|
4369
7033
|
|
|
4370
|
-
// src/templates/strategies/
|
|
4371
|
-
var
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
7034
|
+
// src/templates/strategies/redoc-templates.ts
|
|
7035
|
+
var RedocTemplateStrategy = class {
|
|
7036
|
+
docsConfig(config) {
|
|
7037
|
+
if (config.backend === "nestjs") {
|
|
7038
|
+
return `import type { INestApplication } from '@nestjs/common';
|
|
7039
|
+
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
4376
7040
|
|
|
4377
|
-
export
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
7041
|
+
export function getOpenApiDocument(app: INestApplication) {
|
|
7042
|
+
const config = new DocumentBuilder()
|
|
7043
|
+
.setTitle('${config.pascalCase} API')
|
|
7044
|
+
.setDescription('${config.pascalCase} REST API documentation')
|
|
7045
|
+
.setVersion('1.0.0')
|
|
7046
|
+
.addBearerAuth()
|
|
7047
|
+
.build();
|
|
4383
7048
|
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
7049
|
+
return SwaggerModule.createDocument(app, config);
|
|
7050
|
+
}
|
|
7051
|
+
`;
|
|
7052
|
+
}
|
|
7053
|
+
return `import swaggerJsdoc from 'swagger-jsdoc';
|
|
7054
|
+
|
|
7055
|
+
const options: swaggerJsdoc.Options = {
|
|
7056
|
+
definition: {
|
|
7057
|
+
openapi: '3.0.0',
|
|
7058
|
+
info: {
|
|
7059
|
+
title: '${config.pascalCase} API',
|
|
7060
|
+
description: '${config.pascalCase} REST API documentation',
|
|
7061
|
+
version: '1.0.0',
|
|
4393
7062
|
},
|
|
7063
|
+
servers: [
|
|
7064
|
+
{ url: 'http://localhost:3001', description: 'Development' },
|
|
7065
|
+
],
|
|
4394
7066
|
},
|
|
4395
|
-
|
|
7067
|
+
apis: ['./src/routes/*.ts'],
|
|
7068
|
+
};
|
|
4396
7069
|
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
7070
|
+
export const swaggerSpec = swaggerJsdoc(options);
|
|
7071
|
+
`;
|
|
7072
|
+
}
|
|
7073
|
+
docsSetup(config) {
|
|
7074
|
+
if (config.backend === "nestjs") {
|
|
7075
|
+
return `import type { INestApplication } from '@nestjs/common';
|
|
7076
|
+
import redoc from 'redoc-express';
|
|
7077
|
+
import { getOpenApiDocument } from './swagger-config.js';
|
|
4402
7078
|
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
7079
|
+
export function setupDocs(app: INestApplication) {
|
|
7080
|
+
const httpAdapter = app.getHttpAdapter();
|
|
7081
|
+
const document = getOpenApiDocument(app);
|
|
7082
|
+
|
|
7083
|
+
httpAdapter.get('/api/docs/openapi.json', (_req: any, res: any) => {
|
|
7084
|
+
res.json(document);
|
|
7085
|
+
});
|
|
7086
|
+
|
|
7087
|
+
httpAdapter.use(
|
|
7088
|
+
'/api/docs',
|
|
7089
|
+
redoc({
|
|
7090
|
+
title: '${config.pascalCase} API',
|
|
7091
|
+
specUrl: '/api/docs/openapi.json',
|
|
7092
|
+
}),
|
|
7093
|
+
);
|
|
4411
7094
|
}
|
|
4412
7095
|
`;
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
7096
|
+
}
|
|
7097
|
+
return `import redoc from 'redoc-express';
|
|
7098
|
+
import type { Express } from 'express';
|
|
7099
|
+
import { swaggerSpec } from './swagger-config.js';
|
|
4416
7100
|
|
|
4417
|
-
|
|
4418
|
-
|
|
7101
|
+
export function setupDocs(app: Express) {
|
|
7102
|
+
app.get('/api/docs/openapi.json', (_req, res) => {
|
|
7103
|
+
res.json(swaggerSpec);
|
|
7104
|
+
});
|
|
4419
7105
|
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
{
|
|
4424
|
-
|
|
7106
|
+
app.use(
|
|
7107
|
+
'/api/docs',
|
|
7108
|
+
redoc({
|
|
7109
|
+
title: '${config.pascalCase} API',
|
|
7110
|
+
specUrl: '/api/docs/openapi.json',
|
|
7111
|
+
}),
|
|
4425
7112
|
);
|
|
4426
7113
|
}
|
|
4427
7114
|
`;
|
|
4428
7115
|
}
|
|
4429
7116
|
};
|
|
4430
7117
|
|
|
4431
|
-
// src/templates/strategies/
|
|
4432
|
-
function
|
|
4433
|
-
switch (
|
|
4434
|
-
case "
|
|
4435
|
-
return new
|
|
4436
|
-
case "
|
|
4437
|
-
return new
|
|
4438
|
-
case "redux":
|
|
4439
|
-
return new ReduxTemplateStrategy();
|
|
4440
|
-
case "tanstack-query":
|
|
4441
|
-
return new TanstackQueryTemplateStrategy();
|
|
7118
|
+
// src/templates/strategies/api-docs-factory.ts
|
|
7119
|
+
function createApiDocsStrategy(apiDocs) {
|
|
7120
|
+
switch (apiDocs) {
|
|
7121
|
+
case "swagger":
|
|
7122
|
+
return new SwaggerTemplateStrategy();
|
|
7123
|
+
case "redoc":
|
|
7124
|
+
return new RedocTemplateStrategy();
|
|
4442
7125
|
case "none":
|
|
4443
7126
|
return null;
|
|
4444
7127
|
}
|
|
4445
7128
|
}
|
|
4446
7129
|
|
|
4447
7130
|
// src/generator.ts
|
|
4448
|
-
var TOTAL_STEPS =
|
|
7131
|
+
var TOTAL_STEPS = 26;
|
|
4449
7132
|
var Generator = class {
|
|
4450
7133
|
rootPath;
|
|
4451
7134
|
config;
|
|
@@ -4453,7 +7136,7 @@ var Generator = class {
|
|
|
4453
7136
|
this.config = config;
|
|
4454
7137
|
this.rootPath = rootPath;
|
|
4455
7138
|
}
|
|
4456
|
-
/** Run the full
|
|
7139
|
+
/** Run the full 26-step generation pipeline */
|
|
4457
7140
|
async run() {
|
|
4458
7141
|
logger.header(`Creating ${this.config.pascalCase} monorepo...`);
|
|
4459
7142
|
logger.newline();
|
|
@@ -4466,9 +7149,19 @@ var Generator = class {
|
|
|
4466
7149
|
this.writeLibPackage();
|
|
4467
7150
|
this.writeUiPackage();
|
|
4468
7151
|
this.writeDatabasePackage();
|
|
7152
|
+
this.writeCachePackage();
|
|
7153
|
+
this.writeEmailPackage();
|
|
7154
|
+
this.writeStoragePackage();
|
|
7155
|
+
this.writePaymentsPackage();
|
|
4469
7156
|
this.writeNextjsApp();
|
|
7157
|
+
this.writeI18nLayer();
|
|
4470
7158
|
this.writeBackendApp();
|
|
7159
|
+
this.writeApiDocs();
|
|
7160
|
+
this.writeLoggingLayer();
|
|
4471
7161
|
this.writeAuthLayer();
|
|
7162
|
+
this.writeDockerFiles();
|
|
7163
|
+
this.writeStorybookConfig();
|
|
7164
|
+
this.writeE2eTests();
|
|
4472
7165
|
this.writeSkills();
|
|
4473
7166
|
await this.installDependencies();
|
|
4474
7167
|
await this.initializeGit();
|
|
@@ -4497,8 +7190,7 @@ var Generator = class {
|
|
|
4497
7190
|
logger.step(3, TOTAL_STEPS, "Creating directory structure...");
|
|
4498
7191
|
const dirs = getExpectedDirectories(this.config);
|
|
4499
7192
|
for (const dir of dirs) {
|
|
4500
|
-
|
|
4501
|
-
fs.mkdirSync(fullPath, { recursive: true });
|
|
7193
|
+
fs.mkdirSync(path.join(this.rootPath, dir), { recursive: true });
|
|
4502
7194
|
}
|
|
4503
7195
|
logger.success(`Created ${dirs.length} directories`);
|
|
4504
7196
|
}
|
|
@@ -4512,9 +7204,7 @@ var Generator = class {
|
|
|
4512
7204
|
this.write("CONTRIBUTING.md", contributingMd(this.config));
|
|
4513
7205
|
this.write("LICENSE", licenseText(this.config));
|
|
4514
7206
|
const dockerContent = dockerCompose(this.config);
|
|
4515
|
-
if (dockerContent)
|
|
4516
|
-
this.write("docker-compose.yml", dockerContent);
|
|
4517
|
-
}
|
|
7207
|
+
if (dockerContent) this.write("docker-compose.yml", dockerContent);
|
|
4518
7208
|
logger.success("Root files written");
|
|
4519
7209
|
}
|
|
4520
7210
|
// ── Step 5: Write GitHub files ───────────────────────────────────
|
|
@@ -4530,6 +7220,17 @@ var Generator = class {
|
|
|
4530
7220
|
this.write(".github/ISSUE_TEMPLATE/feature_request.md", featureRequest());
|
|
4531
7221
|
this.write(".github/pull_request_template.md", pullRequestTemplate());
|
|
4532
7222
|
this.write(".github/workflows/ci.yml", ciWorkflow(this.config));
|
|
7223
|
+
if (this.config.hasE2e) {
|
|
7224
|
+
const e2eStrategy = createE2eStrategy(this.config.e2e);
|
|
7225
|
+
if (e2eStrategy) {
|
|
7226
|
+
const ciContent = fs.readFileSync(path.join(this.rootPath, ".github/workflows/ci.yml"), "utf-8");
|
|
7227
|
+
fs.writeFileSync(
|
|
7228
|
+
path.join(this.rootPath, ".github/workflows/ci.yml"),
|
|
7229
|
+
ciContent + e2eStrategy.ciWorkflow(this.config),
|
|
7230
|
+
"utf-8"
|
|
7231
|
+
);
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
4533
7234
|
logger.success("GitHub files written");
|
|
4534
7235
|
}
|
|
4535
7236
|
// ── Step 6: Write config package ─────────────────────────────────
|
|
@@ -4598,9 +7299,114 @@ var Generator = class {
|
|
|
4598
7299
|
}
|
|
4599
7300
|
logger.success("Database package written");
|
|
4600
7301
|
}
|
|
4601
|
-
// ── Step 10: Write
|
|
7302
|
+
// ── Step 10: Write cache package ─────────────────────────────────
|
|
7303
|
+
writeCachePackage() {
|
|
7304
|
+
if (!this.config.hasCache) {
|
|
7305
|
+
logger.step(10, TOTAL_STEPS, "Skipping cache package (--cache none)");
|
|
7306
|
+
return;
|
|
7307
|
+
}
|
|
7308
|
+
logger.step(10, TOTAL_STEPS, "Writing cache package (Redis)...");
|
|
7309
|
+
this.write("packages/cache/package.json", cachePackageJson(this.config));
|
|
7310
|
+
this.write("packages/cache/tsconfig.json", cacheTsConfig(this.config));
|
|
7311
|
+
this.write("packages/cache/src/index.ts", cacheIndex());
|
|
7312
|
+
this.write("packages/cache/src/client.ts", cacheClient());
|
|
7313
|
+
this.write("packages/cache/src/service.ts", cacheService());
|
|
7314
|
+
logger.success("Cache package written");
|
|
7315
|
+
}
|
|
7316
|
+
// ── Step 11: Write email package ─────────────────────────────────
|
|
7317
|
+
writeEmailPackage() {
|
|
7318
|
+
if (!this.config.hasEmail) {
|
|
7319
|
+
logger.step(11, TOTAL_STEPS, "Skipping email package (--email none)");
|
|
7320
|
+
return;
|
|
7321
|
+
}
|
|
7322
|
+
logger.step(11, TOTAL_STEPS, `Writing email package (${this.config.email})...`);
|
|
7323
|
+
const strategy = createEmailStrategy(this.config.email);
|
|
7324
|
+
if (!strategy) return;
|
|
7325
|
+
this.write("packages/email/package.json", strategy.packageJson(this.config));
|
|
7326
|
+
this.write("packages/email/tsconfig.json", `{
|
|
7327
|
+
"extends": "@${this.config.name}/config/typescript/base",
|
|
7328
|
+
"compilerOptions": {
|
|
7329
|
+
"jsx": "react-jsx",
|
|
7330
|
+
"outDir": "./dist",
|
|
7331
|
+
"rootDir": "./src",
|
|
7332
|
+
"verbatimModuleSyntax": false
|
|
7333
|
+
},
|
|
7334
|
+
"include": ["src/**/*"]
|
|
7335
|
+
}
|
|
7336
|
+
`);
|
|
7337
|
+
this.write("packages/email/src/index.ts", strategy.index(this.config));
|
|
7338
|
+
this.write("packages/email/src/client.ts", strategy.client(this.config));
|
|
7339
|
+
this.write("packages/email/src/send.ts", strategy.sendFunction(this.config));
|
|
7340
|
+
this.write("packages/email/src/templates/welcome.tsx", strategy.welcomeTemplate(this.config));
|
|
7341
|
+
this.write("packages/email/src/templates/reset-password.tsx", strategy.resetPasswordTemplate(this.config));
|
|
7342
|
+
logger.success("Email package written");
|
|
7343
|
+
}
|
|
7344
|
+
// ── Step 12: Write storage package ───────────────────────────────
|
|
7345
|
+
writeStoragePackage() {
|
|
7346
|
+
if (!this.config.hasStorage) {
|
|
7347
|
+
logger.step(12, TOTAL_STEPS, "Skipping storage package (--storage none)");
|
|
7348
|
+
return;
|
|
7349
|
+
}
|
|
7350
|
+
logger.step(12, TOTAL_STEPS, `Writing storage package (${this.config.storage})...`);
|
|
7351
|
+
const strategy = createStorageStrategy(this.config.storage);
|
|
7352
|
+
if (!strategy) return;
|
|
7353
|
+
this.write("packages/storage/package.json", strategy.packageJson(this.config));
|
|
7354
|
+
this.write("packages/storage/tsconfig.json", `{
|
|
7355
|
+
"extends": "@${this.config.name}/config/typescript/base",
|
|
7356
|
+
"compilerOptions": {
|
|
7357
|
+
"outDir": "./dist",
|
|
7358
|
+
"rootDir": "./src",
|
|
7359
|
+
"verbatimModuleSyntax": false
|
|
7360
|
+
},
|
|
7361
|
+
"include": ["src/**/*"]
|
|
7362
|
+
}
|
|
7363
|
+
`);
|
|
7364
|
+
this.write("packages/storage/src/index.ts", strategy.index(this.config));
|
|
7365
|
+
this.write("packages/storage/src/client.ts", strategy.client(this.config));
|
|
7366
|
+
this.write("packages/storage/src/upload.ts", strategy.uploadService(this.config));
|
|
7367
|
+
this.write("packages/storage/src/download.ts", strategy.downloadService(this.config));
|
|
7368
|
+
const apiRoutes = strategy.apiRoutes(this.config);
|
|
7369
|
+
for (const [routePath, content] of Object.entries(apiRoutes)) {
|
|
7370
|
+
this.write(routePath, content);
|
|
7371
|
+
}
|
|
7372
|
+
logger.success("Storage package written");
|
|
7373
|
+
}
|
|
7374
|
+
// ── Step 13: Write payments package ──────────────────────────────
|
|
7375
|
+
writePaymentsPackage() {
|
|
7376
|
+
if (!this.config.hasPayments) {
|
|
7377
|
+
logger.step(13, TOTAL_STEPS, "Skipping payments package (--payments none)");
|
|
7378
|
+
return;
|
|
7379
|
+
}
|
|
7380
|
+
logger.step(13, TOTAL_STEPS, `Writing payments package (${this.config.payments})...`);
|
|
7381
|
+
const strategy = createPaymentStrategy(this.config.payments);
|
|
7382
|
+
if (!strategy) return;
|
|
7383
|
+
this.write("packages/payments/package.json", strategy.packageJson(this.config));
|
|
7384
|
+
this.write("packages/payments/tsconfig.json", `{
|
|
7385
|
+
"extends": "@${this.config.name}/config/typescript/base",
|
|
7386
|
+
"compilerOptions": {
|
|
7387
|
+
"outDir": "./dist",
|
|
7388
|
+
"rootDir": "./src",
|
|
7389
|
+
"verbatimModuleSyntax": false
|
|
7390
|
+
},
|
|
7391
|
+
"include": ["src/**/*"]
|
|
7392
|
+
}
|
|
7393
|
+
`);
|
|
7394
|
+
this.write("packages/payments/src/index.ts", strategy.index(this.config));
|
|
7395
|
+
this.write("packages/payments/src/client.ts", strategy.client(this.config));
|
|
7396
|
+
this.write("packages/payments/src/webhook.ts", strategy.webhookHandler(this.config));
|
|
7397
|
+
this.write("packages/payments/src/checkout.ts", strategy.checkout(this.config));
|
|
7398
|
+
this.write("packages/payments/src/subscription.ts", strategy.subscription(this.config));
|
|
7399
|
+
if (this.config.backend === "nestjs") {
|
|
7400
|
+
this.write("apps/api/src/payments/payments.controller.ts", strategy.apiRoute(this.config));
|
|
7401
|
+
} else {
|
|
7402
|
+
this.write("apps/api/src/routes/payments.ts", strategy.apiRoute(this.config));
|
|
7403
|
+
}
|
|
7404
|
+
this.write("apps/web/app/pricing/page.tsx", strategy.pricingPage(this.config));
|
|
7405
|
+
logger.success("Payments package written");
|
|
7406
|
+
}
|
|
7407
|
+
// ── Step 14: Write Next.js app ───────────────────────────────────
|
|
4602
7408
|
writeNextjsApp() {
|
|
4603
|
-
logger.step(
|
|
7409
|
+
logger.step(14, TOTAL_STEPS, "Writing Next.js 15 app...");
|
|
4604
7410
|
this.write("apps/web/package.json", webPackageJson(this.config));
|
|
4605
7411
|
this.write("apps/web/tsconfig.json", webTsConfig(this.config));
|
|
4606
7412
|
this.write("apps/web/next.config.ts", nextConfig(this.config));
|
|
@@ -4628,15 +7434,30 @@ var Generator = class {
|
|
|
4628
7434
|
this.write(`apps/web/${stateDir}/theme-slice.ts`, stateStrategy.exampleStore(this.config));
|
|
4629
7435
|
}
|
|
4630
7436
|
const provider = stateStrategy.providerWrapper(this.config);
|
|
4631
|
-
if (provider) {
|
|
4632
|
-
this.write(`apps/web/${stateDir}/provider.tsx`, provider);
|
|
4633
|
-
}
|
|
7437
|
+
if (provider) this.write(`apps/web/${stateDir}/provider.tsx`, provider);
|
|
4634
7438
|
}
|
|
4635
7439
|
logger.success("Next.js app written");
|
|
4636
7440
|
}
|
|
4637
|
-
// ── Step
|
|
7441
|
+
// ── Step 15: Write i18n layer ────────────────────────────────────
|
|
7442
|
+
writeI18nLayer() {
|
|
7443
|
+
if (!this.config.hasI18n) {
|
|
7444
|
+
logger.step(15, TOTAL_STEPS, "Skipping i18n (--i18n none)");
|
|
7445
|
+
return;
|
|
7446
|
+
}
|
|
7447
|
+
logger.step(15, TOTAL_STEPS, "Writing i18n layer (next-intl)...");
|
|
7448
|
+
this.write("apps/web/i18n/request.ts", i18nRequest());
|
|
7449
|
+
this.write("apps/web/i18n/routing.ts", i18nRouting());
|
|
7450
|
+
this.write("apps/web/i18n/navigation.ts", i18nNavigation());
|
|
7451
|
+
this.write("apps/web/messages/en.json", messagesEn(this.config));
|
|
7452
|
+
this.write("apps/web/messages/ar.json", messagesAr(this.config));
|
|
7453
|
+
this.write("apps/web/app/[locale]/layout.tsx", i18nLayout(this.config));
|
|
7454
|
+
this.write("apps/web/app/[locale]/page.tsx", i18nPage(this.config));
|
|
7455
|
+
this.write("apps/web/components/language-switcher.tsx", languageSwitcher());
|
|
7456
|
+
logger.success("i18n layer written");
|
|
7457
|
+
}
|
|
7458
|
+
// ── Step 16: Write backend app ───────────────────────────────────
|
|
4638
7459
|
writeBackendApp() {
|
|
4639
|
-
logger.step(
|
|
7460
|
+
logger.step(16, TOTAL_STEPS, `Writing ${this.config.backend} backend...`);
|
|
4640
7461
|
const backendStrategy = createBackendStrategy(this.config.backend);
|
|
4641
7462
|
this.write("apps/api/package.json", backendStrategy.packageJson(this.config));
|
|
4642
7463
|
this.write("apps/api/tsconfig.json", backendStrategy.tsConfig(this.config));
|
|
@@ -4661,13 +7482,39 @@ var Generator = class {
|
|
|
4661
7482
|
}
|
|
4662
7483
|
logger.success(`${this.config.backend === "nestjs" ? "NestJS" : "Express"} backend written`);
|
|
4663
7484
|
}
|
|
4664
|
-
// ── Step
|
|
7485
|
+
// ── Step 17: Write API docs ──────────────────────────────────────
|
|
7486
|
+
writeApiDocs() {
|
|
7487
|
+
if (!this.config.hasApiDocs) {
|
|
7488
|
+
logger.step(17, TOTAL_STEPS, "Skipping API docs (--api-docs none)");
|
|
7489
|
+
return;
|
|
7490
|
+
}
|
|
7491
|
+
logger.step(17, TOTAL_STEPS, `Writing API docs (${this.config.apiDocs})...`);
|
|
7492
|
+
const strategy = createApiDocsStrategy(this.config.apiDocs);
|
|
7493
|
+
if (!strategy) return;
|
|
7494
|
+
this.write("apps/api/src/docs/swagger-config.ts", strategy.docsConfig(this.config));
|
|
7495
|
+
this.write("apps/api/src/docs/setup.ts", strategy.docsSetup(this.config));
|
|
7496
|
+
logger.success("API docs written");
|
|
7497
|
+
}
|
|
7498
|
+
// ── Step 18: Write logging layer ─────────────────────────────────
|
|
7499
|
+
writeLoggingLayer() {
|
|
7500
|
+
if (!this.config.hasLogging) {
|
|
7501
|
+
logger.step(18, TOTAL_STEPS, "Skipping logging (--logging default)");
|
|
7502
|
+
return;
|
|
7503
|
+
}
|
|
7504
|
+
logger.step(18, TOTAL_STEPS, `Writing logging layer (${this.config.logging})...`);
|
|
7505
|
+
const strategy = createLoggingStrategy(this.config.logging);
|
|
7506
|
+
if (!strategy) return;
|
|
7507
|
+
this.write("packages/lib/src/logger/index.ts", strategy.index(this.config));
|
|
7508
|
+
this.write("packages/lib/src/logger/logger.ts", strategy.loggerSetup(this.config));
|
|
7509
|
+
logger.success("Logging layer written");
|
|
7510
|
+
}
|
|
7511
|
+
// ── Step 19: Write auth layer ────────────────────────────────────
|
|
4665
7512
|
writeAuthLayer() {
|
|
4666
7513
|
if (!this.config.hasAuth) {
|
|
4667
|
-
logger.step(
|
|
7514
|
+
logger.step(19, TOTAL_STEPS, "Skipping auth layer (--auth none)");
|
|
4668
7515
|
return;
|
|
4669
7516
|
}
|
|
4670
|
-
logger.step(
|
|
7517
|
+
logger.step(19, TOTAL_STEPS, `Writing ${this.config.auth} auth layer...`);
|
|
4671
7518
|
const authStrategy = createAuthStrategy(this.config.auth);
|
|
4672
7519
|
if (!authStrategy) return;
|
|
4673
7520
|
if (this.config.auth === "next-auth") {
|
|
@@ -4683,9 +7530,60 @@ var Generator = class {
|
|
|
4683
7530
|
}
|
|
4684
7531
|
logger.success("Auth layer written");
|
|
4685
7532
|
}
|
|
4686
|
-
// ── Step
|
|
7533
|
+
// ── Step 20: Write Docker files ──────────────────────────────────
|
|
7534
|
+
writeDockerFiles() {
|
|
7535
|
+
if (!this.config.hasDocker) {
|
|
7536
|
+
logger.step(20, TOTAL_STEPS, "Skipping Docker files (--docker none)");
|
|
7537
|
+
return;
|
|
7538
|
+
}
|
|
7539
|
+
logger.step(20, TOTAL_STEPS, `Writing Docker files (${this.config.docker})...`);
|
|
7540
|
+
const strategy = createDockerStrategy(this.config.docker);
|
|
7541
|
+
if (!strategy) return;
|
|
7542
|
+
this.write("apps/web/Dockerfile", strategy.webDockerfile(this.config));
|
|
7543
|
+
this.write("apps/api/Dockerfile", strategy.apiDockerfile(this.config));
|
|
7544
|
+
this.write("docker-compose.prod.yml", strategy.dockerComposeProd(this.config));
|
|
7545
|
+
this.write(".dockerignore", strategy.dockerignore(this.config));
|
|
7546
|
+
const extraFiles = strategy.extraFiles(this.config);
|
|
7547
|
+
for (const [filePath, content] of Object.entries(extraFiles)) {
|
|
7548
|
+
this.write(filePath, content);
|
|
7549
|
+
}
|
|
7550
|
+
logger.success("Docker files written");
|
|
7551
|
+
}
|
|
7552
|
+
// ── Step 21: Write Storybook config ──────────────────────────────
|
|
7553
|
+
writeStorybookConfig() {
|
|
7554
|
+
if (!this.config.storybook) {
|
|
7555
|
+
logger.step(21, TOTAL_STEPS, "Skipping Storybook (--no-storybook)");
|
|
7556
|
+
return;
|
|
7557
|
+
}
|
|
7558
|
+
logger.step(21, TOTAL_STEPS, "Writing Storybook config...");
|
|
7559
|
+
this.write("packages/ui/.storybook/main.ts", storybookMain(this.config));
|
|
7560
|
+
this.write("packages/ui/.storybook/preview.ts", storybookPreview(this.config));
|
|
7561
|
+
this.write("packages/ui/src/components/button.stories.tsx", buttonStories(this.config));
|
|
7562
|
+
this.write("packages/ui/src/components/card.stories.tsx", cardStories(this.config));
|
|
7563
|
+
this.write("packages/ui/src/components/input.stories.tsx", inputStories(this.config));
|
|
7564
|
+
logger.success("Storybook config written");
|
|
7565
|
+
}
|
|
7566
|
+
// ── Step 22: Write E2E tests ─────────────────────────────────────
|
|
7567
|
+
writeE2eTests() {
|
|
7568
|
+
if (!this.config.hasE2e) {
|
|
7569
|
+
logger.step(22, TOTAL_STEPS, "Skipping E2E tests (--e2e none)");
|
|
7570
|
+
return;
|
|
7571
|
+
}
|
|
7572
|
+
logger.step(22, TOTAL_STEPS, `Writing E2E tests (${this.config.e2e})...`);
|
|
7573
|
+
const strategy = createE2eStrategy(this.config.e2e);
|
|
7574
|
+
if (!strategy) return;
|
|
7575
|
+
if (this.config.e2e === "playwright") {
|
|
7576
|
+
this.write("apps/web/playwright.config.ts", strategy.config(this.config));
|
|
7577
|
+
this.write("apps/web/e2e/example.spec.ts", strategy.exampleTest(this.config));
|
|
7578
|
+
} else {
|
|
7579
|
+
this.write("apps/web/cypress.config.ts", strategy.config(this.config));
|
|
7580
|
+
this.write("apps/web/cypress/e2e/example.cy.ts", strategy.exampleTest(this.config));
|
|
7581
|
+
}
|
|
7582
|
+
logger.success("E2E tests written");
|
|
7583
|
+
}
|
|
7584
|
+
// ── Step 23: Write AI skills ─────────────────────────────────────
|
|
4687
7585
|
writeSkills() {
|
|
4688
|
-
logger.step(
|
|
7586
|
+
logger.step(23, TOTAL_STEPS, "Writing AI agent skills...");
|
|
4689
7587
|
this.write(".claude/settings.json", claudeSettings(this.config));
|
|
4690
7588
|
this.write(".claude/skills/component-design/SKILL.md", componentDesignSkill(this.config));
|
|
4691
7589
|
this.write(".claude/skills/page-design/SKILL.md", pageDesignSkill(this.config));
|
|
@@ -4693,42 +7591,35 @@ var Generator = class {
|
|
|
4693
7591
|
this.write(".claude/skills/monorepo-doctor/SKILL.md", monrepoDoctorSkill());
|
|
4694
7592
|
logger.success("AI skills written");
|
|
4695
7593
|
}
|
|
4696
|
-
// ── Step
|
|
7594
|
+
// ── Step 24: Install dependencies ────────────────────────────────
|
|
4697
7595
|
async installDependencies() {
|
|
4698
|
-
logger.step(
|
|
7596
|
+
logger.step(24, TOTAL_STEPS, `Installing dependencies (${this.config.packageManager})...`);
|
|
4699
7597
|
try {
|
|
4700
|
-
execSync(this.config.installCommand, {
|
|
4701
|
-
cwd: this.rootPath,
|
|
4702
|
-
stdio: "pipe",
|
|
4703
|
-
timeout: 12e4
|
|
4704
|
-
});
|
|
7598
|
+
execSync(this.config.installCommand, { cwd: this.rootPath, stdio: "pipe", timeout: 12e4 });
|
|
4705
7599
|
logger.success("Dependencies installed");
|
|
4706
7600
|
} catch {
|
|
4707
7601
|
logger.warn("Dependency installation failed \u2014 run manually after setup");
|
|
4708
7602
|
}
|
|
4709
7603
|
}
|
|
4710
|
-
// ── Step
|
|
7604
|
+
// ── Step 25: Initialize git ──────────────────────────────────────
|
|
4711
7605
|
async initializeGit() {
|
|
4712
7606
|
if (!this.config.gitInit) {
|
|
4713
|
-
logger.step(
|
|
7607
|
+
logger.step(25, TOTAL_STEPS, "Skipping git init (--no-git)");
|
|
4714
7608
|
return;
|
|
4715
7609
|
}
|
|
4716
|
-
logger.step(
|
|
7610
|
+
logger.step(25, TOTAL_STEPS, "Initializing git repository...");
|
|
4717
7611
|
try {
|
|
4718
7612
|
execSync("git init", { cwd: this.rootPath, stdio: "pipe" });
|
|
4719
7613
|
execSync("git add .", { cwd: this.rootPath, stdio: "pipe" });
|
|
4720
|
-
execSync(`git commit -m "Initial commit: ${this.config.pascalCase} monorepo"`, {
|
|
4721
|
-
cwd: this.rootPath,
|
|
4722
|
-
stdio: "pipe"
|
|
4723
|
-
});
|
|
7614
|
+
execSync(`git commit -m "Initial commit: ${this.config.pascalCase} monorepo"`, { cwd: this.rootPath, stdio: "pipe" });
|
|
4724
7615
|
logger.success("Git repository initialized with initial commit");
|
|
4725
7616
|
} catch {
|
|
4726
7617
|
logger.warn("Git initialization failed \u2014 run manually");
|
|
4727
7618
|
}
|
|
4728
7619
|
}
|
|
4729
|
-
// ── Step
|
|
7620
|
+
// ── Step 26: Print summary ───────────────────────────────────────
|
|
4730
7621
|
printSummary() {
|
|
4731
|
-
logger.step(
|
|
7622
|
+
logger.step(26, TOTAL_STEPS, "Done!");
|
|
4732
7623
|
logger.summary([
|
|
4733
7624
|
`${this.config.pascalCase} monorepo created successfully!`,
|
|
4734
7625
|
"",
|
|
@@ -4743,8 +7634,7 @@ var Generator = class {
|
|
|
4743
7634
|
write(relativePath, content) {
|
|
4744
7635
|
if (!content) return;
|
|
4745
7636
|
const fullPath = path.join(this.rootPath, relativePath);
|
|
4746
|
-
|
|
4747
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
7637
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
4748
7638
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
4749
7639
|
}
|
|
4750
7640
|
};
|
|
@@ -4783,7 +7673,17 @@ function detectProjectConfig(rootPath) {
|
|
|
4783
7673
|
packageManager,
|
|
4784
7674
|
gitInit: false,
|
|
4785
7675
|
githubFiles,
|
|
4786
|
-
license
|
|
7676
|
+
license,
|
|
7677
|
+
docker: detectDocker(rootPath),
|
|
7678
|
+
i18n: detectI18n(rootPath),
|
|
7679
|
+
payments: detectPayments(rootPath),
|
|
7680
|
+
email: detectEmail(rootPath),
|
|
7681
|
+
apiDocs: detectApiDocsOption(rootPath),
|
|
7682
|
+
storage: detectStorage(rootPath),
|
|
7683
|
+
e2e: detectE2e(rootPath),
|
|
7684
|
+
storybook: fs2.existsSync(path2.join(rootPath, "packages/ui/.storybook")),
|
|
7685
|
+
cache: detectCache(rootPath),
|
|
7686
|
+
logging: detectLogging(rootPath)
|
|
4787
7687
|
});
|
|
4788
7688
|
}
|
|
4789
7689
|
function detectBackend(rootPath, _name) {
|
|
@@ -4904,6 +7804,72 @@ function detectLicense(rootPath) {
|
|
|
4904
7804
|
if (content.includes("proprietary") || content.includes("All rights reserved")) return "proprietary";
|
|
4905
7805
|
return "MIT";
|
|
4906
7806
|
}
|
|
7807
|
+
function detectDocker(rootPath) {
|
|
7808
|
+
if (fs2.existsSync(path2.join(rootPath, "nginx/nginx.conf"))) return "full";
|
|
7809
|
+
if (fs2.existsSync(path2.join(rootPath, "apps/web/Dockerfile"))) return "minimal";
|
|
7810
|
+
return "none";
|
|
7811
|
+
}
|
|
7812
|
+
function detectI18n(rootPath) {
|
|
7813
|
+
const webPkg = readJson(path2.join(rootPath, "apps/web/package.json"));
|
|
7814
|
+
const deps = { ...webPkg?.dependencies, ...webPkg?.devDependencies };
|
|
7815
|
+
if (deps["next-intl"]) return "next-intl";
|
|
7816
|
+
return "none";
|
|
7817
|
+
}
|
|
7818
|
+
function detectPayments(rootPath) {
|
|
7819
|
+
const pkg = readJson(path2.join(rootPath, "packages/payments/package.json"));
|
|
7820
|
+
if (!pkg) return "none";
|
|
7821
|
+
const deps = pkg.dependencies;
|
|
7822
|
+
if (deps?.["stripe"]) return "stripe";
|
|
7823
|
+
if (deps?.["@lemonsqueezy/lemonsqueezy.js"]) return "lemonsqueezy";
|
|
7824
|
+
if (deps?.["@paddle/paddle-node-sdk"]) return "paddle";
|
|
7825
|
+
return "none";
|
|
7826
|
+
}
|
|
7827
|
+
function detectEmail(rootPath) {
|
|
7828
|
+
const pkg = readJson(path2.join(rootPath, "packages/email/package.json"));
|
|
7829
|
+
if (!pkg) return "none";
|
|
7830
|
+
const deps = pkg.dependencies;
|
|
7831
|
+
if (deps?.["resend"]) return "resend";
|
|
7832
|
+
if (deps?.["nodemailer"]) return "nodemailer";
|
|
7833
|
+
if (deps?.["@sendgrid/mail"]) return "sendgrid";
|
|
7834
|
+
return "none";
|
|
7835
|
+
}
|
|
7836
|
+
function detectApiDocsOption(rootPath) {
|
|
7837
|
+
if (fs2.existsSync(path2.join(rootPath, "apps/api/src/docs/setup.ts"))) {
|
|
7838
|
+
const content = readFile(path2.join(rootPath, "apps/api/src/docs/setup.ts"));
|
|
7839
|
+
if (content?.includes("redoc")) return "redoc";
|
|
7840
|
+
return "swagger";
|
|
7841
|
+
}
|
|
7842
|
+
return "none";
|
|
7843
|
+
}
|
|
7844
|
+
function detectStorage(rootPath) {
|
|
7845
|
+
const pkg = readJson(path2.join(rootPath, "packages/storage/package.json"));
|
|
7846
|
+
if (!pkg) return "none";
|
|
7847
|
+
const deps = pkg.dependencies;
|
|
7848
|
+
if (deps?.["@aws-sdk/client-s3"]) return "s3";
|
|
7849
|
+
if (deps?.["uploadthing"]) return "uploadthing";
|
|
7850
|
+
if (deps?.["cloudinary"]) return "cloudinary";
|
|
7851
|
+
return "none";
|
|
7852
|
+
}
|
|
7853
|
+
function detectE2e(rootPath) {
|
|
7854
|
+
if (fs2.existsSync(path2.join(rootPath, "apps/web/playwright.config.ts"))) return "playwright";
|
|
7855
|
+
if (fs2.existsSync(path2.join(rootPath, "apps/web/cypress.config.ts"))) return "cypress";
|
|
7856
|
+
return "none";
|
|
7857
|
+
}
|
|
7858
|
+
function detectCache(rootPath) {
|
|
7859
|
+
const pkg = readJson(path2.join(rootPath, "packages/cache/package.json"));
|
|
7860
|
+
if (!pkg) return "none";
|
|
7861
|
+
const deps = pkg.dependencies;
|
|
7862
|
+
if (deps?.["ioredis"]) return "redis";
|
|
7863
|
+
return "none";
|
|
7864
|
+
}
|
|
7865
|
+
function detectLogging(rootPath) {
|
|
7866
|
+
if (fs2.existsSync(path2.join(rootPath, "packages/lib/src/logger/logger.ts"))) {
|
|
7867
|
+
const content = readFile(path2.join(rootPath, "packages/lib/src/logger/logger.ts"));
|
|
7868
|
+
if (content?.includes("pino")) return "pino";
|
|
7869
|
+
if (content?.includes("winston")) return "winston";
|
|
7870
|
+
}
|
|
7871
|
+
return "default";
|
|
7872
|
+
}
|
|
4907
7873
|
function readJson(filePath) {
|
|
4908
7874
|
try {
|
|
4909
7875
|
const content = fs2.readFileSync(filePath, "utf-8");
|
|
@@ -5496,10 +8462,10 @@ var Workflow = class {
|
|
|
5496
8462
|
};
|
|
5497
8463
|
|
|
5498
8464
|
// src/cli.ts
|
|
5499
|
-
var VERSION = "
|
|
8465
|
+
var VERSION = "2.0.0";
|
|
5500
8466
|
var program = new Command();
|
|
5501
8467
|
program.name("create-next-monorepo").description("Generate production-ready Next.js + NestJS/Express monorepos with Turborepo").version(VERSION);
|
|
5502
|
-
program.argument("[name]", "Project name (kebab-case, e.g. my-app)").option("-b, --backend <framework>", "Backend framework: nestjs, express", "nestjs").option("-s, --styling <approach>", "Styling: tailwind, css-modules, styled-components", "tailwind").option("--orm <orm>", "Database ORM: prisma, drizzle, none", "prisma").option("--db <database>", "Database: postgres, mysql, sqlite, mongodb", "postgres").option("--auth <auth>", "Auth: next-auth, custom, none", "next-auth").option("--state <state>", "State: zustand, jotai, redux, tanstack-query, none", "zustand").option("--testing <framework>", "Testing: vitest, jest", "vitest").option("--license <license>", "License type", "MIT").option("--git", "Initialize git repository (default: true)", true).option("--no-git", "Skip git initialization").option("--github", "Generate GitHub community files", false).option("--package-manager <pm>", "Package manager: pnpm, npm, yarn, bun", "pnpm").action(async (name, options) => {
|
|
8468
|
+
program.argument("[name]", "Project name (kebab-case, e.g. my-app)").option("-b, --backend <framework>", "Backend framework: nestjs, express", "nestjs").option("-s, --styling <approach>", "Styling: tailwind, css-modules, styled-components", "tailwind").option("--orm <orm>", "Database ORM: prisma, drizzle, none", "prisma").option("--db <database>", "Database: postgres, mysql, sqlite, mongodb", "postgres").option("--auth <auth>", "Auth: next-auth, custom, none", "next-auth").option("--state <state>", "State: zustand, jotai, redux, tanstack-query, none", "zustand").option("--testing <framework>", "Testing: vitest, jest", "vitest").option("--license <license>", "License type", "MIT").option("--git", "Initialize git repository (default: true)", true).option("--no-git", "Skip git initialization").option("--github", "Generate GitHub community files", false).option("--package-manager <pm>", "Package manager: pnpm, npm, yarn, bun", "pnpm").option("--docker <mode>", "Docker: full, minimal, none", "none").option("--i18n <lib>", "i18n: next-intl, none", "none").option("--payments <provider>", "Payments: stripe, lemonsqueezy, paddle, none", "none").option("--email <provider>", "Email: resend, nodemailer, sendgrid, none", "none").option("--api-docs <renderer>", "API docs: swagger, redoc, none", "none").option("--storage <provider>", "Storage: s3, uploadthing, cloudinary, none", "none").option("--e2e <framework>", "E2E testing: playwright, cypress, none", "none").option("--storybook", "Generate Storybook for UI package", false).option("--cache <provider>", "Cache: redis, none", "none").option("--logging <lib>", "Logging: pino, winston, default", "default").action(async (name, options) => {
|
|
5503
8469
|
if (!name) {
|
|
5504
8470
|
program.help();
|
|
5505
8471
|
return;
|
|
@@ -5524,7 +8490,16 @@ program.argument("[name]", "Project name (kebab-case, e.g. my-app)").option("-b,
|
|
|
5524
8490
|
["auth", options.auth, validAuths],
|
|
5525
8491
|
["state", options.state, validStates],
|
|
5526
8492
|
["testing", options.testing, validTesting],
|
|
5527
|
-
["package-manager", options.packageManager, validPms]
|
|
8493
|
+
["package-manager", options.packageManager, validPms],
|
|
8494
|
+
["docker", options.docker, ["full", "minimal", "none"]],
|
|
8495
|
+
["i18n", options.i18n, ["next-intl", "none"]],
|
|
8496
|
+
["payments", options.payments, ["stripe", "lemonsqueezy", "paddle", "none"]],
|
|
8497
|
+
["email", options.email, ["resend", "nodemailer", "sendgrid", "none"]],
|
|
8498
|
+
["api-docs", options.apiDocs, ["swagger", "redoc", "none"]],
|
|
8499
|
+
["storage", options.storage, ["s3", "uploadthing", "cloudinary", "none"]],
|
|
8500
|
+
["e2e", options.e2e, ["playwright", "cypress", "none"]],
|
|
8501
|
+
["cache", options.cache, ["redis", "none"]],
|
|
8502
|
+
["logging", options.logging, ["pino", "winston", "default"]]
|
|
5528
8503
|
];
|
|
5529
8504
|
for (const [flag, value, allowed] of validations) {
|
|
5530
8505
|
if (!allowed.includes(value)) {
|
|
@@ -5545,7 +8520,17 @@ program.argument("[name]", "Project name (kebab-case, e.g. my-app)").option("-b,
|
|
|
5545
8520
|
license: options.license,
|
|
5546
8521
|
packageManager: options.packageManager,
|
|
5547
8522
|
gitInit: options.git,
|
|
5548
|
-
githubFiles: options.github
|
|
8523
|
+
githubFiles: options.github,
|
|
8524
|
+
docker: options.docker,
|
|
8525
|
+
i18n: options.i18n,
|
|
8526
|
+
payments: options.payments,
|
|
8527
|
+
email: options.email,
|
|
8528
|
+
apiDocs: options.apiDocs,
|
|
8529
|
+
storage: options.storage,
|
|
8530
|
+
e2e: options.e2e,
|
|
8531
|
+
storybook: options.storybook,
|
|
8532
|
+
cache: options.cache,
|
|
8533
|
+
logging: options.logging
|
|
5549
8534
|
});
|
|
5550
8535
|
const rootPath = path4.resolve(process.cwd(), name);
|
|
5551
8536
|
const generator = new Generator(config, rootPath);
|