Merge branch 'main' into fix/registry-font

This commit is contained in:
KapishDima
2026-03-13 09:19:02 +02:00
committed by GitHub
3303 changed files with 330 additions and 236405 deletions

View File

@@ -1,5 +1,11 @@
# @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

View File

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

View File

@@ -30,8 +30,6 @@ import { ComponentExample } from "@/components/component-example"
],
monorepo: {
templateDir: "astro-monorepo",
packageManager: "pnpm",
installArgs: ["--no-frozen-lockfile"],
init: fontsourceMonorepoInit,
files: [
{

View File

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

View File

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

View File

@@ -27,8 +27,6 @@ export default function Home() {
],
monorepo: {
templateDir: "react-router-monorepo",
packageManager: "pnpm",
installArgs: ["--no-frozen-lockfile"],
init: fontsourceMonorepoInit,
files: [
{

View File

@@ -32,8 +32,6 @@ function App() {
],
monorepo: {
templateDir: "start-monorepo",
packageManager: "pnpm",
installArgs: ["--no-frozen-lockfile"],
init: fontsourceMonorepoInit,
files: [
{

View File

@@ -29,8 +29,6 @@ export default App;
],
monorepo: {
templateDir: "vite-monorepo",
packageManager: "pnpm",
installArgs: ["--no-frozen-lockfile"],
init: fontsourceMonorepoInit,
files: [
{

View File

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

View File

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