mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-09 06:55:07 +00:00
Merge branch 'main' into fix/registry-font
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.6",
|
||||
"description": "Add components to your apps.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user