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