Compare commits

..

43 Commits

Author SHA1 Message Date
shadcn
8386198073 Merge pull request #9904 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-08 16:12:35 +04:00
github-actions[bot]
f336513d18 chore(release): version packages 2026-03-08 10:36:11 +00:00
shadcn
5755d6aa1f Merge pull request #9903 from shadcn-ui/shadcn/fix-template-scaffold
feat(shadcn): scaffold projects from github remote
2026-03-08 14:35:15 +04:00
shadcn
e363e343b7 chore 2026-03-08 14:29:09 +04:00
shadcn
fe955258c3 fix 2026-03-08 14:21:58 +04:00
shadcn
f5ac4a0d2a feat(shadcn): scaffold projects from github remote 2026-03-08 14:17:30 +04:00
shadcn
97ed7eb35c Merge pull request #9864 from kapishdima/fix/laravel-init
fix: added laravel to validation schema
2026-03-08 13:11:35 +04:00
KapishDima
6909385aea Merge branch 'main' into fix/laravel-init 2026-03-08 11:01:25 +02:00
shadcn
8dabe113fa fix: registries 2026-03-08 12:54:48 +04:00
shadcn
f5556230f1 Merge pull request #9757 from harshjdhv/feat/registry-add-componentry
Feat/registry add componentry
2026-03-08 12:45:46 +04:00
shadcn
327551f8b6 Merge branch 'main' into feat/registry-add-componentry 2026-03-08 12:45:26 +04:00
shadcn
cdb4a4547f Merge pull request #9899 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-08 12:44:39 +04:00
Harsh Jadhav
52f72b9cf7 Merge branch 'main' into feat/registry-add-componentry 2026-03-08 14:13:54 +05:30
github-actions[bot]
048dac9359 chore(release): version packages 2026-03-08 08:41:12 +00:00
shadcn
f93d44730e Merge pull request #9897 from shadcn-ui/shadcn/fix-cli
fix(shadcn): fallback style resolving issue
2026-03-08 12:40:17 +04:00
shadcn
21c64cb561 fix 2026-03-08 12:39:47 +04:00
shadcn
7e405f1568 fix: --base in project form 2026-03-08 12:39:38 +04:00
shadcn
04248d752e Merge pull request #9883 from kapishdima/feat/fonttrio
feat: added fonttrio to directory.json
2026-03-08 12:33:44 +04:00
shadcn
15f6a0fe49 Merge branch 'main' into feat/fonttrio 2026-03-08 12:33:20 +04:00
shadcn
54d254100d fix 2026-03-08 12:32:58 +04:00
shadcn
6d7f3479d1 Merge pull request #9896 from shadcn-ui/fix/exclude-appledouble-from-template-archives
fix(shadcn): apple metadata files in tar
2026-03-08 12:29:09 +04:00
shadcn
5e1fca8b4e fix 2026-03-08 12:27:37 +04:00
shadcn
30229bfd14 Merge pull request #9888 from llanesluis/chore/update-registry-item-supported-types
chore(docs): update registry item types table
2026-03-08 12:26:16 +04:00
shadcn
1ce9c2dd6a fix(shadcn): apple metadata files in tar 2026-03-08 12:14:24 +04:00
shadcn
5edf9c95b7 fix(shadcn): fallback style resolving issue 2026-03-08 12:05:55 +04:00
Luis Llanes
b7786c4b42 chore(docs): update registry item types to include registry:base and registry:font 2026-03-07 13:27:55 -07:00
kapishdima
6ca3784b67 feat: added fonttrio to directory.json 2026-03-07 19:42:15 +02:00
KapishDima
c1b92c3175 Merge branch 'main' into fix/laravel-init 2026-03-07 09:12:30 +02:00
Harsh Jadhav
b7afa9ba73 Merge branch 'main' into feat/registry-add-componentry 2026-03-07 12:24:09 +05:30
shadcn
119d534e85 Merge pull request #9798 from ncdai/fix/block-viewer-toolbar-device-type-switch 2026-03-07 09:08:16 +04:00
kapishdima
4e416dea5e fix: added laravel to validation schema 2026-03-06 21:54:53 +02:00
shadcn
b600dd7091 Merge pull request #9862 from kapishdima/fix/changelog-command 2026-03-06 22:41:36 +04:00
kapishdima
d59e5be214 fix: added shadcn@latest to command 2026-03-06 20:37:13 +02:00
shadcn
cddbc1f3ff Merge pull request #9802 from edwinvakayil/feat/iconiq-registry
feat: add @iconiq registry
2026-03-06 22:14:01 +04:00
shadcn
7c0d413e3c Merge pull request #9801 from xxtomm/add-spell-registry
feat: add @spell registry
2026-03-06 22:13:28 +04:00
shadcn
00de8addfe Merge pull request #9861 from kapishdima/fix/docs-link
fix: fixed registry link in changelog page
2026-03-06 22:11:47 +04:00
kapishdima
869e7bb17f fix: fixed registry link in changelog page 2026-03-06 20:07:38 +02:00
Edwin Vakayil
8491d4207a Merge branch 'main' into feat/iconiq-registry 2026-03-06 23:07:08 +05:30
edwiee
3a431547bb feat: add @iconiq registry 2026-03-06 18:19:28 +05:30
xxtomm
f26db39334 feat: add @spell registry 2026-03-06 19:47:00 +09:00
Nguyễn Chánh Đại
e9f4cfb010 fix(BlockViewerToolbar): fix device type switching not working 2026-03-05 19:25:48 +07:00
jadhavharshh
16a0473b10 feat: add @componentry registry to available registries and directory. 2026-02-28 17:18:52 +05:30
jadhavharshh
4210d1ab05 add new registry componentry 2026-02-28 17:10:11 +05:30
26 changed files with 410 additions and 153 deletions

View File

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

View File

@@ -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!" />

View File

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

View File

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

View File

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

View File

@@ -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."
}
]

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "shadcn",
"version": "4.0.0",
"version": "4.0.2",
"description": "Add components to your apps.",
"publishConfig": {
"access": "public"

View File

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

View File

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

View File

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

View 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
View File

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