create-better-t-stack 3.38.2 → 3.39.2
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 +17 -6
- package/dist/cli.mjs +3 -3
- package/dist/index.d.mts +10 -10
- package/dist/index.mjs +1 -1
- package/dist/{src-ihjK9T9S.mjs → src-BYgmfow5.mjs} +424 -174
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -27,12 +27,23 @@ npx create-better-t-stack@latest
|
|
|
27
27
|
|
|
28
28
|
Follow the prompts to configure your project or use the `--yes` flag for defaults.
|
|
29
29
|
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Node.js 22 or newer to run the CLI with Node.js. The selected framework may require a newer release; the CLI checks the exact stack before writing files.
|
|
33
|
+
- Bun 1.3 or newer is recommended. Bun 1.2.14 remains the minimum because generated workspaces use dependency catalogs.
|
|
34
|
+
- pnpm 10.26.0 or newer when using pnpm (catalogs and `allowBuilds`).
|
|
35
|
+
- npm 11.16.0 or newer when using npm (`allowScripts`).
|
|
36
|
+
|
|
37
|
+
Docker Compose 2.24 or newer is required by generated Docker stacks. Git, Rust, Docker,
|
|
38
|
+
and platform SDKs otherwise remain optional and are only needed when you use their corresponding
|
|
39
|
+
generated commands.
|
|
40
|
+
|
|
30
41
|
## Features
|
|
31
42
|
|
|
32
43
|
| Category | Options |
|
|
33
44
|
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
45
|
| **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>•
|
|
46
|
+
| **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>• SolidStart v2 (SSR)<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
|
|
36
47
|
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
|
|
37
48
|
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
|
|
38
49
|
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
|
|
@@ -245,13 +256,13 @@ npx create-better-t-stack --frontend none --backend hono --api trpc --database n
|
|
|
245
256
|
- **ORM 'none'**: Can be used when you want to handle database operations manually or use a different ORM.
|
|
246
257
|
- **Runtime 'none'**: Only available with Convex backend, backend `none`, or backend `self`.
|
|
247
258
|
- **Cloudflare Workers runtime**: Only compatible with Hono backend. If a database is used, MongoDB is not supported.
|
|
248
|
-
- **Cloudflare D1 setup**: Requires `sqlite` and either `--runtime workers --server-deploy cloudflare` or `--backend self --web-deploy cloudflare`. For `backend self`, D1 is supported on `next`, `tanstack-start`, `nuxt`, `svelte`, and `astro`.
|
|
259
|
+
- **Cloudflare D1 setup**: Requires `sqlite` and either `--runtime workers --server-deploy cloudflare` or `--backend self --web-deploy cloudflare`. For `backend self`, D1 is supported on `next`, `tanstack-start`, `nuxt`, `svelte`, `solid`, and `astro`.
|
|
249
260
|
- **Addons 'none'**: Skips all addons.
|
|
250
261
|
- **Examples 'none'**: Skips all example implementations (todo, AI chat).
|
|
251
|
-
- **Nuxt, Svelte,
|
|
252
|
-
- **PWA support** requires TanStack Router, React Router, Next.js, or
|
|
253
|
-
- **Tauri desktop app** requires TanStack Router, React Router, TanStack Start, Next.js, Nuxt, SvelteKit,
|
|
254
|
-
- **Electrobun desktop app** requires TanStack Router, React Router, TanStack Start, Next.js, Nuxt, SvelteKit,
|
|
262
|
+
- **Nuxt, Svelte, SolidStart, and Astro** frontends are only compatible with the oRPC API layer
|
|
263
|
+
- **PWA support** requires TanStack Router, React Router, Next.js, or SolidStart
|
|
264
|
+
- **Tauri desktop app** requires TanStack Router, React Router, TanStack Start, Next.js, Nuxt, SvelteKit, or Astro
|
|
265
|
+
- **Electrobun desktop app** requires TanStack Router, React Router, TanStack Start, Next.js, Nuxt, SvelteKit, or Astro. Desktop packaging uses static web assets, so SSR-first frontends need a static/export build before desktop builds will work.
|
|
255
266
|
- **AI example** is not compatible with Solid or Astro. With Convex backend, it also excludes Nuxt and Svelte.
|
|
256
267
|
|
|
257
268
|
## Project Structure
|
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-BYgmfow5.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";
|
|
@@ -44,8 +44,8 @@ function formatToolSuccess(data) {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
function formatToolError(
|
|
48
|
-
const message =
|
|
47
|
+
function formatToolError(cause) {
|
|
48
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
49
49
|
return {
|
|
50
50
|
content: [{
|
|
51
51
|
type: "text",
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Result, Result as Result$1 } from "better-result";
|
|
3
|
-
import {
|
|
3
|
+
import { TrpcCli, TrpcCliMeta } from "trpc-cli";
|
|
4
4
|
import z from "zod";
|
|
5
5
|
import { EMBEDDED_TEMPLATES, GeneratorError, GeneratorError as GeneratorError$1, GeneratorOptions, TEMPLATE_COUNT, VirtualDirectory, VirtualFile, VirtualFileSystem, VirtualFileTree, VirtualFileTree as VirtualFileTree$1, VirtualNode, generate } from "@better-t-stack/template-generator";
|
|
6
6
|
import * as import__better_t_stack_types from "@better-t-stack/types";
|
|
@@ -31,12 +31,12 @@ declare const ValidationError_base: import("better-result").TaggedErrorClass<"Va
|
|
|
31
31
|
*/
|
|
32
32
|
declare class ValidationError extends ValidationError_base<{
|
|
33
33
|
field?: string;
|
|
34
|
-
value?:
|
|
34
|
+
value?: string | number | boolean | null;
|
|
35
35
|
message: string;
|
|
36
36
|
}> {
|
|
37
37
|
constructor(args: {
|
|
38
38
|
field?: string;
|
|
39
|
-
value?:
|
|
39
|
+
value?: string | number | boolean | null;
|
|
40
40
|
message: string;
|
|
41
41
|
});
|
|
42
42
|
}
|
|
@@ -179,8 +179,8 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
179
179
|
backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
|
|
180
180
|
runtime?: "none" | "bun" | "node" | "workers" | undefined;
|
|
181
181
|
api?: "none" | "trpc" | "orpc" | undefined;
|
|
182
|
-
webDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
183
|
-
serverDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
182
|
+
webDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
183
|
+
serverDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
184
184
|
directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
|
|
185
185
|
renderTitle?: boolean | undefined;
|
|
186
186
|
disableAnalytics?: boolean | undefined;
|
|
@@ -278,8 +278,8 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
278
278
|
backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
|
|
279
279
|
runtime?: "none" | "bun" | "node" | "workers" | undefined;
|
|
280
280
|
api?: "none" | "trpc" | "orpc" | undefined;
|
|
281
|
-
webDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
282
|
-
serverDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
281
|
+
webDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
282
|
+
serverDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
283
283
|
directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
|
|
284
284
|
renderTitle?: boolean | undefined;
|
|
285
285
|
disableAnalytics?: boolean | undefined;
|
|
@@ -359,8 +359,8 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
359
359
|
hooks?: ("cursor" | "windsurf" | "codebuddy" | "claude" | "copilot")[] | undefined;
|
|
360
360
|
} | undefined;
|
|
361
361
|
} | undefined;
|
|
362
|
-
webDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
363
|
-
serverDeploy?: "none" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
362
|
+
webDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
363
|
+
serverDeploy?: "none" | "prisma" | "docker" | "cloudflare" | "vercel" | undefined;
|
|
364
364
|
projectDir?: string | undefined;
|
|
365
365
|
install?: boolean | undefined;
|
|
366
366
|
packageManager?: "bun" | "npm" | "pnpm" | undefined;
|
|
@@ -379,7 +379,7 @@ declare const router: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
379
379
|
meta: TrpcCliMeta;
|
|
380
380
|
}>;
|
|
381
381
|
}>>;
|
|
382
|
-
declare function createBtsCli():
|
|
382
|
+
declare function createBtsCli(): TrpcCli;
|
|
383
383
|
/**
|
|
384
384
|
* Error types that can be returned from create/createVirtual
|
|
385
385
|
*/
|
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-BYgmfow5.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 };
|
|
@@ -11,7 +11,7 @@ import path from "node:path";
|
|
|
11
11
|
import envPaths from "env-paths";
|
|
12
12
|
import fs from "fs-extra";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { desktopWebFrontends as desktopWebFrontends$
|
|
14
|
+
import { desktopWebFrontends as desktopWebFrontends$2, usesAlchemyManagedDatabase } from "@better-t-stack/types";
|
|
15
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";
|
|
@@ -21,6 +21,7 @@ import { writeTree } from "@better-t-stack/template-generator/fs-writer";
|
|
|
21
21
|
import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
|
|
22
22
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
23
23
|
import os from "node:os";
|
|
24
|
+
import { clean, coerce, satisfies } from "semver";
|
|
24
25
|
import { format } from "oxfmt";
|
|
25
26
|
//#region src/utils/display-config.ts
|
|
26
27
|
const VALUE_LABELS = {
|
|
@@ -90,11 +91,16 @@ const VALUE_LABELS = {
|
|
|
90
91
|
npm: "npm",
|
|
91
92
|
pnpm: "pnpm"
|
|
92
93
|
};
|
|
94
|
+
function isKnownValueLabel(value) {
|
|
95
|
+
return Object.hasOwn(VALUE_LABELS, value);
|
|
96
|
+
}
|
|
93
97
|
function formatConfigValue(value) {
|
|
94
|
-
if (
|
|
98
|
+
if (value === true) return "Yes";
|
|
99
|
+
if (value === false) return "No";
|
|
95
100
|
if (Array.isArray(value)) return value.length > 0 ? value.map(formatConfigValue).join(", ") : "None";
|
|
96
101
|
const text = String(value);
|
|
97
|
-
|
|
102
|
+
if (isKnownValueLabel(text)) return VALUE_LABELS[text];
|
|
103
|
+
return text.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
98
104
|
}
|
|
99
105
|
function section(title, entries) {
|
|
100
106
|
const rows = entries.filter(([, value]) => value !== void 0).map(([label, value, format]) => ({
|
|
@@ -202,8 +208,8 @@ const ADDON_COMPATIBILITY = {
|
|
|
202
208
|
"solid",
|
|
203
209
|
"next"
|
|
204
210
|
],
|
|
205
|
-
tauri: desktopWebFrontends$
|
|
206
|
-
electrobun: desktopWebFrontends$
|
|
211
|
+
tauri: desktopWebFrontends$2,
|
|
212
|
+
electrobun: desktopWebFrontends$2,
|
|
207
213
|
biome: [],
|
|
208
214
|
husky: [],
|
|
209
215
|
lefthook: [],
|
|
@@ -820,19 +826,19 @@ async function fetchSponsorsData({ url = SPONSORS_JSON_URL, withSpinner = false,
|
|
|
820
826
|
if (timeout) clearTimeout(timeout);
|
|
821
827
|
}
|
|
822
828
|
}
|
|
823
|
-
function normalizeSponsorFetchError(
|
|
824
|
-
if (
|
|
829
|
+
function normalizeSponsorFetchError(cause) {
|
|
830
|
+
if (cause instanceof Error && cause.name === "AbortError") return new CLIError({
|
|
825
831
|
message: "Failed to fetch sponsors: request timed out",
|
|
826
|
-
cause
|
|
832
|
+
cause
|
|
827
833
|
});
|
|
828
|
-
if (CLIError.is(
|
|
829
|
-
if (
|
|
830
|
-
message:
|
|
831
|
-
cause
|
|
834
|
+
if (CLIError.is(cause)) return cause;
|
|
835
|
+
if (cause instanceof Error) return new CLIError({
|
|
836
|
+
message: cause.message.startsWith("Failed to fetch sponsors:") ? cause.message : `Failed to fetch sponsors: ${cause.message}`,
|
|
837
|
+
cause
|
|
832
838
|
});
|
|
833
839
|
return new CLIError({
|
|
834
|
-
message: `Failed to fetch sponsors: ${String(
|
|
835
|
-
cause
|
|
840
|
+
message: `Failed to fetch sponsors: ${String(cause)}`,
|
|
841
|
+
cause
|
|
836
842
|
});
|
|
837
843
|
}
|
|
838
844
|
//#endregion
|
|
@@ -927,6 +933,7 @@ const FULLSTACK_FRONTENDS$1 = [
|
|
|
927
933
|
"tanstack-start",
|
|
928
934
|
"nuxt",
|
|
929
935
|
"svelte",
|
|
936
|
+
"solid",
|
|
930
937
|
"astro"
|
|
931
938
|
];
|
|
932
939
|
const EVLOG_SERVER_BACKENDS = [
|
|
@@ -935,7 +942,13 @@ const EVLOG_SERVER_BACKENDS = [
|
|
|
935
942
|
"fastify",
|
|
936
943
|
"elysia"
|
|
937
944
|
];
|
|
938
|
-
const EVLOG_FULLSTACK_FRONTENDS =
|
|
945
|
+
const EVLOG_FULLSTACK_FRONTENDS = [
|
|
946
|
+
"next",
|
|
947
|
+
"tanstack-start",
|
|
948
|
+
"nuxt",
|
|
949
|
+
"svelte",
|
|
950
|
+
"astro"
|
|
951
|
+
];
|
|
939
952
|
const evlogCompatibilityMessage = "evlog addon supports Hono, Express, Fastify, Elysia, or backend self with Next.js, TanStack Start, Nuxt, SvelteKit, or Astro. Convex and backend none are not supported yet.";
|
|
940
953
|
function supportsEvlogAddon(frontend = [], backend, _runtime) {
|
|
941
954
|
if (!backend) return true;
|
|
@@ -951,11 +964,11 @@ function validateSelfBackendCompatibility(providedFlags, options, config) {
|
|
|
951
964
|
const frontends = config.frontend || options.frontend || [];
|
|
952
965
|
if (backend === "self") {
|
|
953
966
|
const { web, native } = splitFrontends(frontends);
|
|
954
|
-
if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, or --frontend astro.");
|
|
967
|
+
if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, SolidStart, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend solid, or --frontend astro.");
|
|
955
968
|
if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
|
|
956
969
|
}
|
|
957
970
|
const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
|
|
958
|
-
if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend astro, or choose a different backend.");
|
|
971
|
+
if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, SolidStart, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend solid, --frontend astro, or choose a different backend.");
|
|
959
972
|
return Result.ok(void 0);
|
|
960
973
|
}
|
|
961
974
|
function validateWorkersCompatibility(providedFlags, options, config) {
|
|
@@ -1041,7 +1054,35 @@ function validateVercelServerDeploy(serverDeploy, backend, runtime) {
|
|
|
1041
1054
|
if (runtime === "workers") return validationErr$1("'--server-deploy vercel' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.");
|
|
1042
1055
|
return Result.ok(void 0);
|
|
1043
1056
|
}
|
|
1044
|
-
|
|
1057
|
+
function validatePrismaServerDeploy(serverDeploy, backend, runtime) {
|
|
1058
|
+
if (serverDeploy !== "prisma") return Result.ok(void 0);
|
|
1059
|
+
if (backend === "convex" || backend === "self") return validationErr$1("'--server-deploy prisma' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy prisma' instead.");
|
|
1060
|
+
if (runtime !== "bun" && runtime !== "node") return validationErr$1("'--server-deploy prisma' requires '--runtime bun' or '--runtime node'. Use '--server-deploy cloudflare' for Workers.");
|
|
1061
|
+
return Result.ok(void 0);
|
|
1062
|
+
}
|
|
1063
|
+
const PRISMA_COMPUTE_WEB_FRONTENDS = [
|
|
1064
|
+
"next",
|
|
1065
|
+
"nuxt",
|
|
1066
|
+
"astro",
|
|
1067
|
+
"react-router",
|
|
1068
|
+
"tanstack-start",
|
|
1069
|
+
"svelte",
|
|
1070
|
+
"solid"
|
|
1071
|
+
];
|
|
1072
|
+
function supportsPrismaWebDeploy(frontend) {
|
|
1073
|
+
return frontend.some((value) => PRISMA_COMPUTE_WEB_FRONTENDS.includes(value));
|
|
1074
|
+
}
|
|
1075
|
+
function validatePrismaWebDeploy(webDeploy, frontend) {
|
|
1076
|
+
if (webDeploy !== "prisma" || !frontend) return Result.ok(void 0);
|
|
1077
|
+
if (!supportsPrismaWebDeploy(frontend)) return validationErr$1("'--web-deploy prisma' requires a supported server frontend. Choose Next.js, Nuxt, Astro, React Router, TanStack Start, SvelteKit, or SolidStart. TanStack Router is a static SPA, while Prisma Compute requires an executable server artifact.");
|
|
1078
|
+
return Result.ok(void 0);
|
|
1079
|
+
}
|
|
1080
|
+
function validateCloudflareWebDeployKnownIssues(config) {
|
|
1081
|
+
if (config.webDeploy !== "cloudflare" || !config.frontend?.includes("next")) return Result.ok(void 0);
|
|
1082
|
+
if (config.database === "postgres" && config.orm === "prisma" && config.dbSetup !== "neon" && config.dbSetup !== "prisma-postgres") return validationErr$1("This Prisma PostgreSQL setup with Next.js on Cloudflare is temporarily unavailable because OpenNext does not preserve pg-cloudflare's workerd files. Use Neon or Prisma Postgres, choose another Cloudflare frontend, or choose Prisma, Docker, or Vercel deployment.");
|
|
1083
|
+
return Result.ok(void 0);
|
|
1084
|
+
}
|
|
1085
|
+
const DESKTOP_STATIC_EXPORT_FRONTENDS = [
|
|
1045
1086
|
"next",
|
|
1046
1087
|
"svelte",
|
|
1047
1088
|
"astro",
|
|
@@ -1051,10 +1092,18 @@ function validateDockerWebDeployDesktopAddons(webDeploy, addons, frontend, backe
|
|
|
1051
1092
|
if (webDeploy !== "docker" || !addons || !frontend) return Result.ok(void 0);
|
|
1052
1093
|
const desktopAddons = addons.filter((addon) => STATIC_DESKTOP_ADDONS.includes(addon));
|
|
1053
1094
|
if (desktopAddons.length === 0) return Result.ok(void 0);
|
|
1054
|
-
const affected = frontend.find((f) =>
|
|
1095
|
+
const affected = frontend.find((f) => DESKTOP_STATIC_EXPORT_FRONTENDS.includes(f));
|
|
1055
1096
|
if (!affected) return Result.ok(void 0);
|
|
1056
1097
|
if (affected === "next" && !desktopAddons.includes("tauri") && backend === "convex" && auth === "better-auth") return Result.ok(void 0);
|
|
1057
|
-
return validationErr$1(`'--web-deploy docker' is not compatible with the ${desktopAddons.join(", ")} addon on '${affected}' because desktop addons switch the web build to a static export, which the docker image cannot serve. Remove the addon or use
|
|
1098
|
+
return validationErr$1(`'--web-deploy docker' is not compatible with the ${desktopAddons.join(", ")} addon on '${affected}' because desktop addons switch the web build to a static export, which the docker image cannot serve. Remove the addon or use the static-serving tanstack-router frontend.`);
|
|
1099
|
+
}
|
|
1100
|
+
function validatePrismaWebDeployDesktopAddons(webDeploy, addons, frontend) {
|
|
1101
|
+
if (webDeploy !== "prisma" || !addons || !frontend) return Result.ok(void 0);
|
|
1102
|
+
const desktopAddons = addons.filter((addon) => STATIC_DESKTOP_ADDONS.includes(addon));
|
|
1103
|
+
if (desktopAddons.length === 0) return Result.ok(void 0);
|
|
1104
|
+
const affected = frontend.find((value) => DESKTOP_STATIC_EXPORT_FRONTENDS.includes(value));
|
|
1105
|
+
if (!affected) return Result.ok(void 0);
|
|
1106
|
+
return validationErr$1(`'--web-deploy prisma' is not compatible with the ${desktopAddons.join(", ")} addon on '${affected}' because desktop addons replace its executable server output with a static export, while Prisma Compute requires an executable server artifact. Remove the addon or choose a server deployment that supports this desktop build.`);
|
|
1058
1107
|
}
|
|
1059
1108
|
function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
|
|
1060
1109
|
if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
|
|
@@ -1101,7 +1150,13 @@ function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backe
|
|
|
1101
1150
|
return Result.ok(void 0);
|
|
1102
1151
|
}
|
|
1103
1152
|
function validateAddonsAgainstConfig(addons = [], config) {
|
|
1104
|
-
|
|
1153
|
+
const addonResult = validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
|
|
1154
|
+
if (addonResult.isErr()) return addonResult;
|
|
1155
|
+
const cloudflareResult = validateCloudflareWebDeployKnownIssues(config);
|
|
1156
|
+
if (cloudflareResult.isErr()) return cloudflareResult;
|
|
1157
|
+
const dockerResult = validateDockerWebDeployDesktopAddons(config.webDeploy, addons, config.frontend, config.backend, config.auth);
|
|
1158
|
+
if (dockerResult.isErr()) return dockerResult;
|
|
1159
|
+
return validatePrismaWebDeployDesktopAddons(config.webDeploy, addons, config.frontend);
|
|
1105
1160
|
}
|
|
1106
1161
|
function validatePaymentsCompatibility(payments, auth, _backend, _frontends = []) {
|
|
1107
1162
|
if (!payments || payments === "none") return Result.ok(void 0);
|
|
@@ -1344,7 +1399,7 @@ async function navigableGroupMultiselect(opts) {
|
|
|
1344
1399
|
const required = opts.required ?? true;
|
|
1345
1400
|
const opt = (option, state, options = []) => {
|
|
1346
1401
|
const label = option.label ?? String(option.value);
|
|
1347
|
-
const isItem =
|
|
1402
|
+
const isItem = option.group !== true && option.group !== false;
|
|
1348
1403
|
const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
|
|
1349
1404
|
const isLast = isItem && next && next.group === true;
|
|
1350
1405
|
const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : "";
|
|
@@ -1378,7 +1433,7 @@ async function navigableGroupMultiselect(opts) {
|
|
|
1378
1433
|
const value = this.value ?? [];
|
|
1379
1434
|
const styleOption = (option, active) => {
|
|
1380
1435
|
const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
|
|
1381
|
-
if (!active &&
|
|
1436
|
+
if (!active && option.group !== true && option.group !== false && this.options[this.cursor]?.value === option.group) return opt(option, selected ? "group-active-selected" : "group-active", this.options);
|
|
1382
1437
|
if (active && selected) return opt(option, "active-selected", this.options);
|
|
1383
1438
|
if (selected) return opt(option, "selected", this.options);
|
|
1384
1439
|
return opt(option, active ? "active" : "inactive", this.options);
|
|
@@ -1665,6 +1720,157 @@ function validateAgentSafePathInput(value, field) {
|
|
|
1665
1720
|
return Result.ok(void 0);
|
|
1666
1721
|
}
|
|
1667
1722
|
//#endregion
|
|
1723
|
+
//#region src/utils/external-commands.ts
|
|
1724
|
+
function shouldSkipExternalCommands() {
|
|
1725
|
+
return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
|
|
1726
|
+
}
|
|
1727
|
+
//#endregion
|
|
1728
|
+
//#region src/utils/requirements.ts
|
|
1729
|
+
const PACKAGE_MANAGER_VERSION_RANGES = {
|
|
1730
|
+
bun: ">=1.2.14",
|
|
1731
|
+
npm: ">=11.16.0",
|
|
1732
|
+
pnpm: ">=10.26.0"
|
|
1733
|
+
};
|
|
1734
|
+
const PACKAGE_MANAGER_REASONS = {
|
|
1735
|
+
bun: "generated Bun workspaces use dependency catalogs",
|
|
1736
|
+
npm: "generated npm workspaces use the allowScripts install-script policy",
|
|
1737
|
+
pnpm: "generated pnpm workspaces use catalogs and the allowBuilds policy"
|
|
1738
|
+
};
|
|
1739
|
+
const PACKAGE_MANAGER_UPGRADE_INSTRUCTIONS = {
|
|
1740
|
+
bun: "Run `bun upgrade`.",
|
|
1741
|
+
npm: "After updating Node.js, run `npm install --global npm@latest`.",
|
|
1742
|
+
pnpm: "Run `pnpm self-update`, or install the latest pnpm globally."
|
|
1743
|
+
};
|
|
1744
|
+
const NODE_UPGRADE_INSTRUCTION = "Install the latest Node.js 24 LTS release from https://nodejs.org, then rerun the command.";
|
|
1745
|
+
function normalizeVersion(value) {
|
|
1746
|
+
const trimmed = value.trim();
|
|
1747
|
+
return clean(trimmed) ?? coerce(trimmed)?.version ?? null;
|
|
1748
|
+
}
|
|
1749
|
+
function addNodeRequirement(requirements, range, reason) {
|
|
1750
|
+
requirements.push({
|
|
1751
|
+
tool: "node",
|
|
1752
|
+
range,
|
|
1753
|
+
reason
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
function getNodeToolingRequirements(config) {
|
|
1757
|
+
const requirements = [];
|
|
1758
|
+
for (const frontend of config.frontend) switch (frontend) {
|
|
1759
|
+
case "astro":
|
|
1760
|
+
addNodeRequirement(requirements, ">=22.12.0", "Astro 7");
|
|
1761
|
+
break;
|
|
1762
|
+
case "nuxt":
|
|
1763
|
+
addNodeRequirement(requirements, "^22.19.0 || ^24.11.0 || >=26.0.0", "Nuxt 4");
|
|
1764
|
+
break;
|
|
1765
|
+
case "solid":
|
|
1766
|
+
addNodeRequirement(requirements, ">=24.0.0", "SolidStart 2");
|
|
1767
|
+
break;
|
|
1768
|
+
case "react-router":
|
|
1769
|
+
addNodeRequirement(requirements, ">=22.22.0", "React Router 8");
|
|
1770
|
+
break;
|
|
1771
|
+
case "svelte":
|
|
1772
|
+
case "tanstack-router":
|
|
1773
|
+
case "tanstack-start":
|
|
1774
|
+
addNodeRequirement(requirements, "^20.19.0 || >=22.12.0", "Vite 8");
|
|
1775
|
+
break;
|
|
1776
|
+
case "next":
|
|
1777
|
+
addNodeRequirement(requirements, ">=20.9.0", "Next.js 16");
|
|
1778
|
+
break;
|
|
1779
|
+
case "native-bare":
|
|
1780
|
+
case "native-uniwind":
|
|
1781
|
+
case "native-unistyles":
|
|
1782
|
+
addNodeRequirement(requirements, "^22.13.0 || ^24.3.0 || >=26.0.0", "React Native 0.86");
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
if (![
|
|
1786
|
+
"none",
|
|
1787
|
+
"self",
|
|
1788
|
+
"convex"
|
|
1789
|
+
].includes(config.backend)) addNodeRequirement(requirements, "^22.18.0 || >=24.11.0", "the generated server build (tsdown)");
|
|
1790
|
+
if (config.orm === "prisma") addNodeRequirement(requirements, "^20.19.0 || ^22.12.0 || >=24.0.0", "Prisma 7");
|
|
1791
|
+
if (config.orm === "mongoose") addNodeRequirement(requirements, ">=20.19.0", "Mongoose 9 and MongoDB 7");
|
|
1792
|
+
if (config.examples.includes("ai")) addNodeRequirement(requirements, ">=22.0.0", "AI SDK 7");
|
|
1793
|
+
if (config.addons.includes("oxlint") || config.addons.includes("ultracite") && config.addonOptions?.ultracite?.linter === "oxlint") addNodeRequirement(requirements, "^20.19.0 || >=22.12.0", "Oxlint and Oxfmt");
|
|
1794
|
+
if (config.addons.includes("husky")) addNodeRequirement(requirements, ">=22.22.1", "lint-staged 17");
|
|
1795
|
+
if (config.addons.includes("vite-plus")) addNodeRequirement(requirements, "^20.19.0 || ^22.18.0 || >=24.11.0", "Vite+");
|
|
1796
|
+
if (config.addons.includes("starlight")) addNodeRequirement(requirements, ">=22.12.0", "Starlight's Astro toolchain");
|
|
1797
|
+
if (config.addons.includes("wxt")) addNodeRequirement(requirements, ">=22.0.0", "WXT");
|
|
1798
|
+
if (config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") addNodeRequirement(requirements, ">=22.0.0", "Wrangler 4");
|
|
1799
|
+
return requirements;
|
|
1800
|
+
}
|
|
1801
|
+
function getLocalVersionRequirements(config, hostRuntime) {
|
|
1802
|
+
const requirements = [{
|
|
1803
|
+
tool: config.packageManager,
|
|
1804
|
+
range: PACKAGE_MANAGER_VERSION_RANGES[config.packageManager],
|
|
1805
|
+
reason: PACKAGE_MANAGER_REASONS[config.packageManager]
|
|
1806
|
+
}];
|
|
1807
|
+
if (hostRuntime === "node") addNodeRequirement(requirements, ">=22.0.0", "create-better-t-stack");
|
|
1808
|
+
if (config.packageManager !== "bun") requirements.push(...getNodeToolingRequirements(config));
|
|
1809
|
+
else if (config.runtime === "node") addNodeRequirement(requirements, ">=22.0.0", "the selected Node.js server runtime");
|
|
1810
|
+
return requirements;
|
|
1811
|
+
}
|
|
1812
|
+
function formatRequirementError(requirement, rawVersion) {
|
|
1813
|
+
const label = requirement.tool === "node" ? "Node.js" : requirement.tool;
|
|
1814
|
+
if (!rawVersion) return `${label} is not available (required ${requirement.range} for ${requirement.reason}).`;
|
|
1815
|
+
const version = normalizeVersion(rawVersion);
|
|
1816
|
+
if (!version) return `Could not read the ${label} version from "${rawVersion}".`;
|
|
1817
|
+
if (!satisfies(version, requirement.range, { includePrerelease: true })) return `${label} ${version} does not satisfy ${requirement.range} required by ${requirement.reason}.`;
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
function validateLocalToolVersions(config, versions, hostRuntime) {
|
|
1821
|
+
const requirements = getLocalVersionRequirements(config, hostRuntime);
|
|
1822
|
+
const failures = requirements.map((requirement) => formatRequirementError(requirement, versions[requirement.tool])).filter((failure) => failure !== null);
|
|
1823
|
+
if (failures.length === 0) return Result.ok(void 0);
|
|
1824
|
+
const upgradeInstructions = /* @__PURE__ */ new Set();
|
|
1825
|
+
for (const requirement of requirements) {
|
|
1826
|
+
if (!formatRequirementError(requirement, versions[requirement.tool])) continue;
|
|
1827
|
+
if (requirement.tool === "node") upgradeInstructions.add(NODE_UPGRADE_INSTRUCTION);
|
|
1828
|
+
else upgradeInstructions.add(PACKAGE_MANAGER_UPGRADE_INSTRUCTIONS[requirement.tool]);
|
|
1829
|
+
}
|
|
1830
|
+
return Result.err(new CLIError({ message: [
|
|
1831
|
+
"Your local toolchain does not meet this stack's requirements:",
|
|
1832
|
+
...failures.map((failure) => `- ${failure}`),
|
|
1833
|
+
"",
|
|
1834
|
+
...upgradeInstructions
|
|
1835
|
+
].join("\n") }));
|
|
1836
|
+
}
|
|
1837
|
+
function getLocalToolRecommendations(config, versions) {
|
|
1838
|
+
if (config.packageManager !== "bun") return [];
|
|
1839
|
+
const rawVersion = versions.bun;
|
|
1840
|
+
if (!rawVersion) return [];
|
|
1841
|
+
const version = normalizeVersion(rawVersion);
|
|
1842
|
+
if (!version || !satisfies(version, PACKAGE_MANAGER_VERSION_RANGES.bun, { includePrerelease: true }) || satisfies(version, ">=1.3.0", { includePrerelease: true })) return [];
|
|
1843
|
+
return [`Bun ${version} meets the minimum requirement, but Bun 1.3 or newer is recommended. Run \`bun upgrade\`.`];
|
|
1844
|
+
}
|
|
1845
|
+
async function readToolVersion(tool) {
|
|
1846
|
+
const result = await Result.tryPromise({
|
|
1847
|
+
try: async () => {
|
|
1848
|
+
const { stdout } = await execa(tool, ["--version"], {
|
|
1849
|
+
cwd: os.tmpdir(),
|
|
1850
|
+
stderr: "pipe"
|
|
1851
|
+
});
|
|
1852
|
+
return normalizeVersion(stdout);
|
|
1853
|
+
},
|
|
1854
|
+
catch: () => null
|
|
1855
|
+
});
|
|
1856
|
+
return result.isOk() ? result.value : null;
|
|
1857
|
+
}
|
|
1858
|
+
async function checkLocalRequirements(config) {
|
|
1859
|
+
const hostRuntime = process.versions.bun ? "bun" : "node";
|
|
1860
|
+
const versions = {};
|
|
1861
|
+
const packageManagerVersion = await readToolVersion(config.packageManager);
|
|
1862
|
+
if (packageManagerVersion) versions[config.packageManager] = packageManagerVersion;
|
|
1863
|
+
if (getLocalVersionRequirements(config, hostRuntime).some((requirement) => requirement.tool === "node")) versions.node = hostRuntime === "node" ? process.versions.node : await readToolVersion("node") ?? void 0;
|
|
1864
|
+
if (!shouldSkipExternalCommands()) {
|
|
1865
|
+
const validationResult = validateLocalToolVersions(config, versions, hostRuntime);
|
|
1866
|
+
if (validationResult.isErr()) return Result.err(validationResult.error);
|
|
1867
|
+
}
|
|
1868
|
+
return Result.ok({
|
|
1869
|
+
packageManagerVersion: packageManagerVersion ?? "latest",
|
|
1870
|
+
warnings: getLocalToolRecommendations(config, versions)
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
//#endregion
|
|
1668
1874
|
//#region src/utils/add-package-deps.ts
|
|
1669
1875
|
const addPackageDependency = async (opts) => {
|
|
1670
1876
|
const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
|
|
@@ -1873,7 +2079,7 @@ function addNuxtEvlogSetup(content, serviceName) {
|
|
|
1873
2079
|
function addSvelteViteEvlogSetup(content, serviceName) {
|
|
1874
2080
|
let nextContent = prependMissingImports(content, ["import evlog from \"evlog/vite\";"]);
|
|
1875
2081
|
if (nextContent.includes("evlog({")) return nextContent;
|
|
1876
|
-
return nextContent.replace("
|
|
2082
|
+
return nextContent.replace("sveltekit(),", `sveltekit(),\n evlog({ service: "${serviceName}" }),`);
|
|
1877
2083
|
}
|
|
1878
2084
|
function getSvelteEvlogHooksCall(fsDrain) {
|
|
1879
2085
|
return fsDrain ? `createEvlogHooks({ drain: ${SVELTE_DEV_FS_DRAIN_EXPRESSION} })` : "createEvlogHooks()";
|
|
@@ -2104,18 +2310,23 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
2104
2310
|
`;
|
|
2105
2311
|
}
|
|
2106
2312
|
function getNuxtEvlogAuthMiddlewareFile(config) {
|
|
2107
|
-
if (usesCreateAuthFactory(config))
|
|
2313
|
+
if (usesCreateAuthFactory(config)) {
|
|
2314
|
+
const usesCloudflareRequestEnv = config.backend === "self" && config.webDeploy === "cloudflare";
|
|
2315
|
+
const authExpression = usesCloudflareRequestEnv ? "createAuth((event.context.cloudflare as { env: CloudflareEnv }).env)" : getAuthExpression(config);
|
|
2316
|
+
return `${getAuthImportLine(config)}
|
|
2317
|
+
${usesCloudflareRequestEnv ? `import type { CloudflareEnv } from "@${config.projectName}/env/server";\n` : ""}
|
|
2108
2318
|
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
2109
2319
|
|
|
2110
2320
|
export default defineEventHandler(async (event) => {
|
|
2111
2321
|
if (!event.context.log) return;
|
|
2112
|
-
const identify = createAuthMiddleware(${
|
|
2322
|
+
const identify = createAuthMiddleware(${authExpression} as BetterAuthInstance, {
|
|
2113
2323
|
exclude: ["/api/auth/**"],
|
|
2114
2324
|
maskEmail: true,
|
|
2115
2325
|
});
|
|
2116
2326
|
await identify(event.context.log, event.headers, event.path);
|
|
2117
2327
|
});
|
|
2118
2328
|
`;
|
|
2329
|
+
}
|
|
2119
2330
|
return `${getAuthImportLine(config)}
|
|
2120
2331
|
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
2121
2332
|
|
|
@@ -2360,7 +2571,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
2360
2571
|
continue;
|
|
2361
2572
|
}
|
|
2362
2573
|
if (isCancel$1(result)) {
|
|
2363
|
-
if (
|
|
2574
|
+
if (opts?.onCancel) {
|
|
2364
2575
|
results[name] = "canceled";
|
|
2365
2576
|
opts.onCancel({ results });
|
|
2366
2577
|
}
|
|
@@ -2383,11 +2594,6 @@ async function navigableGroup(prompts, opts) {
|
|
|
2383
2594
|
return results;
|
|
2384
2595
|
}
|
|
2385
2596
|
//#endregion
|
|
2386
|
-
//#region src/utils/external-commands.ts
|
|
2387
|
-
function shouldSkipExternalCommands() {
|
|
2388
|
-
return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
|
|
2389
|
-
}
|
|
2390
|
-
//#endregion
|
|
2391
2597
|
//#region src/utils/package-runner.ts
|
|
2392
2598
|
function splitCommandArgs(commandWithArgs) {
|
|
2393
2599
|
const args = [];
|
|
@@ -4007,21 +4213,21 @@ const DEFAULT_EDITORS = ["vscode"];
|
|
|
4007
4213
|
const DEFAULT_AGENTS = ["universal"];
|
|
4008
4214
|
const DEFAULT_HOOKS = [];
|
|
4009
4215
|
function getFrameworksFromFrontend(frontend) {
|
|
4010
|
-
const frameworkMap =
|
|
4011
|
-
"tanstack-router"
|
|
4012
|
-
"react-router"
|
|
4013
|
-
"tanstack-start"
|
|
4014
|
-
next
|
|
4015
|
-
nuxt
|
|
4016
|
-
"native-bare"
|
|
4017
|
-
"native-uniwind"
|
|
4018
|
-
"native-unistyles"
|
|
4019
|
-
svelte
|
|
4020
|
-
solid
|
|
4021
|
-
astro
|
|
4022
|
-
|
|
4216
|
+
const frameworkMap = /* @__PURE__ */ new Map([
|
|
4217
|
+
["tanstack-router", ["react", "tanstack"]],
|
|
4218
|
+
["react-router", ["react", "remix"]],
|
|
4219
|
+
["tanstack-start", ["react", "tanstack"]],
|
|
4220
|
+
["next", ["react", "next"]],
|
|
4221
|
+
["nuxt", ["vue"]],
|
|
4222
|
+
["native-bare", ["react"]],
|
|
4223
|
+
["native-uniwind", ["react"]],
|
|
4224
|
+
["native-unistyles", ["react"]],
|
|
4225
|
+
["svelte", ["svelte"]],
|
|
4226
|
+
["solid", ["solid"]],
|
|
4227
|
+
["astro", ["astro"]]
|
|
4228
|
+
]);
|
|
4023
4229
|
const frameworks = /* @__PURE__ */ new Set();
|
|
4024
|
-
for (const f of frontend) for (const framework of frameworkMap
|
|
4230
|
+
for (const f of frontend) for (const framework of frameworkMap.get(f) ?? []) frameworks.add(framework);
|
|
4025
4231
|
return Array.from(frameworks);
|
|
4026
4232
|
}
|
|
4027
4233
|
function buildUltraciteInitArgs({ packageManager, linter, frameworks, editors, agents, hooks, gitHooks }) {
|
|
@@ -4445,16 +4651,23 @@ const TASK_RUNNER_ADDONS = [
|
|
|
4445
4651
|
];
|
|
4446
4652
|
function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
|
|
4447
4653
|
if (!existingAddonOptions && !nextAddonOptions) return;
|
|
4448
|
-
const
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
...
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4654
|
+
const mergeOption = (existing, next) => {
|
|
4655
|
+
if (!existing) return next;
|
|
4656
|
+
if (!next) return existing;
|
|
4657
|
+
return {
|
|
4658
|
+
...existing,
|
|
4659
|
+
...next
|
|
4660
|
+
};
|
|
4661
|
+
};
|
|
4662
|
+
const mergedAddonOptions = {
|
|
4663
|
+
wxt: mergeOption(existingAddonOptions?.wxt, nextAddonOptions?.wxt),
|
|
4664
|
+
fumadocs: mergeOption(existingAddonOptions?.fumadocs, nextAddonOptions?.fumadocs),
|
|
4665
|
+
opentui: mergeOption(existingAddonOptions?.opentui, nextAddonOptions?.opentui),
|
|
4666
|
+
mcp: mergeOption(existingAddonOptions?.mcp, nextAddonOptions?.mcp),
|
|
4667
|
+
skills: mergeOption(existingAddonOptions?.skills, nextAddonOptions?.skills),
|
|
4668
|
+
ultracite: mergeOption(existingAddonOptions?.ultracite, nextAddonOptions?.ultracite)
|
|
4669
|
+
};
|
|
4670
|
+
return Object.values(mergedAddonOptions).some(Boolean) ? mergedAddonOptions : void 0;
|
|
4458
4671
|
}
|
|
4459
4672
|
function getSetupAddons(addonsToAdd, updatedAddons) {
|
|
4460
4673
|
const setupAddons = new Set(addonsToAdd);
|
|
@@ -4536,11 +4749,11 @@ async function addHandlerInternal(input) {
|
|
|
4536
4749
|
else {
|
|
4537
4750
|
const promptResult = await Result.tryPromise({
|
|
4538
4751
|
try: () => getAddonsToAdd(existingConfig),
|
|
4539
|
-
catch: (
|
|
4540
|
-
if (UserCancelledError.is(
|
|
4752
|
+
catch: (cause) => {
|
|
4753
|
+
if (UserCancelledError.is(cause)) return cause;
|
|
4541
4754
|
return new CLIError({
|
|
4542
|
-
message:
|
|
4543
|
-
cause
|
|
4755
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
4756
|
+
cause
|
|
4544
4757
|
});
|
|
4545
4758
|
}
|
|
4546
4759
|
});
|
|
@@ -4587,6 +4800,9 @@ async function addHandlerInternal(input) {
|
|
|
4587
4800
|
...config,
|
|
4588
4801
|
addons: updatedAddons
|
|
4589
4802
|
};
|
|
4803
|
+
const requirementsResult = await checkLocalRequirements(updatedConfig);
|
|
4804
|
+
if (requirementsResult.isErr()) return Result.err(requirementsResult.error);
|
|
4805
|
+
if (!isSilent()) for (const warning of requirementsResult.value.warnings) log.warn(pc.yellow(warning));
|
|
4590
4806
|
if (!isSilent()) log.info(pc.dim("Preparing addon files…"));
|
|
4591
4807
|
const vfs = new VirtualFileSystem();
|
|
4592
4808
|
for (const pkgPath of ADD_PACKAGE_JSON_PATHS) {
|
|
@@ -4639,11 +4855,11 @@ async function addHandlerInternal(input) {
|
|
|
4639
4855
|
...config,
|
|
4640
4856
|
addons: getSetupAddons(addonsToAdd, updatedAddons)
|
|
4641
4857
|
}),
|
|
4642
|
-
catch: (
|
|
4643
|
-
if (UserCancelledError.is(
|
|
4858
|
+
catch: (cause) => {
|
|
4859
|
+
if (UserCancelledError.is(cause)) return cause;
|
|
4644
4860
|
return new CLIError({
|
|
4645
|
-
message:
|
|
4646
|
-
cause
|
|
4861
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
4862
|
+
cause
|
|
4647
4863
|
});
|
|
4648
4864
|
}
|
|
4649
4865
|
});
|
|
@@ -4761,6 +4977,7 @@ const FULLSTACK_FRONTENDS = [
|
|
|
4761
4977
|
"tanstack-start",
|
|
4762
4978
|
"nuxt",
|
|
4763
4979
|
"svelte",
|
|
4980
|
+
"solid",
|
|
4764
4981
|
"astro"
|
|
4765
4982
|
];
|
|
4766
4983
|
async function getBackendFrameworkChoice(backendFramework, frontends, previousValue) {
|
|
@@ -5198,8 +5415,8 @@ function validateDatabaseSetup(config, providedFlags) {
|
|
|
5198
5415
|
const validation = setupValidations[dbSetup];
|
|
5199
5416
|
if (dbSetup === "planetscale") {
|
|
5200
5417
|
if (database !== "postgres" && database !== "mysql") return validationErr(validation.errorMessage);
|
|
5201
|
-
} else if (validation.database && database !== validation.database) return validationErr(validation.errorMessage);
|
|
5202
|
-
if (validation.runtime && runtime !== validation.runtime) return validationErr(validation.errorMessage);
|
|
5418
|
+
} else if ("database" in validation && validation.database && database !== validation.database) return validationErr(validation.errorMessage);
|
|
5419
|
+
if ("runtime" in validation && validation.runtime && runtime !== validation.runtime) return validationErr(validation.errorMessage);
|
|
5203
5420
|
if (dbSetup === "d1") {
|
|
5204
5421
|
const isWorkersTarget = hasResolvedWorkersD1Target(config);
|
|
5205
5422
|
const isSelfCloudflareTarget = hasResolvedSelfCloudflareD1Target(config);
|
|
@@ -5307,7 +5524,11 @@ function validateFullConfig(config, providedFlags, options) {
|
|
|
5307
5524
|
yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
|
5308
5525
|
yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
|
|
5309
5526
|
yield* validateVercelServerDeploy(config.serverDeploy, config.backend, config.runtime);
|
|
5527
|
+
yield* validatePrismaServerDeploy(config.serverDeploy, config.backend, config.runtime);
|
|
5528
|
+
yield* validatePrismaWebDeploy(config.webDeploy, config.frontend);
|
|
5529
|
+
yield* validateCloudflareWebDeployKnownIssues(config);
|
|
5310
5530
|
yield* validateDockerWebDeployDesktopAddons(config.webDeploy, config.addons, config.frontend, config.backend, config.auth);
|
|
5531
|
+
yield* validatePrismaWebDeployDesktopAddons(config.webDeploy, config.addons, config.frontend);
|
|
5311
5532
|
yield* validateSelfBackendCompatibility(providedFlags, options, config);
|
|
5312
5533
|
yield* validateWorkersCompatibility(providedFlags, options, config);
|
|
5313
5534
|
if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
|
|
@@ -5464,9 +5685,13 @@ function getDeploymentDisplay$1(deployment) {
|
|
|
5464
5685
|
label: "Docker",
|
|
5465
5686
|
hint: "Self-host with a Dockerfile and docker-compose.yml"
|
|
5466
5687
|
};
|
|
5688
|
+
if (deployment === "prisma") return {
|
|
5689
|
+
label: "Prisma",
|
|
5690
|
+
hint: "Deploy with Prisma using Alchemy"
|
|
5691
|
+
};
|
|
5467
5692
|
if (deployment === "vercel") return {
|
|
5468
|
-
label: "Vercel",
|
|
5469
|
-
hint: "Deploy to Vercel with Services"
|
|
5693
|
+
label: "Vercel (experimental)",
|
|
5694
|
+
hint: "Deploy to Vercel with Services; not fully tested"
|
|
5470
5695
|
};
|
|
5471
5696
|
return {
|
|
5472
5697
|
label: deployment,
|
|
@@ -5479,6 +5704,7 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
|
|
|
5479
5704
|
if (runtime === "workers") return "cloudflare";
|
|
5480
5705
|
if (runtime !== "bun" && runtime !== "node") return "none";
|
|
5481
5706
|
const options = [
|
|
5707
|
+
"prisma",
|
|
5482
5708
|
"docker",
|
|
5483
5709
|
"vercel",
|
|
5484
5710
|
"none"
|
|
@@ -5515,21 +5741,33 @@ function getDeploymentDisplay(deployment) {
|
|
|
5515
5741
|
label: "Docker",
|
|
5516
5742
|
hint: "Self-host with a Dockerfile and docker-compose.yml"
|
|
5517
5743
|
};
|
|
5744
|
+
if (deployment === "prisma") return {
|
|
5745
|
+
label: "Prisma",
|
|
5746
|
+
hint: "Deploy with Prisma using Alchemy"
|
|
5747
|
+
};
|
|
5518
5748
|
if (deployment === "vercel") return {
|
|
5519
|
-
label: "Vercel",
|
|
5520
|
-
hint: "Deploy to Vercel with Services"
|
|
5749
|
+
label: "Vercel (experimental)",
|
|
5750
|
+
hint: "Deploy to Vercel with Services; not fully tested"
|
|
5521
5751
|
};
|
|
5522
5752
|
return {
|
|
5523
5753
|
label: deployment,
|
|
5524
5754
|
hint: `Add ${deployment} deployment`
|
|
5525
5755
|
};
|
|
5526
5756
|
}
|
|
5527
|
-
async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup, previousValue) {
|
|
5757
|
+
async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup, database, orm, addons = [], previousValue) {
|
|
5528
5758
|
if (deployment !== void 0) return deployment;
|
|
5529
5759
|
if (!hasWebFrontend(frontend)) return "none";
|
|
5530
5760
|
if (backend === "self" && dbSetup === "d1") return "cloudflare";
|
|
5761
|
+
const supportsPrismaCompute = supportsPrismaWebDeploy(frontend);
|
|
5531
5762
|
const options = [
|
|
5532
|
-
|
|
5763
|
+
...validateCloudflareWebDeployKnownIssues({
|
|
5764
|
+
webDeploy: "cloudflare",
|
|
5765
|
+
frontend,
|
|
5766
|
+
dbSetup,
|
|
5767
|
+
database,
|
|
5768
|
+
orm
|
|
5769
|
+
}).isOk() ? ["cloudflare"] : [],
|
|
5770
|
+
...supportsPrismaCompute ? ["prisma"] : [],
|
|
5533
5771
|
"docker",
|
|
5534
5772
|
"vercel",
|
|
5535
5773
|
"none"
|
|
@@ -5587,7 +5825,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath, option
|
|
|
5587
5825
|
payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
|
|
5588
5826
|
addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
|
|
5589
5827
|
examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
|
|
5590
|
-
webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, previousAnswer),
|
|
5828
|
+
webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, results.database, results.orm, results.addons, previousAnswer),
|
|
5591
5829
|
serverDeploy: ({ results, previousAnswer }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy, previousAnswer),
|
|
5592
5830
|
git: ({ previousAnswer }) => getGitChoice(flags.git, previousAnswer),
|
|
5593
5831
|
packageManager: ({ previousAnswer }) => getPackageManagerChoice(flags.packageManager, previousAnswer),
|
|
@@ -5661,6 +5899,11 @@ async function gatherConfig(flags, projectName, projectDir, relativePath, option
|
|
|
5661
5899
|
};
|
|
5662
5900
|
}
|
|
5663
5901
|
//#endregion
|
|
5902
|
+
//#region src/utils/fs-error.ts
|
|
5903
|
+
function isMissingPathError(cause) {
|
|
5904
|
+
return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
5905
|
+
}
|
|
5906
|
+
//#endregion
|
|
5664
5907
|
//#region src/prompts/project-name.ts
|
|
5665
5908
|
function isPathWithinCwd$1(targetPath) {
|
|
5666
5909
|
const resolved = path.resolve(targetPath);
|
|
@@ -5690,7 +5933,7 @@ async function getProjectName(initialName) {
|
|
|
5690
5933
|
try {
|
|
5691
5934
|
stats = await fs.lstat(defaultPath);
|
|
5692
5935
|
} catch (error) {
|
|
5693
|
-
if (
|
|
5936
|
+
if (isMissingPathError(error)) break;
|
|
5694
5937
|
throw error;
|
|
5695
5938
|
}
|
|
5696
5939
|
if (stats.isDirectory() && (await fs.readdir(defaultPath)).length === 0) break;
|
|
@@ -5744,16 +5987,33 @@ async function sendConvexEvent(payload) {
|
|
|
5744
5987
|
catch: () => void 0
|
|
5745
5988
|
});
|
|
5746
5989
|
}
|
|
5990
|
+
function buildAnalyticsEvent(config) {
|
|
5991
|
+
return types_exports.AnalyticsEventSchema.parse({
|
|
5992
|
+
database: config.database,
|
|
5993
|
+
orm: config.orm,
|
|
5994
|
+
backend: config.backend,
|
|
5995
|
+
runtime: config.runtime,
|
|
5996
|
+
frontend: config.frontend,
|
|
5997
|
+
addons: config.addons,
|
|
5998
|
+
examples: config.examples,
|
|
5999
|
+
auth: config.auth,
|
|
6000
|
+
payments: config.payments,
|
|
6001
|
+
git: config.git,
|
|
6002
|
+
packageManager: config.packageManager,
|
|
6003
|
+
install: config.install,
|
|
6004
|
+
dbSetup: config.dbSetup,
|
|
6005
|
+
api: config.api,
|
|
6006
|
+
webDeploy: config.webDeploy,
|
|
6007
|
+
serverDeploy: config.serverDeploy,
|
|
6008
|
+
cli_version: getLatestCLIVersion(),
|
|
6009
|
+
node_version: process.version,
|
|
6010
|
+
platform: process.platform
|
|
6011
|
+
});
|
|
6012
|
+
}
|
|
5747
6013
|
async function trackProjectCreation(config, disableAnalytics = false) {
|
|
5748
6014
|
if (!isTelemetryEnabled() || disableAnalytics) return;
|
|
5749
|
-
const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
|
|
5750
6015
|
await Result.tryPromise({
|
|
5751
|
-
try: () => sendConvexEvent(
|
|
5752
|
-
...safeConfig,
|
|
5753
|
-
cli_version: getLatestCLIVersion(),
|
|
5754
|
-
node_version: typeof process !== "undefined" ? process.version : "",
|
|
5755
|
-
platform: typeof process !== "undefined" ? process.platform : ""
|
|
5756
|
-
}),
|
|
6016
|
+
try: () => sendConvexEvent(buildAnalyticsEvent(config)),
|
|
5757
6017
|
catch: () => void 0
|
|
5758
6018
|
});
|
|
5759
6019
|
}
|
|
@@ -5895,7 +6155,7 @@ async function validateSafeProjectDirectoryPath(finalPathInput) {
|
|
|
5895
6155
|
try {
|
|
5896
6156
|
stats = await fs.lstat(candidatePath);
|
|
5897
6157
|
} catch (error) {
|
|
5898
|
-
if (
|
|
6158
|
+
if (isMissingPathError(error)) break;
|
|
5899
6159
|
throw error;
|
|
5900
6160
|
}
|
|
5901
6161
|
if (stats.isSymbolicLink()) throw new CLIError({ message: `Project path "${finalPathInput}" passes through symbolic link "${path.relative(cwd, candidatePath)}". Choose a real directory within the current working directory.` });
|
|
@@ -5919,7 +6179,7 @@ async function inspectProjectPath(targetPath) {
|
|
|
5919
6179
|
try {
|
|
5920
6180
|
stats = await fs.lstat(targetPath);
|
|
5921
6181
|
} catch (error) {
|
|
5922
|
-
if (
|
|
6182
|
+
if (isMissingPathError(error)) return "missing";
|
|
5923
6183
|
throw error;
|
|
5924
6184
|
}
|
|
5925
6185
|
if (stats.isSymbolicLink()) return "symbolic-link";
|
|
@@ -6248,9 +6508,10 @@ async function setupCloudflareD1(config) {
|
|
|
6248
6508
|
const targetApp = backend === "self" ? "apps/web" : "apps/server";
|
|
6249
6509
|
await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
|
|
6250
6510
|
key: "DATABASE_URL",
|
|
6251
|
-
value:
|
|
6511
|
+
value: "file:./local.db",
|
|
6252
6512
|
condition: true
|
|
6253
6513
|
}]);
|
|
6514
|
+
await fs.ensureFile(path.join(projectDir, "packages/db/local.db"));
|
|
6254
6515
|
await addPackageDependency({
|
|
6255
6516
|
dependencies: ["@prisma/adapter-d1"],
|
|
6256
6517
|
projectDir: path.join(projectDir, backend === "self" ? "apps/web" : "apps/server")
|
|
@@ -6336,7 +6597,7 @@ function mergeResolvedDbSetupOptions(dbSetup, dbSetupOptions, cliOptions = {}) {
|
|
|
6336
6597
|
if (!dbSetupOptions && !resolvedMode) return;
|
|
6337
6598
|
return {
|
|
6338
6599
|
...dbSetupOptions,
|
|
6339
|
-
|
|
6600
|
+
mode: resolvedMode
|
|
6340
6601
|
};
|
|
6341
6602
|
}
|
|
6342
6603
|
//#endregion
|
|
@@ -7510,6 +7771,7 @@ async function setupDatabase(config, cliInput) {
|
|
|
7510
7771
|
}
|
|
7511
7772
|
const dbPackageDir = path.join(projectDir, "packages/db");
|
|
7512
7773
|
if (!await fs.pathExists(dbPackageDir)) return;
|
|
7774
|
+
if (usesAlchemyManagedDatabase(config)) return;
|
|
7513
7775
|
async function runSetup(setupFn) {
|
|
7514
7776
|
const result = await setupFn();
|
|
7515
7777
|
if (result.isErr()) {
|
|
@@ -7653,7 +7915,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
7653
7915
|
const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
|
|
7654
7916
|
const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
|
|
7655
7917
|
const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
|
|
7656
|
-
const hasWeb = frontend?.some((f) => types_exports.
|
|
7918
|
+
const hasWeb = frontend?.some((f) => types_exports.webFrontends.includes(f));
|
|
7657
7919
|
const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
|
|
7658
7920
|
const hasReactRouter = frontend?.includes("react-router");
|
|
7659
7921
|
const hasSvelte = frontend?.includes("svelte");
|
|
@@ -7667,6 +7929,13 @@ async function displayPostInstallInstructions(config) {
|
|
|
7667
7929
|
let stepCounter = 2;
|
|
7668
7930
|
if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
|
|
7669
7931
|
if (database === "sqlite" && dbSetup !== "d1") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local SQLite database)")}\n`;
|
|
7932
|
+
const hasAlchemyD1 = dbSetup === "d1" && (serverDeploy === "cloudflare" || isBackendSelf && webDeploy === "cloudflare");
|
|
7933
|
+
const hasWranglerLocalD1 = (0, types_exports.getLocalD1Owner)(config) === "wrangler";
|
|
7934
|
+
if (hasAlchemyD1 && orm !== "none") {
|
|
7935
|
+
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:generate\n`;
|
|
7936
|
+
if (orm === "prisma") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:migrate\n`;
|
|
7937
|
+
if (hasWranglerLocalD1) output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:migrate:local\n`;
|
|
7938
|
+
}
|
|
7670
7939
|
if (isConvex) {
|
|
7671
7940
|
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(" (this will guide you through Convex project setup)")}\n`;
|
|
7672
7941
|
output += `${pc.cyan(`${stepCounter++}.`)} Copy environment variables from\n${pc.white(" packages/backend/.env.local")} to ${pc.white("apps/*/.env")}\n`;
|
|
@@ -7674,10 +7943,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
7674
7943
|
} else if (isBackendSelf) output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
7675
7944
|
else {
|
|
7676
7945
|
if (runtime !== "workers") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
7677
|
-
if (runtime === "workers") {
|
|
7678
|
-
if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
|
|
7679
|
-
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
7680
|
-
}
|
|
7946
|
+
if (runtime === "workers") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
7681
7947
|
}
|
|
7682
7948
|
const hasStandaloneBackend = backend !== "none";
|
|
7683
7949
|
if (hasWeb || hasStandaloneBackend || addons?.includes("starlight") || addons?.includes("fumadocs")) {
|
|
@@ -7776,27 +8042,22 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
|
|
|
7776
8042
|
const notes = [];
|
|
7777
8043
|
const commands = [];
|
|
7778
8044
|
const isD1Alchemy = dbSetup === "d1" && (serverDeploy === "cloudflare" || backend === "self" && webDeploy === "cloudflare");
|
|
8045
|
+
const isAlchemyManagedDatabase = (0, types_exports.usesAlchemyManagedDatabase)({
|
|
8046
|
+
backend,
|
|
8047
|
+
dbSetup,
|
|
8048
|
+
webDeploy,
|
|
8049
|
+
serverDeploy
|
|
8050
|
+
});
|
|
7779
8051
|
if (dbSetup === "docker") {
|
|
7780
8052
|
const dockerStatus = await getDockerStatus(database);
|
|
7781
8053
|
if (dockerStatus.message) notes.push(dockerStatus.message);
|
|
7782
8054
|
}
|
|
7783
|
-
if (
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
});
|
|
7788
|
-
else if (orm === "prisma") {
|
|
7789
|
-
commands.push({
|
|
7790
|
-
label: "Generate client",
|
|
7791
|
-
command: `${runCmd} db:generate`
|
|
7792
|
-
});
|
|
7793
|
-
commands.push({
|
|
7794
|
-
label: "Apply migrations",
|
|
7795
|
-
command: `${runCmd} db:migrate`
|
|
7796
|
-
});
|
|
7797
|
-
}
|
|
8055
|
+
if (isAlchemyManagedDatabase) {
|
|
8056
|
+
const provider = dbSetup === "prisma-postgres" ? "Prisma Postgres" : dbSetup === "planetscale" ? "PlanetScale" : "Neon";
|
|
8057
|
+
notes.push(`${pc.cyan("INFO:")} Alchemy provisions ${provider}, injects its connection credentials, and applies checked-in migrations during deploy.`);
|
|
8058
|
+
if (dbSetup === "planetscale") notes.push(`${pc.yellow("NOTE:")} The generated PlanetScale database uses the PS_DEV size, which may incur usage charges.`);
|
|
7798
8059
|
}
|
|
7799
|
-
if (dbSetup === "planetscale") {
|
|
8060
|
+
if (dbSetup === "planetscale" && !isAlchemyManagedDatabase) {
|
|
7800
8061
|
if (database === "mysql" && orm === "drizzle") notes.push(`${pc.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
|
|
7801
8062
|
if (database === "mysql" && orm === "prisma") notes.push(`${pc.yellow("NOTE:")} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`);
|
|
7802
8063
|
}
|
|
@@ -7807,7 +8068,11 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
|
|
|
7807
8068
|
label: "Start database",
|
|
7808
8069
|
command: `${runCmd} db:start`
|
|
7809
8070
|
});
|
|
7810
|
-
if (
|
|
8071
|
+
if (isAlchemyManagedDatabase) commands.push({
|
|
8072
|
+
label: "Generate client",
|
|
8073
|
+
command: `${runCmd} db:generate`
|
|
8074
|
+
});
|
|
8075
|
+
else if (!isD1Alchemy) {
|
|
7811
8076
|
commands.push({
|
|
7812
8077
|
label: "Generate client",
|
|
7813
8078
|
command: `${runCmd} db:generate`
|
|
@@ -7817,7 +8082,7 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
|
|
|
7817
8082
|
command: `${runCmd} db:push`
|
|
7818
8083
|
});
|
|
7819
8084
|
}
|
|
7820
|
-
if (!isD1Alchemy) commands.push({
|
|
8085
|
+
if (!isD1Alchemy && !isAlchemyManagedDatabase) commands.push({
|
|
7821
8086
|
label: "Open studio",
|
|
7822
8087
|
command: `${runCmd} db:studio`
|
|
7823
8088
|
});
|
|
@@ -7826,11 +8091,15 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
|
|
|
7826
8091
|
label: "Start database",
|
|
7827
8092
|
command: `${runCmd} db:start`
|
|
7828
8093
|
});
|
|
7829
|
-
if (
|
|
8094
|
+
if (isAlchemyManagedDatabase) commands.push({
|
|
8095
|
+
label: "Generate migrations",
|
|
8096
|
+
command: `${runCmd} db:generate`
|
|
8097
|
+
});
|
|
8098
|
+
else if (!isD1Alchemy) commands.push({
|
|
7830
8099
|
label: "Apply schema",
|
|
7831
8100
|
command: `${runCmd} db:push`
|
|
7832
8101
|
});
|
|
7833
|
-
if (!isD1Alchemy) commands.push({
|
|
8102
|
+
if (!isD1Alchemy && !isAlchemyManagedDatabase) commands.push({
|
|
7834
8103
|
label: "Open studio",
|
|
7835
8104
|
command: `${runCmd} db:studio`
|
|
7836
8105
|
});
|
|
@@ -7944,19 +8213,20 @@ function getPolarInstructions(backend, packageManager) {
|
|
|
7944
8213
|
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
|
|
7945
8214
|
const instructions = [];
|
|
7946
8215
|
const isBackendSelf = backend === "self";
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
const
|
|
7952
|
-
|
|
7953
|
-
|
|
8216
|
+
const hasAlchemyWeb = (0, types_exports.isAlchemyDeployTarget)(webDeploy);
|
|
8217
|
+
const hasAlchemyServer = (0, types_exports.isAlchemyDeployTarget)(serverDeploy);
|
|
8218
|
+
const alchemyExec = runCmd === "npm run" ? "npx" : runCmd === "pnpm run" ? "pnpm exec" : "bunx";
|
|
8219
|
+
if (hasAlchemyWeb || hasAlchemyServer) {
|
|
8220
|
+
const targetParts = [...hasAlchemyWeb ? [`web on ${webDeploy === "cloudflare" ? "Cloudflare" : "Prisma"}`] : [], ...hasAlchemyServer && !isBackendSelf ? [`server on ${serverDeploy === "cloudflare" ? "Cloudflare" : "Prisma"}`] : []];
|
|
8221
|
+
const deployScript = webDeploy === "vercel" ? "deploy:server" : serverDeploy === "vercel" ? "deploy:web" : "deploy";
|
|
8222
|
+
instructions.push(`${pc.bold(`Deploy with Alchemy (${targetParts.join(" + ")}):`)}\n${pc.cyan("•")} Configure provider login: ${`cd packages/infra && ${alchemyExec} alchemy login --configure`}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} ${deployScript}`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
8223
|
+
}
|
|
7954
8224
|
if (webDeploy === "docker" || serverDeploy === "docker") {
|
|
7955
8225
|
const dockerTargets = webDeploy === "docker" && serverDeploy === "docker" ? "web + server" : webDeploy === "docker" ? "web" : "server";
|
|
7956
8226
|
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`);
|
|
7957
8227
|
}
|
|
7958
8228
|
if (webDeploy === "vercel" || serverDeploy === "vercel") {
|
|
7959
|
-
const mixedWithCloudflare =
|
|
8229
|
+
const mixedWithCloudflare = hasAlchemyWeb || hasAlchemyServer;
|
|
7960
8230
|
const vercelSetupScript = "deploy:setup";
|
|
7961
8231
|
const vercelEnvScript = "env:production";
|
|
7962
8232
|
const vercelDeployScript = mixedWithCloudflare ? webDeploy === "vercel" ? "deploy:web:prod" : "deploy:server:prod" : "deploy:prod";
|
|
@@ -7971,7 +8241,7 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
|
|
|
7971
8241
|
* Creates a new project with the given configuration.
|
|
7972
8242
|
* Returns a Result with the project directory path on success, or an error on failure.
|
|
7973
8243
|
*/
|
|
7974
|
-
async function createProject(options, cliInput
|
|
8244
|
+
async function createProject(options, cliInput) {
|
|
7975
8245
|
return Result.gen(async function* () {
|
|
7976
8246
|
const projectDir = options.projectDir;
|
|
7977
8247
|
const isConvex = options.backend === "convex";
|
|
@@ -7997,7 +8267,7 @@ async function createProject(options, cliInput = {}) {
|
|
|
7997
8267
|
message: e.message,
|
|
7998
8268
|
cause: e
|
|
7999
8269
|
}))));
|
|
8000
|
-
yield* Result.await(setPackageManagerVersion(projectDir, options.packageManager));
|
|
8270
|
+
yield* Result.await(setPackageManagerVersion(projectDir, options.packageManager, cliInput.packageManagerVersion));
|
|
8001
8271
|
if (!isConvex && options.database !== "none") yield* Result.await(Result.tryPromise({
|
|
8002
8272
|
try: () => setupDatabase(options, cliInput),
|
|
8003
8273
|
catch: (e) => new ProjectCreationError({
|
|
@@ -8028,21 +8298,13 @@ async function createProject(options, cliInput = {}) {
|
|
|
8028
8298
|
return Result.ok(projectDir);
|
|
8029
8299
|
});
|
|
8030
8300
|
}
|
|
8031
|
-
async function setPackageManagerVersion(projectDir, packageManager) {
|
|
8301
|
+
async function setPackageManagerVersion(projectDir, packageManager, version) {
|
|
8032
8302
|
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
8033
8303
|
if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
|
|
8034
|
-
const versionResult = await Result.tryPromise({
|
|
8035
|
-
try: async () => {
|
|
8036
|
-
const { stdout } = await $({ cwd: os.tmpdir() })`${packageManager} -v`;
|
|
8037
|
-
return stdout.trim();
|
|
8038
|
-
},
|
|
8039
|
-
catch: () => null
|
|
8040
|
-
});
|
|
8041
8304
|
return Result.tryPromise({
|
|
8042
8305
|
try: async () => {
|
|
8043
8306
|
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
8044
|
-
|
|
8045
|
-
else delete pkgJson.packageManager;
|
|
8307
|
+
pkgJson.packageManager = `${packageManager}@${version}`;
|
|
8046
8308
|
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
8047
8309
|
},
|
|
8048
8310
|
catch: (e) => new ProjectCreationError({
|
|
@@ -8134,11 +8396,11 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
8134
8396
|
currentPathInput = defaultName;
|
|
8135
8397
|
} else currentPathInput = yield* Result.await(Result.tryPromise({
|
|
8136
8398
|
try: async () => getProjectName(input.projectName),
|
|
8137
|
-
catch: (
|
|
8138
|
-
if (
|
|
8399
|
+
catch: (cause) => {
|
|
8400
|
+
if (cause instanceof UserCancelledError) return cause;
|
|
8139
8401
|
return new CLIError({
|
|
8140
|
-
message:
|
|
8141
|
-
cause
|
|
8402
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
8403
|
+
cause
|
|
8142
8404
|
});
|
|
8143
8405
|
}
|
|
8144
8406
|
}));
|
|
@@ -8163,8 +8425,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
8163
8425
|
const templateName = input.template.toUpperCase();
|
|
8164
8426
|
const templateDescription = getTemplateDescription(input.template);
|
|
8165
8427
|
if (!isSilent()) log.info(`${pc.dim("Template")} ${pc.bold(pc.cyan(templateName))}\n${pc.dim(templateDescription)}`);
|
|
8166
|
-
const userOverrides =
|
|
8167
|
-
for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
|
|
8428
|
+
const userOverrides = Object.fromEntries(Object.entries(originalInput).filter(([, value]) => value !== void 0));
|
|
8168
8429
|
cliInput = {
|
|
8169
8430
|
...templateConfig,
|
|
8170
8431
|
...userOverrides,
|
|
@@ -8206,11 +8467,11 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
8206
8467
|
if (!isSilent() && !isTemplateSetup && Object.keys(otherFlags).length > 0) log.info(pc.dim("Command-line options applied."));
|
|
8207
8468
|
config = yield* Result.await(Result.tryPromise({
|
|
8208
8469
|
try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput, { skipCompatibilityChecks: cliInput.yolo }),
|
|
8209
|
-
catch: (
|
|
8210
|
-
if (
|
|
8470
|
+
catch: (cause) => {
|
|
8471
|
+
if (cause instanceof UserCancelledError) return cause;
|
|
8211
8472
|
return new CLIError({
|
|
8212
|
-
message:
|
|
8213
|
-
cause
|
|
8473
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
8474
|
+
cause
|
|
8214
8475
|
});
|
|
8215
8476
|
}
|
|
8216
8477
|
}));
|
|
@@ -8230,13 +8491,15 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
8230
8491
|
cause: resolvedConfigValidationResult.error
|
|
8231
8492
|
});
|
|
8232
8493
|
}
|
|
8494
|
+
const localRequirements = yield* Result.await(checkLocalRequirements(config));
|
|
8495
|
+
if (!isSilent()) for (const warning of localRequirements.warnings) log.warn(pc.yellow(warning));
|
|
8233
8496
|
if (!input.dryRun) yield* Result.await(Result.tryPromise({
|
|
8234
8497
|
try: async () => setupProjectDirectory(finalPathInput, shouldClearDirectory),
|
|
8235
|
-
catch: (
|
|
8236
|
-
if (
|
|
8498
|
+
catch: (cause) => {
|
|
8499
|
+
if (cause instanceof UserCancelledError) return cause;
|
|
8237
8500
|
return new CLIError({
|
|
8238
|
-
message:
|
|
8239
|
-
cause
|
|
8501
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
8502
|
+
cause
|
|
8240
8503
|
});
|
|
8241
8504
|
}
|
|
8242
8505
|
}));
|
|
@@ -8266,7 +8529,8 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
8266
8529
|
}
|
|
8267
8530
|
yield* Result.await(createProject(config, {
|
|
8268
8531
|
manualDb: cliInput.manualDb ?? input.manualDb,
|
|
8269
|
-
dbSetupOptions: effectiveDbSetupOptions
|
|
8532
|
+
dbSetupOptions: effectiveDbSetupOptions,
|
|
8533
|
+
packageManagerVersion: localRequirements.packageManagerVersion
|
|
8270
8534
|
}));
|
|
8271
8535
|
await trackProjectCreation(config, input.disableAnalytics);
|
|
8272
8536
|
const historyResult = await addToHistory(config, reproducibleCommand);
|
|
@@ -8322,12 +8586,12 @@ async function handleDirectoryConflictResult(currentPathInput, strategy) {
|
|
|
8322
8586
|
if (strategy) return handleDirectoryConflictProgrammatically(currentPathInput, strategy);
|
|
8323
8587
|
return Result.tryPromise({
|
|
8324
8588
|
try: async () => handleDirectoryConflict(currentPathInput),
|
|
8325
|
-
catch: (
|
|
8326
|
-
if (
|
|
8327
|
-
if (
|
|
8589
|
+
catch: (cause) => {
|
|
8590
|
+
if (cause instanceof UserCancelledError) return cause;
|
|
8591
|
+
if (cause instanceof CLIError) return cause;
|
|
8328
8592
|
return new CLIError({
|
|
8329
|
-
message:
|
|
8330
|
-
cause
|
|
8593
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
8594
|
+
cause
|
|
8331
8595
|
});
|
|
8332
8596
|
}
|
|
8333
8597
|
});
|
|
@@ -8532,10 +8796,10 @@ function formatInputValidationError(label, error) {
|
|
|
8532
8796
|
* ```
|
|
8533
8797
|
*/
|
|
8534
8798
|
async function create(projectName, options) {
|
|
8535
|
-
const rawInput =
|
|
8799
|
+
const rawInput = {
|
|
8536
8800
|
...options,
|
|
8537
8801
|
projectName
|
|
8538
|
-
}
|
|
8802
|
+
};
|
|
8539
8803
|
const parsedInput = types_exports.CreateInputSchema.safeParse(rawInput);
|
|
8540
8804
|
if (!parsedInput.success) return Result.err(new CLIError({
|
|
8541
8805
|
message: formatInputValidationError("create", parsedInput.error),
|
|
@@ -8554,14 +8818,14 @@ async function create(projectName, options) {
|
|
|
8554
8818
|
if (result.isErr()) throw result.error;
|
|
8555
8819
|
return result.value;
|
|
8556
8820
|
},
|
|
8557
|
-
catch: (
|
|
8558
|
-
if (UserCancelledError.is(
|
|
8559
|
-
if (CLIError.is(
|
|
8560
|
-
if (DirectoryConflictError.is(
|
|
8561
|
-
if (ProjectCreationError.is(
|
|
8821
|
+
catch: (cause) => {
|
|
8822
|
+
if (UserCancelledError.is(cause)) return cause;
|
|
8823
|
+
if (CLIError.is(cause)) return cause;
|
|
8824
|
+
if (DirectoryConflictError.is(cause)) return cause;
|
|
8825
|
+
if (ProjectCreationError.is(cause)) return cause;
|
|
8562
8826
|
return new CLIError({
|
|
8563
|
-
message:
|
|
8564
|
-
cause
|
|
8827
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
8828
|
+
cause
|
|
8565
8829
|
});
|
|
8566
8830
|
}
|
|
8567
8831
|
});
|
|
@@ -8629,21 +8893,7 @@ async function createVirtual(options) {
|
|
|
8629
8893
|
webDeploy: virtualOptions.webDeploy || "none",
|
|
8630
8894
|
serverDeploy: virtualOptions.serverDeploy || "none"
|
|
8631
8895
|
};
|
|
8632
|
-
const validationResult =
|
|
8633
|
-
"database",
|
|
8634
|
-
"orm",
|
|
8635
|
-
"backend",
|
|
8636
|
-
"runtime",
|
|
8637
|
-
"frontend",
|
|
8638
|
-
"addons",
|
|
8639
|
-
"examples",
|
|
8640
|
-
"auth",
|
|
8641
|
-
"dbSetup",
|
|
8642
|
-
"payments",
|
|
8643
|
-
"api",
|
|
8644
|
-
"webDeploy",
|
|
8645
|
-
"serverDeploy"
|
|
8646
|
-
]), config);
|
|
8896
|
+
const validationResult = validateResolvedConfigCompatibility(config);
|
|
8647
8897
|
if (validationResult.isErr()) return Result.err(new GeneratorError({
|
|
8648
8898
|
message: validationResult.error.message,
|
|
8649
8899
|
phase: "validation",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.39.2",
|
|
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,8 +69,8 @@
|
|
|
69
69
|
"prepublishOnly": "npm run build"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@better-t-stack/template-generator": "^3.
|
|
73
|
-
"@better-t-stack/types": "^3.
|
|
72
|
+
"@better-t-stack/template-generator": "^3.39.2",
|
|
73
|
+
"@better-t-stack/types": "^3.39.2",
|
|
74
74
|
"@clack/core": "^1.4.3",
|
|
75
75
|
"@clack/prompts": "^1.7.0",
|
|
76
76
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
@@ -83,8 +83,9 @@
|
|
|
83
83
|
"gradient-string": "^3.0.0",
|
|
84
84
|
"handlebars": "^4.7.9",
|
|
85
85
|
"jsonc-parser": "^3.3.1",
|
|
86
|
-
"oxfmt": "^0.
|
|
86
|
+
"oxfmt": "^0.63.0",
|
|
87
87
|
"picocolors": "^1.1.1",
|
|
88
|
+
"semver": "^7.8.5",
|
|
88
89
|
"tinyglobby": "^0.2.17",
|
|
89
90
|
"trpc-cli": "^0.16.0",
|
|
90
91
|
"ts-morph": "^28.0.0",
|
|
@@ -95,8 +96,12 @@
|
|
|
95
96
|
"@types/bun": "^1.3.14",
|
|
96
97
|
"@types/fs-extra": "^11.0.4",
|
|
97
98
|
"@types/node": "^26.1.2",
|
|
99
|
+
"@types/semver": "^7.8.0",
|
|
98
100
|
"publint": "^0.3.22",
|
|
99
101
|
"tsdown": "^0.22.14",
|
|
100
102
|
"typescript": "^6.0.3"
|
|
103
|
+
},
|
|
104
|
+
"engines": {
|
|
105
|
+
"node": ">=22.0.0"
|
|
101
106
|
}
|
|
102
107
|
}
|