mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-20 22:31:35 +00:00
Compare commits
43 Commits
shadcn@4.0
...
shadcn@4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8386198073 | ||
|
|
f336513d18 | ||
|
|
5755d6aa1f | ||
|
|
e363e343b7 | ||
|
|
fe955258c3 | ||
|
|
f5ac4a0d2a | ||
|
|
97ed7eb35c | ||
|
|
6909385aea | ||
|
|
8dabe113fa | ||
|
|
f5556230f1 | ||
|
|
327551f8b6 | ||
|
|
cdb4a4547f | ||
|
|
52f72b9cf7 | ||
|
|
048dac9359 | ||
|
|
f93d44730e | ||
|
|
21c64cb561 | ||
|
|
7e405f1568 | ||
|
|
04248d752e | ||
|
|
15f6a0fe49 | ||
|
|
54d254100d | ||
|
|
6d7f3479d1 | ||
|
|
5e1fca8b4e | ||
|
|
30229bfd14 | ||
|
|
1ce9c2dd6a | ||
|
|
5edf9c95b7 | ||
|
|
b7786c4b42 | ||
|
|
6ca3784b67 | ||
|
|
c1b92c3175 | ||
|
|
b7afa9ba73 | ||
|
|
119d534e85 | ||
|
|
4e416dea5e | ||
|
|
b600dd7091 | ||
|
|
d59e5be214 | ||
|
|
cddbc1f3ff | ||
|
|
7c0d413e3c | ||
|
|
00de8addfe | ||
|
|
869e7bb17f | ||
|
|
8491d4207a | ||
|
|
3a431547bb | ||
|
|
f26db39334 | ||
|
|
e9f4cfb010 | ||
|
|
16a0473b10 | ||
|
|
4210d1ab05 |
@@ -80,10 +80,11 @@ export function ProjectForm({
|
||||
|
||||
const commands = React.useMemo(() => {
|
||||
const presetFlag = ` --preset ${presetCode}`
|
||||
const baseFlag = params.base !== "radix" ? ` --base ${params.base}` : ""
|
||||
const templateFlag = ` --template ${framework}`
|
||||
const monorepoFlag = isMonorepo ? " --monorepo" : ""
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
const flags = `${presetFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
|
||||
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
|
||||
|
||||
return IS_LOCAL_DEV && !process.env.NEXT_PUBLIC_RC
|
||||
? {
|
||||
@@ -98,7 +99,7 @@ export function ProjectForm({
|
||||
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
|
||||
bun: `bunx --bun shadcn${SHADCN_VERSION} init${flags}`,
|
||||
}
|
||||
}, [framework, isMonorepo, params.rtl, presetCode])
|
||||
}, [framework, isMonorepo, params.base, params.rtl, presetCode])
|
||||
|
||||
const command = commands[packageManager]
|
||||
|
||||
|
||||
@@ -170,22 +170,22 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
<div className="h-8 items-center gap-1.5 rounded-md border p-[3px] shadow-none">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
defaultValue="100"
|
||||
defaultValue="100%"
|
||||
onValueChange={(value) => {
|
||||
setView("preview")
|
||||
if (resizablePanelRef?.current) {
|
||||
resizablePanelRef.current.resize(parseInt(value))
|
||||
resizablePanelRef.current.resize(value)
|
||||
}
|
||||
}}
|
||||
className="gap-1 *:data-[slot=toggle-group-item]:size-6! *:data-[slot=toggle-group-item]:rounded-sm!"
|
||||
>
|
||||
<ToggleGroupItem value="100" title="Desktop">
|
||||
<ToggleGroupItem value="100%" title="Desktop">
|
||||
<Monitor />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="60" title="Tablet">
|
||||
<ToggleGroupItem value="60%" title="Tablet">
|
||||
<Tablet />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30" title="Mobile">
|
||||
<ToggleGroupItem value="30%" title="Mobile">
|
||||
<Smartphone />
|
||||
</ToggleGroupItem>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
|
||||
@@ -27,7 +27,7 @@ A preset packs your entire design system config into a short code. Colors, theme
|
||||
Build your preset on [shadcn/create](/create), preview it live and grab the code when you're ready.
|
||||
|
||||
```bash
|
||||
npx shadcn init --preset a1Dg5eFl
|
||||
npx shadcn@latest init --preset a1Dg5eFl
|
||||
```
|
||||
|
||||
Use it to scaffold projects from custom config, share with your team or publish in your registry. Drop it in prompts so your agent knows where to start. Use it across Claude, Codex, v0, Replit. Take your preset with you.
|
||||
@@ -37,7 +37,7 @@ Use it to scaffold projects from custom config, share with your team or publish
|
||||
When you're working on a new app, it can take a few tries to find something you like so we've made switching presets really easy. Run init --preset in your app, and the cli will take care of reconfiguring everything including your components.
|
||||
|
||||
```bash
|
||||
npx shadcn init --preset ad3qkJ7
|
||||
npx shadcn@latest init --preset ad3qkJ7
|
||||
```
|
||||
|
||||
### Skills + Presets
|
||||
@@ -56,9 +56,9 @@ To help you build custom presets, we rebuilt [shadcn/create](/create). It now in
|
||||
Inspect what a registry will add to your project before anything gets written. Review the payload yourself or pipe it to your coding agent for a second look.
|
||||
|
||||
```bash
|
||||
npx shadcn add button --dry-run
|
||||
npx shadcn add button --diff
|
||||
npx shadcn add button --view
|
||||
npx shadcn@latest add button --dry-run
|
||||
npx shadcn@latest add button --diff
|
||||
npx shadcn@latest add button --view
|
||||
```
|
||||
|
||||
### Updating primitives
|
||||
@@ -66,7 +66,7 @@ npx shadcn add button --view
|
||||
You can use the `--diff` flag to check for registry updates. Or ask your agent: "check for updates from @shadcn and merge with my local changes".
|
||||
|
||||
```bash
|
||||
npx shadcn add button --diff
|
||||
npx shadcn@latest add button --diff
|
||||
```
|
||||
|
||||
### shadcn init --template
|
||||
@@ -74,7 +74,7 @@ npx shadcn add button --diff
|
||||
`shadcn init` now scaffolds full project templates for Next.js, Vite, Laravel, React Router, Astro and TanStack Start. Dark mode included for Next.js and Vite.
|
||||
|
||||
```bash
|
||||
npx shadcn init
|
||||
npx shadcn@latest init
|
||||
|
||||
Select a template › - Use arrow-keys. Return to submit.
|
||||
❯ Next.js
|
||||
@@ -88,7 +88,7 @@ Select a template › - Use arrow-keys. Return to submit.
|
||||
Use `--monorepo` to set up a monorepo.
|
||||
|
||||
```bash
|
||||
npx shadcn init -t next --monorepo
|
||||
npx shadcn@latest init -t next --monorepo
|
||||
```
|
||||
|
||||
### shadcn init --base
|
||||
@@ -96,7 +96,7 @@ npx shadcn init -t next --monorepo
|
||||
Pick your primitives. Use `--base` to start a project with Radix or Base UI.
|
||||
|
||||
```bash
|
||||
npx shadcn init --base radix
|
||||
npx shadcn@latest init --base radix
|
||||
```
|
||||
|
||||
### shadcn info
|
||||
@@ -104,7 +104,7 @@ npx shadcn init --base radix
|
||||
The `info` command now shows the full picture: framework, version, CSS vars, which components are installed, and where to find docs and examples for every component. Great for giving coding agents the context they need to work with your project.
|
||||
|
||||
```bash
|
||||
npx shadcn info
|
||||
npx shadcn@latest info
|
||||
```
|
||||
|
||||
### shadcn docs
|
||||
@@ -112,7 +112,7 @@ npx shadcn info
|
||||
Get docs, code and examples for any UI component right from the CLI. Gives your coding agent the context to use your primitives correctly.
|
||||
|
||||
```bash
|
||||
npx shadcn docs combobox
|
||||
npx shadcn@latest docs combobox
|
||||
|
||||
combobox
|
||||
- docs https://ui.shadcn.com/docs/components/radix/combobox
|
||||
@@ -142,7 +142,7 @@ Fonts are now a first-class registry type. Install and configure them the same w
|
||||
```
|
||||
|
||||
```bash
|
||||
npx shadcn add font-inter
|
||||
npx shadcn@latest add font-inter
|
||||
```
|
||||
|
||||
## Links
|
||||
@@ -150,4 +150,4 @@ npx shadcn add font-inter
|
||||
- [shadcn/skills](/skills)
|
||||
- [shadcn/create](/create)
|
||||
- [shadcn/cli](/cli)
|
||||
- [shadcn/registry](/registry)
|
||||
- [shadcn/registry](/docs/registry)
|
||||
|
||||
@@ -103,18 +103,20 @@ The `type` property is used to specify the type of your registry item. This is u
|
||||
|
||||
The following types are supported:
|
||||
|
||||
| Type | Description |
|
||||
| -------------------- | ------------------------------------------------ |
|
||||
| `registry:block` | Use for complex components with multiple files. |
|
||||
| `registry:component` | Use for simple components. |
|
||||
| `registry:lib` | Use for lib and utils. |
|
||||
| `registry:hook` | Use for hooks. |
|
||||
| `registry:ui` | Use for UI components and single-file primitives |
|
||||
| `registry:page` | Use for page or file-based routes. |
|
||||
| `registry:file` | Use for miscellaneous files. |
|
||||
| `registry:style` | Use for registry styles. eg. `new-york` |
|
||||
| `registry:theme` | Use for themes. |
|
||||
| `registry:item` | Use for universal registry items. |
|
||||
| Type | Description |
|
||||
| -------------------- | ------------------------------------------------- |
|
||||
| `registry:base` | Use for entire design systems. |
|
||||
| `registry:block` | Use for complex components with multiple files. |
|
||||
| `registry:component` | Use for simple components. |
|
||||
| `registry:font` | Use for fonts. |
|
||||
| `registry:lib` | Use for lib and utils. |
|
||||
| `registry:hook` | Use for hooks. |
|
||||
| `registry:ui` | Use for UI components and single-file primitives. |
|
||||
| `registry:page` | Use for page or file-based routes. |
|
||||
| `registry:file` | Use for miscellaneous files. |
|
||||
| `registry:style` | Use for registry styles. eg. `new-york`. |
|
||||
| `registry:theme` | Use for themes. |
|
||||
| `registry:item` | Use for universal registry items. |
|
||||
|
||||
### author
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn": "4.0.0",
|
||||
"shadcn": "4.0.2",
|
||||
"shiki": "^1.10.1",
|
||||
"sonner": "^2.0.0",
|
||||
"swr": "^2.3.6",
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
[
|
||||
{
|
||||
"name": "@lmscn",
|
||||
"homepage": "https://lmscn.vercel.app",
|
||||
"url": "https://lmscn.vercel.app/r/{name}.json",
|
||||
"description": "LMS components for building interactive learning experiences — quiz, flashcards, matching, fill-in-the-blank, word scramble, sequencing, reading comprehension, spaced repetition and more."
|
||||
},
|
||||
{
|
||||
"name": "@8bitcn",
|
||||
"homepage": "https://www.8bitcn.com",
|
||||
@@ -305,6 +299,12 @@
|
||||
"url": "https://limeplay.winoffrg.dev/r/{name}.json",
|
||||
"description": "Modern UI Library for building media players in React. Powered by Shaka Player."
|
||||
},
|
||||
{
|
||||
"name": "@lmscn",
|
||||
"homepage": "https://lmscn.vercel.app",
|
||||
"url": "https://lmscn.vercel.app/r/{name}.json",
|
||||
"description": "LMS components for building interactive learning experiences — quiz, flashcards, matching, fill-in-the-blank, word scramble, sequencing, reading comprehension, spaced repetition and more."
|
||||
},
|
||||
{
|
||||
"name": "@lucide-animated",
|
||||
"homepage": "https://lucide-animated.com",
|
||||
@@ -473,6 +473,12 @@
|
||||
"url": "https://www.scrollxui.dev/registry/{name}.json",
|
||||
"description": "ScrollX UI is an open-source React and shadcn-compatible component library for animated, interactive, and customizable user interfaces. It offers motion-driven components that blend seamlessly with modern ShadCN setups."
|
||||
},
|
||||
{
|
||||
"name": "@spell",
|
||||
"homepage": "https://spell.sh",
|
||||
"url": "https://spell.sh/r/{name}.json",
|
||||
"description": "Beautiful, sophisticated UI components designed for modern React and Tailwind CSS applications."
|
||||
},
|
||||
{
|
||||
"name": "@square-ui",
|
||||
"homepage": "https://square.lndev.me",
|
||||
@@ -820,5 +826,23 @@
|
||||
"homepage": "https://emerald-ui.com",
|
||||
"url": "https://emerald-ui.com/r/{name}.json",
|
||||
"description": "Emerald UI - collection of components built with Motion, GSAP, Tailwind CSS and shadcn/ui."
|
||||
},
|
||||
{
|
||||
"name": "@iconiq",
|
||||
"homepage": "https://iconiqs.vercel.app",
|
||||
"url": "https://iconiqs.vercel.app/r/{name}.json",
|
||||
"description": "Iconiq is a collection of icons designed for web applications. It is a modern, clean, and minimalistic icon set that is perfect for web applications."
|
||||
},
|
||||
{
|
||||
"name": "@fonttrio",
|
||||
"homepage": "https://www.fonttrio.xyz",
|
||||
"url": "https://www.fonttrio.xyz/r/{name}.json",
|
||||
"description": "Curated font pairing registry for shadcn. Three fonts. One command. Install perfectly configured typography (heading + body + mono) with shadcn add. Includes editorial-grade type scales, CSS variables, and a live preview site."
|
||||
},
|
||||
{
|
||||
"name": "@componentry",
|
||||
"homepage": "https://componentry.fun",
|
||||
"url": "https://componentry.fun/r/{name}.json",
|
||||
"description": "Beautiful, interactive React + Tailwind components for modern product UIs."
|
||||
}
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -106,6 +106,7 @@ export const designSystemConfigSchema = z
|
||||
"start-monorepo",
|
||||
"astro",
|
||||
"astro-monorepo",
|
||||
"laravel",
|
||||
])
|
||||
.default("next")
|
||||
.optional(),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22,20 +22,6 @@ import { STYLES } from "@/registry/styles"
|
||||
// This is used by the v4 site.
|
||||
const WHITELISTED_STYLES = ["new-york-v4"]
|
||||
|
||||
// Template directories to archive during build.
|
||||
const TEMPLATE_NAMES = [
|
||||
"next-app",
|
||||
"vite-app",
|
||||
"react-router-app",
|
||||
"start-app",
|
||||
"astro-app",
|
||||
"next-monorepo",
|
||||
"vite-monorepo",
|
||||
"react-router-monorepo",
|
||||
"start-monorepo",
|
||||
"astro-monorepo",
|
||||
]
|
||||
|
||||
// Collect paths for batch prettier formatting at the end.
|
||||
const prettierPaths: string[] = []
|
||||
|
||||
@@ -101,9 +87,6 @@ try {
|
||||
console.log("\n⚙️ Building public/r/config.json...")
|
||||
await buildConfig()
|
||||
|
||||
console.log("\n📦 Building public/r/templates...")
|
||||
await buildTemplates()
|
||||
|
||||
// Copy UI to examples before cleanup.
|
||||
console.log("\n📋 Copying UI to examples...")
|
||||
await copyUIToExamples()
|
||||
@@ -746,65 +729,6 @@ async function batchPrettier(paths: string[]) {
|
||||
})
|
||||
}
|
||||
|
||||
async function buildTemplates() {
|
||||
const templatesDir = path.resolve(process.cwd(), "../../templates")
|
||||
const outputDir = path.join(process.cwd(), "public/r/templates")
|
||||
await fs.mkdir(outputDir, { recursive: true })
|
||||
|
||||
await Promise.all(
|
||||
TEMPLATE_NAMES.map(async (name) => {
|
||||
const templatePath = path.join(templatesDir, name)
|
||||
|
||||
// Verify the template directory exists.
|
||||
try {
|
||||
await fs.access(templatePath)
|
||||
} catch {
|
||||
console.log(` ⚠️ templates/${name} not found, skipping`)
|
||||
return
|
||||
}
|
||||
|
||||
const outputPath = path.join(outputDir, `${name}.tar.gz`)
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
"tar",
|
||||
[
|
||||
"-czf",
|
||||
outputPath,
|
||||
"--exclude",
|
||||
"node_modules",
|
||||
"--exclude",
|
||||
".git",
|
||||
"--exclude",
|
||||
"pnpm-lock.yaml",
|
||||
"-C",
|
||||
templatesDir,
|
||||
name,
|
||||
],
|
||||
{ cwd: process.cwd(), stdio: "pipe" }
|
||||
)
|
||||
let stderr = ""
|
||||
proc.stderr?.on("data", (data) => (stderr += data))
|
||||
proc.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`tar exited with code ${code}: ${stderr}`))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
proc.on("error", reject)
|
||||
})
|
||||
|
||||
// Zero out the gzip mtime header (bytes 4-7) for deterministic output.
|
||||
const buf = await fs.readFile(outputPath)
|
||||
buf[4] = buf[5] = buf[6] = buf[7] = 0
|
||||
await fs.writeFile(outputPath, buf)
|
||||
|
||||
console.log(` ✅ ${name}.tar.gz`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function buildColors() {
|
||||
const colorsTargetPath = path.join(process.cwd(), "public/r/colors")
|
||||
await fs.mkdir(colorsTargetPath, { recursive: true })
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @shadcn/ui
|
||||
|
||||
## 4.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#9903](https://github.com/shadcn-ui/ui/pull/9903) [`f5ac4a0d2aa5af87202f67558a4b9b8f92c00bd2`](https://github.com/shadcn-ui/ui/commit/f5ac4a0d2aa5af87202f67558a4b9b8f92c00bd2) Thanks [@shadcn](https://github.com/shadcn)! - scaffold templates from github remote
|
||||
|
||||
## 4.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#9896](https://github.com/shadcn-ui/ui/pull/9896) [`1ce9c2dd6a3d16422a6586e39632ebbccc45d3a4`](https://github.com/shadcn-ui/ui/commit/1ce9c2dd6a3d16422a6586e39632ebbccc45d3a4) Thanks [@shadcn](https://github.com/shadcn)! - fix apple metadata files in template
|
||||
|
||||
- [#9897](https://github.com/shadcn-ui/ui/pull/9897) [`5edf9c95b7d13dcbd325aa4cf6b48d58a53b07c6`](https://github.com/shadcn-ui/ui/commit/5edf9c95b7d13dcbd325aa4cf6b48d58a53b07c6) Thanks [@shadcn](https://github.com/shadcn)! - fix fallback style resolving issue
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.2",
|
||||
"description": "Add components to your apps.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { REGISTRY_URL } from "@/src/registry/constants"
|
||||
import type { RegistryItem } from "@/src/registry/schema"
|
||||
import type { Config } from "@/src/utils/get-config"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
@@ -8,7 +7,8 @@ import { spinner } from "@/src/utils/spinner"
|
||||
import { execa } from "execa"
|
||||
import fs from "fs-extra"
|
||||
|
||||
export const TEMPLATE_BASE_URL = `${REGISTRY_URL}/templates`
|
||||
const GITHUB_REPO_URL =
|
||||
process.env.SHADCN_GITHUB_URL ?? "https://github.com/shadcn-ui/ui.git"
|
||||
|
||||
export interface TemplateOptions {
|
||||
projectPath: string
|
||||
@@ -99,7 +99,7 @@ export function resolveTemplate(
|
||||
return resolved
|
||||
}
|
||||
|
||||
// Default scaffold that fetches a pre-built template archive.
|
||||
// Default scaffold that downloads a template from GitHub.
|
||||
function defaultScaffold({
|
||||
title,
|
||||
templateDir,
|
||||
@@ -123,24 +123,33 @@ function defaultScaffold({
|
||||
filter: (src) => !src.includes("node_modules"),
|
||||
})
|
||||
} else {
|
||||
// Fetch the pre-built template archive.
|
||||
// Clone only the template directory from GitHub using sparse checkout.
|
||||
const templatePath = path.join(
|
||||
os.tmpdir(),
|
||||
`shadcn-template-${Date.now()}`
|
||||
)
|
||||
await fs.ensureDir(templatePath)
|
||||
const response = await fetch(
|
||||
`${TEMPLATE_BASE_URL}/${templateDir}.tar.gz`
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download template: ${response.statusText}`)
|
||||
}
|
||||
await execa("git", [
|
||||
"clone",
|
||||
"--depth",
|
||||
"1",
|
||||
"--filter=blob:none",
|
||||
"--sparse",
|
||||
GITHUB_REPO_URL,
|
||||
templatePath,
|
||||
])
|
||||
await execa("git", [
|
||||
"-C",
|
||||
templatePath,
|
||||
"sparse-checkout",
|
||||
"set",
|
||||
`templates/${templateDir}`,
|
||||
])
|
||||
|
||||
// Write and extract the tar file.
|
||||
const tarPath = path.resolve(templatePath, "template.tar.gz")
|
||||
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
|
||||
await execa("tar", ["-xzf", tarPath, "-C", templatePath])
|
||||
const extractedPath = path.resolve(templatePath, templateDir)
|
||||
const extractedPath = path.resolve(
|
||||
templatePath,
|
||||
"templates",
|
||||
templateDir
|
||||
)
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import { reactRouter } from "./react-router"
|
||||
import { start } from "./start"
|
||||
import { vite } from "./vite"
|
||||
|
||||
export {
|
||||
createTemplate,
|
||||
resolveTemplate,
|
||||
TEMPLATE_BASE_URL,
|
||||
} from "./create-template"
|
||||
export { createTemplate, resolveTemplate } from "./create-template"
|
||||
export type { TemplateInitOptions, TemplateOptions } from "./create-template"
|
||||
|
||||
export const templates = {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("createProject", () => {
|
||||
vi.mocked(fs.move).mockResolvedValue(undefined)
|
||||
vi.mocked(fs.remove).mockResolvedValue(undefined)
|
||||
|
||||
// Mock execa for template scaffold commands.
|
||||
// Mock execa for git clone and package manager install.
|
||||
vi.mocked(execa).mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
@@ -59,12 +59,6 @@ describe("createProject", () => {
|
||||
killed: false,
|
||||
} as any)
|
||||
|
||||
// Mock fetch for template download.
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
|
||||
} as any)
|
||||
|
||||
// Reset prompts mock
|
||||
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
|
||||
|
||||
@@ -96,7 +90,6 @@ describe("createProject", () => {
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks()
|
||||
mockExit?.mockRestore()
|
||||
delete (global as any).fetch
|
||||
})
|
||||
|
||||
it("should create a Next.js project with default options", async () => {
|
||||
|
||||
265
packages/shadcn/src/utils/scaffold.test.ts
Normal file
265
packages/shadcn/src/utils/scaffold.test.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import path from "path"
|
||||
import { createTemplate } from "@/src/templates/create-template"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import { execa } from "execa"
|
||||
import fs from "fs-extra"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
vi.mock("fs-extra")
|
||||
vi.mock("execa")
|
||||
vi.mock("@/src/utils/spinner")
|
||||
vi.mock("@/src/utils/logger", () => ({
|
||||
logger: { break: vi.fn(), error: vi.fn(), info: vi.fn() },
|
||||
}))
|
||||
|
||||
let mockSpinner: Record<string, ReturnType<typeof vi.fn>>
|
||||
|
||||
function setupMocks() {
|
||||
mockSpinner = {
|
||||
start: vi.fn().mockReturnThis(),
|
||||
succeed: vi.fn().mockReturnThis(),
|
||||
fail: vi.fn().mockReturnThis(),
|
||||
}
|
||||
|
||||
vi.mocked(fs.ensureDir).mockResolvedValue(undefined)
|
||||
vi.mocked(fs.writeFile).mockResolvedValue(undefined)
|
||||
vi.mocked(fs.move).mockResolvedValue(undefined)
|
||||
vi.mocked(fs.remove).mockResolvedValue(undefined)
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
vi.mocked(fs.copy).mockResolvedValue(undefined)
|
||||
|
||||
vi.mocked(execa).mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
} as any)
|
||||
|
||||
vi.mocked(spinner).mockReturnValue(mockSpinner as any)
|
||||
}
|
||||
|
||||
describe("defaultScaffold", () => {
|
||||
const originalEnv = { ...process.env }
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mockExit: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
delete process.env.SHADCN_TEMPLATE_DIR
|
||||
delete process.env.SHADCN_GITHUB_URL
|
||||
mockExit = vi
|
||||
.spyOn(process, "exit")
|
||||
.mockImplementation(() => undefined as never)
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks()
|
||||
mockExit.mockRestore()
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
function createTestTemplate() {
|
||||
return createTemplate({
|
||||
name: "next",
|
||||
title: "Next.js",
|
||||
defaultProjectName: "next-app",
|
||||
templateDir: "next-app",
|
||||
create: vi.fn(),
|
||||
})
|
||||
}
|
||||
|
||||
it("should clone the repo with sparse checkout", async () => {
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "pnpm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
// Should clone with --depth 1, --filter=blob:none, --sparse.
|
||||
expect(vi.mocked(execa)).toHaveBeenCalledWith("git", [
|
||||
"clone",
|
||||
"--depth",
|
||||
"1",
|
||||
"--filter=blob:none",
|
||||
"--sparse",
|
||||
"https://github.com/shadcn-ui/ui.git",
|
||||
expect.stringContaining("shadcn-template-"),
|
||||
])
|
||||
|
||||
// Should set sparse-checkout to the template directory.
|
||||
expect(vi.mocked(execa)).toHaveBeenCalledWith("git", [
|
||||
"-C",
|
||||
expect.stringContaining("shadcn-template-"),
|
||||
"sparse-checkout",
|
||||
"set",
|
||||
"templates/next-app",
|
||||
])
|
||||
})
|
||||
|
||||
it("should move template directory to project path", async () => {
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "pnpm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
expect(vi.mocked(fs.move)).toHaveBeenCalledWith(
|
||||
expect.stringContaining(path.join("templates", "next-app")),
|
||||
"/test/my-app"
|
||||
)
|
||||
})
|
||||
|
||||
it("should clean up the temp directory after extraction", async () => {
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "pnpm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
expect(vi.mocked(fs.remove)).toHaveBeenCalledWith(
|
||||
expect.stringContaining("shadcn-template-")
|
||||
)
|
||||
})
|
||||
|
||||
it("should use local templates when SHADCN_TEMPLATE_DIR is set", async () => {
|
||||
process.env.SHADCN_TEMPLATE_DIR = "/local/templates"
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "pnpm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
// Should not call git clone.
|
||||
const execaCalls = vi.mocked(execa).mock.calls
|
||||
expect(
|
||||
execaCalls.some(
|
||||
(call) => call[0] === "git" && (call[1] as string[]).includes("clone")
|
||||
)
|
||||
).toBe(false)
|
||||
|
||||
expect(vi.mocked(fs.copy)).toHaveBeenCalledWith(
|
||||
path.resolve("/local/templates", "next-app"),
|
||||
"/test/my-app",
|
||||
expect.objectContaining({ filter: expect.any(Function) })
|
||||
)
|
||||
})
|
||||
|
||||
it("should exit on git clone failure", async () => {
|
||||
vi.mocked(execa).mockRejectedValueOnce(new Error("git clone failed"))
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "pnpm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
expect(mockSpinner.fail).toHaveBeenCalled()
|
||||
expect(mockExit).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it("should remove pnpm-lock.yaml for non-pnpm package managers", async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p: any) =>
|
||||
p.toString().includes("pnpm-lock.yaml")
|
||||
)
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "npm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
expect(vi.mocked(fs.remove)).toHaveBeenCalledWith(
|
||||
path.join("/test/my-app", "pnpm-lock.yaml")
|
||||
)
|
||||
})
|
||||
|
||||
it("should not remove pnpm-lock.yaml when using pnpm", async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "pnpm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
// fs.remove is called for temp dir cleanup, but not for pnpm-lock.yaml.
|
||||
const removeCalls = vi.mocked(fs.remove).mock.calls
|
||||
expect(
|
||||
removeCalls.some((call) => call[0].toString().includes("pnpm-lock.yaml"))
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should run package manager install", async () => {
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "npm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
expect(vi.mocked(execa)).toHaveBeenCalledWith("npm", ["install"], {
|
||||
cwd: "/test/my-app",
|
||||
})
|
||||
})
|
||||
|
||||
it("should pass custom install args", async () => {
|
||||
const template = createTemplate({
|
||||
name: "start",
|
||||
title: "TanStack Start",
|
||||
defaultProjectName: "start-app",
|
||||
templateDir: "start-app",
|
||||
installArgs: ["--shamefully-hoist"],
|
||||
create: vi.fn(),
|
||||
})
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "pnpm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
expect(vi.mocked(execa)).toHaveBeenCalledWith(
|
||||
"pnpm",
|
||||
["install", "--shamefully-hoist"],
|
||||
{ cwd: "/test/my-app" }
|
||||
)
|
||||
})
|
||||
|
||||
it("should write project name to package.json", async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p: any) =>
|
||||
p.toString().includes("package.json")
|
||||
)
|
||||
vi.mocked(fs.readFile).mockResolvedValue(
|
||||
JSON.stringify({ name: "template-name" }) as any
|
||||
)
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "pnpm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
expect(vi.mocked(fs.writeFile)).toHaveBeenCalledWith(
|
||||
path.join("/test/my-app", "package.json"),
|
||||
JSON.stringify({ name: "my-app" }, null, 2)
|
||||
)
|
||||
})
|
||||
})
|
||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -278,7 +278,7 @@ importers:
|
||||
specifier: ^0.0.1
|
||||
version: 0.0.1
|
||||
shadcn:
|
||||
specifier: 4.0.0
|
||||
specifier: 4.0.2
|
||||
version: link:../../packages/shadcn
|
||||
shiki:
|
||||
specifier: ^1.10.1
|
||||
|
||||
Reference in New Issue
Block a user