@tndhuy/create-app 1.0.0 → 1.1.4

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 CHANGED
@@ -26,6 +26,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/cli.ts
27
27
  var import_prompts = require("@clack/prompts");
28
28
  var import_path3 = require("path");
29
+ var import_promises3 = require("fs/promises");
30
+ var import_execa = require("execa");
29
31
 
30
32
  // src/scaffold.ts
31
33
  var import_promises2 = require("fs/promises");
@@ -185,8 +187,8 @@ async function replaceFileContents(dir, replacements) {
185
187
  async function renamePathsWithPlaceholders(dir, replacements) {
186
188
  const allPaths = await collectPaths(dir);
187
189
  allPaths.sort((a, b) => {
188
- const depthA = a.split("/").length;
189
- const depthB = b.split("/").length;
190
+ const depthA = a.split(import_path2.sep).length;
191
+ const depthB = b.split(import_path2.sep).length;
190
192
  return depthB - depthA;
191
193
  });
192
194
  for (const oldPath of allPaths) {
@@ -214,17 +216,39 @@ async function patchPackageJson(destDir, options) {
214
216
  const raw = await (0, import_promises2.readFile)(pkgPath, "utf-8");
215
217
  const pkg = JSON.parse(raw);
216
218
  pkg.name = options.serviceName;
219
+ const PRISMA_LATEST = "^7.5.0";
220
+ const PRISMA_MONGO_COMPAT = "^6.0.0";
217
221
  if (options.db === "mongo") {
222
+ if (options.orm === "prisma") {
223
+ if (pkg.dependencies) {
224
+ delete pkg.dependencies["@prisma/adapter-pg"];
225
+ delete pkg.dependencies["pg"];
226
+ delete pkg.dependencies["@types/pg"];
227
+ pkg.dependencies["@prisma/client"] = PRISMA_MONGO_COMPAT;
228
+ }
229
+ if (pkg.devDependencies) {
230
+ pkg.devDependencies["prisma"] = PRISMA_MONGO_COMPAT;
231
+ }
232
+ } else {
233
+ if (pkg.dependencies) {
234
+ delete pkg.dependencies["@prisma/client"];
235
+ delete pkg.dependencies["@prisma/adapter-pg"];
236
+ delete pkg.dependencies["pg"];
237
+ delete pkg.dependencies["@types/pg"];
238
+ pkg.dependencies["mongoose"] = "^8.0.0";
239
+ pkg.dependencies["@nestjs/mongoose"] = "^11.0.0";
240
+ }
241
+ if (pkg.devDependencies) {
242
+ delete pkg.devDependencies["prisma"];
243
+ }
244
+ }
245
+ } else {
218
246
  if (pkg.dependencies) {
219
- delete pkg.dependencies["@prisma/client"];
220
- delete pkg.dependencies["@prisma/adapter-pg"];
221
- delete pkg.dependencies["pg"];
222
- delete pkg.dependencies["@types/pg"];
223
- pkg.dependencies["mongoose"] = "^8.0.0";
224
- pkg.dependencies["@nestjs/mongoose"] = "^11.0.0";
247
+ pkg.dependencies["@prisma/client"] = PRISMA_LATEST;
248
+ pkg.dependencies["@prisma/adapter-pg"] = PRISMA_LATEST;
225
249
  }
226
250
  if (pkg.devDependencies) {
227
- delete pkg.devDependencies["prisma"];
251
+ pkg.devDependencies["prisma"] = PRISMA_LATEST;
228
252
  }
229
253
  }
230
254
  if (options.modules.includes("kafka")) {
@@ -263,7 +287,8 @@ async function pathExists(p) {
263
287
  async function safeDeleteFile(destDir, filePath) {
264
288
  const resolved = (0, import_path2.resolve)(filePath);
265
289
  const resolvedDestDir = (0, import_path2.resolve)(destDir);
266
- if (!resolved.startsWith(resolvedDestDir + "/") && resolved !== resolvedDestDir) {
290
+ const prefix = resolvedDestDir.endsWith(import_path2.sep) ? resolvedDestDir : resolvedDestDir + import_path2.sep;
291
+ if (!resolved.startsWith(prefix) && resolved !== resolvedDestDir) {
267
292
  throw new Error(`Security: path '${filePath}' is outside destDir '${destDir}'`);
268
293
  }
269
294
  try {
@@ -274,7 +299,8 @@ async function safeDeleteFile(destDir, filePath) {
274
299
  async function safeDeleteDir(destDir, dirPath) {
275
300
  const resolved = (0, import_path2.resolve)(dirPath);
276
301
  const resolvedDestDir = (0, import_path2.resolve)(destDir);
277
- if (!resolved.startsWith(resolvedDestDir + "/") && resolved !== resolvedDestDir) {
302
+ const prefix = resolvedDestDir.endsWith(import_path2.sep) ? resolvedDestDir : resolvedDestDir + import_path2.sep;
303
+ if (!resolved.startsWith(prefix) && resolved !== resolvedDestDir) {
278
304
  throw new Error(`Security: path '${dirPath}' is outside destDir '${destDir}'`);
279
305
  }
280
306
  try {
@@ -402,8 +428,8 @@ $1`
402
428
  /(imports:\s*\[[^\]]*?)(\s*\])/s,
403
429
  (match, arrayContent, closing) => {
404
430
  const trimmed = arrayContent.trimEnd();
405
- const sep = trimmed.endsWith(",") ? "" : ",";
406
- return `${trimmed}${sep}
431
+ const sep2 = trimmed.endsWith(",") ? "" : ",";
432
+ return `${trimmed}${sep2}
407
433
  KafkaModule,${closing}`;
408
434
  }
409
435
  );
@@ -425,13 +451,50 @@ $1`
425
451
  }
426
452
  }
427
453
  async function scaffold(options) {
428
- const templateDir = (0, import_path2.join)(__dirname, "..", "templates", options.db);
429
- const { destDir, serviceName, modules } = options;
454
+ const templateName = options.orm === "prisma" ? "postgres" : options.db;
455
+ const templateDir = (0, import_path2.join)(__dirname, "..", "templates", templateName);
456
+ const { destDir, serviceName, modules, dryRun } = options;
457
+ if (dryRun) {
458
+ console.log(`
459
+ [Dry Run] Would copy template from ${templateName} to ${destDir}`);
460
+ console.log(` [Dry Run] Would replace placeholders for: ${serviceName}`);
461
+ console.log(` [Dry Run] Would apply modules: ${modules.join(", ") || "none"}
462
+ `);
463
+ return;
464
+ }
430
465
  await (0, import_promises2.cp)(templateDir, destDir, { recursive: true });
431
466
  const replacements = buildReplacements(serviceName);
432
467
  await replaceFileContents(destDir, replacements);
433
468
  await renamePathsWithPlaceholders(destDir, replacements);
434
469
  await patchPackageJson(destDir, options);
470
+ if (options.db === "mongo" && options.orm === "prisma") {
471
+ const schemaPath = (0, import_path2.join)(destDir, "prisma", "schema.prisma");
472
+ if (await pathExists(schemaPath)) {
473
+ try {
474
+ let content = await (0, import_promises2.readFile)(schemaPath, "utf-8");
475
+ content = content.replace(/provider\s*=\s*["']postgresql["']/g, 'provider = "mongodb"');
476
+ content = content.replace(
477
+ /id\s+String\s+@id/g,
478
+ 'id String @id @default(auto()) @map("_id") @db.ObjectId'
479
+ );
480
+ await (0, import_promises2.writeFile)(schemaPath, content, "utf-8");
481
+ } catch (err) {
482
+ console.warn("Warning: Could not update schema.prisma for MongoDB:", err);
483
+ }
484
+ }
485
+ const envPath = (0, import_path2.join)(destDir, ".env.example");
486
+ if (await pathExists(envPath)) {
487
+ try {
488
+ let content = await (0, import_promises2.readFile)(envPath, "utf-8");
489
+ content = content.replace(
490
+ /DATABASE_URL=postgresql:\/\/.*/g,
491
+ 'DATABASE_URL="mongodb+srv://user:password@cluster.mongodb.net/myDatabase?retryWrites=true&w=majority"'
492
+ );
493
+ await (0, import_promises2.writeFile)(envPath, content, "utf-8");
494
+ } catch {
495
+ }
496
+ }
497
+ }
435
498
  if (!modules.includes("redis")) {
436
499
  await removeRedis(destDir);
437
500
  }
@@ -451,81 +514,213 @@ function guardCancel(value) {
451
514
  }
452
515
  return value;
453
516
  }
517
+ async function directoryExists(p) {
518
+ try {
519
+ await (0, import_promises3.stat)(p);
520
+ return true;
521
+ } catch {
522
+ return false;
523
+ }
524
+ }
454
525
  async function main() {
455
- (0, import_prompts.intro)("create-app -- NestJS DDD scaffolder");
456
- const argName = process.argv[2];
457
- let serviceName;
458
- if (argName && !validateServiceName(argName)) {
459
- serviceName = argName;
460
- console.log(` Service name : ${serviceName} (from arguments)`);
461
- } else {
462
- serviceName = guardCancel(
463
- await (0, import_prompts.text)({
464
- message: "Service name (kebab-case)",
465
- placeholder: "my-service",
466
- validate: (v) => validateServiceName(v)
467
- })
468
- );
469
- }
470
- const db = guardCancel(
471
- await (0, import_prompts.select)({
472
- message: "Select database",
473
- options: [
474
- { value: "postgres", label: "PostgreSQL", hint: "default" },
475
- { value: "mongo", label: "MongoDB" }
476
- ]
477
- })
478
- );
479
- const modules = guardCancel(
480
- await (0, import_prompts.multiselect)({
481
- message: "Optional modules (space to toggle, enter to confirm)",
482
- options: [
483
- { value: "redis", label: "Redis", hint: "caching + circuit breaker" },
484
- { value: "otel", label: "OpenTelemetry", hint: "traces + metrics" },
485
- { value: "kafka", label: "Kafka", hint: "message broker boilerplate" }
486
- ],
487
- required: false
488
- })
489
- );
490
- const selectedModules = modules.length > 0 ? modules.join(", ") : "none";
491
- console.log("");
492
- console.log(` Service name : ${serviceName}`);
493
- console.log(` Database : ${db}`);
494
- console.log(` Modules : ${selectedModules}`);
495
- console.log("");
496
- const proceed = guardCancel(
497
- await (0, import_prompts.confirm)({
498
- message: "Scaffold project with these settings?",
499
- initialValue: true
500
- })
501
- );
502
- if (!proceed) {
503
- (0, import_prompts.cancel)("Scaffolding cancelled.");
504
- process.exit(0);
526
+ const args = process.argv.slice(2);
527
+ const dryRun = args.includes("--dry-run");
528
+ const positionalName = args.find((a) => !a.startsWith("--"));
529
+ (0, import_prompts.intro)("create-app -- NestJS DDD scaffolder" + (dryRun ? " [DRY RUN]" : ""));
530
+ const config = {
531
+ serviceName: "",
532
+ db: "postgres",
533
+ orm: "mongoose",
534
+ modules: []
535
+ };
536
+ if (positionalName && !validateServiceName(positionalName)) {
537
+ config.serviceName = positionalName;
538
+ }
539
+ let step = config.serviceName ? 1 : 0;
540
+ const totalSteps = 4;
541
+ while (step < totalSteps) {
542
+ switch (step) {
543
+ case 0: {
544
+ const res = guardCancel(
545
+ await (0, import_prompts.text)({
546
+ message: "Service name (kebab-case)",
547
+ placeholder: "my-service",
548
+ validate: (v) => validateServiceName(v)
549
+ })
550
+ );
551
+ config.serviceName = res;
552
+ step++;
553
+ break;
554
+ }
555
+ case 1: {
556
+ const res = guardCancel(
557
+ await (0, import_prompts.select)({
558
+ message: "Select database",
559
+ options: [
560
+ { value: "postgres", label: "PostgreSQL", hint: "using Prisma" },
561
+ { value: "mongo", label: "MongoDB", hint: "choice of Mongoose or Prisma" },
562
+ { value: "_back", label: "Go Back", hint: "return to service name" }
563
+ ]
564
+ })
565
+ );
566
+ if (res === "_back") {
567
+ step--;
568
+ } else {
569
+ config.db = res;
570
+ if (res === "mongo") {
571
+ config.orm = guardCancel(
572
+ await (0, import_prompts.select)({
573
+ message: "Select MongoDB ORM",
574
+ options: [
575
+ { value: "mongoose", label: "Mongoose", hint: "default for NestJS" },
576
+ { value: "prisma", label: "Prisma", hint: "v6 compatible mode" }
577
+ ]
578
+ })
579
+ );
580
+ } else {
581
+ config.orm = "prisma";
582
+ }
583
+ step++;
584
+ }
585
+ break;
586
+ }
587
+ case 2: {
588
+ const res = guardCancel(
589
+ await (0, import_prompts.multiselect)({
590
+ message: "Optional modules (space to toggle, enter to confirm)",
591
+ options: [
592
+ { value: "redis", label: "Redis", hint: "caching + circuit breaker" },
593
+ { value: "otel", label: "OpenTelemetry", hint: "traces + metrics" },
594
+ { value: "kafka", label: "Kafka", hint: "message broker boilerplate" },
595
+ { value: "_back", label: "Go Back", hint: "return to database selection" }
596
+ ],
597
+ required: false
598
+ })
599
+ );
600
+ if (res.includes("_back")) {
601
+ step--;
602
+ } else {
603
+ config.modules = res;
604
+ step++;
605
+ }
606
+ break;
607
+ }
608
+ case 3: {
609
+ const selectedModules = config.modules.length > 0 ? config.modules.join(", ") : "none";
610
+ (0, import_prompts.note)(
611
+ `Service name : ${config.serviceName}
612
+ Database : ${config.db}
613
+ ORM : ${config.orm}
614
+ Modules : ${selectedModules}`,
615
+ "Confirm Project Summary"
616
+ );
617
+ const res = guardCancel(
618
+ await (0, import_prompts.select)({
619
+ message: "Scaffold project with these settings?",
620
+ options: [
621
+ { value: "confirm", label: "Yes, scaffold project" },
622
+ { value: "back", label: "No, let me change something", hint: "go back" },
623
+ { value: "cancel", label: "Cancel", hint: "exit" }
624
+ ]
625
+ })
626
+ );
627
+ if (res === "confirm") {
628
+ const destDir2 = (0, import_path3.join)(process.cwd(), config.serviceName);
629
+ if (await directoryExists(destDir2) && !dryRun) {
630
+ const overwrite = guardCancel(
631
+ await (0, import_prompts.select)({
632
+ message: `Directory "${config.serviceName}" already exists.`,
633
+ options: [
634
+ { value: "overwrite", label: "Overwrite", hint: "danger: deletes existing directory" },
635
+ { value: "back", label: "Change service name", hint: "go back to step 1" },
636
+ { value: "cancel", label: "Exit", hint: "cancel" }
637
+ ]
638
+ })
639
+ );
640
+ if (overwrite === "overwrite") {
641
+ const confirmDelete = guardCancel(await (0, import_prompts.confirm)({ message: "Are you absolutely sure?", initialValue: false }));
642
+ if (confirmDelete) {
643
+ const s2 = (0, import_prompts.spinner)();
644
+ s2.start("Cleaning up existing directory...");
645
+ await (0, import_promises3.rm)(destDir2, { recursive: true, force: true });
646
+ s2.stop("Directory cleaned.");
647
+ step++;
648
+ }
649
+ } else if (overwrite === "back") {
650
+ step = 0;
651
+ } else {
652
+ (0, import_prompts.cancel)("Operation cancelled.");
653
+ process.exit(0);
654
+ }
655
+ } else {
656
+ step++;
657
+ }
658
+ } else if (res === "back") {
659
+ step--;
660
+ } else {
661
+ (0, import_prompts.cancel)("Scaffolding cancelled.");
662
+ process.exit(0);
663
+ }
664
+ break;
665
+ }
666
+ }
505
667
  }
506
- const destDir = (0, import_path3.join)(process.cwd(), serviceName);
668
+ const destDir = (0, import_path3.join)(process.cwd(), config.serviceName);
507
669
  const s = (0, import_prompts.spinner)();
508
670
  s.start("Scaffolding project...");
509
671
  try {
510
672
  await scaffold({
511
- serviceName,
512
- db,
513
- modules,
514
- destDir
673
+ serviceName: config.serviceName,
674
+ db: config.db,
675
+ orm: config.orm,
676
+ modules: config.modules,
677
+ destDir,
678
+ dryRun
515
679
  });
516
680
  s.stop("Project scaffolded!");
517
681
  } catch (err) {
518
682
  s.stop("Scaffolding failed.");
519
683
  throw err;
520
684
  }
685
+ if (!dryRun) {
686
+ const initGit = guardCancel(await (0, import_prompts.confirm)({ message: "Initialize git repository?", initialValue: true }));
687
+ if (initGit) {
688
+ const gs = (0, import_prompts.spinner)();
689
+ gs.start("Initializing git...");
690
+ try {
691
+ await (0, import_execa.execa)("git", ["init"], { cwd: destDir });
692
+ await (0, import_execa.execa)("git", ["add", "."], { cwd: destDir });
693
+ await (0, import_execa.execa)("git", ["commit", "-m", "chore: initial commit from template"], { cwd: destDir });
694
+ gs.stop("Git initialized with initial commit.");
695
+ } catch (err) {
696
+ gs.stop("Git initialization failed (check if git is installed).");
697
+ }
698
+ }
699
+ const installDeps = guardCancel(await (0, import_prompts.confirm)({ message: "Install dependencies now?", initialValue: true }));
700
+ if (installDeps) {
701
+ const pkgManager = guardCancel(await (0, import_prompts.select)({
702
+ message: "Select package manager",
703
+ options: [
704
+ { value: "pnpm", label: "pnpm", hint: "recommended" },
705
+ { value: "npm", label: "npm" },
706
+ { value: "yarn", label: "yarn" }
707
+ ]
708
+ }));
709
+ const is = (0, import_prompts.spinner)();
710
+ is.start(`Installing dependencies using ${pkgManager}...`);
711
+ try {
712
+ await (0, import_execa.execa)(pkgManager, ["install"], { cwd: destDir, stdio: "inherit" });
713
+ is.stop("Dependencies installed successfully.");
714
+ } catch (err) {
715
+ is.stop("Dependency installation failed.");
716
+ }
717
+ }
718
+ }
521
719
  (0, import_prompts.outro)(
522
720
  `Next steps:
523
721
 
524
- cd ${serviceName}
525
- npm install
526
- cp .env.example .env
527
- npm run start:dev
528
- `
722
+ cd ${config.serviceName}
723
+ ${dryRun ? "" : " npm run start:dev\n"}`
529
724
  );
530
725
  }
531
726
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tndhuy/create-app",
3
- "version": "1.0.0",
3
+ "version": "1.1.4",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "create-app": "dist/cli.js"
@@ -4,6 +4,7 @@ generator client {
4
4
 
5
5
  datasource db {
6
6
  provider = "postgresql"
7
+ url = env("DATABASE_URL")
7
8
  }
8
9
 
9
10
  model Item {