feat(cli): prisma + workers, prisma + turso, planetscale (postgres/mysql) support (#567)

This commit is contained in:
Aman Varshney
2025-09-08 12:15:26 +05:30
committed by GitHub
parent 33344d91be
commit cd5d0f0aeb
66 changed files with 1486 additions and 729 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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>

View File

@@ -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;
};

View 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>
);
}

View File

@@ -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>

View File

@@ -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";

View 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 };

View File

@@ -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>(

View File

@@ -19,4 +19,5 @@ export const stackUrlKeys: UrlKeys<Record<keyof StackState, unknown>> = {
install: "i",
webDeploy: "wd",
serverDeploy: "sd",
yolo: "yolo",
};

View File

@@ -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 = {

View File

@@ -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, {

View File

@@ -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(" ")}`;
}