create-better-t-stack 2.29.3 → 2.29.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/README.md +10 -13
- package/dist/index.js +223 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,14 +15,14 @@ A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with
|
|
|
15
15
|
Run without installing globally:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
# Using
|
|
19
|
-
npx create-better-t-stack@latest
|
|
20
|
-
|
|
21
|
-
# Using bun
|
|
18
|
+
# Using bun (recommended)
|
|
22
19
|
bun create better-t-stack@latest
|
|
23
20
|
|
|
24
21
|
# Using pnpm
|
|
25
22
|
pnpm create better-t-stack@latest
|
|
23
|
+
|
|
24
|
+
# Using npm
|
|
25
|
+
npx create-better-t-stack@latest
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Follow the prompts to configure your project or use the `--yes` flag for defaults.
|
|
@@ -58,14 +58,15 @@ Options:
|
|
|
58
58
|
--auth Include authentication
|
|
59
59
|
--no-auth Exclude authentication
|
|
60
60
|
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none)
|
|
61
|
-
--addons <types...> Additional addons (pwa, tauri, starlight, biome, husky, turborepo, none)
|
|
61
|
+
--addons <types...> Additional addons (pwa, tauri, starlight, biome, husky, turborepo, fumadocs, ultracite, oxlint, none)
|
|
62
62
|
--examples <types...> Examples to include (todo, ai, none)
|
|
63
63
|
--git Initialize git repository
|
|
64
64
|
--no-git Skip git initialization
|
|
65
65
|
--package-manager <pm> Package manager (npm, pnpm, bun)
|
|
66
66
|
--install Install dependencies
|
|
67
67
|
--no-install Skip installing dependencies
|
|
68
|
-
--db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, mongodb-atlas, none)
|
|
68
|
+
--db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, mongodb-atlas, docker, none)
|
|
69
|
+
--web-deploy <setup> Web deployment (workers, none)
|
|
69
70
|
--backend <framework> Backend framework (hono, express, elysia, next, convex, fastify, none)
|
|
70
71
|
--runtime <runtime> Runtime (bun, node, workers, none)
|
|
71
72
|
--api <type> API type (trpc, orpc, none)
|
|
@@ -84,7 +85,7 @@ This CLI collects anonymous usage data to help improve the tool. The data collec
|
|
|
84
85
|
|
|
85
86
|
### Disabling Telemetry
|
|
86
87
|
|
|
87
|
-
You can disable telemetry by setting the `
|
|
88
|
+
You can disable telemetry by setting the `BTS_TELEMETRY_DISABLED` environment variable:
|
|
88
89
|
|
|
89
90
|
```bash
|
|
90
91
|
# Disable telemetry for a single run
|
|
@@ -94,10 +95,6 @@ BTS_TELEMETRY_DISABLED=1 npx create-better-t-stack my-app
|
|
|
94
95
|
export BTS_TELEMETRY_DISABLED=1
|
|
95
96
|
```
|
|
96
97
|
|
|
97
|
-
### Development
|
|
98
|
-
|
|
99
|
-
During development, telemetry is automatically disabled when `NODE_ENV=development`.
|
|
100
|
-
|
|
101
98
|
## Examples
|
|
102
99
|
|
|
103
100
|
Create a project with default configuration:
|
|
@@ -118,10 +115,10 @@ Create a project with Elysia backend and Node.js runtime:
|
|
|
118
115
|
npx create-better-t-stack my-app --backend elysia --runtime node
|
|
119
116
|
```
|
|
120
117
|
|
|
121
|
-
Create a project with multiple frontend options:
|
|
118
|
+
Create a project with multiple frontend options (one web + one native):
|
|
122
119
|
|
|
123
120
|
```bash
|
|
124
|
-
npx create-better-t-stack my-app --frontend tanstack-router native
|
|
121
|
+
npx create-better-t-stack my-app --frontend tanstack-router native-nativewind
|
|
125
122
|
```
|
|
126
123
|
|
|
127
124
|
Create a project with examples:
|
package/dist/index.js
CHANGED
|
@@ -140,15 +140,6 @@ const ADDON_COMPATIBILITY = {
|
|
|
140
140
|
fumadocs: [],
|
|
141
141
|
none: []
|
|
142
142
|
};
|
|
143
|
-
const WEB_FRAMEWORKS = [
|
|
144
|
-
"tanstack-router",
|
|
145
|
-
"react-router",
|
|
146
|
-
"tanstack-start",
|
|
147
|
-
"next",
|
|
148
|
-
"nuxt",
|
|
149
|
-
"svelte",
|
|
150
|
-
"solid"
|
|
151
|
-
];
|
|
152
143
|
|
|
153
144
|
//#endregion
|
|
154
145
|
//#region src/types.ts
|
|
@@ -179,7 +170,7 @@ const RuntimeSchema = z.enum([
|
|
|
179
170
|
"node",
|
|
180
171
|
"workers",
|
|
181
172
|
"none"
|
|
182
|
-
]).describe("Runtime environment
|
|
173
|
+
]).describe("Runtime environment");
|
|
183
174
|
const FrontendSchema = z.enum([
|
|
184
175
|
"tanstack-router",
|
|
185
176
|
"react-router",
|
|
@@ -407,40 +398,180 @@ async function getAddonsToAdd(frontend, existingAddons = []) {
|
|
|
407
398
|
return response;
|
|
408
399
|
}
|
|
409
400
|
|
|
401
|
+
//#endregion
|
|
402
|
+
//#region src/utils/compatibility.ts
|
|
403
|
+
const WEB_FRAMEWORKS = [
|
|
404
|
+
"tanstack-router",
|
|
405
|
+
"react-router",
|
|
406
|
+
"tanstack-start",
|
|
407
|
+
"next",
|
|
408
|
+
"nuxt",
|
|
409
|
+
"svelte",
|
|
410
|
+
"solid"
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/utils/compatibility-rules.ts
|
|
415
|
+
function isWebFrontend(value) {
|
|
416
|
+
return WEB_FRAMEWORKS.includes(value);
|
|
417
|
+
}
|
|
418
|
+
function splitFrontends(values = []) {
|
|
419
|
+
const web = values.filter((f) => isWebFrontend(f));
|
|
420
|
+
const native = values.filter((f) => f === "native-nativewind" || f === "native-unistyles");
|
|
421
|
+
return {
|
|
422
|
+
web,
|
|
423
|
+
native
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function ensureSingleWebAndNative(frontends) {
|
|
427
|
+
const { web, native } = splitFrontends(frontends);
|
|
428
|
+
if (web.length > 1) {
|
|
429
|
+
consola$1.fatal("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
if (native.length > 1) {
|
|
433
|
+
consola$1.fatal("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function validateWorkersCompatibility(providedFlags, options, config) {
|
|
438
|
+
if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") {
|
|
439
|
+
consola$1.fatal(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") {
|
|
443
|
+
consola$1.fatal(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") {
|
|
447
|
+
consola$1.fatal(`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") {
|
|
451
|
+
consola$1.fatal(`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") {
|
|
455
|
+
consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") {
|
|
459
|
+
consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") {
|
|
463
|
+
consola$1.fatal("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
if (providedFlags.has("dbSetup") && options.dbSetup === "docker" && config.runtime === "workers") {
|
|
467
|
+
consola$1.fatal("Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function coerceBackendPresets(config) {
|
|
472
|
+
if (config.backend === "convex") {
|
|
473
|
+
config.auth = false;
|
|
474
|
+
config.database = "none";
|
|
475
|
+
config.orm = "none";
|
|
476
|
+
config.api = "none";
|
|
477
|
+
config.runtime = "none";
|
|
478
|
+
config.dbSetup = "none";
|
|
479
|
+
config.examples = ["todo"];
|
|
480
|
+
}
|
|
481
|
+
if (config.backend === "none") {
|
|
482
|
+
config.auth = false;
|
|
483
|
+
config.database = "none";
|
|
484
|
+
config.orm = "none";
|
|
485
|
+
config.api = "none";
|
|
486
|
+
config.runtime = "none";
|
|
487
|
+
config.dbSetup = "none";
|
|
488
|
+
config.examples = [];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function incompatibleFlagsForBackend(backend, providedFlags, options) {
|
|
492
|
+
const list = [];
|
|
493
|
+
if (backend === "convex") {
|
|
494
|
+
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
|
|
495
|
+
if (providedFlags.has("database") && options.database !== "none") list.push(`--database ${options.database}`);
|
|
496
|
+
if (providedFlags.has("orm") && options.orm !== "none") list.push(`--orm ${options.orm}`);
|
|
497
|
+
if (providedFlags.has("api") && options.api !== "none") list.push(`--api ${options.api}`);
|
|
498
|
+
if (providedFlags.has("runtime") && options.runtime !== "none") list.push(`--runtime ${options.runtime}`);
|
|
499
|
+
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") list.push(`--db-setup ${options.dbSetup}`);
|
|
500
|
+
}
|
|
501
|
+
if (backend === "none") {
|
|
502
|
+
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
|
|
503
|
+
if (providedFlags.has("database") && options.database !== "none") list.push(`--database ${options.database}`);
|
|
504
|
+
if (providedFlags.has("orm") && options.orm !== "none") list.push(`--orm ${options.orm}`);
|
|
505
|
+
if (providedFlags.has("api") && options.api !== "none") list.push(`--api ${options.api}`);
|
|
506
|
+
if (providedFlags.has("runtime") && options.runtime !== "none") list.push(`--runtime ${options.runtime}`);
|
|
507
|
+
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") list.push(`--db-setup ${options.dbSetup}`);
|
|
508
|
+
if (providedFlags.has("examples") && options.examples) {
|
|
509
|
+
const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
|
|
510
|
+
if (hasNonNoneExamples) list.push("--examples");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return list;
|
|
514
|
+
}
|
|
515
|
+
function validateApiFrontendCompatibility(api, frontends = []) {
|
|
516
|
+
const includesNuxt = frontends.includes("nuxt");
|
|
517
|
+
const includesSvelte = frontends.includes("svelte");
|
|
518
|
+
const includesSolid = frontends.includes("solid");
|
|
519
|
+
if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") {
|
|
520
|
+
consola$1.fatal(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function isFrontendAllowedWithBackend(frontend, backend) {
|
|
525
|
+
if (backend === "convex" && frontend === "solid") return false;
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
function allowedApisForFrontends(frontends = []) {
|
|
529
|
+
const includesNuxt = frontends.includes("nuxt");
|
|
530
|
+
const includesSvelte = frontends.includes("svelte");
|
|
531
|
+
const includesSolid = frontends.includes("solid");
|
|
532
|
+
const base = [
|
|
533
|
+
"trpc",
|
|
534
|
+
"orpc",
|
|
535
|
+
"none"
|
|
536
|
+
];
|
|
537
|
+
if (includesNuxt || includesSvelte || includesSolid) return ["orpc", "none"];
|
|
538
|
+
return base;
|
|
539
|
+
}
|
|
540
|
+
function isExampleTodoAllowed(backend, database) {
|
|
541
|
+
return !(backend !== "convex" && backend !== "none" && database === "none");
|
|
542
|
+
}
|
|
543
|
+
function isExampleAIAllowed(backend, frontends = []) {
|
|
544
|
+
const includesSolid = frontends.includes("solid");
|
|
545
|
+
if (backend === "elysia") return false;
|
|
546
|
+
if (includesSolid) return false;
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
|
|
550
|
+
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) {
|
|
551
|
+
consola$1.fatal("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
410
556
|
//#endregion
|
|
411
557
|
//#region src/prompts/api.ts
|
|
412
558
|
async function getApiChoice(Api, frontend, backend) {
|
|
413
559
|
if (backend === "convex" || backend === "none") return "none";
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
label: "tRPC",
|
|
422
|
-
hint: "End-to-end typesafe APIs made easy"
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
value: "orpc",
|
|
426
|
-
label: "oRPC",
|
|
427
|
-
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
value: "none",
|
|
431
|
-
label: "None",
|
|
432
|
-
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
|
|
433
|
-
}
|
|
434
|
-
];
|
|
435
|
-
if (includesNuxt || includesSvelte || includesSolid) apiOptions = [{
|
|
560
|
+
const allowed = allowedApisForFrontends(frontend ?? []);
|
|
561
|
+
if (Api) return allowed.includes(Api) ? Api : allowed[0];
|
|
562
|
+
const apiOptions = allowed.map((a) => a === "trpc" ? {
|
|
563
|
+
value: "trpc",
|
|
564
|
+
label: "tRPC",
|
|
565
|
+
hint: "End-to-end typesafe APIs made easy"
|
|
566
|
+
} : a === "orpc" ? {
|
|
436
567
|
value: "orpc",
|
|
437
568
|
label: "oRPC",
|
|
438
|
-
hint:
|
|
439
|
-
}
|
|
569
|
+
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
|
|
570
|
+
} : {
|
|
440
571
|
value: "none",
|
|
441
572
|
label: "None",
|
|
442
|
-
hint: "No API layer"
|
|
443
|
-
}
|
|
573
|
+
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
|
|
574
|
+
});
|
|
444
575
|
const apiType = await select({
|
|
445
576
|
message: "Select API type",
|
|
446
577
|
options: apiOptions,
|
|
@@ -670,21 +801,23 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
|
670
801
|
const noFrontendSelected = !frontends || frontends.length === 0;
|
|
671
802
|
if (noFrontendSelected) return [];
|
|
672
803
|
let response = [];
|
|
673
|
-
const options = [
|
|
804
|
+
const options = [];
|
|
805
|
+
if (isExampleTodoAllowed(backend, database)) options.push({
|
|
674
806
|
value: "todo",
|
|
675
807
|
label: "Todo App",
|
|
676
808
|
hint: "A simple CRUD example app"
|
|
677
|
-
}
|
|
678
|
-
if (backend
|
|
809
|
+
});
|
|
810
|
+
if (isExampleAIAllowed(backend, frontends ?? [])) options.push({
|
|
679
811
|
value: "ai",
|
|
680
812
|
label: "AI Chat",
|
|
681
813
|
hint: "A simple AI chat interface using AI SDK"
|
|
682
814
|
});
|
|
815
|
+
if (options.length === 0) return [];
|
|
683
816
|
response = await multiselect({
|
|
684
817
|
message: "Include examples",
|
|
685
818
|
options,
|
|
686
819
|
required: false,
|
|
687
|
-
initialValues: DEFAULT_CONFIG.examples
|
|
820
|
+
initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
|
|
688
821
|
});
|
|
689
822
|
if (isCancel(response)) {
|
|
690
823
|
cancel(pc.red("Operation cancelled"));
|
|
@@ -754,10 +887,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
754
887
|
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
|
|
755
888
|
}
|
|
756
889
|
];
|
|
757
|
-
const webOptions = allWebOptions.filter((option) =>
|
|
758
|
-
if (backend === "convex") return option.value !== "solid";
|
|
759
|
-
return true;
|
|
760
|
-
});
|
|
890
|
+
const webOptions = allWebOptions.filter((option) => isFrontendAllowedWithBackend(option.value, backend));
|
|
761
891
|
const webFramework = await select({
|
|
762
892
|
message: "Choose web",
|
|
763
893
|
options: webOptions,
|
|
@@ -1057,6 +1187,11 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1057
1187
|
|
|
1058
1188
|
//#endregion
|
|
1059
1189
|
//#region src/prompts/project-name.ts
|
|
1190
|
+
function isPathWithinCwd(targetPath) {
|
|
1191
|
+
const resolved = path.resolve(targetPath);
|
|
1192
|
+
const rel = path.relative(process.cwd(), resolved);
|
|
1193
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
1194
|
+
}
|
|
1060
1195
|
function validateDirectoryName(name) {
|
|
1061
1196
|
if (name === ".") return void 0;
|
|
1062
1197
|
const result = ProjectNameSchema.safeParse(name);
|
|
@@ -1068,7 +1203,11 @@ async function getProjectName(initialName) {
|
|
|
1068
1203
|
if (initialName === ".") return initialName;
|
|
1069
1204
|
const finalDirName = path.basename(initialName);
|
|
1070
1205
|
const validationError = validateDirectoryName(finalDirName);
|
|
1071
|
-
if (!validationError)
|
|
1206
|
+
if (!validationError) {
|
|
1207
|
+
const projectDir = path.resolve(process.cwd(), initialName);
|
|
1208
|
+
if (isPathWithinCwd(projectDir)) return initialName;
|
|
1209
|
+
consola.error(pc.red("Project path must be within current directory"));
|
|
1210
|
+
}
|
|
1072
1211
|
}
|
|
1073
1212
|
let isValid = false;
|
|
1074
1213
|
let projectPath = "";
|
|
@@ -1091,7 +1230,7 @@ async function getProjectName(initialName) {
|
|
|
1091
1230
|
if (validationError) return validationError;
|
|
1092
1231
|
if (nameToUse !== ".") {
|
|
1093
1232
|
const projectDir = path.resolve(process.cwd(), nameToUse);
|
|
1094
|
-
if (!projectDir
|
|
1233
|
+
if (!isPathWithinCwd(projectDir)) return "Project path must be within current directory";
|
|
1095
1234
|
}
|
|
1096
1235
|
return void 0;
|
|
1097
1236
|
}
|
|
@@ -1224,11 +1363,11 @@ function generateReproducibleCommand(config) {
|
|
|
1224
1363
|
flags.push(config.git ? "--git" : "--no-git");
|
|
1225
1364
|
flags.push(`--package-manager ${config.packageManager}`);
|
|
1226
1365
|
flags.push(config.install ? "--install" : "--no-install");
|
|
1227
|
-
let baseCommand = "";
|
|
1366
|
+
let baseCommand = "npx create-better-t-stack@latest";
|
|
1228
1367
|
const pkgManager = config.packageManager;
|
|
1229
|
-
if (pkgManager === "
|
|
1368
|
+
if (pkgManager === "bun") baseCommand = "bun create better-t-stack@latest";
|
|
1230
1369
|
else if (pkgManager === "pnpm") baseCommand = "pnpm create better-t-stack@latest";
|
|
1231
|
-
else if (pkgManager === "
|
|
1370
|
+
else if (pkgManager === "npm") baseCommand = "npx create-better-t-stack@latest";
|
|
1232
1371
|
const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
|
|
1233
1372
|
return `${baseCommand}${projectPathArg} ${flags.join(" ")}`;
|
|
1234
1373
|
}
|
|
@@ -1422,18 +1561,10 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1422
1561
|
config.frontend = [];
|
|
1423
1562
|
} else {
|
|
1424
1563
|
const validOptions = options.frontend.filter((f) => f !== "none");
|
|
1425
|
-
|
|
1426
|
-
const nativeFrontends = validOptions.filter((f) => f === "native-nativewind" || f === "native-unistyles");
|
|
1427
|
-
if (webFrontends.length > 1) {
|
|
1428
|
-
consola$1.fatal("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
|
|
1429
|
-
process.exit(1);
|
|
1430
|
-
}
|
|
1431
|
-
if (nativeFrontends.length > 1) {
|
|
1432
|
-
consola$1.fatal("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
|
|
1433
|
-
process.exit(1);
|
|
1434
|
-
}
|
|
1564
|
+
ensureSingleWebAndNative(validOptions);
|
|
1435
1565
|
config.frontend = validOptions;
|
|
1436
1566
|
}
|
|
1567
|
+
if (providedFlags.has("api") && providedFlags.has("frontend") && config.api && config.frontend && config.frontend.length > 0) validateApiFrontendCompatibility(config.api, config.frontend);
|
|
1437
1568
|
if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
|
|
1438
1569
|
if (options.addons.length > 1) {
|
|
1439
1570
|
consola$1.fatal(`Cannot combine 'none' with other addons.`);
|
|
@@ -1451,159 +1582,94 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1451
1582
|
config.examples = options.examples.filter((ex) => ex !== "none");
|
|
1452
1583
|
if (options.examples.includes("none") && config.backend !== "convex") config.examples = [];
|
|
1453
1584
|
}
|
|
1454
|
-
if (config.backend === "convex") {
|
|
1455
|
-
const incompatibleFlags =
|
|
1456
|
-
if (providedFlags.has("auth") && options.auth === true) incompatibleFlags.push("--auth");
|
|
1457
|
-
if (providedFlags.has("database") && options.database !== "none") incompatibleFlags.push(`--database ${options.database}`);
|
|
1458
|
-
if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
|
|
1459
|
-
if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
|
|
1460
|
-
if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
|
|
1461
|
-
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
|
1585
|
+
if (config.backend === "convex" || config.backend === "none") {
|
|
1586
|
+
const incompatibleFlags = incompatibleFlagsForBackend(config.backend, providedFlags, options);
|
|
1462
1587
|
if (incompatibleFlags.length > 0) {
|
|
1463
|
-
consola$1.fatal(`The following flags are incompatible with '--backend
|
|
1588
|
+
consola$1.fatal(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1464
1589
|
process.exit(1);
|
|
1465
1590
|
}
|
|
1466
|
-
if (providedFlags.has("frontend") && options.frontend) {
|
|
1591
|
+
if (config.backend === "convex" && providedFlags.has("frontend") && options.frontend) {
|
|
1467
1592
|
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
|
1468
1593
|
if (incompatibleFrontends.length > 0) {
|
|
1469
1594
|
consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1470
1595
|
process.exit(1);
|
|
1471
1596
|
}
|
|
1472
1597
|
}
|
|
1473
|
-
config
|
|
1474
|
-
config.database = "none";
|
|
1475
|
-
config.orm = "none";
|
|
1476
|
-
config.api = "none";
|
|
1477
|
-
config.runtime = "none";
|
|
1478
|
-
config.dbSetup = "none";
|
|
1479
|
-
config.examples = ["todo"];
|
|
1480
|
-
} else if (config.backend === "none") {
|
|
1481
|
-
const incompatibleFlags = [];
|
|
1482
|
-
if (providedFlags.has("auth") && options.auth === true) incompatibleFlags.push("--auth");
|
|
1483
|
-
if (providedFlags.has("database") && options.database !== "none") incompatibleFlags.push(`--database ${options.database}`);
|
|
1484
|
-
if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
|
|
1485
|
-
if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
|
|
1486
|
-
if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
|
|
1487
|
-
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
|
1488
|
-
if (providedFlags.has("examples") && options.examples) {
|
|
1489
|
-
const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
|
|
1490
|
-
if (hasNonNoneExamples) incompatibleFlags.push("--examples");
|
|
1491
|
-
}
|
|
1492
|
-
if (incompatibleFlags.length > 0) {
|
|
1493
|
-
consola$1.fatal(`The following flags are incompatible with '--backend none': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1494
|
-
process.exit(1);
|
|
1495
|
-
}
|
|
1496
|
-
config.auth = false;
|
|
1497
|
-
config.database = "none";
|
|
1498
|
-
config.orm = "none";
|
|
1499
|
-
config.api = "none";
|
|
1500
|
-
config.runtime = "none";
|
|
1501
|
-
config.dbSetup = "none";
|
|
1502
|
-
config.examples = [];
|
|
1598
|
+
coerceBackendPresets(config);
|
|
1503
1599
|
}
|
|
1504
|
-
if (config.orm === "mongoose" && config.database !== "mongodb") {
|
|
1600
|
+
if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "mongoose" && config.database !== "mongodb") {
|
|
1505
1601
|
consola$1.fatal("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
|
|
1506
1602
|
process.exit(1);
|
|
1507
1603
|
}
|
|
1508
|
-
if (config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") {
|
|
1604
|
+
if (providedFlags.has("database") && providedFlags.has("orm") && config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") {
|
|
1509
1605
|
consola$1.fatal("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1510
1606
|
process.exit(1);
|
|
1511
1607
|
}
|
|
1512
|
-
if (config.orm === "drizzle" && config.database === "mongodb") {
|
|
1608
|
+
if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "drizzle" && config.database === "mongodb") {
|
|
1513
1609
|
consola$1.fatal("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1514
1610
|
process.exit(1);
|
|
1515
1611
|
}
|
|
1516
|
-
if (config.database && config.database !== "none" && config.orm === "none") {
|
|
1612
|
+
if (providedFlags.has("database") && providedFlags.has("orm") && config.database && config.database !== "none" && config.orm === "none") {
|
|
1517
1613
|
consola$1.fatal("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
|
|
1518
1614
|
process.exit(1);
|
|
1519
1615
|
}
|
|
1520
|
-
if (config.orm && config.orm !== "none" && config.database === "none") {
|
|
1616
|
+
if (providedFlags.has("orm") && providedFlags.has("database") && config.orm && config.orm !== "none" && config.database === "none") {
|
|
1521
1617
|
consola$1.fatal("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1522
1618
|
process.exit(1);
|
|
1523
1619
|
}
|
|
1524
|
-
if (config.auth && config.database === "none") {
|
|
1620
|
+
if (providedFlags.has("auth") && providedFlags.has("database") && config.auth && config.database === "none") {
|
|
1525
1621
|
consola$1.fatal("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1526
1622
|
process.exit(1);
|
|
1527
1623
|
}
|
|
1528
|
-
if (config.dbSetup && config.dbSetup !== "none" && config.database === "none") {
|
|
1624
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup && config.dbSetup !== "none" && config.database === "none") {
|
|
1529
1625
|
consola$1.fatal("Database setup requires a database. Please choose a database or set '--db-setup none'.");
|
|
1530
1626
|
process.exit(1);
|
|
1531
1627
|
}
|
|
1532
|
-
if (config.dbSetup === "turso" && config.database !== "sqlite") {
|
|
1628
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "turso" && config.database !== "sqlite") {
|
|
1533
1629
|
consola$1.fatal("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1534
1630
|
process.exit(1);
|
|
1535
1631
|
}
|
|
1536
|
-
if (config.dbSetup === "neon" && config.database !== "postgres") {
|
|
1632
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") {
|
|
1537
1633
|
consola$1.fatal("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1538
1634
|
process.exit(1);
|
|
1539
1635
|
}
|
|
1540
|
-
if (config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
|
|
1636
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
|
|
1541
1637
|
consola$1.fatal("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1542
1638
|
process.exit(1);
|
|
1543
1639
|
}
|
|
1544
|
-
if (config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
|
|
1640
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
|
|
1545
1641
|
consola$1.fatal("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
|
|
1546
1642
|
process.exit(1);
|
|
1547
1643
|
}
|
|
1548
|
-
if (config.dbSetup === "supabase" && config.database !== "postgres") {
|
|
1644
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") {
|
|
1549
1645
|
consola$1.fatal("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1550
1646
|
process.exit(1);
|
|
1551
1647
|
}
|
|
1552
1648
|
if (config.dbSetup === "d1") {
|
|
1553
|
-
if (
|
|
1554
|
-
|
|
1555
|
-
|
|
1649
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
|
|
1650
|
+
if (config.database !== "sqlite") {
|
|
1651
|
+
consola$1.fatal("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
}
|
|
1556
1654
|
}
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1559
|
-
|
|
1655
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
|
|
1656
|
+
if (config.runtime !== "workers") {
|
|
1657
|
+
consola$1.fatal("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
|
|
1658
|
+
process.exit(1);
|
|
1659
|
+
}
|
|
1560
1660
|
}
|
|
1561
1661
|
}
|
|
1562
|
-
if (config.dbSetup === "docker" && config.database === "sqlite") {
|
|
1662
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup === "docker" && config.database === "sqlite") {
|
|
1563
1663
|
consola$1.fatal("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
|
|
1564
1664
|
process.exit(1);
|
|
1565
1665
|
}
|
|
1566
|
-
if (config.dbSetup === "docker" && config.runtime === "workers") {
|
|
1666
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") && config.dbSetup === "docker" && config.runtime === "workers") {
|
|
1567
1667
|
consola$1.fatal("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
1568
1668
|
process.exit(1);
|
|
1569
1669
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
}
|
|
1574
|
-
if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") {
|
|
1575
|
-
consola$1.fatal(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
|
|
1576
|
-
process.exit(1);
|
|
1577
|
-
}
|
|
1578
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") {
|
|
1579
|
-
consola$1.fatal(`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
|
|
1580
|
-
process.exit(1);
|
|
1581
|
-
}
|
|
1582
|
-
if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") {
|
|
1583
|
-
consola$1.fatal(`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
|
|
1584
|
-
process.exit(1);
|
|
1585
|
-
}
|
|
1586
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") {
|
|
1587
|
-
consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
1588
|
-
process.exit(1);
|
|
1589
|
-
}
|
|
1590
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") {
|
|
1591
|
-
consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
1592
|
-
process.exit(1);
|
|
1593
|
-
}
|
|
1594
|
-
if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") {
|
|
1595
|
-
consola$1.fatal("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
1596
|
-
process.exit(1);
|
|
1597
|
-
}
|
|
1598
|
-
if (providedFlags.has("db-setup") && options.dbSetup === "docker" && config.runtime === "workers") {
|
|
1599
|
-
consola$1.fatal("Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
1600
|
-
process.exit(1);
|
|
1601
|
-
}
|
|
1602
|
-
const hasWebFrontendFlag = (config.frontend ?? []).some((f) => WEB_FRAMEWORKS.includes(f));
|
|
1603
|
-
if (config.webDeploy && config.webDeploy !== "none" && !hasWebFrontendFlag && providedFlags.has("frontend")) {
|
|
1604
|
-
consola$1.fatal("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
1605
|
-
process.exit(1);
|
|
1606
|
-
}
|
|
1670
|
+
validateWorkersCompatibility(providedFlags, options, config);
|
|
1671
|
+
const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
|
|
1672
|
+
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
1607
1673
|
return config;
|
|
1608
1674
|
}
|
|
1609
1675
|
function getProvidedFlags(options) {
|
|
@@ -1779,7 +1845,7 @@ async function setupFumadocs(config) {
|
|
|
1779
1845
|
process.exit(0);
|
|
1780
1846
|
}
|
|
1781
1847
|
const templateArg = TEMPLATES[template].value;
|
|
1782
|
-
const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint`;
|
|
1848
|
+
const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint --no-git`;
|
|
1783
1849
|
const fumadocsInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
1784
1850
|
await execa(fumadocsInitCommand, {
|
|
1785
1851
|
cwd: path.join(projectDir, "apps"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.29.
|
|
3
|
+
"version": "2.29.4",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|