mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-08 06:28:37 +00:00
fix: respect detected package manager for monorepo templates
All monorepo templates hardcoded `packageManager: "pnpm"` which meant running `bunx --bun shadcn@latest init --monorepo` would still shell out to `pnpm install`, triggering Corepack and crashing under Bun because `process.mainModule` is readonly there. This removes the hardcoded pnpm override from every monorepo template config so `getPackageManager()` can actually detect what the user is running. The scaffold step now adapts the cloned template on the fly: - strips the `packageManager` field from package.json (avoids Corepack) - converts pnpm-workspace.yaml to a `"workspaces"` array in package.json - removes pnpm-lock.yaml - rewrites `workspace:*` refs to `"*"` when the detected PM is npm (npm doesn't support the workspace: protocol) - picks the right install flags per PM (`--no-frozen-lockfile` for pnpm, nothing extra for bun/npm/yarn) pnpm behavior is completely unchanged — `adaptWorkspaceConfig` early- returns when the detected PM is pnpm.
This commit is contained in:
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
|
||||
@@ -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,7 +229,7 @@ describe("defaultScaffold", () => {
|
||||
|
||||
expect(vi.mocked(execa)).toHaveBeenCalledWith(
|
||||
"pnpm",
|
||||
["install", "--shamefully-hoist"],
|
||||
["install", "--no-frozen-lockfile"],
|
||||
{ cwd: "/test/my-app" }
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user