create-better-t-stack 3.31.1 → 3.33.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/README.md +24 -18
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +12 -12
- package/dist/index.mjs +1 -1
- package/dist/{src-ByCnjYDk.mjs → src-_FzI_58B.mjs} +333 -138
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -29,21 +29,21 @@ Follow the prompts to configure your project or use the `--yes` flag for default
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
|
-
| Category | Options
|
|
33
|
-
| ------------------------ |
|
|
34
|
-
| **TypeScript** | End-to-end type safety across all parts of your application
|
|
35
|
-
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None
|
|
36
|
-
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None
|
|
37
|
-
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None
|
|
38
|
-
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None
|
|
39
|
-
| **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None
|
|
40
|
-
| **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None
|
|
41
|
-
| **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup)
|
|
42
|
-
| **Authentication** | • Better Auth<br>• Clerk
|
|
43
|
-
| **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps
|
|
44
|
-
| **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• evlog (request logging for server/fullstack backends)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo or
|
|
45
|
-
| **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK)
|
|
46
|
-
| **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation
|
|
32
|
+
| Category | Options |
|
|
33
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| **TypeScript** | End-to-end type safety across all parts of your application |
|
|
35
|
+
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
|
|
36
|
+
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
|
|
37
|
+
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
|
|
38
|
+
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
|
|
39
|
+
| **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
|
|
40
|
+
| **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None |
|
|
41
|
+
| **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) |
|
|
42
|
+
| **Authentication** | • Better Auth<br>• Clerk |
|
|
43
|
+
| **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps |
|
|
44
|
+
| **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite, or Vite+ (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• evlog (request logging for server/fullstack backends)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo, Nx, or Vite+ (monorepo orchestration) |
|
|
45
|
+
| **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) |
|
|
46
|
+
| **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation |
|
|
47
47
|
|
|
48
48
|
## Usage
|
|
49
49
|
|
|
@@ -60,7 +60,7 @@ Options:
|
|
|
60
60
|
--auth <provider> Authentication (better-auth, clerk, none)
|
|
61
61
|
--payments <provider> Payments provider (polar, none)
|
|
62
62
|
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, astro, native-bare, native-uniwind, native-unistyles, none)
|
|
63
|
-
--addons <types...> Additional addons (pwa, tauri, electrobun, starlight, biome, lefthook, husky, mcp, turborepo, nx, fumadocs, ultracite, oxlint, opentui, wxt, skills, evlog, none)
|
|
63
|
+
--addons <types...> Additional addons (pwa, tauri, electrobun, starlight, biome, lefthook, husky, mcp, turborepo, nx, vite-plus, fumadocs, ultracite, oxlint, opentui, wxt, skills, evlog, none)
|
|
64
64
|
--examples <types...> Examples to include (todo, ai, none)
|
|
65
65
|
--git Initialize git repository
|
|
66
66
|
--no-git Skip git initialization
|
|
@@ -68,8 +68,8 @@ Options:
|
|
|
68
68
|
--install Install dependencies
|
|
69
69
|
--no-install Skip installing dependencies
|
|
70
70
|
--db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, planetscale, mongodb-atlas, docker, none)
|
|
71
|
-
--web-deploy <setup> Web deployment (cloudflare, none)
|
|
72
|
-
--server-deploy <setup> Server deployment (cloudflare, none)
|
|
71
|
+
--web-deploy <setup> Web deployment (cloudflare, docker, none)
|
|
72
|
+
--server-deploy <setup> Server deployment (cloudflare, docker, none)
|
|
73
73
|
--backend <framework> Backend framework (hono, express, fastify, elysia, convex, self, none)
|
|
74
74
|
--runtime <runtime> Runtime (bun, node, workers, none)
|
|
75
75
|
--api <type> API type (trpc, orpc, none)
|
|
@@ -223,6 +223,12 @@ Create a self-hosted fullstack project on Cloudflare with D1:
|
|
|
223
223
|
npx create-better-t-stack --backend self --frontend next --api trpc --database sqlite --orm drizzle --db-setup d1 --web-deploy cloudflare
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
+
Create a self-hosted project that ships as Docker containers (web + server + database via Docker Compose):
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npx create-better-t-stack --frontend tanstack-router --backend hono --runtime bun --database postgres --orm drizzle --db-setup docker --web-deploy docker --server-deploy docker
|
|
230
|
+
```
|
|
231
|
+
|
|
226
232
|
Create a minimal API-only project:
|
|
227
233
|
|
|
228
234
|
```bash
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-
|
|
2
|
+
import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-_FzI_58B.mjs";
|
|
3
3
|
import z from "zod";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
package/dist/index.d.mts
CHANGED
|
@@ -175,7 +175,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
175
175
|
auth?: "none" | "better-auth" | "clerk" | undefined;
|
|
176
176
|
payments?: "none" | "polar" | undefined;
|
|
177
177
|
frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-bare" | "native-uniwind" | "native-unistyles" | "svelte" | "solid" | "astro")[] | undefined;
|
|
178
|
-
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
178
|
+
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
179
179
|
examples?: ("none" | "todo" | "ai")[] | undefined;
|
|
180
180
|
git?: boolean | undefined;
|
|
181
181
|
packageManager?: "bun" | "npm" | "pnpm" | undefined;
|
|
@@ -184,8 +184,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
184
184
|
backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
|
|
185
185
|
runtime?: "none" | "bun" | "node" | "workers" | undefined;
|
|
186
186
|
api?: "none" | "trpc" | "orpc" | undefined;
|
|
187
|
-
webDeploy?: "none" | "cloudflare" | undefined;
|
|
188
|
-
serverDeploy?: "none" | "cloudflare" | undefined;
|
|
187
|
+
webDeploy?: "none" | "docker" | "cloudflare" | undefined;
|
|
188
|
+
serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
|
|
189
189
|
directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
|
|
190
190
|
renderTitle?: boolean | undefined;
|
|
191
191
|
disableAnalytics?: boolean | undefined;
|
|
@@ -228,7 +228,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
228
228
|
devPort?: number | undefined;
|
|
229
229
|
search?: "orama" | "orama-cloud" | undefined;
|
|
230
230
|
ogImage?: "next-og" | "takumi" | undefined;
|
|
231
|
-
aiChat?: "openrouter" | "inkeep" | undefined;
|
|
231
|
+
aiChat?: "openrouter" | "llmgateway" | "inkeep" | undefined;
|
|
232
232
|
} | undefined;
|
|
233
233
|
opentui?: {
|
|
234
234
|
template: "solid" | "react" | "core";
|
|
@@ -274,7 +274,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
274
274
|
auth?: "none" | "better-auth" | "clerk" | undefined;
|
|
275
275
|
payments?: "none" | "polar" | undefined;
|
|
276
276
|
frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-bare" | "native-uniwind" | "native-unistyles" | "svelte" | "solid" | "astro")[] | undefined;
|
|
277
|
-
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
277
|
+
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
278
278
|
examples?: ("none" | "todo" | "ai")[] | undefined;
|
|
279
279
|
git?: boolean | undefined;
|
|
280
280
|
packageManager?: "bun" | "npm" | "pnpm" | undefined;
|
|
@@ -283,8 +283,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
283
283
|
backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
|
|
284
284
|
runtime?: "none" | "bun" | "node" | "workers" | undefined;
|
|
285
285
|
api?: "none" | "trpc" | "orpc" | undefined;
|
|
286
|
-
webDeploy?: "none" | "cloudflare" | undefined;
|
|
287
|
-
serverDeploy?: "none" | "cloudflare" | undefined;
|
|
286
|
+
webDeploy?: "none" | "docker" | "cloudflare" | undefined;
|
|
287
|
+
serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
|
|
288
288
|
directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
|
|
289
289
|
renderTitle?: boolean | undefined;
|
|
290
290
|
disableAnalytics?: boolean | undefined;
|
|
@@ -317,7 +317,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
317
317
|
}>;
|
|
318
318
|
add: _$_trpc_server0.TRPCMutationProcedure<{
|
|
319
319
|
input: {
|
|
320
|
-
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
320
|
+
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
321
321
|
install?: boolean | undefined;
|
|
322
322
|
packageManager?: "bun" | "npm" | "pnpm" | undefined;
|
|
323
323
|
projectDir?: string | undefined;
|
|
@@ -327,7 +327,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
327
327
|
}>;
|
|
328
328
|
addJson: _$_trpc_server0.TRPCMutationProcedure<{
|
|
329
329
|
input: {
|
|
330
|
-
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
330
|
+
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "vite-plus" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
331
331
|
addonOptions?: {
|
|
332
332
|
wxt?: {
|
|
333
333
|
template: "svelte" | "solid" | "vanilla" | "vue" | "react";
|
|
@@ -338,7 +338,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
338
338
|
devPort?: number | undefined;
|
|
339
339
|
search?: "orama" | "orama-cloud" | undefined;
|
|
340
340
|
ogImage?: "next-og" | "takumi" | undefined;
|
|
341
|
-
aiChat?: "openrouter" | "inkeep" | undefined;
|
|
341
|
+
aiChat?: "openrouter" | "llmgateway" | "inkeep" | undefined;
|
|
342
342
|
} | undefined;
|
|
343
343
|
opentui?: {
|
|
344
344
|
template: "solid" | "react" | "core";
|
|
@@ -363,8 +363,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
363
363
|
hooks?: ("cursor" | "windsurf" | "codebuddy" | "claude" | "copilot")[] | undefined;
|
|
364
364
|
} | undefined;
|
|
365
365
|
} | undefined;
|
|
366
|
-
webDeploy?: "none" | "cloudflare" | undefined;
|
|
367
|
-
serverDeploy?: "none" | "cloudflare" | undefined;
|
|
366
|
+
webDeploy?: "none" | "docker" | "cloudflare" | undefined;
|
|
367
|
+
serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
|
|
368
368
|
projectDir?: string | undefined;
|
|
369
369
|
install?: boolean | undefined;
|
|
370
370
|
packageManager?: "bun" | "npm" | "pnpm" | undefined;
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-
|
|
2
|
+
import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-_FzI_58B.mjs";
|
|
3
3
|
export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, Result, SchemaNameSchema, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, getSchemaResult, router, sponsors };
|
|
@@ -12,7 +12,7 @@ import envPaths from "env-paths";
|
|
|
12
12
|
import fs from "fs-extra";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { desktopWebFrontends as desktopWebFrontends$3 } from "@better-t-stack/types";
|
|
15
|
-
import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps } from "@better-t-stack/template-generator";
|
|
15
|
+
import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps, processNxConfig, processPackageConfigs, processTemplateString, processTurboConfig, processVitePlusConfig } from "@better-t-stack/template-generator";
|
|
16
16
|
import { consola, createConsola } from "consola";
|
|
17
17
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
18
|
import gradient from "gradient-string";
|
|
@@ -78,6 +78,7 @@ const ADDON_COMPATIBILITY = {
|
|
|
78
78
|
lefthook: [],
|
|
79
79
|
turborepo: [],
|
|
80
80
|
nx: [],
|
|
81
|
+
"vite-plus": [],
|
|
81
82
|
starlight: [],
|
|
82
83
|
ultracite: [],
|
|
83
84
|
mcp: [],
|
|
@@ -729,6 +730,11 @@ const WEB_FRAMEWORKS = [
|
|
|
729
730
|
];
|
|
730
731
|
//#endregion
|
|
731
732
|
//#region src/utils/compatibility-rules.ts
|
|
733
|
+
const TASK_RUNNER_ADDONS$1 = [
|
|
734
|
+
"turborepo",
|
|
735
|
+
"nx",
|
|
736
|
+
"vite-plus"
|
|
737
|
+
];
|
|
732
738
|
const CONVEX_BETTER_AUTH_INCOMPATIBLE_FRONTENDS = [
|
|
733
739
|
"nuxt",
|
|
734
740
|
"svelte",
|
|
@@ -869,6 +875,12 @@ function validateServerDeployRequiresBackend(serverDeploy, backend) {
|
|
|
869
875
|
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
|
|
870
876
|
return Result.ok(void 0);
|
|
871
877
|
}
|
|
878
|
+
function validateDockerServerDeploy(serverDeploy, backend, runtime) {
|
|
879
|
+
if (serverDeploy !== "docker") return Result.ok(void 0);
|
|
880
|
+
if (backend === "convex" || backend === "self") return validationErr$1("'--server-deploy docker' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy docker' instead.");
|
|
881
|
+
if (runtime === "workers") return validationErr$1("'--server-deploy docker' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.");
|
|
882
|
+
return Result.ok(void 0);
|
|
883
|
+
}
|
|
872
884
|
function validateAddonCompatibility(addon, frontend, _auth, backend, runtime) {
|
|
873
885
|
if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
|
|
874
886
|
isCompatible: false,
|
|
@@ -887,12 +899,13 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
|
|
|
887
899
|
return allAddons.filter((addon) => {
|
|
888
900
|
if (existingAddons.includes(addon)) return false;
|
|
889
901
|
if (addon === "none") return false;
|
|
902
|
+
if (TASK_RUNNER_ADDONS$1.includes(addon) && existingAddons.some((existingAddon) => TASK_RUNNER_ADDONS$1.includes(existingAddon))) return false;
|
|
890
903
|
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth, backend, runtime);
|
|
891
904
|
return isCompatible;
|
|
892
905
|
});
|
|
893
906
|
}
|
|
894
907
|
function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
|
|
895
|
-
if (addons.
|
|
908
|
+
if (addons.filter((addon) => TASK_RUNNER_ADDONS$1.includes(addon)).length > 1) return validationErr$1("Cannot combine 'turborepo', 'nx', and 'vite-plus' addons. Choose one task runner.");
|
|
896
909
|
for (const addon of addons) {
|
|
897
910
|
if (addon === "none") continue;
|
|
898
911
|
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
|
|
@@ -978,7 +991,7 @@ function normalizeValidationMessage(validationMessage) {
|
|
|
978
991
|
async function runWithNavigation(prompt) {
|
|
979
992
|
let goBack = false;
|
|
980
993
|
prompt.on("key", (char) => {
|
|
981
|
-
if (char === "b" && !isFirstPrompt()) {
|
|
994
|
+
if ((char === "b" || char === "B") && !isFirstPrompt()) {
|
|
982
995
|
goBack = true;
|
|
983
996
|
prompt.state = "cancel";
|
|
984
997
|
}
|
|
@@ -1164,6 +1177,10 @@ async function navigableGroupMultiselect(opts) {
|
|
|
1164
1177
|
}
|
|
1165
1178
|
}));
|
|
1166
1179
|
}
|
|
1180
|
+
/** Use the remembered answer as the initial value only while it is still selectable. */
|
|
1181
|
+
function preferValidInitial(options, previous, fallback) {
|
|
1182
|
+
return previous !== void 0 && options.some((o) => o.value === previous) ? previous : fallback;
|
|
1183
|
+
}
|
|
1167
1184
|
//#endregion
|
|
1168
1185
|
//#region src/prompts/addons.ts
|
|
1169
1186
|
function getAddonDisplay(addon) {
|
|
@@ -1178,6 +1195,10 @@ function getAddonDisplay(addon) {
|
|
|
1178
1195
|
label = "Nx";
|
|
1179
1196
|
hint = "Smart monorepo orchestration and task graph";
|
|
1180
1197
|
break;
|
|
1198
|
+
case "vite-plus":
|
|
1199
|
+
label = "Vite+";
|
|
1200
|
+
hint = "Unified Vite toolchain and workspace task runner";
|
|
1201
|
+
break;
|
|
1181
1202
|
case "pwa":
|
|
1182
1203
|
label = "PWA";
|
|
1183
1204
|
hint = "Make your app installable and work offline";
|
|
@@ -1248,7 +1269,11 @@ function getAddonDisplay(addon) {
|
|
|
1248
1269
|
};
|
|
1249
1270
|
}
|
|
1250
1271
|
const ADDON_GROUPS = {
|
|
1251
|
-
"Monorepo & Tasks": [
|
|
1272
|
+
"Monorepo & Tasks": [
|
|
1273
|
+
"turborepo",
|
|
1274
|
+
"nx",
|
|
1275
|
+
"vite-plus"
|
|
1276
|
+
],
|
|
1252
1277
|
"Code Quality": [
|
|
1253
1278
|
"biome",
|
|
1254
1279
|
"oxlint",
|
|
@@ -1289,9 +1314,13 @@ function sortAndPruneGroupedOptions(groupedOptions) {
|
|
|
1289
1314
|
});
|
|
1290
1315
|
}
|
|
1291
1316
|
function validateAddonSelection(selected) {
|
|
1292
|
-
if (selected?.
|
|
1317
|
+
if ((selected?.filter((addon) => [
|
|
1318
|
+
"turborepo",
|
|
1319
|
+
"nx",
|
|
1320
|
+
"vite-plus"
|
|
1321
|
+
].includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, not more than one.";
|
|
1293
1322
|
}
|
|
1294
|
-
async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
|
|
1323
|
+
async function getAddonsChoice(addons, frontends, auth, backend, runtime, previousValue) {
|
|
1295
1324
|
if (addons !== void 0) return addons;
|
|
1296
1325
|
const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
|
|
1297
1326
|
const groupedOptions = createGroupedOptions();
|
|
@@ -1310,7 +1339,7 @@ async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
|
|
|
1310
1339
|
const response = await navigableGroupMultiselect({
|
|
1311
1340
|
message: "Select addons",
|
|
1312
1341
|
options: groupedOptions,
|
|
1313
|
-
initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
|
|
1342
|
+
initialValues: (previousValue ?? DEFAULT_CONFIG.addons).filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
|
|
1314
1343
|
required: false,
|
|
1315
1344
|
validate: validateAddonSelection
|
|
1316
1345
|
});
|
|
@@ -1955,26 +1984,36 @@ async function setupEvlog(config) {
|
|
|
1955
1984
|
/**
|
|
1956
1985
|
* Define a group of prompts that supports going back to previous prompts.
|
|
1957
1986
|
* Returns a result object with all the values, or handles cancel/go-back navigation.
|
|
1987
|
+
* When navigating back, the discarded answers are replayed to the prompts as
|
|
1988
|
+
* `previousAnswer` so they can preselect what the user chose before.
|
|
1958
1989
|
*/
|
|
1959
1990
|
async function navigableGroup(prompts, opts) {
|
|
1960
1991
|
const results = {};
|
|
1992
|
+
const previousAnswers = {};
|
|
1961
1993
|
const promptNames = Object.keys(prompts);
|
|
1962
1994
|
let currentIndex = 0;
|
|
1963
1995
|
let goingBack = false;
|
|
1996
|
+
const stepBack = () => {
|
|
1997
|
+
const prevName = promptNames[currentIndex - 1];
|
|
1998
|
+
previousAnswers[prevName] = results[prevName];
|
|
1999
|
+
delete results[prevName];
|
|
2000
|
+
currentIndex--;
|
|
2001
|
+
};
|
|
1964
2002
|
while (currentIndex < promptNames.length) {
|
|
1965
2003
|
const name = promptNames[currentIndex];
|
|
1966
2004
|
const prompt = prompts[name];
|
|
1967
2005
|
setIsFirstPrompt$1(currentIndex === 0);
|
|
1968
2006
|
setLastPromptShownUI(false);
|
|
1969
|
-
const result = await prompt({
|
|
2007
|
+
const result = await prompt({
|
|
2008
|
+
results,
|
|
2009
|
+
previousAnswer: previousAnswers[name]
|
|
2010
|
+
})?.catch((e) => {
|
|
1970
2011
|
throw e;
|
|
1971
2012
|
});
|
|
1972
2013
|
if (isGoBack(result)) {
|
|
1973
2014
|
goingBack = true;
|
|
1974
2015
|
if (currentIndex > 0) {
|
|
1975
|
-
|
|
1976
|
-
delete results[prevName];
|
|
1977
|
-
currentIndex--;
|
|
2016
|
+
stepBack();
|
|
1978
2017
|
continue;
|
|
1979
2018
|
}
|
|
1980
2019
|
goingBack = false;
|
|
@@ -1990,9 +2029,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
1990
2029
|
}
|
|
1991
2030
|
if (goingBack && !didLastPromptShowUI()) {
|
|
1992
2031
|
if (currentIndex > 0) {
|
|
1993
|
-
|
|
1994
|
-
delete results[prevName];
|
|
1995
|
-
currentIndex--;
|
|
2032
|
+
stepBack();
|
|
1996
2033
|
continue;
|
|
1997
2034
|
}
|
|
1998
2035
|
}
|
|
@@ -2142,6 +2179,14 @@ const DEFAULT_DEV_PORT$1 = 4e3;
|
|
|
2142
2179
|
function aiChatDisabledForTemplate(template) {
|
|
2143
2180
|
return template === "next-mdx-static" || template.endsWith("-spa");
|
|
2144
2181
|
}
|
|
2182
|
+
function getFumadocsLinter(addons) {
|
|
2183
|
+
if (addons.includes("oxlint")) return "oxlint";
|
|
2184
|
+
if (addons.includes("biome") || addons.includes("ultracite")) return "biome";
|
|
2185
|
+
if (addons.includes("vite-plus")) return "oxlint";
|
|
2186
|
+
}
|
|
2187
|
+
function getFumadocsAddonContext(currentAddons, persistedAddons) {
|
|
2188
|
+
return Array.from(new Set([...persistedAddons ?? [], ...currentAddons]));
|
|
2189
|
+
}
|
|
2145
2190
|
async function setupFumadocs(config) {
|
|
2146
2191
|
if (shouldSkipExternalCommands()) return Result.ok(void 0);
|
|
2147
2192
|
const { packageManager, projectDir } = config;
|
|
@@ -2215,6 +2260,10 @@ async function setupFumadocs(config) {
|
|
|
2215
2260
|
label: "AI SDK",
|
|
2216
2261
|
hint: "default to OpenRouter"
|
|
2217
2262
|
},
|
|
2263
|
+
{
|
|
2264
|
+
value: "llmgateway",
|
|
2265
|
+
label: "LLM Gateway"
|
|
2266
|
+
},
|
|
2218
2267
|
{
|
|
2219
2268
|
value: "inkeep",
|
|
2220
2269
|
label: "Inkeep AI",
|
|
@@ -2251,7 +2300,9 @@ async function setupFumadocs(config) {
|
|
|
2251
2300
|
"--no-git"
|
|
2252
2301
|
];
|
|
2253
2302
|
if (isNextTemplate) options.push("--src");
|
|
2254
|
-
|
|
2303
|
+
const persistedConfig = await readBtsConfig(projectDir);
|
|
2304
|
+
const linter = getFumadocsLinter(getFumadocsAddonContext(config.addons, persistedConfig?.addons));
|
|
2305
|
+
if (linter) options.push(`--linter ${linter}`);
|
|
2255
2306
|
if (search) options.push(`--search ${search}`);
|
|
2256
2307
|
if (ogImage) options.push(`--og-image ${ogImage}`);
|
|
2257
2308
|
if (aiChat) options.push(`--ai-chat ${aiChat}`);
|
|
@@ -3630,6 +3681,7 @@ async function setupAddons(config) {
|
|
|
3630
3681
|
const hasHusky = addons.includes("husky");
|
|
3631
3682
|
const hasLefthook = addons.includes("lefthook");
|
|
3632
3683
|
const hasOxlint = addons.includes("oxlint");
|
|
3684
|
+
const hasVitePlus = addons.includes("vite-plus");
|
|
3633
3685
|
if (hasUltracite) {
|
|
3634
3686
|
const gitHooks = [];
|
|
3635
3687
|
if (hasHusky) gitHooks.push("husky");
|
|
@@ -3642,6 +3694,7 @@ async function setupAddons(config) {
|
|
|
3642
3694
|
let linter;
|
|
3643
3695
|
if (hasOxlint) linter = "oxlint";
|
|
3644
3696
|
else if (hasBiome) linter = "biome";
|
|
3697
|
+
else if (hasVitePlus) linter = "vite-plus";
|
|
3645
3698
|
if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
|
|
3646
3699
|
if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
|
|
3647
3700
|
}
|
|
@@ -3683,6 +3736,7 @@ async function setupHusky(projectDir, linter) {
|
|
|
3683
3736
|
};
|
|
3684
3737
|
if (linter === "oxlint") packageJson["lint-staged"] = { "*": ["oxlint", "oxfmt --write"] };
|
|
3685
3738
|
else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
|
|
3739
|
+
else if (linter === "vite-plus") packageJson["lint-staged"] = { "*.{js,ts,jsx,tsx,vue,svelte,json,jsonc,css,md}": ["vp check --fix"] };
|
|
3686
3740
|
else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
|
|
3687
3741
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
3688
3742
|
}
|
|
@@ -3750,6 +3804,35 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
3750
3804
|
}
|
|
3751
3805
|
//#endregion
|
|
3752
3806
|
//#region src/helpers/core/add-handler.ts
|
|
3807
|
+
const ADD_PACKAGE_JSON_PATHS = [
|
|
3808
|
+
"package.json",
|
|
3809
|
+
"apps/server/package.json",
|
|
3810
|
+
"apps/web/package.json",
|
|
3811
|
+
"apps/native/package.json",
|
|
3812
|
+
"apps/desktop/package.json",
|
|
3813
|
+
"apps/fumadocs/package.json",
|
|
3814
|
+
"apps/docs/package.json",
|
|
3815
|
+
"packages/api/package.json",
|
|
3816
|
+
"packages/db/package.json",
|
|
3817
|
+
"packages/auth/package.json",
|
|
3818
|
+
"packages/backend/package.json",
|
|
3819
|
+
"packages/config/package.json",
|
|
3820
|
+
"packages/env/package.json",
|
|
3821
|
+
"packages/infra/package.json",
|
|
3822
|
+
"packages/ui/package.json"
|
|
3823
|
+
];
|
|
3824
|
+
const ADD_TEXT_FILE_PATHS = ["apps/web/vite.config.ts", "lefthook.yml"];
|
|
3825
|
+
const HOOK_ADDONS = ["husky", "lefthook"];
|
|
3826
|
+
const HOOK_LINTER_ADDONS = [
|
|
3827
|
+
"biome",
|
|
3828
|
+
"oxlint",
|
|
3829
|
+
"vite-plus"
|
|
3830
|
+
];
|
|
3831
|
+
const TASK_RUNNER_ADDONS = [
|
|
3832
|
+
"turborepo",
|
|
3833
|
+
"nx",
|
|
3834
|
+
"vite-plus"
|
|
3835
|
+
];
|
|
3753
3836
|
function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
|
|
3754
3837
|
if (!existingAddonOptions && !nextAddonOptions) return;
|
|
3755
3838
|
const mergedAddonOptions = { ...existingAddonOptions };
|
|
@@ -3763,6 +3846,29 @@ function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
|
|
|
3763
3846
|
}
|
|
3764
3847
|
return Object.keys(mergedAddonOptions).length > 0 ? mergedAddonOptions : void 0;
|
|
3765
3848
|
}
|
|
3849
|
+
function getSetupAddons(addonsToAdd, updatedAddons) {
|
|
3850
|
+
const setupAddons = new Set(addonsToAdd);
|
|
3851
|
+
if (addonsToAdd.some((addon) => HOOK_ADDONS.includes(addon) || HOOK_LINTER_ADDONS.includes(addon))) {
|
|
3852
|
+
for (const addon of [...HOOK_ADDONS, ...HOOK_LINTER_ADDONS]) if (updatedAddons.includes(addon)) setupAddons.add(addon);
|
|
3853
|
+
}
|
|
3854
|
+
return [...setupAddons];
|
|
3855
|
+
}
|
|
3856
|
+
function shouldRefreshLefthook(addonsToAdd, updatedAddons) {
|
|
3857
|
+
return updatedAddons.includes("lefthook") && addonsToAdd.some((addon) => addon === "lefthook" || HOOK_LINTER_ADDONS.includes(addon));
|
|
3858
|
+
}
|
|
3859
|
+
function refreshLefthookTemplate(vfs, config) {
|
|
3860
|
+
const template = EMBEDDED_TEMPLATES.get("addons/lefthook/lefthook.yml.hbs");
|
|
3861
|
+
if (!template) return;
|
|
3862
|
+
vfs.writeFile("lefthook.yml", processTemplateString(template, config));
|
|
3863
|
+
}
|
|
3864
|
+
function updateViteConfigImportsForVitePlus(vfs) {
|
|
3865
|
+
for (const viteConfigPath of ["apps/web/vite.config.ts"]) {
|
|
3866
|
+
const content = vfs.readFile(viteConfigPath);
|
|
3867
|
+
if (!content) continue;
|
|
3868
|
+
const updatedContent = content.replaceAll("from \"vite\";", "from \"vite-plus\";").replaceAll("from 'vite';", "from 'vite-plus';");
|
|
3869
|
+
if (updatedContent !== content) vfs.writeFile(viteConfigPath, updatedContent);
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3766
3872
|
async function addHandler(input, options = {}) {
|
|
3767
3873
|
const { silent = false } = options;
|
|
3768
3874
|
return runWithContextAsync({ silent }, async () => {
|
|
@@ -3840,10 +3946,10 @@ async function addHandlerInternal(input) {
|
|
|
3840
3946
|
}
|
|
3841
3947
|
addonsToAdd = selectedAddons;
|
|
3842
3948
|
}
|
|
3843
|
-
const
|
|
3949
|
+
const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
|
|
3950
|
+
const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
|
|
3844
3951
|
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
3845
3952
|
if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
|
|
3846
|
-
const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
|
|
3847
3953
|
const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
|
|
3848
3954
|
const config = {
|
|
3849
3955
|
projectName: existingConfig.projectName,
|
|
@@ -3867,27 +3973,39 @@ async function addHandlerInternal(input) {
|
|
|
3867
3973
|
webDeploy: existingConfig.webDeploy,
|
|
3868
3974
|
serverDeploy: existingConfig.serverDeploy
|
|
3869
3975
|
};
|
|
3976
|
+
const updatedConfig = {
|
|
3977
|
+
...config,
|
|
3978
|
+
addons: updatedAddons
|
|
3979
|
+
};
|
|
3870
3980
|
if (!isSilent()) log.info(pc.dim("Installing addon files..."));
|
|
3871
3981
|
const vfs = new VirtualFileSystem();
|
|
3872
|
-
for (const pkgPath of
|
|
3873
|
-
"package.json",
|
|
3874
|
-
"apps/web/package.json",
|
|
3875
|
-
"apps/server/package.json",
|
|
3876
|
-
"apps/native/package.json"
|
|
3877
|
-
]) {
|
|
3982
|
+
for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
|
|
3878
3983
|
const fullPath = path.join(projectDir, pkgPath);
|
|
3879
3984
|
if (await fs.pathExists(fullPath)) {
|
|
3880
3985
|
const content = await fs.readFile(fullPath, "utf-8");
|
|
3881
3986
|
vfs.writeFile(pkgPath, content);
|
|
3882
3987
|
}
|
|
3883
3988
|
}
|
|
3989
|
+
for (const filePath of ADD_TEXT_FILE_PATHS) {
|
|
3990
|
+
const fullPath = path.join(projectDir, filePath);
|
|
3991
|
+
if (await fs.pathExists(fullPath)) {
|
|
3992
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
3993
|
+
vfs.writeFile(filePath, content);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3884
3996
|
await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
|
|
3885
3997
|
processAddonsDeps(vfs, config);
|
|
3998
|
+
if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
|
|
3999
|
+
if (addonsToAdd.includes("nx")) processNxConfig(vfs, updatedConfig);
|
|
4000
|
+
if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
|
|
4001
|
+
if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
|
|
4002
|
+
if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
|
|
4003
|
+
if (shouldRefreshLefthook(addonsToAdd, updatedAddons)) refreshLefthookTemplate(vfs, updatedConfig);
|
|
3886
4004
|
const tree = {
|
|
3887
4005
|
root: vfs.toTree(""),
|
|
3888
4006
|
fileCount: vfs.getFileCount(),
|
|
3889
4007
|
directoryCount: vfs.getDirectoryCount(),
|
|
3890
|
-
config
|
|
4008
|
+
config: updatedConfig
|
|
3891
4009
|
};
|
|
3892
4010
|
if (input.dryRun) {
|
|
3893
4011
|
if (!isSilent()) {
|
|
@@ -3907,7 +4025,10 @@ async function addHandlerInternal(input) {
|
|
|
3907
4025
|
if (writeResult.isErr()) return Result.err(new CLIError({ message: `Failed to write addon files: ${writeResult.error.message}` }));
|
|
3908
4026
|
if (vfs.getFileCount() > 0 && !isSilent()) log.info(pc.dim(`Wrote ${vfs.getFileCount()} addon files`));
|
|
3909
4027
|
const setupResult = await Result.tryPromise({
|
|
3910
|
-
try: () => setupAddons(
|
|
4028
|
+
try: () => setupAddons({
|
|
4029
|
+
...config,
|
|
4030
|
+
addons: getSetupAddons(addonsToAdd, updatedAddons)
|
|
4031
|
+
}),
|
|
3911
4032
|
catch: (e) => {
|
|
3912
4033
|
if (UserCancelledError.is(e)) return e;
|
|
3913
4034
|
return new CLIError({
|
|
@@ -3919,7 +4040,7 @@ async function addHandlerInternal(input) {
|
|
|
3919
4040
|
if (setupResult.isErr()) return Result.err(setupResult.error);
|
|
3920
4041
|
await updateBtsConfig(projectDir, {
|
|
3921
4042
|
addons: updatedAddons,
|
|
3922
|
-
addonOptions:
|
|
4043
|
+
addonOptions: updatedConfig.addonOptions
|
|
3923
4044
|
});
|
|
3924
4045
|
if (input.install) {
|
|
3925
4046
|
if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
|
|
@@ -3942,7 +4063,7 @@ async function addHandlerInternal(input) {
|
|
|
3942
4063
|
}
|
|
3943
4064
|
//#endregion
|
|
3944
4065
|
//#region src/prompts/api.ts
|
|
3945
|
-
async function getApiChoice(Api, frontend, backend) {
|
|
4066
|
+
async function getApiChoice(Api, frontend, backend, previousValue) {
|
|
3946
4067
|
if (backend === "convex" || backend === "none") return "none";
|
|
3947
4068
|
const allowed = allowedApisForFrontends(frontend ?? []);
|
|
3948
4069
|
if (Api) {
|
|
@@ -3966,7 +4087,7 @@ async function getApiChoice(Api, frontend, backend) {
|
|
|
3966
4087
|
const apiType = await navigableSelect({
|
|
3967
4088
|
message: "Select API type",
|
|
3968
4089
|
options: apiOptions,
|
|
3969
|
-
initialValue: apiOptions[0].value
|
|
4090
|
+
initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
|
|
3970
4091
|
});
|
|
3971
4092
|
if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
3972
4093
|
return apiType;
|
|
@@ -3992,7 +4113,7 @@ function getAvailableAuthProviders(backend, frontend = []) {
|
|
|
3992
4113
|
if (options.length === 0) return ["none"];
|
|
3993
4114
|
return [...options, "none"];
|
|
3994
4115
|
}
|
|
3995
|
-
async function getAuthChoice(auth, backend, frontend = []) {
|
|
4116
|
+
async function getAuthChoice(auth, backend, frontend = [], previousValue) {
|
|
3996
4117
|
if (auth !== void 0) return auth;
|
|
3997
4118
|
const availableProviders = getAvailableAuthProviders(backend, frontend);
|
|
3998
4119
|
if (availableProviders.length === 1 && availableProviders[0] === "none") return "none";
|
|
@@ -4018,7 +4139,7 @@ async function getAuthChoice(auth, backend, frontend = []) {
|
|
|
4018
4139
|
const response = await navigableSelect({
|
|
4019
4140
|
message: "Select authentication provider",
|
|
4020
4141
|
options,
|
|
4021
|
-
initialValue: options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none"
|
|
4142
|
+
initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
|
|
4022
4143
|
});
|
|
4023
4144
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4024
4145
|
return response;
|
|
@@ -4032,7 +4153,7 @@ const FULLSTACK_FRONTENDS = [
|
|
|
4032
4153
|
"svelte",
|
|
4033
4154
|
"astro"
|
|
4034
4155
|
];
|
|
4035
|
-
async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
4156
|
+
async function getBackendFrameworkChoice(backendFramework, frontends, previousValue) {
|
|
4036
4157
|
if (backendFramework !== void 0) return backendFramework;
|
|
4037
4158
|
const hasIncompatibleFrontend = frontends?.some((f) => f === "solid" || f === "astro");
|
|
4038
4159
|
const hasFullstackFrontend = frontends?.some((f) => FULLSTACK_FRONTENDS.includes(f));
|
|
@@ -4072,14 +4193,14 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
|
4072
4193
|
const response = await navigableSelect({
|
|
4073
4194
|
message: "Select backend",
|
|
4074
4195
|
options: backendOptions,
|
|
4075
|
-
initialValue: hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend
|
|
4196
|
+
initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
|
|
4076
4197
|
});
|
|
4077
4198
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4078
4199
|
return response;
|
|
4079
4200
|
}
|
|
4080
4201
|
//#endregion
|
|
4081
4202
|
//#region src/prompts/database.ts
|
|
4082
|
-
async function getDatabaseChoice(database, backend, runtime) {
|
|
4203
|
+
async function getDatabaseChoice(database, backend, runtime, previousValue) {
|
|
4083
4204
|
if (backend === "convex" || backend === "none") return "none";
|
|
4084
4205
|
if (database !== void 0) return database;
|
|
4085
4206
|
const databaseOptions = [
|
|
@@ -4112,14 +4233,14 @@ async function getDatabaseChoice(database, backend, runtime) {
|
|
|
4112
4233
|
const response = await navigableSelect({
|
|
4113
4234
|
message: "Select database",
|
|
4114
4235
|
options: databaseOptions,
|
|
4115
|
-
initialValue: DEFAULT_CONFIG.database
|
|
4236
|
+
initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
|
|
4116
4237
|
});
|
|
4117
4238
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4118
4239
|
return response;
|
|
4119
4240
|
}
|
|
4120
4241
|
//#endregion
|
|
4121
4242
|
//#region src/prompts/database-setup.ts
|
|
4122
|
-
async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
|
|
4243
|
+
async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, previousValue) {
|
|
4123
4244
|
if (backend === "convex") return "none";
|
|
4124
4245
|
if (dbSetup !== void 0) return dbSetup;
|
|
4125
4246
|
if (databaseType === "none") return "none";
|
|
@@ -4211,14 +4332,14 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
|
|
|
4211
4332
|
const response = await navigableSelect({
|
|
4212
4333
|
message: `Select ${databaseType} setup option`,
|
|
4213
4334
|
options,
|
|
4214
|
-
initialValue: "none"
|
|
4335
|
+
initialValue: preferValidInitial(options, previousValue, "none")
|
|
4215
4336
|
});
|
|
4216
4337
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4217
4338
|
return response;
|
|
4218
4339
|
}
|
|
4219
4340
|
//#endregion
|
|
4220
4341
|
//#region src/prompts/examples.ts
|
|
4221
|
-
async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
4342
|
+
async function getExamplesChoice(examples, database, frontends, backend, api, previousValue) {
|
|
4222
4343
|
if (examples !== void 0) return examples;
|
|
4223
4344
|
if (backend === "none") return [];
|
|
4224
4345
|
let response = [];
|
|
@@ -4238,15 +4359,27 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
|
4238
4359
|
message: "Include examples",
|
|
4239
4360
|
options,
|
|
4240
4361
|
required: false,
|
|
4241
|
-
initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
|
|
4362
|
+
initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
|
|
4242
4363
|
});
|
|
4243
4364
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4244
4365
|
return response;
|
|
4245
4366
|
}
|
|
4246
4367
|
//#endregion
|
|
4247
4368
|
//#region src/prompts/frontend.ts
|
|
4248
|
-
|
|
4369
|
+
const WEB_FRONTEND_VALUES = [
|
|
4370
|
+
"tanstack-router",
|
|
4371
|
+
"react-router",
|
|
4372
|
+
"next",
|
|
4373
|
+
"nuxt",
|
|
4374
|
+
"svelte",
|
|
4375
|
+
"solid",
|
|
4376
|
+
"astro",
|
|
4377
|
+
"tanstack-start"
|
|
4378
|
+
];
|
|
4379
|
+
async function getFrontendChoice(frontendOptions, backend, auth, previousValue) {
|
|
4249
4380
|
if (frontendOptions !== void 0) return frontendOptions;
|
|
4381
|
+
const previousWeb = previousValue?.find((f) => WEB_FRONTEND_VALUES.includes(f));
|
|
4382
|
+
const previousNative = previousValue?.find((f) => f.startsWith("native-"));
|
|
4250
4383
|
while (true) {
|
|
4251
4384
|
const wasFirstPrompt = isFirstPrompt();
|
|
4252
4385
|
const frontendTypes = await navigableMultiselect({
|
|
@@ -4261,7 +4394,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
4261
4394
|
hint: "Create a React Native/Expo app"
|
|
4262
4395
|
}],
|
|
4263
4396
|
required: false,
|
|
4264
|
-
initialValues: ["web"]
|
|
4397
|
+
initialValues: previousValue ? [...previousWeb ? ["web"] : [], ...previousNative ? ["native"] : []] : ["web"]
|
|
4265
4398
|
});
|
|
4266
4399
|
if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
|
|
4267
4400
|
if (isCancel$1(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
@@ -4269,51 +4402,52 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
4269
4402
|
const result = [];
|
|
4270
4403
|
let shouldRestart = false;
|
|
4271
4404
|
if (frontendTypes.includes("web")) {
|
|
4405
|
+
const webOptions = [
|
|
4406
|
+
{
|
|
4407
|
+
value: "tanstack-router",
|
|
4408
|
+
label: "TanStack Router",
|
|
4409
|
+
hint: "Modern and scalable routing for React Applications"
|
|
4410
|
+
},
|
|
4411
|
+
{
|
|
4412
|
+
value: "react-router",
|
|
4413
|
+
label: "React Router",
|
|
4414
|
+
hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
|
|
4415
|
+
},
|
|
4416
|
+
{
|
|
4417
|
+
value: "next",
|
|
4418
|
+
label: "Next.js",
|
|
4419
|
+
hint: "The React Framework for the Web"
|
|
4420
|
+
},
|
|
4421
|
+
{
|
|
4422
|
+
value: "nuxt",
|
|
4423
|
+
label: "Nuxt",
|
|
4424
|
+
hint: "The Progressive Web Framework for Vue.js"
|
|
4425
|
+
},
|
|
4426
|
+
{
|
|
4427
|
+
value: "svelte",
|
|
4428
|
+
label: "Svelte",
|
|
4429
|
+
hint: "web development for the rest of us"
|
|
4430
|
+
},
|
|
4431
|
+
{
|
|
4432
|
+
value: "solid",
|
|
4433
|
+
label: "Solid",
|
|
4434
|
+
hint: "Simple and performant reactivity for building user interfaces"
|
|
4435
|
+
},
|
|
4436
|
+
{
|
|
4437
|
+
value: "astro",
|
|
4438
|
+
label: "Astro",
|
|
4439
|
+
hint: "The web framework for content-driven websites"
|
|
4440
|
+
},
|
|
4441
|
+
{
|
|
4442
|
+
value: "tanstack-start",
|
|
4443
|
+
label: "TanStack Start",
|
|
4444
|
+
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
|
|
4445
|
+
}
|
|
4446
|
+
].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth));
|
|
4272
4447
|
const webFramework = await navigableSelect({
|
|
4273
4448
|
message: "Choose web",
|
|
4274
|
-
options:
|
|
4275
|
-
|
|
4276
|
-
value: "tanstack-router",
|
|
4277
|
-
label: "TanStack Router",
|
|
4278
|
-
hint: "Modern and scalable routing for React Applications"
|
|
4279
|
-
},
|
|
4280
|
-
{
|
|
4281
|
-
value: "react-router",
|
|
4282
|
-
label: "React Router",
|
|
4283
|
-
hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
|
|
4284
|
-
},
|
|
4285
|
-
{
|
|
4286
|
-
value: "next",
|
|
4287
|
-
label: "Next.js",
|
|
4288
|
-
hint: "The React Framework for the Web"
|
|
4289
|
-
},
|
|
4290
|
-
{
|
|
4291
|
-
value: "nuxt",
|
|
4292
|
-
label: "Nuxt",
|
|
4293
|
-
hint: "The Progressive Web Framework for Vue.js"
|
|
4294
|
-
},
|
|
4295
|
-
{
|
|
4296
|
-
value: "svelte",
|
|
4297
|
-
label: "Svelte",
|
|
4298
|
-
hint: "web development for the rest of us"
|
|
4299
|
-
},
|
|
4300
|
-
{
|
|
4301
|
-
value: "solid",
|
|
4302
|
-
label: "Solid",
|
|
4303
|
-
hint: "Simple and performant reactivity for building user interfaces"
|
|
4304
|
-
},
|
|
4305
|
-
{
|
|
4306
|
-
value: "astro",
|
|
4307
|
-
label: "Astro",
|
|
4308
|
-
hint: "The web framework for content-driven websites"
|
|
4309
|
-
},
|
|
4310
|
-
{
|
|
4311
|
-
value: "tanstack-start",
|
|
4312
|
-
label: "TanStack Start",
|
|
4313
|
-
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
|
|
4314
|
-
}
|
|
4315
|
-
].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth)),
|
|
4316
|
-
initialValue: DEFAULT_CONFIG.frontend[0]
|
|
4449
|
+
options: webOptions,
|
|
4450
|
+
initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
|
|
4317
4451
|
});
|
|
4318
4452
|
if (isGoBack(webFramework)) shouldRestart = true;
|
|
4319
4453
|
else if (isCancel$1(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
@@ -4343,7 +4477,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
4343
4477
|
hint: "Consistent styling for React Native"
|
|
4344
4478
|
}
|
|
4345
4479
|
],
|
|
4346
|
-
initialValue: "native-bare"
|
|
4480
|
+
initialValue: previousNative ?? "native-bare"
|
|
4347
4481
|
});
|
|
4348
4482
|
if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
|
|
4349
4483
|
else {
|
|
@@ -4362,22 +4496,22 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
4362
4496
|
}
|
|
4363
4497
|
//#endregion
|
|
4364
4498
|
//#region src/prompts/git.ts
|
|
4365
|
-
async function getGitChoice(git) {
|
|
4499
|
+
async function getGitChoice(git, previousValue) {
|
|
4366
4500
|
if (git !== void 0) return git;
|
|
4367
4501
|
const response = await navigableConfirm({
|
|
4368
4502
|
message: "Initialize git repository?",
|
|
4369
|
-
initialValue: DEFAULT_CONFIG.git
|
|
4503
|
+
initialValue: previousValue ?? DEFAULT_CONFIG.git
|
|
4370
4504
|
});
|
|
4371
4505
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4372
4506
|
return response;
|
|
4373
4507
|
}
|
|
4374
4508
|
//#endregion
|
|
4375
4509
|
//#region src/prompts/install.ts
|
|
4376
|
-
async function getinstallChoice(install) {
|
|
4510
|
+
async function getinstallChoice(install, previousValue) {
|
|
4377
4511
|
if (install !== void 0) return install;
|
|
4378
4512
|
const response = await navigableConfirm({
|
|
4379
4513
|
message: "Install dependencies?",
|
|
4380
|
-
initialValue: DEFAULT_CONFIG.install
|
|
4514
|
+
initialValue: previousValue ?? DEFAULT_CONFIG.install
|
|
4381
4515
|
});
|
|
4382
4516
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4383
4517
|
return response;
|
|
@@ -4561,6 +4695,7 @@ function validateFullConfig(config, providedFlags, options) {
|
|
|
4561
4695
|
yield* validateFrontendConstraints(config, providedFlags);
|
|
4562
4696
|
yield* validateApiConstraints(config, options);
|
|
4563
4697
|
yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
|
4698
|
+
yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
|
|
4564
4699
|
yield* validateSelfBackendCompatibility(providedFlags, options, config);
|
|
4565
4700
|
yield* validateWorkersCompatibility(providedFlags, options, config);
|
|
4566
4701
|
if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
|
|
@@ -4604,7 +4739,7 @@ const ormOptions = {
|
|
|
4604
4739
|
hint: "Lightweight and performant TypeScript ORM"
|
|
4605
4740
|
}
|
|
4606
4741
|
};
|
|
4607
|
-
async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
|
|
4742
|
+
async function getORMChoice(orm, hasDatabase, database, backend, runtime, previousValue) {
|
|
4608
4743
|
if (backend === "convex") return "none";
|
|
4609
4744
|
if (!hasDatabase) return "none";
|
|
4610
4745
|
if (orm !== void 0) {
|
|
@@ -4612,18 +4747,20 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
|
|
|
4612
4747
|
if (compat.isErr()) throw compat.error;
|
|
4613
4748
|
return orm;
|
|
4614
4749
|
}
|
|
4750
|
+
const options = database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma];
|
|
4615
4751
|
const response = await navigableSelect({
|
|
4616
4752
|
message: "Select ORM",
|
|
4617
|
-
options
|
|
4618
|
-
initialValue: database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm
|
|
4753
|
+
options,
|
|
4754
|
+
initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
|
|
4619
4755
|
});
|
|
4620
4756
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4621
4757
|
return response;
|
|
4622
4758
|
}
|
|
4623
4759
|
//#endregion
|
|
4624
4760
|
//#region src/prompts/package-manager.ts
|
|
4625
|
-
async function getPackageManagerChoice(packageManager) {
|
|
4761
|
+
async function getPackageManagerChoice(packageManager, previousValue) {
|
|
4626
4762
|
if (packageManager !== void 0) return packageManager;
|
|
4763
|
+
const detectedPackageManager = getUserPkgManager();
|
|
4627
4764
|
const response = await navigableSelect({
|
|
4628
4765
|
message: "Choose package manager",
|
|
4629
4766
|
options: [
|
|
@@ -4643,36 +4780,37 @@ async function getPackageManagerChoice(packageManager) {
|
|
|
4643
4780
|
hint: "All-in-one JavaScript runtime & toolkit"
|
|
4644
4781
|
}
|
|
4645
4782
|
],
|
|
4646
|
-
initialValue:
|
|
4783
|
+
initialValue: previousValue ?? detectedPackageManager
|
|
4647
4784
|
});
|
|
4648
4785
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4649
4786
|
return response;
|
|
4650
4787
|
}
|
|
4651
4788
|
//#endregion
|
|
4652
4789
|
//#region src/prompts/payments.ts
|
|
4653
|
-
async function getPaymentsChoice(payments, auth, backend, _frontends) {
|
|
4790
|
+
async function getPaymentsChoice(payments, auth, backend, _frontends, previousValue) {
|
|
4654
4791
|
if (payments !== void 0) return payments;
|
|
4655
4792
|
if (backend === "none") return "none";
|
|
4656
4793
|
if (!(auth === "better-auth")) return "none";
|
|
4794
|
+
const options = [{
|
|
4795
|
+
value: "polar",
|
|
4796
|
+
label: "Polar",
|
|
4797
|
+
hint: "Turn your software into a business. 6 lines of code."
|
|
4798
|
+
}, {
|
|
4799
|
+
value: "none",
|
|
4800
|
+
label: "None",
|
|
4801
|
+
hint: "No payments integration"
|
|
4802
|
+
}];
|
|
4657
4803
|
const response = await navigableSelect({
|
|
4658
4804
|
message: "Select payments provider",
|
|
4659
|
-
options
|
|
4660
|
-
|
|
4661
|
-
label: "Polar",
|
|
4662
|
-
hint: "Turn your software into a business. 6 lines of code."
|
|
4663
|
-
}, {
|
|
4664
|
-
value: "none",
|
|
4665
|
-
label: "None",
|
|
4666
|
-
hint: "No payments integration"
|
|
4667
|
-
}],
|
|
4668
|
-
initialValue: DEFAULT_CONFIG.payments
|
|
4805
|
+
options,
|
|
4806
|
+
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.payments)
|
|
4669
4807
|
});
|
|
4670
4808
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4671
4809
|
return response;
|
|
4672
4810
|
}
|
|
4673
4811
|
//#endregion
|
|
4674
4812
|
//#region src/prompts/runtime.ts
|
|
4675
|
-
async function getRuntimeChoice(runtime, backend) {
|
|
4813
|
+
async function getRuntimeChoice(runtime, backend, previousValue) {
|
|
4676
4814
|
if (backend === "convex" || backend === "none" || backend === "self") return "none";
|
|
4677
4815
|
if (runtime !== void 0) return runtime;
|
|
4678
4816
|
const runtimeOptions = [{
|
|
@@ -4692,19 +4830,56 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
4692
4830
|
const response = await navigableSelect({
|
|
4693
4831
|
message: "Select runtime",
|
|
4694
4832
|
options: runtimeOptions,
|
|
4695
|
-
initialValue: DEFAULT_CONFIG.runtime
|
|
4833
|
+
initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
|
|
4696
4834
|
});
|
|
4697
4835
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4698
4836
|
return response;
|
|
4699
4837
|
}
|
|
4700
4838
|
//#endregion
|
|
4701
4839
|
//#region src/prompts/server-deploy.ts
|
|
4702
|
-
|
|
4840
|
+
const SERVER_APP_BACKENDS = [
|
|
4841
|
+
"hono",
|
|
4842
|
+
"express",
|
|
4843
|
+
"fastify",
|
|
4844
|
+
"elysia"
|
|
4845
|
+
];
|
|
4846
|
+
function getDeploymentDisplay$1(deployment) {
|
|
4847
|
+
if (deployment === "cloudflare") return {
|
|
4848
|
+
label: "Cloudflare",
|
|
4849
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
4850
|
+
};
|
|
4851
|
+
if (deployment === "docker") return {
|
|
4852
|
+
label: "Docker",
|
|
4853
|
+
hint: "Self-host with a Dockerfile and docker-compose.yml"
|
|
4854
|
+
};
|
|
4855
|
+
return {
|
|
4856
|
+
label: deployment,
|
|
4857
|
+
hint: `Add ${deployment} deployment`
|
|
4858
|
+
};
|
|
4859
|
+
}
|
|
4860
|
+
async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy, previousValue) {
|
|
4703
4861
|
if (deployment !== void 0) return deployment;
|
|
4704
|
-
if (backend
|
|
4705
|
-
if (backend !== "hono") return "none";
|
|
4862
|
+
if (!backend || !SERVER_APP_BACKENDS.includes(backend)) return "none";
|
|
4706
4863
|
if (runtime === "workers") return "cloudflare";
|
|
4707
|
-
return "none";
|
|
4864
|
+
if (runtime !== "bun" && runtime !== "node") return "none";
|
|
4865
|
+
const options = ["docker", "none"].map((deploy) => {
|
|
4866
|
+
const { label, hint } = deploy === "none" ? {
|
|
4867
|
+
label: "None",
|
|
4868
|
+
hint: "Skip deployment setup"
|
|
4869
|
+
} : getDeploymentDisplay$1(deploy);
|
|
4870
|
+
return {
|
|
4871
|
+
value: deploy,
|
|
4872
|
+
label,
|
|
4873
|
+
hint
|
|
4874
|
+
};
|
|
4875
|
+
});
|
|
4876
|
+
const response = await navigableSelect({
|
|
4877
|
+
message: "Select server deployment",
|
|
4878
|
+
options,
|
|
4879
|
+
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
|
|
4880
|
+
});
|
|
4881
|
+
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4882
|
+
return response;
|
|
4708
4883
|
}
|
|
4709
4884
|
//#endregion
|
|
4710
4885
|
//#region src/prompts/web-deploy.ts
|
|
@@ -4716,26 +4891,35 @@ function getDeploymentDisplay(deployment) {
|
|
|
4716
4891
|
label: "Cloudflare",
|
|
4717
4892
|
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
4718
4893
|
};
|
|
4894
|
+
if (deployment === "docker") return {
|
|
4895
|
+
label: "Docker",
|
|
4896
|
+
hint: "Self-host with a Dockerfile and docker-compose.yml"
|
|
4897
|
+
};
|
|
4719
4898
|
return {
|
|
4720
4899
|
label: deployment,
|
|
4721
4900
|
hint: `Add ${deployment} deployment`
|
|
4722
4901
|
};
|
|
4723
4902
|
}
|
|
4724
|
-
async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup) {
|
|
4903
|
+
async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup, previousValue) {
|
|
4725
4904
|
if (deployment !== void 0) return deployment;
|
|
4726
4905
|
if (!hasWebFrontend(frontend)) return "none";
|
|
4727
4906
|
if (backend === "self" && dbSetup === "d1") return "cloudflare";
|
|
4907
|
+
const options = [
|
|
4908
|
+
"cloudflare",
|
|
4909
|
+
"docker",
|
|
4910
|
+
"none"
|
|
4911
|
+
].map((deploy) => {
|
|
4912
|
+
const { label, hint } = getDeploymentDisplay(deploy);
|
|
4913
|
+
return {
|
|
4914
|
+
value: deploy,
|
|
4915
|
+
label,
|
|
4916
|
+
hint
|
|
4917
|
+
};
|
|
4918
|
+
});
|
|
4728
4919
|
const response = await navigableSelect({
|
|
4729
4920
|
message: "Select web deployment",
|
|
4730
|
-
options
|
|
4731
|
-
|
|
4732
|
-
return {
|
|
4733
|
-
value: deploy,
|
|
4734
|
-
label,
|
|
4735
|
-
hint
|
|
4736
|
-
};
|
|
4737
|
-
}),
|
|
4738
|
-
initialValue: DEFAULT_CONFIG.webDeploy
|
|
4921
|
+
options,
|
|
4922
|
+
initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
|
|
4739
4923
|
});
|
|
4740
4924
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4741
4925
|
return response;
|
|
@@ -4767,22 +4951,22 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
4767
4951
|
serverDeploy: flags.serverDeploy ?? DEFAULT_CONFIG.serverDeploy
|
|
4768
4952
|
};
|
|
4769
4953
|
const result = await navigableGroup({
|
|
4770
|
-
frontend: () => getFrontendChoice(flags.frontend, flags.backend, flags.auth),
|
|
4771
|
-
backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
|
|
4772
|
-
runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend),
|
|
4773
|
-
database: ({ results }) => getDatabaseChoice(flags.database, results.backend, results.runtime),
|
|
4774
|
-
orm: ({ results }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime),
|
|
4775
|
-
api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
|
|
4776
|
-
auth: ({ results }) => getAuthChoice(flags.auth, results.backend, results.frontend),
|
|
4777
|
-
payments: ({ results }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend),
|
|
4778
|
-
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime),
|
|
4779
|
-
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
|
|
4780
|
-
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
|
|
4781
|
-
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup),
|
|
4782
|
-
serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
|
|
4783
|
-
git: () => getGitChoice(flags.git),
|
|
4784
|
-
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
|
4785
|
-
install: () => getinstallChoice(flags.install)
|
|
4954
|
+
frontend: ({ previousAnswer }) => getFrontendChoice(flags.frontend, flags.backend, flags.auth, previousAnswer),
|
|
4955
|
+
backend: ({ results, previousAnswer }) => getBackendFrameworkChoice(flags.backend, results.frontend, previousAnswer),
|
|
4956
|
+
runtime: ({ results, previousAnswer }) => getRuntimeChoice(flags.runtime, results.backend, previousAnswer),
|
|
4957
|
+
database: ({ results, previousAnswer }) => getDatabaseChoice(flags.database, results.backend, results.runtime, previousAnswer),
|
|
4958
|
+
orm: ({ results, previousAnswer }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime, previousAnswer),
|
|
4959
|
+
api: ({ results, previousAnswer }) => getApiChoice(flags.api, results.frontend, results.backend, previousAnswer),
|
|
4960
|
+
auth: ({ results, previousAnswer }) => getAuthChoice(flags.auth, results.backend, results.frontend, previousAnswer),
|
|
4961
|
+
payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
|
|
4962
|
+
addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
|
|
4963
|
+
examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
|
|
4964
|
+
dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
|
|
4965
|
+
webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, previousAnswer),
|
|
4966
|
+
serverDeploy: ({ results, previousAnswer }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy, previousAnswer),
|
|
4967
|
+
git: ({ previousAnswer }) => getGitChoice(flags.git, previousAnswer),
|
|
4968
|
+
packageManager: ({ previousAnswer }) => getPackageManagerChoice(flags.packageManager, previousAnswer),
|
|
4969
|
+
install: ({ previousAnswer }) => getinstallChoice(flags.install, previousAnswer)
|
|
4786
4970
|
}, { onCancel: () => {
|
|
4787
4971
|
throw new UserCancelledError({ message: "Operation cancelled" });
|
|
4788
4972
|
} });
|
|
@@ -6713,12 +6897,15 @@ async function displayPostInstallInstructions(config) {
|
|
|
6713
6897
|
const cdCmd = `cd ${relativePath}`;
|
|
6714
6898
|
const hasHusky = addons?.includes("husky");
|
|
6715
6899
|
const hasLefthook = addons?.includes("lefthook");
|
|
6716
|
-
const
|
|
6900
|
+
const hasVitePlus = addons?.includes("vite-plus");
|
|
6901
|
+
const hasVitePlusNativeHooks = hasVitePlus && !hasHusky && !hasLefthook;
|
|
6902
|
+
const hasGitHooksOrLinting = addons?.includes("husky") || addons?.includes("biome") || addons?.includes("lefthook") || addons?.includes("oxlint") || hasVitePlus;
|
|
6717
6903
|
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, webDeploy, serverDeploy, backend) : "";
|
|
6718
6904
|
const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd, frontend) : "";
|
|
6719
6905
|
const electrobunInstructions = addons?.includes("electrobun") ? getElectrobunInstructions(runCmd, frontend) : "";
|
|
6720
6906
|
const huskyInstructions = hasHusky ? getHuskyInstructions(runCmd) : "";
|
|
6721
6907
|
const lefthookInstructions = hasLefthook ? getLefthookInstructions(packageManager) : "";
|
|
6908
|
+
const vitePlusNativeHooksInstructions = hasVitePlusNativeHooks ? getVitePlusNativeHooksInstructions(runCmd) : "";
|
|
6722
6909
|
const lintingInstructions = hasGitHooksOrLinting ? getLintingInstructions(runCmd) : "";
|
|
6723
6910
|
const nativeInstructions = (frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles")) && backend !== "none" ? getNativeInstructions(isConvex, isBackendSelf, frontend || [], runCmd) : "";
|
|
6724
6911
|
const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
|
|
@@ -6774,6 +6961,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
6774
6961
|
if (electrobunInstructions) output += `\n${electrobunInstructions.trim()}\n`;
|
|
6775
6962
|
if (huskyInstructions) output += `\n${huskyInstructions.trim()}\n`;
|
|
6776
6963
|
if (lefthookInstructions) output += `\n${lefthookInstructions.trim()}\n`;
|
|
6964
|
+
if (vitePlusNativeHooksInstructions) output += `\n${vitePlusNativeHooksInstructions.trim()}\n`;
|
|
6777
6965
|
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
|
|
6778
6966
|
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
|
|
6779
6967
|
if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
|
|
@@ -6805,12 +6993,15 @@ function getHuskyInstructions(runCmd) {
|
|
|
6805
6993
|
return `${pc.bold("Git hooks with Husky:")}\n${pc.cyan("•")} Initialize hooks: ${`${runCmd} prepare`}\n`;
|
|
6806
6994
|
}
|
|
6807
6995
|
function getLintingInstructions(runCmd) {
|
|
6808
|
-
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")}
|
|
6996
|
+
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Run checks: ${`${runCmd} check`}\n`;
|
|
6809
6997
|
}
|
|
6810
6998
|
function getLefthookInstructions(packageManager) {
|
|
6811
6999
|
const cmd = packageManager === "npm" ? "npx" : packageManager;
|
|
6812
7000
|
return `${pc.bold("Git hooks with Lefthook:")}\n${pc.cyan("•")} Install hooks: ${cmd} lefthook install\n`;
|
|
6813
7001
|
}
|
|
7002
|
+
function getVitePlusNativeHooksInstructions(runCmd) {
|
|
7003
|
+
return `${pc.bold("Vite+ native Git hooks:")}\n${pc.cyan("•")} Optional hook setup: ${`${runCmd} hooks:setup`}\n${pc.dim(" (runs vp config; hooks install into .vite-hooks and use vp staged)")}\n`;
|
|
7004
|
+
}
|
|
6814
7005
|
async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
|
|
6815
7006
|
const instructions = [];
|
|
6816
7007
|
const isD1Alchemy = dbSetup === "d1" && (serverDeploy === "cloudflare" || backend === "self" && webDeploy === "cloudflare");
|
|
@@ -6947,6 +7138,10 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
|
|
|
6947
7138
|
if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
6948
7139
|
else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
6949
7140
|
else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
7141
|
+
if (webDeploy === "docker" || serverDeploy === "docker") {
|
|
7142
|
+
const dockerTargets = webDeploy === "docker" && serverDeploy === "docker" ? "web + server" : webDeploy === "docker" ? "web" : "server";
|
|
7143
|
+
instructions.push(`${pc.bold(`Deploy ${dockerTargets} with Docker Compose:`)}\n${pc.cyan("•")} Start: ${`${runCmd} docker:up`}\n${pc.cyan("•")} Logs: ${`${runCmd} docker:logs`}\n${pc.cyan("•")} Stop: ${`${runCmd} docker:down`}\n${pc.cyan("•")} Config: docker-compose.yml`);
|
|
7144
|
+
}
|
|
6950
7145
|
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
6951
7146
|
}
|
|
6952
7147
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.33.0",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"better-auth",
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
"prepublishOnly": "npm run build"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@better-t-stack/template-generator": "^3.
|
|
73
|
-
"@better-t-stack/types": "^3.
|
|
74
|
-
"@clack/core": "^1.
|
|
75
|
-
"@clack/prompts": "^1.
|
|
72
|
+
"@better-t-stack/template-generator": "^3.33.0",
|
|
73
|
+
"@better-t-stack/types": "^3.33.0",
|
|
74
|
+
"@clack/core": "^1.4.1",
|
|
75
|
+
"@clack/prompts": "^1.5.1",
|
|
76
76
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
77
77
|
"@trpc/server": "^11.17.0",
|
|
78
78
|
"better-result": "^2.9.2",
|