mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-17 12:51:37 +00:00
Compare commits
23 Commits
shadcn@4.0
...
shadcn@4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e653c1a833 | ||
|
|
8b819e1db4 | ||
|
|
5236bfdf07 | ||
|
|
7e93eb81ea | ||
|
|
abf1555a65 | ||
|
|
584db77fee | ||
|
|
3faa91d670 | ||
|
|
3af2ba80e8 | ||
|
|
ca374ad0a0 | ||
|
|
bc9f556c38 | ||
|
|
f413598ba3 | ||
|
|
34c04d5f01 | ||
|
|
0029b3b6f7 | ||
|
|
821ac7ee4d | ||
|
|
8df46c4ded | ||
|
|
2303ce2372 | ||
|
|
cf672a9575 | ||
|
|
5ee4567353 | ||
|
|
6f72dba9c4 | ||
|
|
cd717896fa | ||
|
|
d47562cc08 | ||
|
|
aff5d7f0c1 | ||
|
|
4c0be13dcc |
@@ -44,10 +44,12 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
track("create_app", {
|
||||
...result.data,
|
||||
preset: presetCode,
|
||||
})
|
||||
if (searchParams.get("track") === "1") {
|
||||
track("create_app", {
|
||||
...result.data,
|
||||
preset: presetCode,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(parseResult.data)
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,7 +6,7 @@ date: 2026-03-06
|
||||
|
||||
We're releasing version 4 of shadcn/cli. More capable, easier to use. Built for you and your coding agents. Here's everything new.
|
||||
|
||||
### shadcn/skills
|
||||
## shadcn/skills
|
||||
|
||||
shadcn/skills gives coding agents the context they need to work with your components and registry correctly. It covers both Radix and Base UI primitives, updated APIs, component patterns and registry workflows. The skill also knows how to use the shadcn CLI, when to invoke it and which flags to pass. Agents make fewer mistakes and produce code that actually matches your design system.
|
||||
|
||||
@@ -20,7 +20,7 @@ You can ask your agent things like:
|
||||
npx skills add shadcn/ui
|
||||
```
|
||||
|
||||
### Introducing --preset
|
||||
## Introducing --preset
|
||||
|
||||
A preset packs your entire design system config into a short code. Colors, theme, icon library, fonts, radius. One string, everything included.
|
||||
|
||||
@@ -32,7 +32,7 @@ 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.
|
||||
|
||||
### Switching presets
|
||||
## Switching presets
|
||||
|
||||
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.
|
||||
|
||||
@@ -40,18 +40,18 @@ When you're working on a new app, it can take a few tries to find something you
|
||||
npx shadcn@latest init --preset ad3qkJ7
|
||||
```
|
||||
|
||||
### Skills + Presets
|
||||
## Skills + Presets
|
||||
|
||||
Your agent knows how to use presets. Scaffold a project, switch design systems, try something new.
|
||||
|
||||
- "create a new next app using --preset adtk27v"
|
||||
- "let's try --preset adtk27v"
|
||||
|
||||
### New shadcn/create
|
||||
## New shadcn/create
|
||||
|
||||
To help you build custom presets, we rebuilt [shadcn/create](/create). It now includes a library of UI components you can use to preview your presets. See how your colors, fonts and radius apply to real components before you start building.
|
||||
|
||||
### New --dry-run, --diff, and --view flags
|
||||
## New --dry-run, --diff, and --view flags
|
||||
|
||||
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.
|
||||
|
||||
@@ -61,7 +61,7 @@ npx shadcn@latest add button --diff
|
||||
npx shadcn@latest add button --view
|
||||
```
|
||||
|
||||
### Updating primitives
|
||||
## Updating primitives
|
||||
|
||||
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".
|
||||
|
||||
@@ -69,7 +69,7 @@ You can use the `--diff` flag to check for registry updates. Or ask your agent:
|
||||
npx shadcn@latest add button --diff
|
||||
```
|
||||
|
||||
### shadcn init --template
|
||||
## shadcn init --template
|
||||
|
||||
`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.
|
||||
|
||||
@@ -91,7 +91,7 @@ Use `--monorepo` to set up a monorepo.
|
||||
npx shadcn@latest init -t next --monorepo
|
||||
```
|
||||
|
||||
### shadcn init --base
|
||||
## shadcn init --base
|
||||
|
||||
Pick your primitives. Use `--base` to start a project with Radix or Base UI.
|
||||
|
||||
@@ -99,7 +99,7 @@ Pick your primitives. Use `--base` to start a project with Radix or Base UI.
|
||||
npx shadcn@latest init --base radix
|
||||
```
|
||||
|
||||
### shadcn info
|
||||
## shadcn info
|
||||
|
||||
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.
|
||||
|
||||
@@ -107,7 +107,7 @@ The `info` command now shows the full picture: framework, version, CSS vars, whi
|
||||
npx shadcn@latest info
|
||||
```
|
||||
|
||||
### shadcn docs
|
||||
## shadcn docs
|
||||
|
||||
Get docs, code and examples for any UI component right from the CLI. Gives your coding agent the context to use your primitives correctly.
|
||||
|
||||
@@ -120,7 +120,7 @@ combobox
|
||||
- api https://base-ui.com/react/components/combobox
|
||||
```
|
||||
|
||||
### registry:base and registry:font
|
||||
## registry:base and registry:font
|
||||
|
||||
Registries can now distribute an entire design system as a single payload using `registry:base`. Components, dependencies, CSS vars, fonts, config. One install, everything set up.
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn": "4.0.4",
|
||||
"shadcn": "4.0.6",
|
||||
"shiki": "^1.10.1",
|
||||
"sonner": "^2.0.0",
|
||||
"swr": "^2.3.6",
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
},
|
||||
{
|
||||
"name": "@agents-ui",
|
||||
"homepage": "https://livekit.io/ui",
|
||||
"url": "https://livekit.io/ui/r/{name}.json",
|
||||
"homepage": "https://livekit.com/ui",
|
||||
"url": "https://livekit.com/ui/r/{name}.json",
|
||||
"description": "This is a shadcn/ui component registry that distributes copy-paste React components for building LiveKit AI Agent interfaces."
|
||||
},
|
||||
{
|
||||
@@ -539,6 +539,12 @@
|
||||
"url": "https://shadcndesign-free.vercel.app/r/{name}.json",
|
||||
"description": "A growing collection of high-quality blocks and themes for shadcn/ui."
|
||||
},
|
||||
{
|
||||
"name": "@shadcnmaps",
|
||||
"homepage": "https://shadcnmaps.com",
|
||||
"url": "https://shadcnmaps.com/r/{name}.json",
|
||||
"description": "Beautiful map components powered by pure SVG."
|
||||
},
|
||||
{
|
||||
"name": "@shadcnstore",
|
||||
"homepage": "https://www.shadcnstore.com",
|
||||
@@ -868,5 +874,11 @@
|
||||
"homepage": "https://www.fluidfunctionalism.com",
|
||||
"url": "https://www.fluidfunctionalism.com/r/{name}.json",
|
||||
"description": "Fluid components used exclusively in service of functional clarity. Proximity hover, spring animations, font-weight transitions, and animated focus rings."
|
||||
},
|
||||
{
|
||||
"name": "@gammaui",
|
||||
"homepage": "https://www.gammaui.com",
|
||||
"url": "https://www.gammaui.com/r/{name}.json",
|
||||
"description": "Beautifully designed landing page components built with React & Tailwind CSS & Motion."
|
||||
}
|
||||
]
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
},
|
||||
{
|
||||
"name": "@agents-ui",
|
||||
"homepage": "https://livekit.io/ui",
|
||||
"url": "https://livekit.io/ui/r/{name}.json",
|
||||
"homepage": "https://livekit.com/ui",
|
||||
"url": "https://livekit.com/ui/r/{name}.json",
|
||||
"description": "This is a shadcn/ui component registry that distributes copy-paste React components for building LiveKit AI Agent interfaces.",
|
||||
"logo": "<svg role='img' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><title>LiveKit</title><path d='M0 0v24h14.4v-4.799h4.8V24H24v-4.8h-4.799v-4.8h-4.8v4.8H4.8V0H0zm14.4 14.4V9.602h4.801V4.8H24V0h-4.8v4.8h-4.8v4.8H9.6v4.8h4.8z'/></svg>"
|
||||
},
|
||||
@@ -630,6 +630,13 @@
|
||||
"description": "A growing collection of high-quality blocks and themes for shadcn/ui.",
|
||||
"logo": "<svg width='80' height='80' viewBox='0 0 80 80' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_22651_9557)'><g clip-path='url(#clip1_22651_9557)'><rect x='10' y='10' width='60' height='60' fill='black' stroke='#4497F7' stroke-width='5'/><rect x='2.5' y='2.5' width='15' height='15' fill='white' stroke='#4497F7' stroke-width='5'/><rect x='62.5' y='2.5' width='15' height='15' fill='white' stroke='#4497F7' stroke-width='5'/><rect x='2.5' y='62.5' width='15' height='15' fill='white' stroke='#4497F7' stroke-width='5'/><rect x='62.5' y='62.5' width='15' height='15' fill='white' stroke='#4497F7' stroke-width='5'/><path d='M23.75 56.25L56.25 23.75' stroke='white' stroke-width='5'/><path d='M43.75 56.25L56.25 43.75' stroke='white' stroke-width='5'/></g></g><defs><clipPath id='clip0_22651_9557'><rect width='80' height='80' fill='white'/></clipPath><clipPath id='clip1_22651_9557'><rect width='80' height='80' fill='white'/></clipPath></defs></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@shadcnmaps",
|
||||
"homepage": "https://shadcnmaps.com",
|
||||
"url": "https://shadcnmaps.com/r/{name}.json",
|
||||
"description": "Beautiful map components powered by pure SVG.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' width='46.876' height='46.876' viewBox='0 0 12.403 12.403'><path d='M6.201 0A6.21 6.21 0 0 0 0 6.201a6.21 6.21 0 0 0 6.201 6.202A6.21 6.21 0 0 0 12.403 6.2 6.21 6.21 0 0 0 6.2 0m2.977 3.99L7.616 9.573a.62.62 0 0 1-.552.452l-.045.002a.62.62 0 0 1-.567-.368L5.31 7.095 2.743 5.951a.62.62 0 0 1 .086-1.164l5.585-1.562a.62.62 0 0 1 .765.765z'/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@shadcnstore",
|
||||
"homepage": "https://www.shadcnstore.com",
|
||||
@@ -1014,5 +1021,12 @@
|
||||
"url": "https://www.fluidfunctionalism.com/r/{name}.json",
|
||||
"description": "Fluid components used exclusively in service of functional clarity. Proximity hover, spring animations, font-weight transitions, and animated focus rings.",
|
||||
"logo": "<svg width='40' height='40' viewBox='0 0 40 40' fill='none' xmlns='http://www.w3.org/2000/svg'><rect width='40' height='40' rx='8' fill='var(--foreground)'/><text x='20' y='26' text-anchor='middle' font-size='20' font-weight='700' font-family='system-ui' fill='var(--background)'>F</text></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@gammaui",
|
||||
"homepage": "https://www.gammaui.com",
|
||||
"url": "https://www.gammaui.com/r/{name}.json",
|
||||
"description": "Beautifully designed landing page components built with React & Tailwind CSS & Motion.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 1000'><g><g><path d='M561.27,349.76v-54.72H342.36c-11.96,0-23.77,3.15-33.89,9.51c-21.73,14.52-29.14,36.49-31.32,44.1H561.27z' /><path d='M275.43,358.4s-2.88,38.88,13.68,56.88l65.52,102.96s67.52,41.04,145-5.76s61.64-136.08,61.64-136.08H423.03l-0.72,54.72l74.16,0.72s0,29.52-61.2,48.24c0,0-73.44,14.4-99.36-72c0,0,5.04-37.44-21.6-45.36c-26.64-7.92-38.88-4.32-38.88-4.32z' /></g><path d='M444.63,562.16h-61.92l79.92,130.32s29.52-45.36,12.96-79.2l-30.96-51.12z' /><path d='M471.99,698.24l115.92-186.48V295.04h55.44v229.68l-85.68,138.96s-35.28,61.2-85.68,34.56z' /><path d='M669.27,480.08V295.04h55.44v78.48s2.16,24.48-19.44,51.84l-36,54.72z' /></g></svg>"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @shadcn/ui
|
||||
|
||||
## 4.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#10022](https://github.com/shadcn-ui/ui/pull/10022) [`7e93eb81ea8160c06c86f98bb6bfeb1ddfd0d237`](https://github.com/shadcn-ui/ui/commit/7e93eb81ea8160c06c86f98bb6bfeb1ddfd0d237) Thanks [@shadcn](https://github.com/shadcn)! - ensure monorepo respect package manager
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#9960](https://github.com/shadcn-ui/ui/pull/9960) [`5ee456735377158c12cf55eefbe872f7303e1325`](https://github.com/shadcn-ui/ui/commit/5ee456735377158c12cf55eefbe872f7303e1325) Thanks [@shadcn](https://github.com/shadcn)! - update handling of init urls
|
||||
|
||||
## 4.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"version": "4.0.4",
|
||||
"version": "4.0.6",
|
||||
"description": "Add components to your apps.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -187,8 +187,11 @@ export const add = new Command()
|
||||
})
|
||||
|
||||
// Resolve registry:base config.
|
||||
const { registryBaseConfig, installStyleIndex } =
|
||||
await resolveRegistryBaseConfig(initUrl, options.cwd)
|
||||
const {
|
||||
registryBaseConfig,
|
||||
installStyleIndex,
|
||||
url: cleanInitUrl,
|
||||
} = await resolveRegistryBaseConfig(initUrl, options.cwd)
|
||||
|
||||
config = await runInit({
|
||||
cwd: options.cwd,
|
||||
@@ -201,7 +204,7 @@ export const add = new Command()
|
||||
cssVariables: true,
|
||||
rtl: false,
|
||||
installStyleIndex,
|
||||
components: [initUrl, ...(options.components ?? [])],
|
||||
components: [cleanInitUrl, ...(options.components ?? [])],
|
||||
registryBaseConfig,
|
||||
})
|
||||
initHasRun = true
|
||||
@@ -228,8 +231,11 @@ export const add = new Command()
|
||||
base: selectedBase,
|
||||
template,
|
||||
})
|
||||
const { registryBaseConfig, installStyleIndex } =
|
||||
await resolveRegistryBaseConfig(initUrl, options.cwd)
|
||||
const {
|
||||
registryBaseConfig,
|
||||
installStyleIndex,
|
||||
url: cleanInitUrl,
|
||||
} = await resolveRegistryBaseConfig(initUrl, options.cwd)
|
||||
|
||||
config = await runInit({
|
||||
cwd: options.cwd,
|
||||
@@ -242,7 +248,7 @@ export const add = new Command()
|
||||
cssVariables: true,
|
||||
rtl: false,
|
||||
installStyleIndex,
|
||||
components: [initUrl, ...(options.components ?? [])],
|
||||
components: [cleanInitUrl, ...(options.components ?? [])],
|
||||
registryBaseConfig,
|
||||
})
|
||||
initHasRun = true
|
||||
|
||||
@@ -384,6 +384,9 @@ export const init = new Command()
|
||||
} else if (options.rtl === false) {
|
||||
url.searchParams.delete("rtl")
|
||||
}
|
||||
if (url.pathname === "/init" && presetArg.startsWith(SHADCN_URL)) {
|
||||
url.searchParams.set("track", "1")
|
||||
}
|
||||
initUrl = url.toString()
|
||||
presetBase = url.searchParams.get("base") ?? undefined
|
||||
} else if (isPresetCode(presetArg)) {
|
||||
@@ -512,12 +515,18 @@ export const init = new Command()
|
||||
}
|
||||
|
||||
// Resolve registry:base config from the first component.
|
||||
const { registryBaseConfig, installStyleIndex } =
|
||||
await resolveRegistryBaseConfig(components[0], cwd, {
|
||||
registries: existingConfig?.registries as
|
||||
| z.infer<typeof registryConfigSchema>
|
||||
| undefined,
|
||||
})
|
||||
const {
|
||||
registryBaseConfig,
|
||||
installStyleIndex,
|
||||
url: cleanUrl,
|
||||
} = await resolveRegistryBaseConfig(components[0], cwd, {
|
||||
registries: existingConfig?.registries as
|
||||
| z.infer<typeof registryConfigSchema>
|
||||
| undefined,
|
||||
})
|
||||
|
||||
// Use the clean URL (track param stripped) for subsequent fetches.
|
||||
components[0] = cleanUrl
|
||||
|
||||
if (!installStyleIndex) {
|
||||
options.installStyleIndex = false
|
||||
|
||||
@@ -3,6 +3,7 @@ import { buildUrlAndHeadersForRegistryItem } from "@/src/registry/builder"
|
||||
import { configWithDefaults } from "@/src/registry/config"
|
||||
import { REGISTRY_URL, SHADCN_URL } from "@/src/registry/constants"
|
||||
import { type registryConfigSchema } from "@/src/registry/schema"
|
||||
import { isUrl } from "@/src/registry/utils"
|
||||
import { createConfig } from "@/src/utils/get-config"
|
||||
import { highlighter } from "@/src/utils/highlighter"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
@@ -141,6 +142,9 @@ export function resolveInitUrl(
|
||||
params.set("template", options.template)
|
||||
}
|
||||
|
||||
// Signal the server to record this init run.
|
||||
params.set("track", "1")
|
||||
|
||||
return `${SHADCN_URL}/init?${params.toString()}`
|
||||
}
|
||||
|
||||
@@ -272,8 +276,25 @@ export async function resolveRegistryBaseConfig(
|
||||
const registryBaseConfig =
|
||||
item?.type === "registry:base" && item.config ? item.config : undefined
|
||||
|
||||
// Strip the track param so subsequent fetches don't re-trigger tracking.
|
||||
let cleanUrl = initUrl
|
||||
if (isShadcnInitUrl(initUrl)) {
|
||||
const url = new URL(initUrl)
|
||||
url.searchParams.delete("track")
|
||||
cleanUrl = url.toString()
|
||||
}
|
||||
|
||||
return {
|
||||
registryBaseConfig,
|
||||
installStyleIndex: item?.extends !== "none",
|
||||
url: cleanUrl,
|
||||
}
|
||||
}
|
||||
|
||||
function isShadcnInitUrl(url: string) {
|
||||
try {
|
||||
return new URL(url).pathname === "/init" && url.startsWith(SHADCN_URL)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ import { ComponentExample } from "@/components/component-example"
|
||||
],
|
||||
monorepo: {
|
||||
templateDir: "astro-monorepo",
|
||||
packageManager: "pnpm",
|
||||
installArgs: ["--no-frozen-lockfile"],
|
||||
init: fontsourceMonorepoInit,
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -34,12 +34,8 @@ export interface TemplateConfig {
|
||||
defaultProjectName: string
|
||||
// The template directory name (e.g. "next-app", "vite-app").
|
||||
templateDir: string
|
||||
// Force a specific package manager for this template.
|
||||
packageManager?: string
|
||||
// Framework names that map to this template.
|
||||
frameworks?: string[]
|
||||
// Custom args passed to `packageManager install`.
|
||||
installArgs?: string[]
|
||||
scaffold?: (options: TemplateOptions) => Promise<void>
|
||||
create: (options: TemplateOptions) => Promise<void>
|
||||
init?: (options: TemplateInitOptions) => Promise<Config>
|
||||
@@ -50,8 +46,6 @@ export interface TemplateConfig {
|
||||
monorepo?: {
|
||||
templateDir: string
|
||||
defaultProjectName?: string
|
||||
packageManager?: string
|
||||
installArgs?: string[]
|
||||
init?: (options: TemplateInitOptions) => Promise<Config>
|
||||
files?: RegistryItem["files"]
|
||||
}
|
||||
@@ -66,7 +60,6 @@ export function createTemplate(config: TemplateConfig) {
|
||||
defaultScaffold({
|
||||
title: config.title,
|
||||
templateDir: config.templateDir,
|
||||
installArgs: config.installArgs,
|
||||
}),
|
||||
postInit: config.postInit ?? defaultPostInit,
|
||||
}
|
||||
@@ -86,8 +79,6 @@ export function resolveTemplate(
|
||||
...template,
|
||||
templateDir: m.templateDir,
|
||||
defaultProjectName: m.defaultProjectName ?? m.templateDir,
|
||||
packageManager: m.packageManager ?? template.packageManager,
|
||||
installArgs: m.installArgs ?? template.installArgs,
|
||||
init: m.init ?? template.init,
|
||||
files: m.files ?? template.files,
|
||||
}
|
||||
@@ -96,21 +87,122 @@ export function resolveTemplate(
|
||||
resolved.scaffold = defaultScaffold({
|
||||
title: template.title,
|
||||
templateDir: m.templateDir,
|
||||
installArgs: resolved.installArgs,
|
||||
})
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
// Get the appropriate install args for the given package manager.
|
||||
function getInstallArgs(packageManager: string): string[] {
|
||||
switch (packageManager) {
|
||||
case "pnpm":
|
||||
// pnpm enables frozen lockfile in CI by default.
|
||||
// The template lockfile may drift, so force-disable it explicitly.
|
||||
return ["--no-frozen-lockfile"]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt a pnpm-based monorepo template to the target package manager.
|
||||
async function adaptWorkspaceConfig(
|
||||
projectPath: string,
|
||||
packageManager: string
|
||||
) {
|
||||
if (packageManager === "pnpm") {
|
||||
return
|
||||
}
|
||||
|
||||
const pnpmWorkspacePath = path.join(projectPath, "pnpm-workspace.yaml")
|
||||
const packageJsonPath = path.join(projectPath, "package.json")
|
||||
|
||||
// Remove pnpm-lock.yaml.
|
||||
const lockFilePath = path.join(projectPath, "pnpm-lock.yaml")
|
||||
if (fs.existsSync(lockFilePath)) {
|
||||
await fs.remove(lockFilePath)
|
||||
}
|
||||
|
||||
const isMonorepo = fs.existsSync(pnpmWorkspacePath)
|
||||
|
||||
// Update root package.json: strip "packageManager" field to avoid
|
||||
// triggering Corepack, and add "workspaces" for npm/bun/yarn.
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJsonContent = await fs.readFile(packageJsonPath, "utf8")
|
||||
const packageJson = JSON.parse(packageJsonContent)
|
||||
delete packageJson.packageManager
|
||||
|
||||
if (isMonorepo) {
|
||||
// Read workspace patterns from pnpm-workspace.yaml.
|
||||
const workspaceContent = await fs.readFile(pnpmWorkspacePath, "utf8")
|
||||
const patterns: string[] = []
|
||||
for (const line of workspaceContent.split("\n")) {
|
||||
const match = line.match(/^\s*-\s*["']?(.+?)["']?\s*$/)
|
||||
if (match) {
|
||||
patterns.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
packageJson.workspaces = patterns
|
||||
await fs.remove(pnpmWorkspacePath)
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
// Rewrite workspace: protocol references in nested package.json files.
|
||||
// npm does not support workspace: protocol; bun and yarn do, so only
|
||||
// rewrite for npm monorepo templates.
|
||||
if (isMonorepo && packageManager === "npm") {
|
||||
await rewriteWorkspaceProtocol(projectPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively find all package.json files and replace workspace: protocol
|
||||
// version specifiers with "*", which npm understands.
|
||||
async function rewriteWorkspaceProtocol(dir: string) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
await rewriteWorkspaceProtocol(fullPath)
|
||||
} else if (entry.name === "package.json") {
|
||||
const content = await fs.readFile(fullPath, "utf8")
|
||||
if (!content.includes("workspace:")) continue
|
||||
const pkg = JSON.parse(content)
|
||||
let changed = false
|
||||
for (const depKey of [
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"peerDependencies",
|
||||
"optionalDependencies",
|
||||
]) {
|
||||
const deps = pkg[depKey]
|
||||
if (!deps) continue
|
||||
for (const [name, version] of Object.entries(deps)) {
|
||||
if (typeof version === "string" && version.startsWith("workspace:")) {
|
||||
deps[name] = "*"
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
await fs.writeFile(fullPath, JSON.stringify(pkg, null, 2) + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default scaffold that downloads a template from GitHub.
|
||||
function defaultScaffold({
|
||||
title,
|
||||
templateDir,
|
||||
installArgs,
|
||||
}: {
|
||||
title: string
|
||||
templateDir: string
|
||||
installArgs?: string[]
|
||||
}) {
|
||||
return async ({ projectPath, packageManager }: TemplateOptions) => {
|
||||
const createSpinner = spinner(
|
||||
@@ -157,16 +249,12 @@ function defaultScaffold({
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// Remove pnpm-lock.yaml if using a different package manager.
|
||||
if (packageManager !== "pnpm") {
|
||||
const lockFilePath = path.join(projectPath, "pnpm-lock.yaml")
|
||||
if (fs.existsSync(lockFilePath)) {
|
||||
await fs.remove(lockFilePath)
|
||||
}
|
||||
}
|
||||
// Adapt workspace config and lockfiles for the target package manager.
|
||||
await adaptWorkspaceConfig(projectPath, packageManager)
|
||||
|
||||
// Run install.
|
||||
const args = ["install", ...(installArgs ?? [])]
|
||||
const installArgs = getInstallArgs(packageManager)
|
||||
const args = ["install", ...installArgs]
|
||||
await execa(packageManager, args, {
|
||||
cwd: projectPath,
|
||||
})
|
||||
@@ -179,7 +267,7 @@ function defaultScaffold({
|
||||
packageJson.name = path.basename(projectPath)
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2)
|
||||
JSON.stringify(packageJson, null, 2) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,6 @@ export default function Page() {
|
||||
],
|
||||
monorepo: {
|
||||
templateDir: "next-monorepo",
|
||||
packageManager: "pnpm",
|
||||
// pnpm enables frozen lockfile in CI by default.
|
||||
// The template lockfile may drift, so force-disable it explicitly.
|
||||
installArgs: ["--no-frozen-lockfile"],
|
||||
init: async (options) => {
|
||||
const packagesUiPath = path.resolve(options.projectPath, "packages/ui")
|
||||
const appsWebPath = path.resolve(options.projectPath, "apps/web")
|
||||
|
||||
@@ -27,8 +27,6 @@ export default function Home() {
|
||||
],
|
||||
monorepo: {
|
||||
templateDir: "react-router-monorepo",
|
||||
packageManager: "pnpm",
|
||||
installArgs: ["--no-frozen-lockfile"],
|
||||
init: fontsourceMonorepoInit,
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -32,8 +32,6 @@ function App() {
|
||||
],
|
||||
monorepo: {
|
||||
templateDir: "start-monorepo",
|
||||
packageManager: "pnpm",
|
||||
installArgs: ["--no-frozen-lockfile"],
|
||||
init: fontsourceMonorepoInit,
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -29,8 +29,6 @@ export default App;
|
||||
],
|
||||
monorepo: {
|
||||
templateDir: "vite-monorepo",
|
||||
packageManager: "pnpm",
|
||||
installArgs: ["--no-frozen-lockfile"],
|
||||
init: fontsourceMonorepoInit,
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -70,11 +70,9 @@ export async function createProject(
|
||||
monorepo: options.monorepo,
|
||||
})
|
||||
|
||||
const packageManager =
|
||||
effectiveTemplate.packageManager ??
|
||||
(await getPackageManager(options.cwd, {
|
||||
withFallback: true,
|
||||
}))
|
||||
const packageManager = await getPackageManager(options.cwd, {
|
||||
withFallback: true,
|
||||
})
|
||||
|
||||
const projectPath = path.join(options.cwd, projectName)
|
||||
|
||||
|
||||
@@ -209,24 +209,17 @@ describe("defaultScaffold", () => {
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "npm",
|
||||
packageManager: "bun",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
expect(vi.mocked(execa)).toHaveBeenCalledWith("npm", ["install"], {
|
||||
expect(vi.mocked(execa)).toHaveBeenCalledWith("bun", ["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(),
|
||||
})
|
||||
it("should pass --no-frozen-lockfile for pnpm", async () => {
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
@@ -236,11 +229,168 @@ describe("defaultScaffold", () => {
|
||||
|
||||
expect(vi.mocked(execa)).toHaveBeenCalledWith(
|
||||
"pnpm",
|
||||
["install", "--shamefully-hoist"],
|
||||
["install", "--no-frozen-lockfile"],
|
||||
{ cwd: "/test/my-app" }
|
||||
)
|
||||
})
|
||||
|
||||
it("should strip packageManager field from package.json for non-pnpm", async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p: any) =>
|
||||
p.toString().includes("package.json")
|
||||
)
|
||||
vi.mocked(fs.readFile).mockResolvedValue(
|
||||
JSON.stringify({
|
||||
name: "my-app",
|
||||
packageManager: "pnpm@9.0.0",
|
||||
}) as any
|
||||
)
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "bun",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
// The first writeFile call is from adaptWorkspaceConfig.
|
||||
const writeCalls = vi.mocked(fs.writeFile).mock.calls
|
||||
const adaptCall = writeCalls.find(
|
||||
(call) => call[0] === path.join("/test/my-app", "package.json")
|
||||
)
|
||||
expect(adaptCall).toBeDefined()
|
||||
const written = JSON.parse(adaptCall![1] as string)
|
||||
expect(written.packageManager).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should convert pnpm-workspace.yaml to workspaces field for non-pnpm monorepo", async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p: any) => {
|
||||
const s = p.toString()
|
||||
return s.includes("pnpm-workspace.yaml") || s.includes("package.json")
|
||||
})
|
||||
|
||||
// Return different content based on which file is being read.
|
||||
vi.mocked(fs.readFile).mockImplementation(((filePath: string) => {
|
||||
if (filePath.includes("pnpm-workspace.yaml")) {
|
||||
return Promise.resolve("packages:\n - 'apps/*'\n - 'packages/*'\n")
|
||||
}
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ name: "my-mono", packageManager: "pnpm@9.0.0" })
|
||||
)
|
||||
}) as any)
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "bun",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
// Should remove pnpm-workspace.yaml.
|
||||
expect(vi.mocked(fs.remove)).toHaveBeenCalledWith(
|
||||
path.join("/test/my-app", "pnpm-workspace.yaml")
|
||||
)
|
||||
|
||||
// Should write workspaces array to package.json.
|
||||
const writeCalls = vi.mocked(fs.writeFile).mock.calls
|
||||
const adaptCall = writeCalls.find(
|
||||
(call) => call[0] === path.join("/test/my-app", "package.json")
|
||||
)
|
||||
expect(adaptCall).toBeDefined()
|
||||
const written = JSON.parse(adaptCall![1] as string)
|
||||
expect(written.workspaces).toEqual(["apps/*", "packages/*"])
|
||||
expect(written.packageManager).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should rewrite workspace: protocol refs to * for npm monorepo", async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p: any) => {
|
||||
const s = p.toString()
|
||||
return s.includes("pnpm-workspace.yaml") || s.includes("package.json")
|
||||
})
|
||||
|
||||
const rootPkg = JSON.stringify({
|
||||
name: "my-mono",
|
||||
packageManager: "pnpm@9.0.0",
|
||||
})
|
||||
const nestedPkg = JSON.stringify({
|
||||
name: "web",
|
||||
dependencies: { "@workspace/ui": "workspace:*" },
|
||||
})
|
||||
|
||||
vi.mocked(fs.readFile).mockImplementation(((filePath: string) => {
|
||||
if (filePath.includes("pnpm-workspace.yaml")) {
|
||||
return Promise.resolve("packages:\n - 'apps/*'\n")
|
||||
}
|
||||
if (filePath.includes("apps")) {
|
||||
return Promise.resolve(nestedPkg)
|
||||
}
|
||||
return Promise.resolve(rootPkg)
|
||||
}) as any)
|
||||
|
||||
// Mock readdir for the recursive rewriteWorkspaceProtocol walk.
|
||||
vi.mocked(fs.readdir).mockImplementation(((dir: string) => {
|
||||
if (dir === "/test/my-app") {
|
||||
return Promise.resolve([
|
||||
{ name: "apps", isDirectory: () => true },
|
||||
{ name: "package.json", isDirectory: () => false },
|
||||
])
|
||||
}
|
||||
if (dir.includes("apps")) {
|
||||
return Promise.resolve([
|
||||
{ name: "package.json", isDirectory: () => false },
|
||||
])
|
||||
}
|
||||
return Promise.resolve([])
|
||||
}) as any)
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "npm",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
// Should have rewritten workspace:* to * in nested package.json.
|
||||
const writeCalls = vi.mocked(fs.writeFile).mock.calls
|
||||
const nestedWrite = writeCalls.find(
|
||||
(call) =>
|
||||
(call[0] as string).includes("apps") &&
|
||||
(call[0] as string).includes("package.json")
|
||||
)
|
||||
expect(nestedWrite).toBeDefined()
|
||||
const written = JSON.parse(nestedWrite![1] as string)
|
||||
expect(written.dependencies["@workspace/ui"]).toBe("*")
|
||||
})
|
||||
|
||||
it("should not rewrite workspace: protocol refs for bun", async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p: any) => {
|
||||
const s = p.toString()
|
||||
return s.includes("pnpm-workspace.yaml") || s.includes("package.json")
|
||||
})
|
||||
|
||||
vi.mocked(fs.readFile).mockImplementation(((filePath: string) => {
|
||||
if (filePath.includes("pnpm-workspace.yaml")) {
|
||||
return Promise.resolve("packages:\n - 'apps/*'\n")
|
||||
}
|
||||
return Promise.resolve(
|
||||
JSON.stringify({ name: "my-mono", packageManager: "pnpm@9.0.0" })
|
||||
)
|
||||
}) as any)
|
||||
|
||||
const template = createTestTemplate()
|
||||
|
||||
await template.scaffold({
|
||||
projectPath: "/test/my-app",
|
||||
packageManager: "bun",
|
||||
cwd: "/test",
|
||||
})
|
||||
|
||||
// readdir should not be called since rewriteWorkspaceProtocol is skipped for bun.
|
||||
expect(vi.mocked(fs.readdir)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should write project name to package.json", async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p: any) =>
|
||||
p.toString().includes("package.json")
|
||||
@@ -259,7 +409,7 @@ describe("defaultScaffold", () => {
|
||||
|
||||
expect(vi.mocked(fs.writeFile)).toHaveBeenCalledWith(
|
||||
path.join("/test/my-app", "package.json"),
|
||||
JSON.stringify({ name: "my-app" }, null, 2)
|
||||
JSON.stringify({ name: "my-app" }, null, 2) + "\n"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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.4
|
||||
specifier: 4.0.6
|
||||
version: link:../../packages/shadcn
|
||||
shiki:
|
||||
specifier: ^1.10.1
|
||||
|
||||
Reference in New Issue
Block a user