@yoms/create-monorepo 1.2.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/index.js +244 -65
- package/package.json +6 -2
- package/templates/backend-hono/features/jwt-auth/env-additions.txt +5 -0
- package/templates/backend-hono/features/jwt-auth/package-additions.json +10 -0
- package/templates/backend-hono/features/jwt-auth/src/config/env-additions.ts +16 -0
- package/templates/backend-hono/features/jwt-auth/src/lib/jwt.ts +75 -0
- package/templates/backend-hono/features/jwt-auth/src/middleware/auth.middleware.ts +50 -0
- package/templates/backend-hono/features/jwt-auth/src/routes/auth.route.ts +157 -0
- package/templates/docker-compose.yml +70 -0
package/dist/index.js
CHANGED
|
@@ -6,7 +6,9 @@ import { cac } from "cac";
|
|
|
6
6
|
// src/commands/create.ts
|
|
7
7
|
import path7 from "path";
|
|
8
8
|
import ora from "ora";
|
|
9
|
+
import chalk from "chalk";
|
|
9
10
|
import { execa as execa2 } from "execa";
|
|
11
|
+
import enquirer4 from "enquirer";
|
|
10
12
|
|
|
11
13
|
// src/utils/file-ops.ts
|
|
12
14
|
import fs from "fs-extra";
|
|
@@ -74,6 +76,15 @@ var logger = {
|
|
|
74
76
|
|
|
75
77
|
// src/utils/package-manager.ts
|
|
76
78
|
import { execa } from "execa";
|
|
79
|
+
async function detectPackageManager() {
|
|
80
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
81
|
+
if (userAgent) {
|
|
82
|
+
if (userAgent.includes("pnpm")) return "pnpm";
|
|
83
|
+
if (userAgent.includes("yarn")) return "yarn";
|
|
84
|
+
if (userAgent.includes("bun")) return "bun";
|
|
85
|
+
}
|
|
86
|
+
return "pnpm";
|
|
87
|
+
}
|
|
77
88
|
async function installDependencies(cwd, pm) {
|
|
78
89
|
await execa(pm, ["install"], { cwd, stdio: "inherit" });
|
|
79
90
|
}
|
|
@@ -187,13 +198,24 @@ async function promptBackendConfig() {
|
|
|
187
198
|
});
|
|
188
199
|
includeSwagger = swagger;
|
|
189
200
|
}
|
|
201
|
+
let includeAuth = false;
|
|
202
|
+
if (backendType === "web") {
|
|
203
|
+
const { auth } = await enquirer2.prompt({
|
|
204
|
+
type: "confirm",
|
|
205
|
+
name: "auth",
|
|
206
|
+
message: "Include JWT authentication?",
|
|
207
|
+
initial: false
|
|
208
|
+
});
|
|
209
|
+
includeAuth = auth;
|
|
210
|
+
}
|
|
190
211
|
return {
|
|
191
212
|
type: backendType,
|
|
192
213
|
framework,
|
|
193
214
|
database: database === "none" ? void 0 : database,
|
|
194
215
|
includeRedis,
|
|
195
216
|
includeSmtp,
|
|
196
|
-
includeSwagger
|
|
217
|
+
includeSwagger,
|
|
218
|
+
includeAuth
|
|
197
219
|
};
|
|
198
220
|
}
|
|
199
221
|
|
|
@@ -230,6 +252,7 @@ async function promptFrontendConfig() {
|
|
|
230
252
|
|
|
231
253
|
// src/generators/monorepo.generator.ts
|
|
232
254
|
import path2 from "path";
|
|
255
|
+
import { fileURLToPath } from "url";
|
|
233
256
|
|
|
234
257
|
// src/generators/base.generator.ts
|
|
235
258
|
var BaseGenerator = class {
|
|
@@ -254,11 +277,19 @@ var BaseGenerator = class {
|
|
|
254
277
|
};
|
|
255
278
|
|
|
256
279
|
// src/generators/monorepo.generator.ts
|
|
280
|
+
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
281
|
+
var getPackageRoot = () => {
|
|
282
|
+
const dirname = __dirname;
|
|
283
|
+
return path2.resolve(dirname, "..");
|
|
284
|
+
};
|
|
257
285
|
var MonorepoGenerator = class extends BaseGenerator {
|
|
258
286
|
async generate(config) {
|
|
259
287
|
await ensureDir(this.options.projectDir);
|
|
260
288
|
await this.createRootFiles(config);
|
|
261
289
|
await this.createPackagesDirectory();
|
|
290
|
+
if (config.includeDocker) {
|
|
291
|
+
await this.createDockerCompose(config);
|
|
292
|
+
}
|
|
262
293
|
}
|
|
263
294
|
async createRootFiles(config) {
|
|
264
295
|
const workspaceContent = `packages:
|
|
@@ -393,11 +424,34 @@ ${config.includeBackend ? "\u2502 \u251C\u2500\u2500 api/ # Backend AP
|
|
|
393
424
|
async createPackagesDirectory() {
|
|
394
425
|
await ensureDir(path2.join(this.options.projectDir, "packages"));
|
|
395
426
|
}
|
|
427
|
+
async createDockerCompose(config) {
|
|
428
|
+
const packageRoot = getPackageRoot();
|
|
429
|
+
const templatePath = path2.join(packageRoot, "templates", "docker-compose.yml");
|
|
430
|
+
const template = await readFile(templatePath);
|
|
431
|
+
const hasPostgres = config.backend?.database === "postgres";
|
|
432
|
+
const hasMongodb = config.backend?.database === "mongodb";
|
|
433
|
+
const hasRedis = config.backend?.includeRedis || false;
|
|
434
|
+
const hasSmtp = config.backend?.includeSmtp || false;
|
|
435
|
+
let content = template.split("\n").filter((line) => {
|
|
436
|
+
if (line.includes("__IF_POSTGRES__")) return hasPostgres;
|
|
437
|
+
if (line.includes("__IF_MONGODB__")) return hasMongodb;
|
|
438
|
+
if (line.includes("__IF_REDIS__")) return hasRedis;
|
|
439
|
+
if (line.includes("__IF_SMTP__")) return hasSmtp;
|
|
440
|
+
return true;
|
|
441
|
+
}).map((line) => {
|
|
442
|
+
return line.replace(/__IF_POSTGRES__/g, "").replace(/__IF_MONGODB__/g, "").replace(/__IF_REDIS__/g, "").replace(/__IF_SMTP__/g, "");
|
|
443
|
+
}).join("\n");
|
|
444
|
+
const tokens = this.getTokens();
|
|
445
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
446
|
+
content = content.replace(new RegExp(token, "g"), value);
|
|
447
|
+
}
|
|
448
|
+
await writeFile(path2.join(this.options.projectDir, "docker-compose.yml"), content);
|
|
449
|
+
}
|
|
396
450
|
};
|
|
397
451
|
|
|
398
452
|
// src/generators/backend.generator.ts
|
|
399
453
|
import path4 from "path";
|
|
400
|
-
import { fileURLToPath } from "url";
|
|
454
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
401
455
|
|
|
402
456
|
// src/utils/template-merger.ts
|
|
403
457
|
import fs2 from "fs-extra";
|
|
@@ -459,9 +513,9 @@ async function mergeEnvFile(baseDir, additionsPath) {
|
|
|
459
513
|
}
|
|
460
514
|
|
|
461
515
|
// src/generators/backend.generator.ts
|
|
462
|
-
var
|
|
463
|
-
var
|
|
464
|
-
const dirname =
|
|
516
|
+
var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
|
|
517
|
+
var getPackageRoot2 = () => {
|
|
518
|
+
const dirname = __dirname2;
|
|
465
519
|
return path4.resolve(dirname, "..");
|
|
466
520
|
};
|
|
467
521
|
var BackendGenerator = class extends BaseGenerator {
|
|
@@ -479,7 +533,7 @@ var BackendGenerator = class extends BaseGenerator {
|
|
|
479
533
|
__HAS_REDIS__: String(this.config.includeRedis),
|
|
480
534
|
__HAS_SMTP__: String(this.config.includeSmtp)
|
|
481
535
|
});
|
|
482
|
-
const packageRoot =
|
|
536
|
+
const packageRoot = getPackageRoot2();
|
|
483
537
|
let templatePath;
|
|
484
538
|
if (this.config.type === "web") {
|
|
485
539
|
templatePath = path4.join(packageRoot, "templates", `backend-${this.config.framework}`, "base");
|
|
@@ -507,16 +561,20 @@ var BackendGenerator = class extends BaseGenerator {
|
|
|
507
561
|
const swaggerFeaturePath = path4.join(featuresPath, "swagger");
|
|
508
562
|
await mergeFeature(backendDir, swaggerFeaturePath, tokens);
|
|
509
563
|
}
|
|
564
|
+
if (this.config.includeAuth) {
|
|
565
|
+
const authFeaturePath = path4.join(featuresPath, "jwt-auth");
|
|
566
|
+
await mergeFeature(backendDir, authFeaturePath, tokens);
|
|
567
|
+
}
|
|
510
568
|
}
|
|
511
569
|
}
|
|
512
570
|
};
|
|
513
571
|
|
|
514
572
|
// src/generators/frontend.generator.ts
|
|
515
573
|
import path5 from "path";
|
|
516
|
-
import { fileURLToPath as
|
|
517
|
-
var
|
|
518
|
-
var
|
|
519
|
-
const dirname =
|
|
574
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
575
|
+
var __dirname3 = path5.dirname(fileURLToPath3(import.meta.url));
|
|
576
|
+
var getPackageRoot3 = () => {
|
|
577
|
+
const dirname = __dirname3;
|
|
520
578
|
return path5.resolve(dirname, "..");
|
|
521
579
|
};
|
|
522
580
|
var FrontendGenerator = class extends BaseGenerator {
|
|
@@ -531,7 +589,7 @@ var FrontendGenerator = class extends BaseGenerator {
|
|
|
531
589
|
const tokens = this.getTokens({
|
|
532
590
|
__API_URL__: this.config.apiUrl
|
|
533
591
|
});
|
|
534
|
-
const packageRoot =
|
|
592
|
+
const packageRoot = getPackageRoot3();
|
|
535
593
|
const templatePath = path5.join(packageRoot, "templates", "frontend-nextjs", "base");
|
|
536
594
|
await copyDirRecursive(templatePath, frontendDir, tokens);
|
|
537
595
|
}
|
|
@@ -539,10 +597,10 @@ var FrontendGenerator = class extends BaseGenerator {
|
|
|
539
597
|
|
|
540
598
|
// src/generators/shared.generator.ts
|
|
541
599
|
import path6 from "path";
|
|
542
|
-
import { fileURLToPath as
|
|
543
|
-
var
|
|
544
|
-
var
|
|
545
|
-
const dirname =
|
|
600
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
601
|
+
var __dirname4 = path6.dirname(fileURLToPath4(import.meta.url));
|
|
602
|
+
var getPackageRoot4 = () => {
|
|
603
|
+
const dirname = __dirname4;
|
|
546
604
|
return path6.resolve(dirname, "..");
|
|
547
605
|
};
|
|
548
606
|
var SharedGenerator = class extends BaseGenerator {
|
|
@@ -550,57 +608,151 @@ var SharedGenerator = class extends BaseGenerator {
|
|
|
550
608
|
const sharedDir = path6.join(this.options.projectDir, "packages", "shared");
|
|
551
609
|
await ensureDir(sharedDir);
|
|
552
610
|
const tokens = this.getTokens();
|
|
553
|
-
const packageRoot =
|
|
611
|
+
const packageRoot = getPackageRoot4();
|
|
554
612
|
const templatePath = path6.join(packageRoot, "templates", "shared", "base");
|
|
555
613
|
await copyDirRecursive(templatePath, sharedDir, tokens);
|
|
556
614
|
}
|
|
557
615
|
};
|
|
558
616
|
|
|
559
617
|
// src/commands/create.ts
|
|
560
|
-
|
|
618
|
+
function showPreview(config) {
|
|
561
619
|
console.log("");
|
|
562
|
-
|
|
620
|
+
console.log(chalk.bold("\u{1F4E6} Project Preview:"));
|
|
563
621
|
console.log("");
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
|
|
622
|
+
console.log(chalk.cyan(`${config.projectName}/`));
|
|
623
|
+
console.log(chalk.gray("\u251C\u2500\u2500 packages/"));
|
|
624
|
+
if (config.includeBackend && config.backend) {
|
|
625
|
+
const features = [];
|
|
626
|
+
if (config.backend.database) features.push(config.backend.database.toUpperCase());
|
|
627
|
+
if (config.backend.includeRedis) features.push("Redis");
|
|
628
|
+
if (config.backend.includeSmtp) features.push("SMTP");
|
|
629
|
+
if (config.backend.includeSwagger) features.push("Swagger");
|
|
630
|
+
if (config.backend.includeAuth) features.push("JWT Auth");
|
|
631
|
+
const featureStr = features.length > 0 ? chalk.gray(` (${features.join(", ")})`) : "";
|
|
632
|
+
console.log(chalk.gray(`\u2502 \u251C\u2500\u2500 api/`) + chalk.yellow(` ${config.backend.framework}`) + featureStr);
|
|
569
633
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
process.exit(1);
|
|
634
|
+
if (config.includeFrontend && config.frontend) {
|
|
635
|
+
const features = [];
|
|
636
|
+
if (config.frontend.includeShadcn) features.push("shadcn/ui");
|
|
637
|
+
const featureStr = features.length > 0 ? chalk.gray(` (${features.join(", ")})`) : "";
|
|
638
|
+
console.log(chalk.gray(`\u2502 \u251C\u2500\u2500 web/`) + chalk.yellow(` Next.js 15`) + featureStr);
|
|
576
639
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
packageManager,
|
|
580
|
-
includeBackend,
|
|
581
|
-
includeFrontend
|
|
582
|
-
};
|
|
583
|
-
if (includeBackend) {
|
|
584
|
-
console.log("");
|
|
585
|
-
logger.info("Configure backend package:");
|
|
586
|
-
config.backend = await promptBackendConfig();
|
|
640
|
+
if (config.includeBackend && config.includeFrontend) {
|
|
641
|
+
console.log(chalk.gray(`\u2502 \u2514\u2500\u2500 shared/`) + chalk.yellow(` Zod schemas + types`));
|
|
587
642
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
643
|
+
console.log(chalk.gray("\u251C\u2500\u2500 pnpm-workspace.yaml"));
|
|
644
|
+
console.log(chalk.gray("\u251C\u2500\u2500 package.json"));
|
|
645
|
+
if (config.includeDocker) {
|
|
646
|
+
console.log(chalk.gray("\u251C\u2500\u2500 docker-compose.yml") + chalk.yellow(" \u26A1"));
|
|
592
647
|
}
|
|
648
|
+
console.log(chalk.gray("\u2514\u2500\u2500 ..."));
|
|
649
|
+
console.log("");
|
|
650
|
+
}
|
|
651
|
+
async function confirmPreview() {
|
|
652
|
+
const { confirm } = await enquirer4.prompt({
|
|
653
|
+
type: "confirm",
|
|
654
|
+
name: "confirm",
|
|
655
|
+
message: "Does this look good?",
|
|
656
|
+
initial: true
|
|
657
|
+
});
|
|
658
|
+
return confirm;
|
|
659
|
+
}
|
|
660
|
+
async function createMonorepo(targetDir, options = {}) {
|
|
593
661
|
console.log("");
|
|
594
|
-
logger.info("
|
|
662
|
+
logger.info("Welcome to create-monorepo!");
|
|
663
|
+
console.log("");
|
|
664
|
+
const isInteractive = !options.yes && process.stdin.isTTY;
|
|
595
665
|
try {
|
|
666
|
+
const projectName = isInteractive ? await promptProjectName(targetDir ? path7.basename(targetDir) : void 0) : targetDir ? path7.basename(targetDir) : "my-monorepo";
|
|
667
|
+
const projectDir = path7.resolve(process.cwd(), targetDir || projectName);
|
|
668
|
+
if (!await isDirEmpty(projectDir)) {
|
|
669
|
+
throw new Error(`Directory "${projectDir}" is not empty. Please choose an empty directory.`);
|
|
670
|
+
}
|
|
671
|
+
let packageManager;
|
|
672
|
+
if (options.pm) {
|
|
673
|
+
packageManager = options.pm;
|
|
674
|
+
} else if (isInteractive) {
|
|
675
|
+
packageManager = await promptPackageManager();
|
|
676
|
+
} else {
|
|
677
|
+
packageManager = await detectPackageManager();
|
|
678
|
+
}
|
|
679
|
+
let includeBackend;
|
|
680
|
+
let includeFrontend;
|
|
681
|
+
if (options.yes) {
|
|
682
|
+
includeBackend = options.backend !== void 0 || options.database !== void 0;
|
|
683
|
+
includeFrontend = options.frontend === true;
|
|
684
|
+
if (!includeBackend && !includeFrontend) {
|
|
685
|
+
includeBackend = true;
|
|
686
|
+
includeFrontend = true;
|
|
687
|
+
}
|
|
688
|
+
} else if (isInteractive) {
|
|
689
|
+
includeBackend = await promptIncludeBackend();
|
|
690
|
+
includeFrontend = await promptIncludeFrontend();
|
|
691
|
+
} else {
|
|
692
|
+
includeBackend = true;
|
|
693
|
+
includeFrontend = true;
|
|
694
|
+
}
|
|
695
|
+
if (!includeBackend && !includeFrontend) {
|
|
696
|
+
throw new Error("You must include at least one package (backend or frontend).");
|
|
697
|
+
}
|
|
698
|
+
const config = {
|
|
699
|
+
projectName,
|
|
700
|
+
packageManager,
|
|
701
|
+
includeBackend,
|
|
702
|
+
includeFrontend,
|
|
703
|
+
includeDocker: options.docker
|
|
704
|
+
};
|
|
705
|
+
if (includeBackend) {
|
|
706
|
+
if (options.yes || !isInteractive) {
|
|
707
|
+
config.backend = {
|
|
708
|
+
type: "web",
|
|
709
|
+
framework: options.backend || "hono",
|
|
710
|
+
database: options.database,
|
|
711
|
+
includeRedis: options.redis || false,
|
|
712
|
+
includeSmtp: options.smtp || false,
|
|
713
|
+
includeSwagger: options.swagger || false,
|
|
714
|
+
includeAuth: options.auth || false
|
|
715
|
+
};
|
|
716
|
+
} else {
|
|
717
|
+
console.log("");
|
|
718
|
+
logger.info("Configure backend package:");
|
|
719
|
+
config.backend = await promptBackendConfig();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (includeFrontend) {
|
|
723
|
+
if (options.yes || !isInteractive) {
|
|
724
|
+
config.frontend = {
|
|
725
|
+
framework: "nextjs",
|
|
726
|
+
includeShadcn: options.shadcn || false,
|
|
727
|
+
apiUrl: "http://localhost:3001"
|
|
728
|
+
};
|
|
729
|
+
} else {
|
|
730
|
+
console.log("");
|
|
731
|
+
logger.info("Configure frontend package:");
|
|
732
|
+
config.frontend = await promptFrontendConfig();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (isInteractive) {
|
|
736
|
+
showPreview(config);
|
|
737
|
+
const confirmed = await confirmPreview();
|
|
738
|
+
if (!confirmed) {
|
|
739
|
+
console.log("");
|
|
740
|
+
logger.info("Cancelled. No files were created.");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
console.log("");
|
|
745
|
+
logger.info("Generating project...");
|
|
746
|
+
console.log("");
|
|
747
|
+
const spinner = ora();
|
|
748
|
+
spinner.start("Creating monorepo structure...");
|
|
596
749
|
const monorepoGenerator = new MonorepoGenerator({
|
|
597
750
|
projectName,
|
|
598
751
|
projectDir,
|
|
599
752
|
packageManager
|
|
600
753
|
});
|
|
601
|
-
const spinner = ora("Creating monorepo structure...").start();
|
|
602
754
|
await monorepoGenerator.generate(config);
|
|
603
|
-
spinner.succeed("Monorepo structure created");
|
|
755
|
+
spinner.succeed(chalk.green("Monorepo structure created"));
|
|
604
756
|
if (includeBackend && includeFrontend) {
|
|
605
757
|
spinner.start("Generating shared package...");
|
|
606
758
|
const sharedGenerator = new SharedGenerator({
|
|
@@ -609,7 +761,7 @@ async function createMonorepo(targetDir) {
|
|
|
609
761
|
packageManager
|
|
610
762
|
});
|
|
611
763
|
await sharedGenerator.generate();
|
|
612
|
-
spinner.succeed("Shared package generated");
|
|
764
|
+
spinner.succeed(chalk.green("Shared package generated"));
|
|
613
765
|
}
|
|
614
766
|
if (includeBackend && config.backend) {
|
|
615
767
|
spinner.start("Generating backend package...");
|
|
@@ -620,7 +772,7 @@ async function createMonorepo(targetDir) {
|
|
|
620
772
|
config: config.backend
|
|
621
773
|
});
|
|
622
774
|
await backendGenerator.generate();
|
|
623
|
-
spinner.succeed(
|
|
775
|
+
spinner.succeed(chalk.green(`Backend package generated (${config.backend.framework})`));
|
|
624
776
|
}
|
|
625
777
|
if (includeFrontend && config.frontend) {
|
|
626
778
|
spinner.start("Generating frontend package...");
|
|
@@ -631,34 +783,61 @@ async function createMonorepo(targetDir) {
|
|
|
631
783
|
config: config.frontend
|
|
632
784
|
});
|
|
633
785
|
await frontendGenerator.generate();
|
|
634
|
-
spinner.succeed("Frontend package generated");
|
|
786
|
+
spinner.succeed(chalk.green("Frontend package generated (Next.js 15)"));
|
|
787
|
+
}
|
|
788
|
+
if (!options.skipInstall) {
|
|
789
|
+
spinner.start(`Installing dependencies with ${packageManager}...`);
|
|
790
|
+
spinner.text = `Installing dependencies... ${chalk.gray("(this may take a minute)")}`;
|
|
791
|
+
await installDependencies(projectDir, packageManager);
|
|
792
|
+
spinner.succeed(chalk.green("Dependencies installed"));
|
|
793
|
+
} else {
|
|
794
|
+
logger.info(chalk.yellow("\u26A0 Skipped dependency installation"));
|
|
795
|
+
}
|
|
796
|
+
if (!options.skipGit) {
|
|
797
|
+
spinner.start("Initializing git repository...");
|
|
798
|
+
await execa2("git", ["init"], { cwd: projectDir });
|
|
799
|
+
await execa2("git", ["add", "."], { cwd: projectDir });
|
|
800
|
+
await execa2("git", ["commit", "-m", "Initial commit from create-monorepo"], { cwd: projectDir });
|
|
801
|
+
spinner.succeed(chalk.green("Git repository initialized"));
|
|
802
|
+
} else {
|
|
803
|
+
logger.info(chalk.yellow("\u26A0 Skipped git initialization"));
|
|
635
804
|
}
|
|
636
|
-
spinner.start("Installing dependencies...");
|
|
637
|
-
await installDependencies(projectDir, packageManager);
|
|
638
|
-
spinner.succeed("Dependencies installed");
|
|
639
|
-
spinner.start("Initializing git repository...");
|
|
640
|
-
await execa2("git", ["init"], { cwd: projectDir });
|
|
641
|
-
await execa2("git", ["add", "."], { cwd: projectDir });
|
|
642
|
-
await execa2("git", ["commit", "-m", "Initial commit from create-monorepo"], { cwd: projectDir });
|
|
643
|
-
spinner.succeed("Git repository initialized");
|
|
644
805
|
console.log("");
|
|
645
|
-
logger.success(
|
|
806
|
+
logger.success(`${chalk.bold('\u2728 Project "')}${chalk.bold.cyan(projectName)}${chalk.bold('" created successfully!')}`);
|
|
646
807
|
console.log("");
|
|
647
|
-
logger.info("Next steps:");
|
|
648
|
-
console.log(`
|
|
649
|
-
|
|
808
|
+
logger.info(chalk.bold("Next steps:"));
|
|
809
|
+
console.log(chalk.gray(" $ ") + chalk.cyan(`cd ${targetDir || projectName}`));
|
|
810
|
+
if (options.skipInstall) {
|
|
811
|
+
console.log(chalk.gray(" $ ") + chalk.cyan(`${packageManager} install`));
|
|
812
|
+
}
|
|
813
|
+
console.log(chalk.gray(" $ ") + chalk.cyan(`${packageManager} dev`));
|
|
650
814
|
console.log("");
|
|
815
|
+
if (config.includeDocker) {
|
|
816
|
+
logger.info(chalk.bold("\u{1F433} Docker Compose:"));
|
|
817
|
+
console.log(chalk.gray(" $ ") + chalk.cyan("docker-compose up -d"));
|
|
818
|
+
console.log("");
|
|
819
|
+
}
|
|
651
820
|
} catch (error) {
|
|
652
|
-
|
|
821
|
+
console.log("");
|
|
822
|
+
if (error instanceof Error) {
|
|
823
|
+
logger.error(chalk.red(`\u2716 ${error.message}`));
|
|
824
|
+
if (error.stack && process.env.DEBUG) {
|
|
825
|
+
console.log("");
|
|
826
|
+
console.log(chalk.gray(error.stack));
|
|
827
|
+
}
|
|
828
|
+
} else {
|
|
829
|
+
logger.error(chalk.red(`\u2716 Failed to create project: ${String(error)}`));
|
|
830
|
+
}
|
|
831
|
+
console.log("");
|
|
653
832
|
process.exit(1);
|
|
654
833
|
}
|
|
655
834
|
}
|
|
656
835
|
|
|
657
836
|
// src/index.ts
|
|
658
837
|
var cli = cac("create-monorepo");
|
|
659
|
-
cli.command("[dir]", "Create a new monorepo project").action(async (dir) => {
|
|
660
|
-
await createMonorepo(dir);
|
|
838
|
+
cli.command("[dir]", "Create a new monorepo project").option("--backend <framework>", "Backend framework (hono)").option("--database <db>", "Database (postgres, mongodb, none)").option("--redis", "Include Redis cache").option("--smtp", "Include SMTP email").option("--swagger", "Include Swagger docs").option("--auth", "Include JWT authentication").option("--frontend", "Include Next.js frontend").option("--shadcn", "Include shadcn/ui components").option("--pm <manager>", "Package manager (pnpm, npm, yarn, bun)").option("--docker", "Include Docker Compose setup").option("--skip-install", "Skip dependency installation").option("--skip-git", "Skip git initialization").option("-y, --yes", "Skip all prompts and use defaults").action(async (dir, options) => {
|
|
839
|
+
await createMonorepo(dir, options);
|
|
661
840
|
});
|
|
662
841
|
cli.help();
|
|
663
|
-
cli.version("
|
|
842
|
+
cli.version("1.2.0");
|
|
664
843
|
cli.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoms/create-monorepo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "CLI tool to scaffold monorepo projects from templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"prettier": "^3.4.2",
|
|
54
54
|
"tsup": "^8.3.5",
|
|
55
55
|
"tsx": "^4.19.2",
|
|
56
|
-
"typescript": "^5.7.2"
|
|
56
|
+
"typescript": "^5.7.2",
|
|
57
|
+
"vitest": "^2.1.8"
|
|
57
58
|
},
|
|
58
59
|
"engines": {
|
|
59
60
|
"node": ">=18.0.0"
|
|
@@ -64,6 +65,9 @@
|
|
|
64
65
|
"typecheck": "tsc --noEmit",
|
|
65
66
|
"lint": "eslint src --ext .ts",
|
|
66
67
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"test:watch": "vitest",
|
|
70
|
+
"test:e2e": "vitest run tests/e2e",
|
|
67
71
|
"test:generate": "tsx scripts/test-generate.ts",
|
|
68
72
|
"test:validate": "tsx scripts/validate-templates.ts",
|
|
69
73
|
"changeset": "changeset",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Add these to your env schema validation
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
export const jwtEnvSchema = z.object({
|
|
6
|
+
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
|
|
7
|
+
JWT_EXPIRES_IN: z.string().default('15m'),
|
|
8
|
+
JWT_REFRESH_SECRET: z.string().min(32, 'JWT_REFRESH_SECRET must be at least 32 characters'),
|
|
9
|
+
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Merge this with your existing env schema:
|
|
13
|
+
// const envSchema = z.object({
|
|
14
|
+
// ...existingFields,
|
|
15
|
+
// ...jwtEnvSchema.shape,
|
|
16
|
+
// });
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import { env } from '../config/env.js';
|
|
3
|
+
import { UnauthorizedError } from '../lib/errors.js';
|
|
4
|
+
|
|
5
|
+
export interface JwtPayload {
|
|
6
|
+
userId: string;
|
|
7
|
+
email: string;
|
|
8
|
+
type: 'access' | 'refresh';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TokenPair {
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate access and refresh tokens for a user
|
|
18
|
+
*/
|
|
19
|
+
export function generateTokens(userId: string, email: string): TokenPair {
|
|
20
|
+
const accessToken = jwt.sign(
|
|
21
|
+
{ userId, email, type: 'access' } as JwtPayload,
|
|
22
|
+
env.JWT_SECRET,
|
|
23
|
+
{ expiresIn: env.JWT_EXPIRES_IN }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const refreshToken = jwt.sign(
|
|
27
|
+
{ userId, email, type: 'refresh' } as JwtPayload,
|
|
28
|
+
env.JWT_REFRESH_SECRET,
|
|
29
|
+
{ expiresIn: env.JWT_REFRESH_EXPIRES_IN }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return { accessToken, refreshToken };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Verify and decode an access token
|
|
37
|
+
*/
|
|
38
|
+
export function verifyAccessToken(token: string): JwtPayload {
|
|
39
|
+
try {
|
|
40
|
+
const decoded = jwt.verify(token, env.JWT_SECRET) as JwtPayload;
|
|
41
|
+
if (decoded.type !== 'access') {
|
|
42
|
+
throw new UnauthorizedError('Invalid token type');
|
|
43
|
+
}
|
|
44
|
+
return decoded;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof jwt.TokenExpiredError) {
|
|
47
|
+
throw new UnauthorizedError('Token expired');
|
|
48
|
+
}
|
|
49
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
50
|
+
throw new UnauthorizedError('Invalid token');
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Verify and decode a refresh token
|
|
58
|
+
*/
|
|
59
|
+
export function verifyRefreshToken(token: string): JwtPayload {
|
|
60
|
+
try {
|
|
61
|
+
const decoded = jwt.verify(token, env.JWT_REFRESH_SECRET) as JwtPayload;
|
|
62
|
+
if (decoded.type !== 'refresh') {
|
|
63
|
+
throw new UnauthorizedError('Invalid token type');
|
|
64
|
+
}
|
|
65
|
+
return decoded;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof jwt.TokenExpiredError) {
|
|
68
|
+
throw new UnauthorizedError('Refresh token expired');
|
|
69
|
+
}
|
|
70
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
71
|
+
throw new UnauthorizedError('Invalid refresh token');
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Context, Next } from 'hono';
|
|
2
|
+
import { verifyAccessToken } from '../lib/jwt.js';
|
|
3
|
+
import { UnauthorizedError } from '../lib/errors.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Middleware to authenticate requests using JWT
|
|
7
|
+
* Extracts and verifies the JWT token from the Authorization header
|
|
8
|
+
* and attaches the decoded payload to c.get('jwtPayload')
|
|
9
|
+
*/
|
|
10
|
+
export async function authMiddleware(c: Context, next: Next): Promise<void> {
|
|
11
|
+
const authHeader = c.req.header('Authorization');
|
|
12
|
+
|
|
13
|
+
if (!authHeader) {
|
|
14
|
+
throw new UnauthorizedError('No authorization header');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [type, token] = authHeader.split(' ');
|
|
18
|
+
|
|
19
|
+
if (type !== 'Bearer' || !token) {
|
|
20
|
+
throw new UnauthorizedError('Invalid authorization header format');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const payload = verifyAccessToken(token);
|
|
24
|
+
c.set('jwtPayload', payload);
|
|
25
|
+
|
|
26
|
+
await next();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optional authentication middleware
|
|
31
|
+
* Similar to authMiddleware but doesn't throw if no token is present
|
|
32
|
+
* Use this for routes that work for both authenticated and unauthenticated users
|
|
33
|
+
*/
|
|
34
|
+
export async function optionalAuthMiddleware(c: Context, next: Next): Promise<void> {
|
|
35
|
+
const authHeader = c.req.header('Authorization');
|
|
36
|
+
|
|
37
|
+
if (authHeader) {
|
|
38
|
+
try {
|
|
39
|
+
const [type, token] = authHeader.split(' ');
|
|
40
|
+
if (type === 'Bearer' && token) {
|
|
41
|
+
const payload = verifyAccessToken(token);
|
|
42
|
+
c.set('jwtPayload', payload);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Silently ignore invalid tokens for optional auth
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await next();
|
|
50
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { zValidator } from '@hono/zod-validator';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import bcrypt from 'bcryptjs';
|
|
5
|
+
import { generateTokens, verifyRefreshToken } from '../lib/jwt.js';
|
|
6
|
+
import { success, created } from '../lib/response.js';
|
|
7
|
+
import { UnauthorizedError, ConflictError } from '../lib/errors.js';
|
|
8
|
+
|
|
9
|
+
// NOTE: This is a basic example. In a real application, you would:
|
|
10
|
+
// 1. Store users in a database
|
|
11
|
+
// 2. Store refresh tokens in a database or Redis for invalidation
|
|
12
|
+
// 3. Add email verification
|
|
13
|
+
// 4. Add password reset functionality
|
|
14
|
+
// 5. Add rate limiting on auth endpoints
|
|
15
|
+
// 6. Add account lockout after failed attempts
|
|
16
|
+
|
|
17
|
+
const auth = new Hono();
|
|
18
|
+
|
|
19
|
+
// Validation schemas
|
|
20
|
+
const registerSchema = z.object({
|
|
21
|
+
email: z.string().email(),
|
|
22
|
+
password: z.string().min(8),
|
|
23
|
+
name: z.string().min(1),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const loginSchema = z.object({
|
|
27
|
+
email: z.string().email(),
|
|
28
|
+
password: z.string(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const refreshSchema = z.object({
|
|
32
|
+
refreshToken: z.string(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// In-memory user store (replace with database in production)
|
|
36
|
+
interface User {
|
|
37
|
+
id: string;
|
|
38
|
+
email: string;
|
|
39
|
+
password: string;
|
|
40
|
+
name: string;
|
|
41
|
+
createdAt: Date;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const users: Map<string, User> = new Map();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* POST /auth/register
|
|
48
|
+
* Register a new user
|
|
49
|
+
*/
|
|
50
|
+
auth.post('/register', zValidator('json', registerSchema), async (c) => {
|
|
51
|
+
const { email, password, name } = c.req.valid('json');
|
|
52
|
+
|
|
53
|
+
// Check if user already exists
|
|
54
|
+
const existingUser = Array.from(users.values()).find((u) => u.email === email);
|
|
55
|
+
if (existingUser) {
|
|
56
|
+
throw new ConflictError('User with this email already exists');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Hash password
|
|
60
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
61
|
+
|
|
62
|
+
// Create user
|
|
63
|
+
const userId = crypto.randomUUID();
|
|
64
|
+
const user: User = {
|
|
65
|
+
id: userId,
|
|
66
|
+
email,
|
|
67
|
+
password: hashedPassword,
|
|
68
|
+
name,
|
|
69
|
+
createdAt: new Date(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
users.set(userId, user);
|
|
73
|
+
|
|
74
|
+
// Generate tokens
|
|
75
|
+
const tokens = generateTokens(userId, email);
|
|
76
|
+
|
|
77
|
+
return created(c, {
|
|
78
|
+
user: {
|
|
79
|
+
id: user.id,
|
|
80
|
+
email: user.email,
|
|
81
|
+
name: user.name,
|
|
82
|
+
createdAt: user.createdAt,
|
|
83
|
+
},
|
|
84
|
+
...tokens,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* POST /auth/login
|
|
90
|
+
* Login with email and password
|
|
91
|
+
*/
|
|
92
|
+
auth.post('/login', zValidator('json', loginSchema), async (c) => {
|
|
93
|
+
const { email, password } = c.req.valid('json');
|
|
94
|
+
|
|
95
|
+
// Find user
|
|
96
|
+
const user = Array.from(users.values()).find((u) => u.email === email);
|
|
97
|
+
if (!user) {
|
|
98
|
+
throw new UnauthorizedError('Invalid email or password');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Verify password
|
|
102
|
+
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
103
|
+
if (!isValidPassword) {
|
|
104
|
+
throw new UnauthorizedError('Invalid email or password');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Generate tokens
|
|
108
|
+
const tokens = generateTokens(user.id, user.email);
|
|
109
|
+
|
|
110
|
+
return success(c, {
|
|
111
|
+
user: {
|
|
112
|
+
id: user.id,
|
|
113
|
+
email: user.email,
|
|
114
|
+
name: user.name,
|
|
115
|
+
createdAt: user.createdAt,
|
|
116
|
+
},
|
|
117
|
+
...tokens,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* POST /auth/refresh
|
|
123
|
+
* Refresh access token using refresh token
|
|
124
|
+
*/
|
|
125
|
+
auth.post('/refresh', zValidator('json', refreshSchema), async (c) => {
|
|
126
|
+
const { refreshToken } = c.req.valid('json');
|
|
127
|
+
|
|
128
|
+
// Verify refresh token
|
|
129
|
+
const payload = verifyRefreshToken(refreshToken);
|
|
130
|
+
|
|
131
|
+
// Get user
|
|
132
|
+
const user = users.get(payload.userId);
|
|
133
|
+
if (!user) {
|
|
134
|
+
throw new UnauthorizedError('User not found');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Generate new tokens
|
|
138
|
+
const tokens = generateTokens(user.id, user.email);
|
|
139
|
+
|
|
140
|
+
return success(c, tokens);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* POST /auth/logout
|
|
145
|
+
* Logout (client should discard tokens)
|
|
146
|
+
* In a real app, you would invalidate the refresh token in the database
|
|
147
|
+
*/
|
|
148
|
+
auth.post('/logout', async (c) => {
|
|
149
|
+
// In a real application, you would:
|
|
150
|
+
// 1. Get refresh token from request
|
|
151
|
+
// 2. Delete it from database/Redis
|
|
152
|
+
// 3. Optionally blacklist the access token until it expires
|
|
153
|
+
|
|
154
|
+
return success(c, { message: 'Logged out successfully' });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export default auth;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
__IF_POSTGRES__ postgres:
|
|
5
|
+
__IF_POSTGRES__ image: postgres:16-alpine
|
|
6
|
+
__IF_POSTGRES__ container_name: __PROJECT_NAME__-postgres
|
|
7
|
+
__IF_POSTGRES__ environment:
|
|
8
|
+
__IF_POSTGRES__ POSTGRES_USER: postgres
|
|
9
|
+
__IF_POSTGRES__ POSTGRES_PASSWORD: postgres
|
|
10
|
+
__IF_POSTGRES__ POSTGRES_DB: __PROJECT_NAME__
|
|
11
|
+
__IF_POSTGRES__ ports:
|
|
12
|
+
__IF_POSTGRES__ - '5432:5432'
|
|
13
|
+
__IF_POSTGRES__ volumes:
|
|
14
|
+
__IF_POSTGRES__ - postgres_data:/var/lib/postgresql/data
|
|
15
|
+
__IF_POSTGRES__ healthcheck:
|
|
16
|
+
__IF_POSTGRES__ test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
17
|
+
__IF_POSTGRES__ interval: 10s
|
|
18
|
+
__IF_POSTGRES__ timeout: 5s
|
|
19
|
+
__IF_POSTGRES__ retries: 5
|
|
20
|
+
__IF_POSTGRES__
|
|
21
|
+
__IF_MONGODB__ mongodb:
|
|
22
|
+
__IF_MONGODB__ image: mongo:7-jammy
|
|
23
|
+
__IF_MONGODB__ container_name: __PROJECT_NAME__-mongodb
|
|
24
|
+
__IF_MONGODB__ environment:
|
|
25
|
+
__IF_MONGODB__ MONGO_INITDB_ROOT_USERNAME: root
|
|
26
|
+
__IF_MONGODB__ MONGO_INITDB_ROOT_PASSWORD: password
|
|
27
|
+
__IF_MONGODB__ MONGO_INITDB_DATABASE: __PROJECT_NAME__
|
|
28
|
+
__IF_MONGODB__ ports:
|
|
29
|
+
__IF_MONGODB__ - '27017:27017'
|
|
30
|
+
__IF_MONGODB__ volumes:
|
|
31
|
+
__IF_MONGODB__ - mongodb_data:/data/db
|
|
32
|
+
__IF_MONGODB__ healthcheck:
|
|
33
|
+
__IF_MONGODB__ test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/__PROJECT_NAME__ --quiet
|
|
34
|
+
__IF_MONGODB__ interval: 10s
|
|
35
|
+
__IF_MONGODB__ timeout: 5s
|
|
36
|
+
__IF_MONGODB__ retries: 5
|
|
37
|
+
__IF_MONGODB__
|
|
38
|
+
__IF_REDIS__ redis:
|
|
39
|
+
__IF_REDIS__ image: redis:7-alpine
|
|
40
|
+
__IF_REDIS__ container_name: __PROJECT_NAME__-redis
|
|
41
|
+
__IF_REDIS__ ports:
|
|
42
|
+
__IF_REDIS__ - '6379:6379'
|
|
43
|
+
__IF_REDIS__ volumes:
|
|
44
|
+
__IF_REDIS__ - redis_data:/data
|
|
45
|
+
__IF_REDIS__ healthcheck:
|
|
46
|
+
__IF_REDIS__ test: ['CMD', 'redis-cli', 'ping']
|
|
47
|
+
__IF_REDIS__ interval: 10s
|
|
48
|
+
__IF_REDIS__ timeout: 5s
|
|
49
|
+
__IF_REDIS__ retries: 5
|
|
50
|
+
__IF_REDIS__
|
|
51
|
+
__IF_SMTP__ mailhog:
|
|
52
|
+
__IF_SMTP__ image: mailhog/mailhog:latest
|
|
53
|
+
__IF_SMTP__ container_name: __PROJECT_NAME__-mailhog
|
|
54
|
+
__IF_SMTP__ ports:
|
|
55
|
+
__IF_SMTP__ - '1025:1025' # SMTP
|
|
56
|
+
__IF_SMTP__ - '8025:8025' # Web UI
|
|
57
|
+
__IF_SMTP__ healthcheck:
|
|
58
|
+
__IF_SMTP__ test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:8025']
|
|
59
|
+
__IF_SMTP__ interval: 10s
|
|
60
|
+
__IF_SMTP__ timeout: 5s
|
|
61
|
+
__IF_SMTP__ retries: 5
|
|
62
|
+
|
|
63
|
+
volumes:
|
|
64
|
+
__IF_POSTGRES__ postgres_data:
|
|
65
|
+
__IF_MONGODB__ mongodb_data:
|
|
66
|
+
__IF_REDIS__ redis_data:
|
|
67
|
+
|
|
68
|
+
networks:
|
|
69
|
+
default:
|
|
70
|
+
name: __PROJECT_NAME__-network
|