create-blitzpack 0.1.9 → 0.1.11

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.
Files changed (2) hide show
  1. package/dist/index.js +331 -16
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -155,6 +155,65 @@ var REPLACEABLE_FILES = [
155
155
  "README.md"
156
156
  ];
157
157
  var DEFAULT_DESCRIPTION = "A full-stack TypeScript monorepo built with Blitzpack";
158
+ var OPTIONAL_FEATURES = [
159
+ {
160
+ key: "testing",
161
+ name: "Testing",
162
+ description: "vitest, integration tests, test helpers"
163
+ },
164
+ {
165
+ key: "admin",
166
+ name: "Admin Dashboard",
167
+ description: "user management, stats, sessions admin"
168
+ },
169
+ {
170
+ key: "uploads",
171
+ name: "File Uploads",
172
+ description: "S3 storage, upload routes, file components"
173
+ }
174
+ ];
175
+ var FEATURE_EXCLUSIONS = {
176
+ testing: [
177
+ "vitest.workspace.ts",
178
+ "vitest.shared.ts",
179
+ "apps/api/test",
180
+ "apps/api/vitest.config.ts",
181
+ "apps/web/src/test",
182
+ "apps/web/vitest.config.ts",
183
+ "packages/types/src/__tests__",
184
+ "packages/types/vitest.config.ts",
185
+ "packages/utils/src/__tests__",
186
+ "packages/utils/vitest.config.ts",
187
+ "packages/ui/src/__tests__",
188
+ "packages/ui/vitest.config.ts",
189
+ "packages/ui/vitest.setup.ts",
190
+ "packages/ui/test-config.js",
191
+ "apps/web/src/hooks/api/__tests__"
192
+ ],
193
+ admin: [
194
+ "apps/web/src/app/(admin)",
195
+ "apps/web/src/components/admin",
196
+ "apps/web/src/hooks/api/use-admin-sessions.ts",
197
+ "apps/web/src/hooks/api/use-admin-stats.ts",
198
+ "apps/web/src/hooks/use-realtime-metrics.ts",
199
+ "apps/api/src/routes/admin-sessions.ts",
200
+ "apps/api/src/routes/stats.ts",
201
+ "apps/api/src/routes/metrics.ts",
202
+ "apps/api/src/services/stats.service.ts",
203
+ "apps/api/src/services/metrics.service.ts",
204
+ "packages/types/src/stats.ts"
205
+ ],
206
+ uploads: [
207
+ "apps/api/src/routes/uploads.ts",
208
+ "apps/api/src/routes/uploads-serve.ts",
209
+ "apps/api/src/services/uploads.service.ts",
210
+ "apps/api/src/services/file-storage.service.ts",
211
+ "apps/api/public/uploads",
212
+ "apps/web/src/hooks/api/use-uploads.ts",
213
+ "packages/ui/src/file-upload-input.tsx",
214
+ "packages/types/src/upload.ts"
215
+ ]
216
+ };
158
217
 
159
218
  // src/utils.ts
160
219
  import chalk2 from "chalk";
@@ -310,6 +369,10 @@ async function getProjectOptions(providedName, flags = {}) {
310
369
  console.log(`Invalid project name: ${validation.problems?.[0]}`);
311
370
  return null;
312
371
  }
372
+ const features = await promptFeatureSelection();
373
+ if (!features) {
374
+ return null;
375
+ }
313
376
  const useCurrentDir = projectName === ".";
314
377
  const actualProjectName = useCurrentDir ? getCurrentDirName() : projectName;
315
378
  return {
@@ -318,7 +381,40 @@ async function getProjectOptions(providedName, flags = {}) {
318
381
  projectDescription: response.projectDescription || DEFAULT_DESCRIPTION,
319
382
  skipGit: flags.skipGit || false,
320
383
  skipInstall: flags.skipInstall || false,
321
- useCurrentDir
384
+ useCurrentDir,
385
+ features
386
+ };
387
+ }
388
+ async function promptFeatureSelection() {
389
+ const featureChoices = OPTIONAL_FEATURES.map((feature) => ({
390
+ title: `${feature.name} ${chalk3.dim(`(${feature.description})`)}`,
391
+ value: feature.key,
392
+ selected: true
393
+ }));
394
+ let cancelled = false;
395
+ const { selectedFeatures } = await prompts(
396
+ {
397
+ type: "multiselect",
398
+ name: "selectedFeatures",
399
+ message: "Include optional features:",
400
+ choices: featureChoices,
401
+ hint: "- Space to toggle, Enter to confirm",
402
+ instructions: false
403
+ },
404
+ {
405
+ onCancel: () => {
406
+ cancelled = true;
407
+ }
408
+ }
409
+ );
410
+ if (cancelled) {
411
+ return null;
412
+ }
413
+ const selected = selectedFeatures || [];
414
+ return {
415
+ testing: selected.includes("testing"),
416
+ admin: selected.includes("admin"),
417
+ uploads: selected.includes("uploads")
322
418
  };
323
419
  }
324
420
  async function promptAutomaticSetup() {
@@ -357,15 +453,25 @@ var POST_DOWNLOAD_EXCLUDES = [
357
453
  "Dockerfile",
358
454
  "docker-compose.prod.yml"
359
455
  ];
360
- async function cleanupExcludes(targetDir) {
361
- for (const exclude of POST_DOWNLOAD_EXCLUDES) {
456
+ function getFeatureExclusions(features) {
457
+ const exclusions = [];
458
+ for (const [key, enabled] of Object.entries(features)) {
459
+ if (!enabled) {
460
+ exclusions.push(...FEATURE_EXCLUSIONS[key]);
461
+ }
462
+ }
463
+ return exclusions;
464
+ }
465
+ async function cleanupExcludes(targetDir, additionalExcludes = []) {
466
+ const allExcludes = [...POST_DOWNLOAD_EXCLUDES, ...additionalExcludes];
467
+ for (const exclude of allExcludes) {
362
468
  const fullPath = path2.join(targetDir, exclude);
363
469
  if (await fs.pathExists(fullPath)) {
364
470
  await fs.remove(fullPath);
365
471
  }
366
472
  }
367
473
  }
368
- async function downloadAndPrepareTemplate(targetDir, spinner) {
474
+ async function downloadAndPrepareTemplate(targetDir, spinner, features) {
369
475
  spinner.text = "Fetching template from GitHub...";
370
476
  await downloadTemplate(GITHUB_REPO, {
371
477
  dir: targetDir,
@@ -374,7 +480,8 @@ async function downloadAndPrepareTemplate(targetDir, spinner) {
374
480
  spinner.text = "Extracting files...";
375
481
  await new Promise((resolve) => setTimeout(resolve, 100));
376
482
  spinner.text = "Cleaning up template files...";
377
- await cleanupExcludes(targetDir);
483
+ const featureExclusions = getFeatureExclusions(features);
484
+ await cleanupExcludes(targetDir, featureExclusions);
378
485
  const files = await countFiles(targetDir);
379
486
  spinner.succeed(`Downloaded template (${files} files)`);
380
487
  }
@@ -400,7 +507,25 @@ async function countFiles(dir) {
400
507
  // src/transform.ts
401
508
  import fs2 from "fs-extra";
402
509
  import path3 from "path";
403
- function transformPackageJson(content, vars, filePath) {
510
+ var TESTING_SCRIPTS = [
511
+ "test",
512
+ "test:unit",
513
+ "test:integration",
514
+ "test:watch",
515
+ "test:coverage",
516
+ "test:parallel"
517
+ ];
518
+ var TESTING_ROOT_DEVDEPS = [
519
+ "@testing-library/jest-dom",
520
+ "@testing-library/react",
521
+ "@testing-library/user-event",
522
+ "@vitest/coverage-v8",
523
+ "jsdom",
524
+ "vitest"
525
+ ];
526
+ var TESTING_APP_DEVDEPS = ["vitest", "vite-tsconfig-paths"];
527
+ var UPLOADS_API_DEPS = ["@aws-sdk/client-s3", "sharp"];
528
+ function transformPackageJson(content, vars, filePath, features) {
404
529
  const pkg = JSON.parse(content);
405
530
  if (filePath === "package.json") {
406
531
  pkg.name = vars.projectSlug;
@@ -409,6 +534,31 @@ function transformPackageJson(content, vars, filePath) {
409
534
  delete pkg.homepage;
410
535
  delete pkg.scripts?.["init:project"];
411
536
  pkg.version = "0.1.0";
537
+ if (!features.testing) {
538
+ for (const script of TESTING_SCRIPTS) {
539
+ delete pkg.scripts?.[script];
540
+ }
541
+ for (const dep of TESTING_ROOT_DEVDEPS) {
542
+ delete pkg.devDependencies?.[dep];
543
+ }
544
+ }
545
+ }
546
+ if (filePath === "apps/api/package.json" || filePath === "apps/web/package.json") {
547
+ if (!features.testing) {
548
+ for (const script of TESTING_SCRIPTS) {
549
+ delete pkg.scripts?.[script];
550
+ }
551
+ for (const dep of TESTING_APP_DEVDEPS) {
552
+ delete pkg.devDependencies?.[dep];
553
+ }
554
+ }
555
+ }
556
+ if (filePath === "apps/api/package.json") {
557
+ if (!features.uploads) {
558
+ for (const dep of UPLOADS_API_DEPS) {
559
+ delete pkg.dependencies?.[dep];
560
+ }
561
+ }
412
562
  }
413
563
  return JSON.stringify(pkg, null, 2) + "\n";
414
564
  }
@@ -475,8 +625,13 @@ pnpm db:seed # Seed database
475
625
  Built with [Blitzpack](https://github.com/CarboxyDev/blitzpack)
476
626
  `;
477
627
  }
478
- async function transformFiles(targetDir, vars) {
479
- for (const relativePath of REPLACEABLE_FILES) {
628
+ async function transformFiles(targetDir, vars, features) {
629
+ const filesToTransform = [
630
+ ...REPLACEABLE_FILES,
631
+ "apps/api/package.json",
632
+ "apps/web/package.json"
633
+ ];
634
+ for (const relativePath of filesToTransform) {
480
635
  const filePath = path3.join(targetDir, relativePath);
481
636
  if (!await fs2.pathExists(filePath)) {
482
637
  continue;
@@ -486,7 +641,7 @@ async function transformFiles(targetDir, vars) {
486
641
  if (relativePath === "README.md") {
487
642
  transformed = generateReadme(vars);
488
643
  } else if (relativePath.endsWith("package.json")) {
489
- transformed = transformPackageJson(content, vars, relativePath);
644
+ transformed = transformPackageJson(content, vars, relativePath, features);
490
645
  } else if (relativePath.includes("site.ts")) {
491
646
  transformed = transformSiteConfig(content, vars);
492
647
  } else if (relativePath.includes("layout.tsx")) {
@@ -498,6 +653,148 @@ async function transformFiles(targetDir, vars) {
498
653
  }
499
654
  await fs2.writeFile(filePath, transformed, "utf-8");
500
655
  }
656
+ await applyFeatureTransforms(targetDir, features);
657
+ }
658
+ async function applyFeatureTransforms(targetDir, features) {
659
+ if (!features.testing) {
660
+ await transformForNoTesting(targetDir);
661
+ }
662
+ if (!features.admin) {
663
+ await transformForNoAdmin(targetDir);
664
+ }
665
+ if (!features.uploads) {
666
+ await transformForNoUploads(targetDir);
667
+ }
668
+ }
669
+ async function transformForNoTesting(targetDir) {
670
+ const turboPath = path3.join(targetDir, "turbo.json");
671
+ if (await fs2.pathExists(turboPath)) {
672
+ const content = await fs2.readFile(turboPath, "utf-8");
673
+ const turbo = JSON.parse(content);
674
+ delete turbo.tasks?.test;
675
+ delete turbo.tasks?.["test:unit"];
676
+ delete turbo.tasks?.["test:integration"];
677
+ delete turbo.tasks?.["test:watch"];
678
+ delete turbo.tasks?.["test:coverage"];
679
+ await fs2.writeFile(turboPath, JSON.stringify(turbo, null, 2) + "\n");
680
+ }
681
+ const huskyPath = path3.join(targetDir, ".husky/pre-push");
682
+ if (await fs2.pathExists(huskyPath)) {
683
+ await fs2.writeFile(huskyPath, "pnpm typecheck\n");
684
+ }
685
+ }
686
+ async function transformForNoAdmin(targetDir) {
687
+ const appPath = path3.join(targetDir, "apps/api/src/app.ts");
688
+ if (await fs2.pathExists(appPath)) {
689
+ let content = await fs2.readFile(appPath, "utf-8");
690
+ content = content.replace(
691
+ /import { metricsService } from '@\/services\/metrics\.service';\n/,
692
+ ""
693
+ );
694
+ content = content.replace(
695
+ /const { default: statsRoutes } = await import\('@\/routes\/stats\.js'\);\n/,
696
+ ""
697
+ );
698
+ content = content.replace(
699
+ /const { default: metricsRoutes } = await import\('@\/routes\/metrics\.js'\);\n/,
700
+ ""
701
+ );
702
+ content = content.replace(
703
+ /const { default: adminSessionsRoutes } = await import\(\n\s*'@\/routes\/admin-sessions\.js'\n\s*\);\n/,
704
+ ""
705
+ );
706
+ content = content.replace(/metricsService\.start\(\);\n\n/, "");
707
+ content = content.replace(
708
+ /\s*metricsService\.recordRequest\(responseTime, reply\.statusCode\);\n/,
709
+ ""
710
+ );
711
+ content = content.replace(/\s*await app\.register\(statsRoutes\);/g, "");
712
+ content = content.replace(/\s*await app\.register\(metricsRoutes\);/g, "");
713
+ content = content.replace(
714
+ /\s*await app\.register\(adminSessionsRoutes\);/g,
715
+ ""
716
+ );
717
+ await fs2.writeFile(appPath, content);
718
+ }
719
+ const servicesPath = path3.join(targetDir, "apps/api/src/plugins/services.ts");
720
+ if (await fs2.pathExists(servicesPath)) {
721
+ let content = await fs2.readFile(servicesPath, "utf-8");
722
+ content = content.replace(
723
+ /import { StatsService } from '@\/services\/stats\.service';\n/,
724
+ ""
725
+ );
726
+ content = content.replace(
727
+ /\s*const statsService = new StatsService\(app\.prisma, app\.logger\);/,
728
+ ""
729
+ );
730
+ content = content.replace(
731
+ /\s*app\.decorate\('statsService', statsService\);/,
732
+ ""
733
+ );
734
+ content = content.replace(/\s*statsService: StatsService;/, "");
735
+ await fs2.writeFile(servicesPath, content);
736
+ }
737
+ }
738
+ async function transformForNoUploads(targetDir) {
739
+ const appPath = path3.join(targetDir, "apps/api/src/app.ts");
740
+ if (await fs2.pathExists(appPath)) {
741
+ let content = await fs2.readFile(appPath, "utf-8");
742
+ content = content.replace(
743
+ /const { default: uploadsRoutes } = await import\('@\/routes\/uploads\.js'\);\n/,
744
+ ""
745
+ );
746
+ content = content.replace(
747
+ /const { default: uploadsServeRoutes } = await import\(\n\s*'@\/routes\/uploads-serve\.js'\n\s*\);\n/,
748
+ ""
749
+ );
750
+ content = content.replace(
751
+ /await app\.register\(uploadsServeRoutes\);\n\n/,
752
+ ""
753
+ );
754
+ content = content.replace(/\s*await app\.register\(uploadsRoutes\);/g, "");
755
+ await fs2.writeFile(appPath, content);
756
+ }
757
+ const servicesPath = path3.join(targetDir, "apps/api/src/plugins/services.ts");
758
+ if (await fs2.pathExists(servicesPath)) {
759
+ let content = await fs2.readFile(servicesPath, "utf-8");
760
+ content = content.replace(
761
+ /import { FileStorageService } from '@\/services\/file-storage\.service';\n/,
762
+ ""
763
+ );
764
+ content = content.replace(
765
+ /import { UploadsService } from '@\/services\/uploads\.service';\n/,
766
+ ""
767
+ );
768
+ content = content.replace(
769
+ /\s*const fileStorageService = new FileStorageService\(env, app\.logger\);/,
770
+ ""
771
+ );
772
+ content = content.replace(
773
+ /\s*const uploadsService = new UploadsService\(\n\s*app\.prisma,\n\s*fileStorageService,\n\s*app\.logger\n\s*\);/,
774
+ ""
775
+ );
776
+ content = content.replace(
777
+ /\s*app\.decorate\('fileStorageService', fileStorageService\);/,
778
+ ""
779
+ );
780
+ content = content.replace(
781
+ /\s*app\.decorate\('uploadsService', uploadsService\);/,
782
+ ""
783
+ );
784
+ content = content.replace(/\s*fileStorageService: FileStorageService;/, "");
785
+ content = content.replace(/\s*uploadsService: UploadsService;/, "");
786
+ await fs2.writeFile(servicesPath, content);
787
+ }
788
+ const schemaPath = path3.join(targetDir, "apps/api/prisma/schema.prisma");
789
+ if (await fs2.pathExists(schemaPath)) {
790
+ let content = await fs2.readFile(schemaPath, "utf-8");
791
+ content = content.replace(/\s*uploads\s+Upload\[\]/, "");
792
+ content = content.replace(
793
+ /\n\nmodel Upload \{[\s\S]*?@@map\("uploads"\)\n\}/,
794
+ ""
795
+ );
796
+ await fs2.writeFile(schemaPath, content);
797
+ }
501
798
  }
502
799
 
503
800
  // src/commands/create.ts
@@ -537,6 +834,19 @@ function printDryRun(options) {
537
834
  ` ${chalk4.cyan("Description:")} ${options.projectDescription}`
538
835
  );
539
836
  console.log();
837
+ console.log(chalk4.bold(" Features:"));
838
+ console.log();
839
+ const featureStatus = (enabled) => enabled ? chalk4.green("\u2713") : chalk4.red("\u2717");
840
+ console.log(
841
+ ` ${featureStatus(options.features.testing)} Testing ${chalk4.dim("(vitest, integration tests)")}`
842
+ );
843
+ console.log(
844
+ ` ${featureStatus(options.features.admin)} Admin Dashboard ${chalk4.dim("(user management, stats)")}`
845
+ );
846
+ console.log(
847
+ ` ${featureStatus(options.features.uploads)} File Uploads ${chalk4.dim("(S3 storage, upload routes)")}`
848
+ );
849
+ console.log();
540
850
  console.log(chalk4.bold(" Would run:"));
541
851
  console.log();
542
852
  console.log(` ${chalk4.dim("\u2022")} Download template from GitHub`);
@@ -568,7 +878,8 @@ async function create(projectName, flags) {
568
878
  projectDescription: options.projectDescription,
569
879
  targetDir,
570
880
  skipGit: options.skipGit,
571
- skipInstall: options.skipInstall
881
+ skipInstall: options.skipInstall,
882
+ features: options.features
572
883
  });
573
884
  return;
574
885
  }
@@ -594,13 +905,17 @@ async function create(projectName, flags) {
594
905
  const spinner = ora();
595
906
  try {
596
907
  spinner.start("Downloading template from GitHub...");
597
- await downloadAndPrepareTemplate(targetDir, spinner);
908
+ await downloadAndPrepareTemplate(targetDir, spinner, options.features);
598
909
  spinner.start("Configuring project...");
599
- await transformFiles(targetDir, {
600
- projectName: options.projectName,
601
- projectSlug: options.projectSlug,
602
- projectDescription: options.projectDescription
603
- });
910
+ await transformFiles(
911
+ targetDir,
912
+ {
913
+ projectName: options.projectName,
914
+ projectSlug: options.projectSlug,
915
+ projectDescription: options.projectDescription
916
+ },
917
+ options.features
918
+ );
604
919
  await copyEnvFiles(targetDir);
605
920
  spinner.succeed("Configured project");
606
921
  if (!options.skipGit && isGitInstalled()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-blitzpack",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Create a new Blitzpack project - full-stack TypeScript monorepo with Next.js and Fastify",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,21 +16,21 @@
16
16
  "clean": "rm -rf dist"
17
17
  },
18
18
  "dependencies": {
19
- "chalk": "^5.4.1",
19
+ "chalk": "^5.6.2",
20
20
  "commander": "^13.1.0",
21
- "fs-extra": "^11.3.0",
21
+ "fs-extra": "^11.3.3",
22
22
  "giget": "^2.0.0",
23
23
  "ora": "^8.2.0",
24
24
  "prompts": "^2.4.2",
25
- "validate-npm-package-name": "^6.0.0"
25
+ "validate-npm-package-name": "^6.0.2"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/fs-extra": "^11.0.4",
29
- "@types/node": "^22.10.5",
29
+ "@types/node": "^22.19.3",
30
30
  "@types/prompts": "^2.4.9",
31
31
  "@types/validate-npm-package-name": "^4.0.2",
32
- "tsup": "^8.5.0",
33
- "typescript": "^5.7.2"
32
+ "tsup": "^8.5.1",
33
+ "typescript": "^5.9.3"
34
34
  },
35
35
  "keywords": [
36
36
  "create",