Skip to content

Commit abf1555

Browse files
authored
Merge pull request #9962 from devinscodebase/fix/monorepo-package-manager-detection
fix: monorepo templates ignore the user's package manager
2 parents 3af2ba8 + 584db77 commit abf1555

8 files changed

Lines changed: 275 additions & 51 deletions

File tree

packages/shadcn/src/templates/astro.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ import { ComponentExample } from "@/components/component-example"
3030
],
3131
monorepo: {
3232
templateDir: "astro-monorepo",
33-
packageManager: "pnpm",
34-
installArgs: ["--no-frozen-lockfile"],
3533
init: fontsourceMonorepoInit,
3634
files: [
3735
{

packages/shadcn/src/templates/create-template.ts

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,8 @@ export interface TemplateConfig {
3434
defaultProjectName: string
3535
// The template directory name (e.g. "next-app", "vite-app").
3636
templateDir: string
37-
// Force a specific package manager for this template.
38-
packageManager?: string
3937
// Framework names that map to this template.
4038
frameworks?: string[]
41-
// Custom args passed to `packageManager install`.
42-
installArgs?: string[]
4339
scaffold?: (options: TemplateOptions) => Promise<void>
4440
create: (options: TemplateOptions) => Promise<void>
4541
init?: (options: TemplateInitOptions) => Promise<Config>
@@ -50,8 +46,6 @@ export interface TemplateConfig {
5046
monorepo?: {
5147
templateDir: string
5248
defaultProjectName?: string
53-
packageManager?: string
54-
installArgs?: string[]
5549
init?: (options: TemplateInitOptions) => Promise<Config>
5650
files?: RegistryItem["files"]
5751
}
@@ -66,7 +60,6 @@ export function createTemplate(config: TemplateConfig) {
6660
defaultScaffold({
6761
title: config.title,
6862
templateDir: config.templateDir,
69-
installArgs: config.installArgs,
7063
}),
7164
postInit: config.postInit ?? defaultPostInit,
7265
}
@@ -86,8 +79,6 @@ export function resolveTemplate(
8679
...template,
8780
templateDir: m.templateDir,
8881
defaultProjectName: m.defaultProjectName ?? m.templateDir,
89-
packageManager: m.packageManager ?? template.packageManager,
90-
installArgs: m.installArgs ?? template.installArgs,
9182
init: m.init ?? template.init,
9283
files: m.files ?? template.files,
9384
}
@@ -96,21 +87,122 @@ export function resolveTemplate(
9687
resolved.scaffold = defaultScaffold({
9788
title: template.title,
9889
templateDir: m.templateDir,
99-
installArgs: resolved.installArgs,
10090
})
10191

10292
return resolved
10393
}
10494

95+
// Get the appropriate install args for the given package manager.
96+
function getInstallArgs(packageManager: string): string[] {
97+
switch (packageManager) {
98+
case "pnpm":
99+
// pnpm enables frozen lockfile in CI by default.
100+
// The template lockfile may drift, so force-disable it explicitly.
101+
return ["--no-frozen-lockfile"]
102+
default:
103+
return []
104+
}
105+
}
106+
107+
// Adapt a pnpm-based monorepo template to the target package manager.
108+
async function adaptWorkspaceConfig(
109+
projectPath: string,
110+
packageManager: string
111+
) {
112+
if (packageManager === "pnpm") {
113+
return
114+
}
115+
116+
const pnpmWorkspacePath = path.join(projectPath, "pnpm-workspace.yaml")
117+
const packageJsonPath = path.join(projectPath, "package.json")
118+
119+
// Remove pnpm-lock.yaml.
120+
const lockFilePath = path.join(projectPath, "pnpm-lock.yaml")
121+
if (fs.existsSync(lockFilePath)) {
122+
await fs.remove(lockFilePath)
123+
}
124+
125+
const isMonorepo = fs.existsSync(pnpmWorkspacePath)
126+
127+
// Update root package.json: strip "packageManager" field to avoid
128+
// triggering Corepack, and add "workspaces" for npm/bun/yarn.
129+
if (fs.existsSync(packageJsonPath)) {
130+
const packageJsonContent = await fs.readFile(packageJsonPath, "utf8")
131+
const packageJson = JSON.parse(packageJsonContent)
132+
delete packageJson.packageManager
133+
134+
if (isMonorepo) {
135+
// Read workspace patterns from pnpm-workspace.yaml.
136+
const workspaceContent = await fs.readFile(pnpmWorkspacePath, "utf8")
137+
const patterns: string[] = []
138+
for (const line of workspaceContent.split("\n")) {
139+
const match = line.match(/^\s*-\s*["']?(.+?)["']?\s*$/)
140+
if (match) {
141+
patterns.push(match[1])
142+
}
143+
}
144+
145+
packageJson.workspaces = patterns
146+
await fs.remove(pnpmWorkspacePath)
147+
}
148+
149+
await fs.writeFile(
150+
packageJsonPath,
151+
JSON.stringify(packageJson, null, 2) + "\n"
152+
)
153+
}
154+
155+
// Rewrite workspace: protocol references in nested package.json files.
156+
// npm does not support workspace: protocol; bun and yarn do, so only
157+
// rewrite for npm monorepo templates.
158+
if (isMonorepo && packageManager === "npm") {
159+
await rewriteWorkspaceProtocol(projectPath)
160+
}
161+
}
162+
163+
// Recursively find all package.json files and replace workspace: protocol
164+
// version specifiers with "*", which npm understands.
165+
async function rewriteWorkspaceProtocol(dir: string) {
166+
const entries = await fs.readdir(dir, { withFileTypes: true })
167+
for (const entry of entries) {
168+
if (entry.name === "node_modules") continue
169+
const fullPath = path.join(dir, entry.name)
170+
if (entry.isDirectory()) {
171+
await rewriteWorkspaceProtocol(fullPath)
172+
} else if (entry.name === "package.json") {
173+
const content = await fs.readFile(fullPath, "utf8")
174+
if (!content.includes("workspace:")) continue
175+
const pkg = JSON.parse(content)
176+
let changed = false
177+
for (const depKey of [
178+
"dependencies",
179+
"devDependencies",
180+
"peerDependencies",
181+
"optionalDependencies",
182+
]) {
183+
const deps = pkg[depKey]
184+
if (!deps) continue
185+
for (const [name, version] of Object.entries(deps)) {
186+
if (typeof version === "string" && version.startsWith("workspace:")) {
187+
deps[name] = "*"
188+
changed = true
189+
}
190+
}
191+
}
192+
if (changed) {
193+
await fs.writeFile(fullPath, JSON.stringify(pkg, null, 2) + "\n")
194+
}
195+
}
196+
}
197+
}
198+
105199
// Default scaffold that downloads a template from GitHub.
106200
function defaultScaffold({
107201
title,
108202
templateDir,
109-
installArgs,
110203
}: {
111204
title: string
112205
templateDir: string
113-
installArgs?: string[]
114206
}) {
115207
return async ({ projectPath, packageManager }: TemplateOptions) => {
116208
const createSpinner = spinner(
@@ -157,16 +249,12 @@ function defaultScaffold({
157249
await fs.remove(templatePath)
158250
}
159251

160-
// Remove pnpm-lock.yaml if using a different package manager.
161-
if (packageManager !== "pnpm") {
162-
const lockFilePath = path.join(projectPath, "pnpm-lock.yaml")
163-
if (fs.existsSync(lockFilePath)) {
164-
await fs.remove(lockFilePath)
165-
}
166-
}
252+
// Adapt workspace config and lockfiles for the target package manager.
253+
await adaptWorkspaceConfig(projectPath, packageManager)
167254

168255
// Run install.
169-
const args = ["install", ...(installArgs ?? [])]
256+
const installArgs = getInstallArgs(packageManager)
257+
const args = ["install", ...installArgs]
170258
await execa(packageManager, args, {
171259
cwd: projectPath,
172260
})
@@ -179,7 +267,7 @@ function defaultScaffold({
179267
packageJson.name = path.basename(projectPath)
180268
await fs.writeFile(
181269
packageJsonPath,
182-
JSON.stringify(packageJson, null, 2)
270+
JSON.stringify(packageJson, null, 2) + "\n"
183271
)
184272
}
185273

packages/shadcn/src/templates/next.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ export default function Page() {
3939
],
4040
monorepo: {
4141
templateDir: "next-monorepo",
42-
packageManager: "pnpm",
43-
// pnpm enables frozen lockfile in CI by default.
44-
// The template lockfile may drift, so force-disable it explicitly.
45-
installArgs: ["--no-frozen-lockfile"],
4642
init: async (options) => {
4743
const packagesUiPath = path.resolve(options.projectPath, "packages/ui")
4844
const appsWebPath = path.resolve(options.projectPath, "apps/web")

packages/shadcn/src/templates/react-router.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ export default function Home() {
2727
],
2828
monorepo: {
2929
templateDir: "react-router-monorepo",
30-
packageManager: "pnpm",
31-
installArgs: ["--no-frozen-lockfile"],
3230
init: fontsourceMonorepoInit,
3331
files: [
3432
{

packages/shadcn/src/templates/start.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ function App() {
3232
],
3333
monorepo: {
3434
templateDir: "start-monorepo",
35-
packageManager: "pnpm",
36-
installArgs: ["--no-frozen-lockfile"],
3735
init: fontsourceMonorepoInit,
3836
files: [
3937
{

packages/shadcn/src/templates/vite.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ export default App;
2929
],
3030
monorepo: {
3131
templateDir: "vite-monorepo",
32-
packageManager: "pnpm",
33-
installArgs: ["--no-frozen-lockfile"],
3432
init: fontsourceMonorepoInit,
3533
files: [
3634
{

packages/shadcn/src/utils/create-project.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,9 @@ export async function createProject(
7070
monorepo: options.monorepo,
7171
})
7272

73-
const packageManager =
74-
effectiveTemplate.packageManager ??
75-
(await getPackageManager(options.cwd, {
76-
withFallback: true,
77-
}))
73+
const packageManager = await getPackageManager(options.cwd, {
74+
withFallback: true,
75+
})
7876

7977
const projectPath = path.join(options.cwd, projectName)
8078

0 commit comments

Comments
 (0)