example-shadcn/ for a working example.paths to your tsconfig.json to map @/* to ./src/*. This is a TypeScript-only hack — it doesn't work at runtime in Node.js, and other workspace packages can't resolve @/ imports from your code.package.json exports to define a self-referencing alias. This is an official Node.js feature that Vite, Bun, and all modern bundlers resolve natively:123456{ "name": "my-app", "exports": { "./src/*": "./src/*" } }
12import { Button } from 'my-app/src/components/ui/button.tsx' import { cn } from 'my-app/src/lib/utils.ts'
@/*:exports fieldresolve.alias in Vite — no path hacking in vite.config.tsfrom "my-app/src/lib/utils". Vite resolves these fine at runtime (it adds extensions automatically), but TypeScript's bundler resolution can't self-resolve the package name without help. Add a single paths entry:123456789{ "compilerOptions": { "moduleResolution": "Bundler", "baseUrl": ".", "paths": { "my-app/src/*": ["./src/*"] } } }
.ts/.tsx extensions (recommended), and shadcn-generated extensionless imports also typecheck1pnpm add @radix-ui/react-slot class-variance-authority clsx tailwind-merge lucide-react
package.json, add the exports field:12345678{ "name": "my-app", "type": "module", "exports": { "./package.json": "./package.json", "./src/*": "./src/*" } }
1234567// src/lib/utils.ts import { clsx, type ClassValue } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
src/globals.css needs tailwind v4 theme variables for shadcn's semantic color tokens. See example-shadcn/src/globals.css for the full file.12345678910111213141516@import 'tailwindcss'; @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); /* ... other tokens */ } :root { --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); /* ... other values */ }
components.json at your project root. Aliases use the package name instead of @/:12345678910111213141516171819{ "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "css": "src/globals.css", "baseColor": "neutral", "cssVariables": true }, "aliases": { "components": "my-app/src/components", "utils": "my-app/src/lib/utils", "ui": "my-app/src/components/ui", "lib": "my-app/src/lib", "hooks": "my-app/src/hooks" }, "iconLibrary": "lucide" }
my-app with your actual package.json name.rsc: true makes the shadcn CLI automatically add "use client" to interactive components since Spiceflow uses React Server Components by default.1pnpm dlx shadcn@latest add button card
paths entry. Your own code can use .ts/.tsx extensions; both styles resolve correctly.1234567891011121314151617181920212223242526272829303132// src/main.tsx import './globals.css' import { Spiceflow } from 'spiceflow' import { Head } from 'spiceflow/react' import { Card, CardContent, CardHeader, CardTitle } from 'my-app/src/components/ui/card.tsx' import { Button } from 'my-app/src/components/ui/button.tsx' export const app = new Spiceflow() .layout('/*', async ({ children }) => { return ( <html lang="en"> <Head> <Head.Title>My App</Head.Title> </Head> <body className="min-h-screen bg-background font-sans antialiased"> {children} </body> </html> ) }) .page('/', async () => { return ( <Card> <CardHeader> <CardTitle>Hello</CardTitle> </CardHeader> <CardContent> <Button>Click me</Button> </CardContent> </Card> ) })
1234567891011121314151617181920212223{ "compilerOptions": { "target": "es2022", "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleResolution": "Bundler", "allowImportingTsExtensions": true, "jsx": "preserve", "strict": true, "noEmit": true, "skipLibCheck": true, "isolatedModules": true, "resolveJsonModule": true, "noImplicitAny": false, "baseUrl": ".", "paths": { "my-app/src/*": ["./src/*"] }, "types": ["vite/client", "node"] }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["node_modules", "dist"] }