From cb6e798b90bf4405990fffeb301afa7d1e10b97c Mon Sep 17 00:00:00 2001 From: shadcn Date: Wed, 4 Mar 2026 08:49:23 +0400 Subject: [PATCH] fix: rework v0 init route --- .../v4/app/(create)/components/customizer.tsx | 2 +- .../app/(create)/components/project-form.tsx | 127 +++++++++--------- .../app/(create)/components/random-button.tsx | 2 +- apps/v4/app/(create)/components/v0-button.tsx | 38 +++--- apps/v4/app/(create)/init/v0/route.ts | 33 +++++ .../{create/v0/route.ts => lib/v0.ts} | 103 ++++++-------- 6 files changed, 155 insertions(+), 150 deletions(-) create mode 100644 apps/v4/app/(create)/init/v0/route.ts rename apps/v4/app/(create)/{create/v0/route.ts => lib/v0.ts} (80%) diff --git a/apps/v4/app/(create)/components/customizer.tsx b/apps/v4/app/(create)/components/customizer.tsx index c6056d5895..04913c88fc 100644 --- a/apps/v4/app/(create)/components/customizer.tsx +++ b/apps/v4/app/(create)/components/customizer.tsx @@ -79,7 +79,7 @@ export function Customizer({ - + diff --git a/apps/v4/app/(create)/components/project-form.tsx b/apps/v4/app/(create)/components/project-form.tsx index 3330379125..2e197f3572 100644 --- a/apps/v4/app/(create)/components/project-form.tsx +++ b/apps/v4/app/(create)/components/project-form.tsx @@ -124,19 +124,10 @@ export function ProjectForm({ return ( - - } - > + }> Create Project - + Create Project @@ -198,58 +189,64 @@ export function ProjectForm({ - - { - setConfig((prev) => ({ - ...prev, - packageManager: value as PackageManager, - })) - }} - className="min-w-0 gap-0 overflow-hidden rounded-lg border bg-surface" - > -
- - {PACKAGE_MANAGERS.map((manager) => { - return ( - - {manager} - - ) - })} - - -
- {Object.entries(commands).map(([key, cmd]) => { - return ( - -
-
- - {cmd} - + +
+ { + setConfig((prev) => ({ + ...prev, + packageManager: value as PackageManager, + })) + }} + className="min-w-0 gap-0 overflow-hidden rounded-lg border bg-surface" + > +
+ + {PACKAGE_MANAGERS.map((manager) => { + return ( + + {manager} + + ) + })} + + +
+ {Object.entries(commands).map(([key, cmd]) => { + return ( + +
+
+ + {cmd} + +
-
- - ) - })} - - + + ) + })} + + +
@@ -288,17 +285,17 @@ const TemplateGrid = React.memo(function TemplateGrid({
- {template.title} + {template.title}
- `${process.env.NEXT_PUBLIC_APP_URL}/create/v0?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&item=${params.item}`, - [ - params.base, - params.style, - params.baseColor, - params.theme, - params.iconLibrary, - params.font, - params.menuAccent, - params.menuColor, - params.radius, - params.item, - ] - ) + const url = React.useMemo(() => { + const searchParams = new URLSearchParams() + + if (params.preset) { + searchParams.set("preset", params.preset) + } + + searchParams.set("base", params.base) + + if (params.item) { + searchParams.set("item", params.item) + } + + return `${process.env.NEXT_PUBLIC_APP_URL}/init/v0?${searchParams.toString()}` + }, [params.preset, params.base, params.item]) if (!isMounted) { return @@ -45,12 +44,7 @@ export function V0Button({ className }: { className?: string }) { "gap-1 pointer-coarse:h-10! pointer-coarse:text-sm!", className )} - render={ - - } + render={} > Open in diff --git a/apps/v4/app/(create)/init/v0/route.ts b/apps/v4/app/(create)/init/v0/route.ts new file mode 100644 index 0000000000..86b4a230a8 --- /dev/null +++ b/apps/v4/app/(create)/init/v0/route.ts @@ -0,0 +1,33 @@ +import { after, NextResponse, type NextRequest } from "next/server" +import { track } from "@vercel/analytics/server" + +import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config" +import { buildV0Payload } from "@/app/(create)/lib/build-payload" + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const result = parseDesignSystemConfig(searchParams) + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 400 }) + } + + // Defer analytics to after response is sent. + after(() => { + track("create_open_in_v0", result.data) + }) + + const payload = await buildV0Payload(result.data) + + return NextResponse.json(payload) + } catch (error) { + return NextResponse.json( + { + error: + error instanceof Error ? error.message : "An unknown error occurred", + }, + { status: 500 } + ) + } +} diff --git a/apps/v4/app/(create)/create/v0/route.ts b/apps/v4/app/(create)/lib/v0.ts similarity index 80% rename from apps/v4/app/(create)/create/v0/route.ts rename to apps/v4/app/(create)/lib/v0.ts index f4ffd8125d..a6960623ca 100644 --- a/apps/v4/app/(create)/create/v0/route.ts +++ b/apps/v4/app/(create)/lib/v0.ts @@ -1,5 +1,3 @@ -import { after, NextResponse, type NextRequest } from "next/server" -import { track } from "@vercel/analytics/server" import dedent from "dedent" import { registryItemFileSchema, @@ -13,59 +11,14 @@ import { z } from "zod" import { buildRegistryBase, - designSystemConfigSchema, fonts, type DesignSystemConfig, } from "@/registry/config" const { Index } = await import("@/registry/bases/__index__") -export async function GET(request: NextRequest) { - try { - const searchParams = request.nextUrl.searchParams - - const parseResult = designSystemConfigSchema.safeParse({ - base: searchParams.get("base"), - style: searchParams.get("style"), - iconLibrary: searchParams.get("iconLibrary"), - baseColor: searchParams.get("baseColor"), - theme: searchParams.get("theme"), - font: searchParams.get("font"), - item: searchParams.get("item"), - menuAccent: searchParams.get("menuAccent"), - menuColor: searchParams.get("menuColor"), - radius: searchParams.get("radius"), - }) - - if (!parseResult.success) { - return NextResponse.json( - { error: parseResult.error.issues[0].message }, - { status: 400 } - ) - } - - const designSystemConfig = parseResult.data - - // Defer analytics to after response is sent. (server-after-nonblocking) - after(() => { - track("create_open_in_v0", designSystemConfig) - }) - - const payload = await buildV0Payload(designSystemConfig) - - return NextResponse.json(payload) - } catch (error) { - return NextResponse.json( - { - error: - error instanceof Error ? error.message : "An unknown error occurred", - }, - { status: 500 } - ) - } -} - -async function buildV0Payload(designSystemConfig: DesignSystemConfig) { +// Builds a full v0 payload from a design system config. +export async function buildV0Payload(designSystemConfig: DesignSystemConfig) { const registryBase = buildRegistryBase(designSystemConfig) // Build all files in parallel. @@ -78,6 +31,7 @@ async function buildV0Payload(designSystemConfig: DesignSystemConfig) { return registryItemSchema.parse({ name: designSystemConfig.item ?? "Item", type: "registry:item", + dependencies: registryBase.dependencies, files: [globalsCss, layoutFile, ...componentFiles], }) } @@ -100,6 +54,7 @@ function buildGlobalsCss(registryBase: RegistryItem) { @theme inline { --font-sans: var(--font-sans); --font-mono: var(--font-mono); + --font-serif: var(--font-serif); --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -175,12 +130,27 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) { throw new Error(`Font "${designSystemConfig.font}" not found`) } + // Derive const name from the font's CSS variable (e.g. --font-sans → fontSans). + const constName = font.font.variable + .replace(/^--/, "") + .replace(/-./g, (m) => m[1].toUpperCase()) + + // Add font-serif or font-mono class to body when needed. Sans is the default. + const fontClass = + font.font.variable === "--font-serif" + ? "font-serif" + : font.font.variable === "--font-mono" + ? "font-mono" + : "" + + const bodyClassName = fontClass ? `antialiased ${fontClass}` : "antialiased" + const content = dedent` import type { Metadata } from "next"; import { ${font.font.import} } from "next/font/google"; import "./globals.css"; - const fontSans = ${font.font.import}({subsets:['latin'],variable:'--font-sans'}); + const ${constName} = ${font.font.import}({subsets:['latin'],variable:'${font.font.variable}'}); export const metadata: Metadata = { title: "Create Next App", @@ -193,9 +163,9 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) { children: React.ReactNode; }>) { return ( - + {children} @@ -215,13 +185,10 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) { async function buildComponentFiles(designSystemConfig: DesignSystemConfig) { const files = [] const allItemsForBase = Object.values(Index[designSystemConfig.base]) - .filter( - (item: RegistryItem) => - item.type === "registry:ui" || item.name === "example" - ) + .filter((item: RegistryItem) => item.type === "registry:ui") .map((item) => item.name) - // Fetch UI components and the item component in parallel. (async-parallel) + // Fetch UI components and the item component in parallel. const itemComponentPromise = designSystemConfig.item ? getRegistryItemFile(designSystemConfig.item, designSystemConfig) : null @@ -236,9 +203,23 @@ async function buildComponentFiles(designSystemConfig: DesignSystemConfig) { type: "registry:page", target: "app/page.tsx", content: dedent` - import { Button } from "@/components/ui/button"; export default function Page() { - return + return ( + + ) } `, } @@ -296,7 +277,7 @@ async function getRegistryItemFile( const json = await response.json() const item = registryItemSchema.parse(json) - // Build a v0 config i.e components.json + // Build a v0 config i.e components.json. const config = { $schema: "https://ui.shadcn.com/schema.json", style: `${designSystemConfig.base}-${designSystemConfig.style}`, @@ -351,7 +332,7 @@ async function getRegistryItemFile( const transformers = [transformIcons, transformMenu, transformRender] -// Reuse a single ts-morph Project — avoids re-creating the compiler host per file. (js-cache-function-results) +// Reuse a single ts-morph Project — avoids re-creating the compiler host per file. const project = new Project({ compilerOptions: {} }) async function transformFileContent(