mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-23 04:35:46 +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 commands = React.useMemo(() => {
|
||||||
const presetFlag = ` --preset ${presetCode}`
|
const presetFlag = ` --preset ${presetCode}`
|
||||||
|
const baseFlag = params.base !== "radix" ? ` --base ${params.base}` : ""
|
||||||
const templateFlag = ` --template ${framework}`
|
const templateFlag = ` --template ${framework}`
|
||||||
const monorepoFlag = isMonorepo ? " --monorepo" : ""
|
const monorepoFlag = isMonorepo ? " --monorepo" : ""
|
||||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
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
|
return IS_LOCAL_DEV && !process.env.NEXT_PUBLIC_RC
|
||||||
? {
|
? {
|
||||||
@@ -98,7 +99,7 @@ export function ProjectForm({
|
|||||||
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
|
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
|
||||||
bun: `bunx --bun 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]
|
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">
|
<div className="h-8 items-center gap-1.5 rounded-md border p-[3px] shadow-none">
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
defaultValue="100"
|
defaultValue="100%"
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setView("preview")
|
setView("preview")
|
||||||
if (resizablePanelRef?.current) {
|
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!"
|
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 />
|
<Monitor />
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="60" title="Tablet">
|
<ToggleGroupItem value="60%" title="Tablet">
|
||||||
<Tablet />
|
<Tablet />
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="30" title="Mobile">
|
<ToggleGroupItem value="30%" title="Mobile">
|
||||||
<Smartphone />
|
<Smartphone />
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<Separator orientation="vertical" className="h-4!" />
|
<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.
|
Build your preset on [shadcn/create](/create), preview it live and grab the code when you're ready.
|
||||||
|
|
||||||
```bash
|
```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.
|
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.
|
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
|
```bash
|
||||||
npx shadcn init --preset ad3qkJ7
|
npx shadcn@latest init --preset ad3qkJ7
|
||||||
```
|
```
|
||||||
|
|
||||||
### Skills + Presets
|
### 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.
|
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
|
```bash
|
||||||
npx shadcn add button --dry-run
|
npx shadcn@latest add button --dry-run
|
||||||
npx shadcn add button --diff
|
npx shadcn@latest add button --diff
|
||||||
npx shadcn add button --view
|
npx shadcn@latest add button --view
|
||||||
```
|
```
|
||||||
|
|
||||||
### Updating primitives
|
### 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".
|
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
|
```bash
|
||||||
npx shadcn add button --diff
|
npx shadcn@latest add button --diff
|
||||||
```
|
```
|
||||||
|
|
||||||
### shadcn init --template
|
### 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.
|
`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
|
```bash
|
||||||
npx shadcn init
|
npx shadcn@latest init
|
||||||
|
|
||||||
Select a template › - Use arrow-keys. Return to submit.
|
Select a template › - Use arrow-keys. Return to submit.
|
||||||
❯ Next.js
|
❯ Next.js
|
||||||
@@ -88,7 +88,7 @@ Select a template › - Use arrow-keys. Return to submit.
|
|||||||
Use `--monorepo` to set up a monorepo.
|
Use `--monorepo` to set up a monorepo.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn init -t next --monorepo
|
npx shadcn@latest init -t next --monorepo
|
||||||
```
|
```
|
||||||
|
|
||||||
### shadcn init --base
|
### 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.
|
Pick your primitives. Use `--base` to start a project with Radix or Base UI.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx shadcn init --base radix
|
npx shadcn@latest init --base radix
|
||||||
```
|
```
|
||||||
|
|
||||||
### shadcn info
|
### 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.
|
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
|
```bash
|
||||||
npx shadcn info
|
npx shadcn@latest info
|
||||||
```
|
```
|
||||||
|
|
||||||
### shadcn docs
|
### 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.
|
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
|
```bash
|
||||||
npx shadcn docs combobox
|
npx shadcn@latest docs combobox
|
||||||
|
|
||||||
combobox
|
combobox
|
||||||
- docs https://ui.shadcn.com/docs/components/radix/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
|
```bash
|
||||||
npx shadcn add font-inter
|
npx shadcn@latest add font-inter
|
||||||
```
|
```
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
@@ -150,4 +150,4 @@ npx shadcn add font-inter
|
|||||||
- [shadcn/skills](/skills)
|
- [shadcn/skills](/skills)
|
||||||
- [shadcn/create](/create)
|
- [shadcn/create](/create)
|
||||||
- [shadcn/cli](/cli)
|
- [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:
|
The following types are supported:
|
||||||
|
|
||||||
| Type | Description |
|
| Type | Description |
|
||||||
| -------------------- | ------------------------------------------------ |
|
| -------------------- | ------------------------------------------------- |
|
||||||
| `registry:block` | Use for complex components with multiple files. |
|
| `registry:base` | Use for entire design systems. |
|
||||||
| `registry:component` | Use for simple components. |
|
| `registry:block` | Use for complex components with multiple files. |
|
||||||
| `registry:lib` | Use for lib and utils. |
|
| `registry:component` | Use for simple components. |
|
||||||
| `registry:hook` | Use for hooks. |
|
| `registry:font` | Use for fonts. |
|
||||||
| `registry:ui` | Use for UI components and single-file primitives |
|
| `registry:lib` | Use for lib and utils. |
|
||||||
| `registry:page` | Use for page or file-based routes. |
|
| `registry:hook` | Use for hooks. |
|
||||||
| `registry:file` | Use for miscellaneous files. |
|
| `registry:ui` | Use for UI components and single-file primitives. |
|
||||||
| `registry:style` | Use for registry styles. eg. `new-york` |
|
| `registry:page` | Use for page or file-based routes. |
|
||||||
| `registry:theme` | Use for themes. |
|
| `registry:file` | Use for miscellaneous files. |
|
||||||
| `registry:item` | Use for universal registry items. |
|
| `registry:style` | Use for registry styles. eg. `new-york`. |
|
||||||
|
| `registry:theme` | Use for themes. |
|
||||||
|
| `registry:item` | Use for universal registry items. |
|
||||||
|
|
||||||
### author
|
### author
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
"rehype-pretty-code": "^0.14.1",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shadcn": "4.0.0",
|
"shadcn": "4.0.2",
|
||||||
"shiki": "^1.10.1",
|
"shiki": "^1.10.1",
|
||||||
"sonner": "^2.0.0",
|
"sonner": "^2.0.0",
|
||||||
"swr": "^2.3.6",
|
"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",
|
"name": "@8bitcn",
|
||||||
"homepage": "https://www.8bitcn.com",
|
"homepage": "https://www.8bitcn.com",
|
||||||
@@ -305,6 +299,12 @@
|
|||||||
"url": "https://limeplay.winoffrg.dev/r/{name}.json",
|
"url": "https://limeplay.winoffrg.dev/r/{name}.json",
|
||||||
"description": "Modern UI Library for building media players in React. Powered by Shaka Player."
|
"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",
|
"name": "@lucide-animated",
|
||||||
"homepage": "https://lucide-animated.com",
|
"homepage": "https://lucide-animated.com",
|
||||||
@@ -473,6 +473,12 @@
|
|||||||
"url": "https://www.scrollxui.dev/registry/{name}.json",
|
"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."
|
"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",
|
"name": "@square-ui",
|
||||||
"homepage": "https://square.lndev.me",
|
"homepage": "https://square.lndev.me",
|
||||||
@@ -820,5 +826,23 @@
|
|||||||
"homepage": "https://emerald-ui.com",
|
"homepage": "https://emerald-ui.com",
|
||||||
"url": "https://emerald-ui.com/r/{name}.json",
|
"url": "https://emerald-ui.com/r/{name}.json",
|
||||||
"description": "Emerald UI - collection of components built with Motion, GSAP, Tailwind CSS and shadcn/ui."
|
"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",
|
"start-monorepo",
|
||||||
"astro",
|
"astro",
|
||||||
"astro-monorepo",
|
"astro-monorepo",
|
||||||
|
"laravel",
|
||||||
])
|
])
|
||||||
.default("next")
|
.default("next")
|
||||||
.optional(),
|
.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.
|
// This is used by the v4 site.
|
||||||
const WHITELISTED_STYLES = ["new-york-v4"]
|
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.
|
// Collect paths for batch prettier formatting at the end.
|
||||||
const prettierPaths: string[] = []
|
const prettierPaths: string[] = []
|
||||||
|
|
||||||
@@ -101,9 +87,6 @@ try {
|
|||||||
console.log("\n⚙️ Building public/r/config.json...")
|
console.log("\n⚙️ Building public/r/config.json...")
|
||||||
await buildConfig()
|
await buildConfig()
|
||||||
|
|
||||||
console.log("\n📦 Building public/r/templates...")
|
|
||||||
await buildTemplates()
|
|
||||||
|
|
||||||
// Copy UI to examples before cleanup.
|
// Copy UI to examples before cleanup.
|
||||||
console.log("\n📋 Copying UI to examples...")
|
console.log("\n📋 Copying UI to examples...")
|
||||||
await copyUIToExamples()
|
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() {
|
async function buildColors() {
|
||||||
const colorsTargetPath = path.join(process.cwd(), "public/r/colors")
|
const colorsTargetPath = path.join(process.cwd(), "public/r/colors")
|
||||||
await fs.mkdir(colorsTargetPath, { recursive: true })
|
await fs.mkdir(colorsTargetPath, { recursive: true })
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @shadcn/ui
|
# @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
|
## 4.0.0
|
||||||
|
|
||||||
### Major Changes
|
### Major Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "shadcn",
|
"name": "shadcn",
|
||||||
"version": "4.0.0",
|
"version": "4.0.2",
|
||||||
"description": "Add components to your apps.",
|
"description": "Add components to your apps.",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { REGISTRY_URL } from "@/src/registry/constants"
|
|
||||||
import type { RegistryItem } from "@/src/registry/schema"
|
import type { RegistryItem } from "@/src/registry/schema"
|
||||||
import type { Config } from "@/src/utils/get-config"
|
import type { Config } from "@/src/utils/get-config"
|
||||||
import { handleError } from "@/src/utils/handle-error"
|
import { handleError } from "@/src/utils/handle-error"
|
||||||
@@ -8,7 +7,8 @@ import { spinner } from "@/src/utils/spinner"
|
|||||||
import { execa } from "execa"
|
import { execa } from "execa"
|
||||||
import fs from "fs-extra"
|
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 {
|
export interface TemplateOptions {
|
||||||
projectPath: string
|
projectPath: string
|
||||||
@@ -99,7 +99,7 @@ export function resolveTemplate(
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default scaffold that fetches a pre-built template archive.
|
// Default scaffold that downloads a template from GitHub.
|
||||||
function defaultScaffold({
|
function defaultScaffold({
|
||||||
title,
|
title,
|
||||||
templateDir,
|
templateDir,
|
||||||
@@ -123,24 +123,33 @@ function defaultScaffold({
|
|||||||
filter: (src) => !src.includes("node_modules"),
|
filter: (src) => !src.includes("node_modules"),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Fetch the pre-built template archive.
|
// Clone only the template directory from GitHub using sparse checkout.
|
||||||
const templatePath = path.join(
|
const templatePath = path.join(
|
||||||
os.tmpdir(),
|
os.tmpdir(),
|
||||||
`shadcn-template-${Date.now()}`
|
`shadcn-template-${Date.now()}`
|
||||||
)
|
)
|
||||||
await fs.ensureDir(templatePath)
|
await execa("git", [
|
||||||
const response = await fetch(
|
"clone",
|
||||||
`${TEMPLATE_BASE_URL}/${templateDir}.tar.gz`
|
"--depth",
|
||||||
)
|
"1",
|
||||||
if (!response.ok) {
|
"--filter=blob:none",
|
||||||
throw new Error(`Failed to download template: ${response.statusText}`)
|
"--sparse",
|
||||||
}
|
GITHUB_REPO_URL,
|
||||||
|
templatePath,
|
||||||
|
])
|
||||||
|
await execa("git", [
|
||||||
|
"-C",
|
||||||
|
templatePath,
|
||||||
|
"sparse-checkout",
|
||||||
|
"set",
|
||||||
|
`templates/${templateDir}`,
|
||||||
|
])
|
||||||
|
|
||||||
// Write and extract the tar file.
|
const extractedPath = path.resolve(
|
||||||
const tarPath = path.resolve(templatePath, "template.tar.gz")
|
templatePath,
|
||||||
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
|
"templates",
|
||||||
await execa("tar", ["-xzf", tarPath, "-C", templatePath])
|
templateDir
|
||||||
const extractedPath = path.resolve(templatePath, templateDir)
|
)
|
||||||
await fs.move(extractedPath, projectPath)
|
await fs.move(extractedPath, projectPath)
|
||||||
await fs.remove(templatePath)
|
await fs.remove(templatePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import { reactRouter } from "./react-router"
|
|||||||
import { start } from "./start"
|
import { start } from "./start"
|
||||||
import { vite } from "./vite"
|
import { vite } from "./vite"
|
||||||
|
|
||||||
export {
|
export { createTemplate, resolveTemplate } from "./create-template"
|
||||||
createTemplate,
|
|
||||||
resolveTemplate,
|
|
||||||
TEMPLATE_BASE_URL,
|
|
||||||
} from "./create-template"
|
|
||||||
export type { TemplateInitOptions, TemplateOptions } from "./create-template"
|
export type { TemplateInitOptions, TemplateOptions } from "./create-template"
|
||||||
|
|
||||||
export const templates = {
|
export const templates = {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe("createProject", () => {
|
|||||||
vi.mocked(fs.move).mockResolvedValue(undefined)
|
vi.mocked(fs.move).mockResolvedValue(undefined)
|
||||||
vi.mocked(fs.remove).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({
|
vi.mocked(execa).mockResolvedValue({
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "",
|
stderr: "",
|
||||||
@@ -59,12 +59,6 @@ describe("createProject", () => {
|
|||||||
killed: false,
|
killed: false,
|
||||||
} as any)
|
} 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
|
// Reset prompts mock
|
||||||
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
|
vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })
|
||||||
|
|
||||||
@@ -96,7 +90,6 @@ describe("createProject", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.resetAllMocks()
|
vi.resetAllMocks()
|
||||||
mockExit?.mockRestore()
|
mockExit?.mockRestore()
|
||||||
delete (global as any).fetch
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create a Next.js project with default options", async () => {
|
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
|
specifier: ^0.0.1
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
shadcn:
|
shadcn:
|
||||||
specifier: 4.0.0
|
specifier: 4.0.2
|
||||||
version: link:../../packages/shadcn
|
version: link:../../packages/shadcn
|
||||||
shiki:
|
shiki:
|
||||||
specifier: ^1.10.1
|
specifier: ^1.10.1
|
||||||
|
|||||||
Reference in New Issue
Block a user