mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): prisma + workers, prisma + turso, planetscale (postgres/mysql) support (#567)
This commit is contained in:
@@ -42,7 +42,7 @@ Cloudflare Workers has specific compatibility requirements:
|
||||
| Component | Requirement | Reason |
|
||||
|-----------|-------------|--------|
|
||||
| Backend | Must be `hono` | Only Hono supports Workers runtime |
|
||||
| ORM | Must be `drizzle` or `none` | Workers doesn't support Prisma/Mongoose |
|
||||
| ORM | Must be `drizzle` or `prisma` | Workers supports Drizzle and Prisma; Mongoose is not supported |
|
||||
| Database | Cannot be `mongodb` | MongoDB requires Prisma/Mongoose |
|
||||
| Database Setup | Cannot be `docker` | Workers is serverless, no Docker support |
|
||||
|
||||
@@ -52,6 +52,9 @@ create-better-t-stack --runtime workers --backend express
|
||||
|
||||
# ✅ Valid - Workers with Hono
|
||||
create-better-t-stack --runtime workers --backend hono --database sqlite --orm drizzle --db-setup d1
|
||||
|
||||
# ✅ Also valid - Workers with Prisma (D1)
|
||||
create-better-t-stack --runtime workers --backend hono --database sqlite --orm prisma --db-setup d1
|
||||
```
|
||||
|
||||
### Backend Presets
|
||||
@@ -123,8 +126,8 @@ create-better-t-stack --frontend next native-nativewind
|
||||
|
||||
| Setup Provider | Required Database | Notes |
|
||||
|---------------|------------------|-------|
|
||||
| `turso` | `sqlite` | Distributed SQLite |
|
||||
| `d1` | `sqlite` | Cloudflare D1 (requires Workers runtime) |
|
||||
| `turso` | `sqlite` | Distributed SQLite; works with Drizzle and Prisma |
|
||||
| `d1` | `sqlite` | Cloudflare D1 (requires Workers runtime); works with Drizzle and Prisma |
|
||||
| `neon` | `postgres` | Serverless PostgreSQL |
|
||||
| `supabase` | `postgres` | PostgreSQL with additional features |
|
||||
| `prisma-postgres` | `postgres` | Managed PostgreSQL via Prisma |
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
title: Compatibility
|
||||
description: Valid and invalid combinations across frontend, backend, runtime, database, and addons
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- **Convex backend**: Sets database, ORM, and API to `none`; auth to `clerk` (if compatible frontends) or `none`
|
||||
- **Backend `none`**: Forces API, ORM, database, authentication, and runtime to `none`; disables examples
|
||||
- **Frontend `none`**: Backend-only project; PWA/Tauri/examples may be disabled
|
||||
- **API `none`**: No tRPC/oRPC setup; use framework-native APIs
|
||||
- **Database `none`**: Disables ORM and Better-Auth (but allows Clerk with Convex)
|
||||
- **ORM `none`**: No ORM setup; manage DB manually
|
||||
- **Runtime `none`**: Only with Convex backend or when backend is `none`
|
||||
- **Auth `clerk`**: Only available with Convex backend and compatible frontends
|
||||
|
||||
## Cloudflare Workers
|
||||
|
||||
- Backend: `hono` only
|
||||
- Database: `sqlite` with Cloudflare D1
|
||||
- ORM: `drizzle` (or none)
|
||||
- Not compatible with MongoDB
|
||||
|
||||
## Framework Notes
|
||||
|
||||
- SvelteKit, Nuxt, and SolidJS frontends are only compatible with `orpc` API layer
|
||||
- PWA addon requires a web frontend: TanStack Router, React Router, Next.js, or SolidJS
|
||||
- Tauri addon requires React (TanStack Router/React Router), Nuxt, SvelteKit, SolidJS, or Next.js
|
||||
- AI example is not compatible with Elysia backend or SolidJS frontend
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"bts-config",
|
||||
"analytics",
|
||||
"contributing",
|
||||
"compatibility",
|
||||
"faq"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,72 +1,74 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev --turbopack",
|
||||
"start": "next start",
|
||||
"check": "biome check --write .",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||
"generate-analytics": "bun scripts/generate-analytics.ts",
|
||||
"generate-schema": "bun scripts/generate-schema.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-t-stack/backend": "workspace:*",
|
||||
"@erquhart/convex-oss-stats": "^0.8.1",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@opennextjs/cloudflare": "^1.6.3",
|
||||
"@orama/orama": "^3.1.11",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.25.4",
|
||||
"convex-helpers": "^0.1.104",
|
||||
"culori": "^4.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"fumadocs-core": "15.6.7",
|
||||
"fumadocs-mdx": "11.7.3",
|
||||
"fumadocs-ui": "15.6.7",
|
||||
"lucide-react": "^0.536.0",
|
||||
"motion": "^12.23.12",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.5.2",
|
||||
"papaparse": "^5.5.3",
|
||||
"posthog-js": "^1.258.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"radix-ui": "^1.4.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-tweet": "^3.2.2",
|
||||
"recharts": "2.15.4",
|
||||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx": "^3.1.0",
|
||||
"shiki": "^3.9.1",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/culori": "^4.0.0",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "24.1.0",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-next": "15.4.5",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^4.27.0"
|
||||
}
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev --turbopack",
|
||||
"start": "next start",
|
||||
"check": "biome check --write .",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||
"generate-analytics": "bun scripts/generate-analytics.ts",
|
||||
"generate-schema": "bun scripts/generate-schema.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-t-stack/backend": "workspace:*",
|
||||
"@erquhart/convex-oss-stats": "^0.8.1",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@opennextjs/cloudflare": "^1.6.3",
|
||||
"@orama/orama": "^3.1.11",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.25.4",
|
||||
"convex-helpers": "^0.1.104",
|
||||
"culori": "^4.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"fumadocs-core": "15.6.7",
|
||||
"fumadocs-mdx": "11.7.3",
|
||||
"fumadocs-ui": "15.6.7",
|
||||
"lucide-react": "^0.536.0",
|
||||
"motion": "^12.23.12",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.5.2",
|
||||
"papaparse": "^5.5.3",
|
||||
"posthog-js": "^1.258.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"radix-ui": "^1.4.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-tweet": "^3.2.2",
|
||||
"recharts": "2.15.4",
|
||||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx": "^3.1.0",
|
||||
"shiki": "^3.9.1",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/culori": "^4.0.0",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "24.1.0",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-next": "15.4.5",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^4.27.0"
|
||||
}
|
||||
}
|
||||
|
||||
62
apps/web/src/app/(home)/new/_components/action-buttons.tsx
Normal file
62
apps/web/src/app/(home)/new/_components/action-buttons.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { RefreshCw, Settings, Shuffle, Star } from "lucide-react";
|
||||
|
||||
interface ActionButtonsProps {
|
||||
onReset: () => void;
|
||||
onRandom: () => void;
|
||||
onSave: () => void;
|
||||
onLoad: () => void;
|
||||
hasSavedStack: boolean;
|
||||
}
|
||||
|
||||
export function ActionButtons({
|
||||
onReset,
|
||||
onRandom,
|
||||
onSave,
|
||||
onLoad,
|
||||
hasSavedStack,
|
||||
}: ActionButtonsProps) {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Reset to defaults"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRandom}
|
||||
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Generate a random stack"
|
||||
>
|
||||
<Shuffle className="h-3 w-3" />
|
||||
Random
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSave}
|
||||
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Save current preferences"
|
||||
>
|
||||
<Star className="h-3 w-3" />
|
||||
Save
|
||||
</button>
|
||||
{hasSavedStack && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLoad}
|
||||
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Load saved preferences"
|
||||
>
|
||||
<Settings className="h-3 w-3" />
|
||||
Load
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
apps/web/src/app/(home)/new/_components/preset-dropdown.tsx
Normal file
43
apps/web/src/app/(home)/new/_components/preset-dropdown.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, Zap } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { PRESET_TEMPLATES } from "@/lib/constant";
|
||||
|
||||
interface PresetDropdownProps {
|
||||
onApplyPreset: (presetId: string) => void;
|
||||
}
|
||||
|
||||
export function PresetDropdown({ onApplyPreset }: PresetDropdownProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<Zap className="h-3 w-3" />
|
||||
Presets
|
||||
<ChevronDown className="ml-auto h-3 w-3" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64 bg-fd-background">
|
||||
{PRESET_TEMPLATES.map((preset) => (
|
||||
<DropdownMenuItem
|
||||
key={preset.id}
|
||||
onClick={() => onApplyPreset(preset.id)}
|
||||
className="flex flex-col items-start gap-1 p-3"
|
||||
>
|
||||
<div className="font-medium text-sm">{preset.name}</div>
|
||||
<div className="text-xs">{preset.description}</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
25
apps/web/src/app/(home)/new/_components/share-button.tsx
Normal file
25
apps/web/src/app/(home)/new/_components/share-button.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { Share2 } from "lucide-react";
|
||||
import { ShareDialog } from "@/components/ui/share-dialog";
|
||||
import type { StackState } from "@/lib/constant";
|
||||
|
||||
interface ShareButtonProps {
|
||||
stackUrl: string;
|
||||
stackState: StackState;
|
||||
}
|
||||
|
||||
export function ShareButton({ stackUrl, stackState }: ShareButtonProps) {
|
||||
return (
|
||||
<ShareDialog stackUrl={stackUrl} stackState={stackState}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Share your stack"
|
||||
>
|
||||
<Share2 className="h-3 w-3" />
|
||||
Share
|
||||
</button>
|
||||
</ShareDialog>
|
||||
);
|
||||
}
|
||||
@@ -5,13 +5,8 @@ import {
|
||||
ChevronDown,
|
||||
ClipboardCopy,
|
||||
InfoIcon,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Share2,
|
||||
Shuffle,
|
||||
Star,
|
||||
Terminal,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import type React from "react";
|
||||
@@ -27,11 +22,9 @@ import { toast } from "sonner";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ShareDialog } from "@/components/ui/share-dialog";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -51,7 +44,10 @@ import {
|
||||
generateStackSharingUrl,
|
||||
} from "@/lib/stack-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ActionButtons } from "./action-buttons";
|
||||
import { getBadgeColors } from "./get-badge-color";
|
||||
import { PresetDropdown } from "./preset-dropdown";
|
||||
import { ShareButton } from "./share-button";
|
||||
import { TechIcon } from "./tech-icon";
|
||||
import {
|
||||
analyzeStackCompatibility,
|
||||
@@ -60,6 +56,7 @@ import {
|
||||
isOptionCompatible,
|
||||
validateProjectName,
|
||||
} from "./utils";
|
||||
import { YoloToggle } from "./yolo-toggle";
|
||||
|
||||
const StackBuilder = () => {
|
||||
const [stack, setStack] = useStackState();
|
||||
@@ -407,7 +404,12 @@ const StackBuilder = () => {
|
||||
|
||||
const applyPreset = (presetId: string) => {
|
||||
const preset = PRESET_TEMPLATES.find(
|
||||
(template) => template.id === presetId,
|
||||
(template: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
stack: StackState;
|
||||
}) => template.id === presetId,
|
||||
);
|
||||
if (preset) {
|
||||
startTransition(() => {
|
||||
@@ -505,92 +507,41 @@ const StackBuilder = () => {
|
||||
|
||||
<div className="mt-auto border-border border-t pt-4">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetStack}
|
||||
className="flex items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Reset to defaults"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={getRandomStack}
|
||||
className="flex items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Generate a random stack"
|
||||
>
|
||||
<Shuffle className="h-3.5 w-3.5" />
|
||||
Random
|
||||
</button>
|
||||
</div>
|
||||
<ActionButtons
|
||||
onReset={resetStack}
|
||||
onRandom={getRandomStack}
|
||||
onSave={saveCurrentStack}
|
||||
onLoad={loadSavedStack}
|
||||
hasSavedStack={!!lastSavedStack}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={saveCurrentStack}
|
||||
className="flex items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Save current preferences"
|
||||
>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
Save
|
||||
</button>
|
||||
{lastSavedStack ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={loadSavedStack}
|
||||
className="flex items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Load saved preferences"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
Load
|
||||
</button>
|
||||
) : (
|
||||
<div className="h-9" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<ShareButton stackUrl={getStackUrl()} stackState={stack} />
|
||||
|
||||
<ShareDialog stackUrl={getStackUrl()} stackState={stack}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
title="Share your stack"
|
||||
>
|
||||
<Share2 className="h-3.5 w-3.5" />
|
||||
Share Stack
|
||||
</button>
|
||||
</ShareDialog>
|
||||
<PresetDropdown onApplyPreset={applyPreset} />
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-center gap-2 rounded-md border border-border bg-fd-background px-3 py-2 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
Quick Preset
|
||||
<ChevronDown className="ml-auto h-3.5 w-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-64 bg-fd-background"
|
||||
>
|
||||
{PRESET_TEMPLATES.map((preset) => (
|
||||
<DropdownMenuItem
|
||||
key={preset.id}
|
||||
onClick={() => applyPreset(preset.id)}
|
||||
className="flex flex-col items-start gap-1 p-3"
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<div className="font-medium text-sm">
|
||||
{preset.name}
|
||||
</div>
|
||||
<div className="text-xs">{preset.description}</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Settings className="h-3 w-3" />
|
||||
Settings
|
||||
<ChevronDown className="ml-auto h-3 w-3" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-64 bg-fd-background"
|
||||
>
|
||||
<YoloToggle
|
||||
stack={stack}
|
||||
onToggle={(yolo) => setStack({ yolo })}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,15 @@ interface CompatibilityResult {
|
||||
export const analyzeStackCompatibility = (
|
||||
stack: StackState,
|
||||
): CompatibilityResult => {
|
||||
// Skip all validation if YOLO mode is enabled
|
||||
if (stack.yolo === "true") {
|
||||
return {
|
||||
adjustedStack: null,
|
||||
notes: {},
|
||||
changes: [],
|
||||
};
|
||||
}
|
||||
|
||||
const nextStack = { ...stack };
|
||||
let changed = false;
|
||||
const notes: CompatibilityResult["notes"] = {};
|
||||
@@ -352,12 +361,12 @@ export const analyzeStackCompatibility = (
|
||||
"Database set to 'SQLite' (Turso hosting requires SQLite database)",
|
||||
});
|
||||
}
|
||||
if (nextStack.orm !== "drizzle") {
|
||||
if (nextStack.orm !== "drizzle" && nextStack.orm !== "prisma") {
|
||||
notes.dbSetup.notes.push(
|
||||
"Turso requires Drizzle ORM. It will be selected.",
|
||||
"Turso requires Drizzle or Prisma ORM. Drizzle will be selected.",
|
||||
);
|
||||
notes.orm.notes.push(
|
||||
"Turso DB setup requires Drizzle ORM. It will be selected.",
|
||||
"Turso DB setup requires Drizzle or Prisma ORM. Drizzle will be selected.",
|
||||
);
|
||||
notes.dbSetup.hasIssue = true;
|
||||
notes.orm.hasIssue = true;
|
||||
@@ -366,7 +375,7 @@ export const analyzeStackCompatibility = (
|
||||
changes.push({
|
||||
category: "dbSetup",
|
||||
message:
|
||||
"ORM set to 'Drizzle' (Turso hosting requires Drizzle ORM)",
|
||||
"ORM set to 'Drizzle' (Turso hosting requires Drizzle or Prisma ORM)",
|
||||
});
|
||||
}
|
||||
} else if (nextStack.dbSetup === "prisma-postgres") {
|
||||
@@ -454,6 +463,27 @@ export const analyzeStackCompatibility = (
|
||||
"Database set to 'PostgreSQL' (Supabase hosting requires PostgreSQL database)",
|
||||
});
|
||||
}
|
||||
} else if (nextStack.dbSetup === "planetscale") {
|
||||
if (
|
||||
nextStack.database !== "postgres" &&
|
||||
nextStack.database !== "mysql"
|
||||
) {
|
||||
notes.dbSetup.notes.push(
|
||||
"PlanetScale requires PostgreSQL or MySQL. PostgreSQL will be selected.",
|
||||
);
|
||||
notes.database.notes.push(
|
||||
"PlanetScale DB setup requires PostgreSQL or MySQL. PostgreSQL will be selected.",
|
||||
);
|
||||
notes.dbSetup.hasIssue = true;
|
||||
notes.database.hasIssue = true;
|
||||
nextStack.database = "postgres";
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "dbSetup",
|
||||
message:
|
||||
"Database set to 'PostgreSQL' (PlanetScale supports PostgreSQL and MySQL)",
|
||||
});
|
||||
}
|
||||
} else if (nextStack.dbSetup === "d1") {
|
||||
if (nextStack.database !== "sqlite") {
|
||||
notes.dbSetup.notes.push(
|
||||
@@ -471,6 +501,23 @@ export const analyzeStackCompatibility = (
|
||||
message: "Database set to 'SQLite' (required by Cloudflare D1)",
|
||||
});
|
||||
}
|
||||
if (nextStack.orm !== "drizzle" && nextStack.orm !== "prisma") {
|
||||
notes.dbSetup.notes.push(
|
||||
"Cloudflare D1 requires Drizzle or Prisma ORM. Drizzle will be selected.",
|
||||
);
|
||||
notes.orm.notes.push(
|
||||
"Cloudflare D1 DB setup requires Drizzle or Prisma ORM. Drizzle will be selected.",
|
||||
);
|
||||
notes.dbSetup.hasIssue = true;
|
||||
notes.orm.hasIssue = true;
|
||||
nextStack.orm = "drizzle";
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "dbSetup",
|
||||
message:
|
||||
"ORM set to 'Drizzle' (Cloudflare D1 requires Drizzle or Prisma ORM)",
|
||||
});
|
||||
}
|
||||
if (nextStack.runtime !== "workers") {
|
||||
notes.dbSetup.notes.push(
|
||||
"Cloudflare D1 requires Cloudflare Workers runtime. It will be selected.",
|
||||
@@ -487,22 +534,6 @@ export const analyzeStackCompatibility = (
|
||||
message: "Runtime set to 'Cloudflare Workers' (required by D1)",
|
||||
});
|
||||
}
|
||||
if (nextStack.orm !== "drizzle") {
|
||||
notes.dbSetup.notes.push(
|
||||
"Cloudflare D1 requires Drizzle ORM. It will be selected.",
|
||||
);
|
||||
notes.orm.notes.push(
|
||||
"Cloudflare D1 DB setup requires Drizzle ORM. It will be selected.",
|
||||
);
|
||||
notes.dbSetup.hasIssue = true;
|
||||
notes.orm.hasIssue = true;
|
||||
nextStack.orm = "drizzle";
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "dbSetup",
|
||||
message: "ORM set to 'Drizzle' (required by Cloudflare D1)",
|
||||
});
|
||||
}
|
||||
if (nextStack.backend !== "hono") {
|
||||
notes.dbSetup.notes.push(
|
||||
"Cloudflare D1 requires Hono backend. It will be selected.",
|
||||
@@ -581,6 +612,9 @@ export const analyzeStackCompatibility = (
|
||||
} else if (nextStack.dbSetup === "mongodb-atlas") {
|
||||
selectedDatabase = "mongodb";
|
||||
databaseName = "MongoDB";
|
||||
} else if (nextStack.dbSetup === "planetscale") {
|
||||
selectedDatabase = "postgres";
|
||||
databaseName = "PostgreSQL";
|
||||
}
|
||||
|
||||
notes.dbSetup.notes.push(
|
||||
@@ -618,24 +652,6 @@ export const analyzeStackCompatibility = (
|
||||
});
|
||||
}
|
||||
|
||||
if (nextStack.orm !== "drizzle" && nextStack.orm !== "none") {
|
||||
notes.runtime.notes.push(
|
||||
"Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.",
|
||||
);
|
||||
notes.orm.notes.push(
|
||||
"Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.",
|
||||
);
|
||||
notes.runtime.hasIssue = true;
|
||||
notes.orm.hasIssue = true;
|
||||
nextStack.orm = "drizzle";
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "runtime",
|
||||
message:
|
||||
"ORM set to 'Drizzle' (Cloudflare Workers runtime only supports Drizzle or no ORM)",
|
||||
});
|
||||
}
|
||||
|
||||
if (nextStack.database === "mongodb") {
|
||||
notes.runtime.notes.push(
|
||||
"Cloudflare Workers runtime is not compatible with MongoDB. SQLite will be selected.",
|
||||
@@ -1050,49 +1066,6 @@ export const analyzeStackCompatibility = (
|
||||
});
|
||||
}
|
||||
|
||||
const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy";
|
||||
const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy";
|
||||
|
||||
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
|
||||
const incompatibleFrontends = nextStack.webFrontend.filter(
|
||||
(f) => f === "next",
|
||||
);
|
||||
|
||||
if (incompatibleFrontends.length > 0) {
|
||||
const deployType =
|
||||
isAlchemyWebDeploy && isAlchemyServerDeploy
|
||||
? "web and server deployment"
|
||||
: isAlchemyWebDeploy
|
||||
? "web deployment"
|
||||
: "server deployment";
|
||||
|
||||
notes.webFrontend.notes.push(
|
||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}. These frontends will be removed.`,
|
||||
);
|
||||
notes.webDeploy.notes.push(
|
||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}.`,
|
||||
);
|
||||
notes.serverDeploy.notes.push(
|
||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}.`,
|
||||
);
|
||||
notes.webFrontend.hasIssue = true;
|
||||
notes.webDeploy.hasIssue = true;
|
||||
notes.serverDeploy.hasIssue = true;
|
||||
|
||||
nextStack.webFrontend = nextStack.webFrontend.filter((f) => f !== "next");
|
||||
|
||||
if (nextStack.webFrontend.length === 0) {
|
||||
nextStack.webFrontend = ["tanstack-router"];
|
||||
}
|
||||
|
||||
changed = true;
|
||||
changes.push({
|
||||
category: "alchemy",
|
||||
message: `Removed ${incompatibleFrontends.join(" and ")} frontend (temporarily not compatible with Alchemy ${deployType} - support coming soon)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
nextStack.serverDeploy === "alchemy" &&
|
||||
(nextStack.runtime !== "workers" || nextStack.backend !== "hono")
|
||||
@@ -1223,15 +1196,6 @@ export const getDisabledReason = (
|
||||
const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
|
||||
const finalStack = adjustedStack ?? simulatedStack;
|
||||
|
||||
if (category === "webFrontend" && optionId === "next") {
|
||||
const isAlchemyWebDeploy = finalStack.webDeploy === "alchemy";
|
||||
const isAlchemyServerDeploy = finalStack.serverDeploy === "alchemy";
|
||||
|
||||
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
|
||||
return "Next.js is temporarily not compatible with Alchemy deployment. Support coming soon!";
|
||||
}
|
||||
}
|
||||
|
||||
if (category === "webFrontend" && optionId === "solid") {
|
||||
if (finalStack.backend === "convex") {
|
||||
return "Solid is not compatible with Convex backend. Try TanStack Router, React Router, or Next.js instead.";
|
||||
@@ -1383,11 +1347,17 @@ export const getDisabledReason = (
|
||||
if (finalStack.database === "none") {
|
||||
return "Prisma ORM requires a database. Select a database first (SQLite, PostgreSQL, MySQL, or MongoDB).";
|
||||
}
|
||||
if (finalStack.dbSetup === "turso" && finalStack.database !== "sqlite") {
|
||||
return "Turso setup requires SQLite database. Select SQLite first.";
|
||||
}
|
||||
if (finalStack.dbSetup === "d1" && finalStack.database !== "sqlite") {
|
||||
return "Cloudflare D1 setup requires SQLite database. Select SQLite first.";
|
||||
}
|
||||
}
|
||||
|
||||
if (category === "dbSetup" && optionId === "turso") {
|
||||
if (finalStack.orm !== "drizzle") {
|
||||
return "Turso requires Drizzle ORM. Select Drizzle first.";
|
||||
if (finalStack.orm !== "drizzle" && finalStack.orm !== "prisma") {
|
||||
return "Turso requires Drizzle or Prisma ORM. Select Drizzle or Prisma first.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1398,8 +1368,8 @@ export const getDisabledReason = (
|
||||
}
|
||||
|
||||
if (category === "dbSetup" && optionId === "d1") {
|
||||
if (finalStack.orm !== "drizzle") {
|
||||
return "Cloudflare D1 requires Drizzle ORM. Select Drizzle first.";
|
||||
if (finalStack.orm !== "drizzle" && finalStack.orm !== "prisma") {
|
||||
return "Cloudflare D1 requires Drizzle or Prisma ORM. Select Drizzle or Prisma first.";
|
||||
}
|
||||
if (finalStack.runtime !== "workers") {
|
||||
return "Cloudflare D1 requires Cloudflare Workers runtime. Select Workers runtime first.";
|
||||
@@ -1461,15 +1431,20 @@ export const getDisabledReason = (
|
||||
finalStack.dbSetup !== "docker" &&
|
||||
finalStack.dbSetup !== "prisma-postgres" &&
|
||||
finalStack.dbSetup !== "neon" &&
|
||||
finalStack.dbSetup !== "supabase"
|
||||
finalStack.dbSetup !== "supabase" &&
|
||||
finalStack.dbSetup !== "planetscale"
|
||||
) {
|
||||
return "PostgreSQL database only works with Docker, Prisma PostgreSQL, Neon, Supabase, or Basic Setup. Select one of these options or change database.";
|
||||
return "PostgreSQL database only works with Docker, Prisma PostgreSQL, Neon, Supabase, PlanetScale, or Basic Setup. Select one of these options or change database.";
|
||||
}
|
||||
}
|
||||
|
||||
if (category === "database" && optionId === "mysql") {
|
||||
if (finalStack.dbSetup !== "none" && finalStack.dbSetup !== "docker") {
|
||||
return "MySQL database only works with Docker or Basic Setup. Select one of these options or change database.";
|
||||
if (
|
||||
finalStack.dbSetup !== "none" &&
|
||||
finalStack.dbSetup !== "docker" &&
|
||||
finalStack.dbSetup !== "planetscale"
|
||||
) {
|
||||
return "MySQL database only works with Docker, PlanetScale, or Basic Setup. Select one of these options or change database.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1584,12 +1559,27 @@ export const getDisabledReason = (
|
||||
}
|
||||
}
|
||||
|
||||
if (category === "dbSetup" && optionId === "planetscale") {
|
||||
if (finalStack.database !== "postgres" && finalStack.database !== "mysql") {
|
||||
return "PlanetScale requires PostgreSQL or MySQL database. Select PostgreSQL or MySQL first.";
|
||||
}
|
||||
}
|
||||
|
||||
if (category === "dbSetup" && optionId === "supabase") {
|
||||
if ((finalStack.database as string) !== "postgres") {
|
||||
return "Supabase requires PostgreSQL database. Select PostgreSQL first.";
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
category === "database" &&
|
||||
(finalStack.dbSetup as string) === "planetscale"
|
||||
) {
|
||||
if (optionId !== "postgres" && optionId !== "mysql") {
|
||||
return "Selected DB Setup 'PlanetScale' requires PostgreSQL or MySQL. Select PostgreSQL or MySQL, or change DB Setup.";
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
category === "database" &&
|
||||
(finalStack.dbSetup as string) === "supabase"
|
||||
@@ -1607,5 +1597,8 @@ export const isOptionCompatible = (
|
||||
category: keyof typeof TECH_OPTIONS,
|
||||
optionId: string,
|
||||
): boolean => {
|
||||
if (currentStack.yolo === "true") {
|
||||
return true;
|
||||
}
|
||||
return getDisabledReason(currentStack, category, optionId) === null;
|
||||
};
|
||||
|
||||
49
apps/web/src/app/(home)/new/_components/yolo-toggle.tsx
Normal file
49
apps/web/src/app/(home)/new/_components/yolo-toggle.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { StackState } from "@/lib/constant";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface YoloToggleProps {
|
||||
stack: StackState;
|
||||
onToggle: (yolo: string) => void;
|
||||
}
|
||||
|
||||
export function YoloToggle({ stack, onToggle }: YoloToggleProps) {
|
||||
const isYoloEnabled = stack.yolo === "true";
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex w-full items-center gap-3 p-3">
|
||||
<AlertTriangle className="h-4 w-4 flex-shrink-0" />
|
||||
<div className="flex flex-1 flex-col items-start">
|
||||
<div className="font-medium text-sm">YOLO Mode</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{isYoloEnabled ? "Enabled" : "Disabled"}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isYoloEnabled}
|
||||
onCheckedChange={(checked) => onToggle(checked ? "true" : "false")}
|
||||
className={cn(
|
||||
isYoloEnabled && "data-[state=checked]:bg-destructive",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Disables all validation and adds --yolo flag to the command. Use at
|
||||
your own risk!
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -114,7 +114,6 @@ export function ShareDialog({
|
||||
);
|
||||
};
|
||||
|
||||
// Generate QR code using local qrcode library
|
||||
useEffect(() => {
|
||||
const generateQRCode = async () => {
|
||||
try {
|
||||
@@ -264,25 +263,6 @@ export function ShareDialog({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className="font-mono font-semibold text-foreground text-xs">
|
||||
OUTPUT.URL
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-primary">$</span>
|
||||
<code className="flex-1 truncate text-muted-foreground">
|
||||
{stackUrl}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Switch as SwitchPrimitive } from "radix-ui";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
47
apps/web/src/components/ui/toggle.tsx
Normal file
47
apps/web/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,box-shadow] hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 min-w-9 px-2",
|
||||
sm: "h-8 min-w-8 px-1.5",
|
||||
lg: "h-10 min-w-10 px-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
@@ -328,6 +328,13 @@ export const TECH_OPTIONS: Record<
|
||||
icon: `${ICON_BASE_URL}/supabase.svg`,
|
||||
color: "from-emerald-400 to-emerald-600",
|
||||
},
|
||||
{
|
||||
id: "planetscale",
|
||||
name: "PlanetScale",
|
||||
description: "Serverless MySQL platform with branching",
|
||||
icon: `${ICON_BASE_URL}/planetscale.svg`,
|
||||
color: "from-orange-400 to-orange-600",
|
||||
},
|
||||
{
|
||||
id: "docker",
|
||||
name: "Docker",
|
||||
@@ -604,6 +611,7 @@ export const PRESET_TEMPLATES = [
|
||||
api: "trpc",
|
||||
webDeploy: "none",
|
||||
serverDeploy: "none",
|
||||
yolo: "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -628,6 +636,7 @@ export const PRESET_TEMPLATES = [
|
||||
api: "none",
|
||||
webDeploy: "none",
|
||||
serverDeploy: "none",
|
||||
yolo: "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -652,6 +661,7 @@ export const PRESET_TEMPLATES = [
|
||||
api: "trpc",
|
||||
webDeploy: "none",
|
||||
serverDeploy: "none",
|
||||
yolo: "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -676,6 +686,7 @@ export const PRESET_TEMPLATES = [
|
||||
api: "trpc",
|
||||
webDeploy: "none",
|
||||
serverDeploy: "none",
|
||||
yolo: "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -700,6 +711,7 @@ export const PRESET_TEMPLATES = [
|
||||
api: "trpc",
|
||||
webDeploy: "alchemy",
|
||||
serverDeploy: "alchemy",
|
||||
yolo: "false",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -722,6 +734,7 @@ export type StackState = {
|
||||
api: string;
|
||||
webDeploy: string;
|
||||
serverDeploy: string;
|
||||
yolo: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_STACK: StackState = {
|
||||
@@ -742,6 +755,7 @@ export const DEFAULT_STACK: StackState = {
|
||||
api: "trpc",
|
||||
webDeploy: "none",
|
||||
serverDeploy: "none",
|
||||
yolo: "false",
|
||||
};
|
||||
|
||||
export const isStackDefault = <K extends keyof StackState>(
|
||||
|
||||
@@ -19,4 +19,5 @@ export const stackUrlKeys: UrlKeys<Record<keyof StackState, unknown>> = {
|
||||
install: "i",
|
||||
webDeploy: "wd",
|
||||
serverDeploy: "sd",
|
||||
yolo: "yolo",
|
||||
};
|
||||
|
||||
@@ -61,6 +61,9 @@ export const stackParsers = {
|
||||
serverDeploy: parseAsStringEnum<StackState["serverDeploy"]>(
|
||||
getValidIds("serverDeploy"),
|
||||
).withDefault(DEFAULT_STACK.serverDeploy),
|
||||
yolo: parseAsStringEnum<StackState["yolo"]>(["true", "false"]).withDefault(
|
||||
DEFAULT_STACK.yolo,
|
||||
),
|
||||
};
|
||||
|
||||
export const stackQueryStatesOptions = {
|
||||
|
||||
@@ -67,6 +67,10 @@ const serverStackParsers = {
|
||||
serverDeploy: parseAsStringEnumServer<StackState["serverDeploy"]>(
|
||||
getValidIds("serverDeploy"),
|
||||
).withDefault(DEFAULT_STACK.serverDeploy),
|
||||
yolo: parseAsStringEnumServer<StackState["yolo"]>([
|
||||
"true",
|
||||
"false",
|
||||
]).withDefault(DEFAULT_STACK.yolo),
|
||||
};
|
||||
|
||||
export const loadStackParams = createLoader(serverStackParsers, {
|
||||
|
||||
@@ -119,6 +119,10 @@ export function generateStackCommand(stack: StackState): string {
|
||||
`--examples ${stack.examples.join(" ") || "none"}`,
|
||||
];
|
||||
|
||||
if (stack.yolo === "true") {
|
||||
flags.push("--yolo");
|
||||
}
|
||||
|
||||
return `${base} ${projectName} ${flags.join(" ")}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user