@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 +270 -75
- package/package.json +1 -1
- package/templates/postgres/prisma/schema.prisma +1 -0
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(
|
|
189
|
-
const depthB = b.split(
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
406
|
-
return `${trimmed}${
|
|
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
|
|
429
|
-
const
|
|
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
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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
|
|
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