fix(cli): update template handling for pnpm 11 (#10659)

* fix(cli): allow esbuild builds in Vite templates

* fix(cli): extend pnpm 11 build-script allowlists across app templates

- Add packages: [] to single-app pnpm-workspace.yaml so pnpm 9 does
  not reject the file with "packages field missing or empty".
- Add astro-app, react-router-app, start-app, next-app workspace
  yamls with the build-script allowlist each template needs
  (esbuild, sharp, unrs-resolver as applicable).
- Set msw: false across all app allowlists so the registry component
  install runs cleanly under pnpm 11 without executing msw's
  service-worker postinstall.
- Add a scaffold test pinning the packages:[] + allowBuilds shape
  so the parser keeps treating it as single-app.

* chore: changeset

* fix(templates): allow monorepo pnpm builds

* ci(templates): validate app workspace conversion

---------

Co-authored-by: shadcn <m@shadcn.com>
This commit is contained in:
Raashish Aggarwal
2026-05-29 09:54:31 +05:30
committed by GitHub
parent 360e8a19c3
commit 07900769d9
14 changed files with 186 additions and 61 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
update template handling

View File

@@ -131,6 +131,7 @@ jobs:
local package_manager="$1" local package_manager="$1"
local project_path="$2" local project_path="$2"
local check_workspace_protocol="$3" local check_workspace_protocol="$3"
local is_monorepo="$4"
cd "$project_path" cd "$project_path"
test ! -f pnpm-workspace.yaml test ! -f pnpm-workspace.yaml
@@ -138,6 +139,7 @@ jobs:
EXPECTED_PACKAGE_MANAGER="$package_manager" \ EXPECTED_PACKAGE_MANAGER="$package_manager" \
CHECK_WORKSPACE_PROTOCOL="$check_workspace_protocol" \ CHECK_WORKSPACE_PROTOCOL="$check_workspace_protocol" \
IS_MONOREPO="$is_monorepo" \
node <<'NODE' node <<'NODE'
const fs = require("node:fs") const fs = require("node:fs")
const path = require("node:path") const path = require("node:path")
@@ -145,27 +147,41 @@ jobs:
const expectedPackageManager = process.env.EXPECTED_PACKAGE_MANAGER const expectedPackageManager = process.env.EXPECTED_PACKAGE_MANAGER
const checkWorkspaceProtocol = const checkWorkspaceProtocol =
process.env.CHECK_WORKSPACE_PROTOCOL === "true" process.env.CHECK_WORKSPACE_PROTOCOL === "true"
const isMonorepo = process.env.IS_MONOREPO === "true"
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")) const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
const workspaces = pkg.workspaces ?? []
if (!Array.isArray(workspaces)) { if (isMonorepo) {
throw new Error("Expected package.json workspaces to be an array.") const workspaces = pkg.workspaces ?? []
}
if (workspaces.length === 0) { if (!Array.isArray(workspaces)) {
throw new Error("Expected package.json workspaces to have entries.") throw new Error("Expected package.json workspaces to be an array.")
}
for (const workspace of ["sharp", "unrs-resolver", "esbuild"]) {
if (workspaces.includes(workspace)) {
throw new Error(`Unexpected workspace entry: ${workspace}`)
} }
}
if (!pkg.packageManager?.startsWith(`${expectedPackageManager}@`)) { if (workspaces.length === 0) {
throw new Error( throw new Error("Expected package.json workspaces to have entries.")
`Expected packageManager to use ${expectedPackageManager}, got ${pkg.packageManager}` }
)
for (const workspace of ["sharp", "unrs-resolver", "esbuild"]) {
if (workspaces.includes(workspace)) {
throw new Error(`Unexpected workspace entry: ${workspace}`)
}
}
if (!pkg.packageManager?.startsWith(`${expectedPackageManager}@`)) {
throw new Error(
`Expected packageManager to use ${expectedPackageManager}, got ${pkg.packageManager}`
)
}
} else {
if (pkg.workspaces !== undefined) {
throw new Error("Did not expect package.json workspaces for app template.")
}
if (pkg.packageManager !== undefined) {
throw new Error(
`Did not expect packageManager for app template, got ${pkg.packageManager}`
)
}
} }
if (checkWorkspaceProtocol) { if (checkWorkspaceProtocol) {
@@ -213,8 +229,10 @@ jobs:
if [ "$mode" = "monorepo" ]; then if [ "$mode" = "monorepo" ]; then
args+=(--monorepo) args+=(--monorepo)
is_monorepo="true"
else else
args+=(--no-monorepo) args+=(--no-monorepo)
is_monorepo="false"
fi fi
case "$TEMPLATE_PACKAGE_MANAGER" in case "$TEMPLATE_PACKAGE_MANAGER" in
@@ -238,7 +256,11 @@ jobs:
bunx --bun --package "$GITHUB_WORKSPACE/packages/shadcn" \ bunx --bun --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}" shadcn "${args[@]}"
) )
validate_non_pnpm_project "bun" "$project_path" "false" validate_non_pnpm_project \
"bun" \
"$project_path" \
"false" \
"$is_monorepo"
;; ;;
npm) npm)
( (
@@ -249,7 +271,11 @@ jobs:
npx --yes --package "$GITHUB_WORKSPACE/packages/shadcn" \ npx --yes --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}" shadcn "${args[@]}"
) )
validate_non_pnpm_project "npm" "$project_path" "true" validate_non_pnpm_project \
"npm" \
"$project_path" \
"true" \
"$is_monorepo"
;; ;;
yarn) yarn)
( (
@@ -261,7 +287,11 @@ jobs:
yarn dlx --package "$GITHUB_WORKSPACE/packages/shadcn" \ yarn dlx --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}" shadcn "${args[@]}"
) )
validate_non_pnpm_project "yarn" "$project_path" "false" validate_non_pnpm_project \
"yarn" \
"$project_path" \
"false" \
"$is_monorepo"
;; ;;
esac esac

View File

@@ -127,7 +127,11 @@ async function adaptWorkspaceConfig(
await fs.remove(lockFilePath) await fs.remove(lockFilePath)
} }
const isMonorepo = fs.existsSync(pnpmWorkspacePath) const hasPnpmWorkspaceConfig = fs.existsSync(pnpmWorkspacePath)
const workspacePatterns = hasPnpmWorkspaceConfig
? parsePnpmWorkspacePackages(await fs.readFile(pnpmWorkspacePath, "utf8"))
: []
const isMonorepo = workspacePatterns.length > 0
// Update root package.json: update "packageManager" field for the // Update root package.json: update "packageManager" field for the
// target package manager, and add "workspaces" for npm/bun/yarn. // target package manager, and add "workspaces" for npm/bun/yarn.
@@ -145,12 +149,7 @@ async function adaptWorkspaceConfig(
} }
if (isMonorepo) { if (isMonorepo) {
// Read workspace patterns from pnpm-workspace.yaml. packageJson.workspaces = workspacePatterns
const workspaceContent = await fs.readFile(pnpmWorkspacePath, "utf8")
const patterns = parsePnpmWorkspacePackages(workspaceContent)
packageJson.workspaces = patterns
await fs.remove(pnpmWorkspacePath)
} }
await fs.writeFile( await fs.writeFile(
@@ -159,6 +158,10 @@ async function adaptWorkspaceConfig(
) )
} }
if (hasPnpmWorkspaceConfig) {
await fs.remove(pnpmWorkspacePath)
}
// Rewrite workspace: protocol references in nested package.json files. // Rewrite workspace: protocol references in nested package.json files.
// npm does not support workspace: protocol; bun and yarn do, so only // npm does not support workspace: protocol; bun and yarn do, so only
// rewrite for npm monorepo templates. // rewrite for npm monorepo templates.

View File

@@ -338,6 +338,84 @@ describe("defaultScaffold", () => {
expect(written.packageManager).toBe("bun@1.2.0") expect(written.packageManager).toBe("bun@1.2.0")
}) })
it("should remove pnpm-only workspace config for non-pnpm templates", 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("allowBuilds:\n esbuild: true\n")
}
return Promise.resolve(
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",
})
expect(vi.mocked(fs.remove)).toHaveBeenCalledWith(
path.join("/test/my-app", "pnpm-workspace.yaml")
)
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()
expect(written.workspaces).toBeUndefined()
})
it("should treat single-app workspace yaml (packages:[] + allowBuilds) as non-monorepo", 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\nallowBuilds:\n esbuild: true\n"
)
}
return Promise.resolve(
JSON.stringify({ name: "my-app", packageManager: "pnpm@9.0.0" })
)
}) as any)
const template = createTestTemplate()
await template.scaffold({
projectPath: "/test/my-app",
packageManager: "npm",
cwd: "/test",
})
// Inline empty packages array must not be parsed as a monorepo;
// the yaml is stripped and no workspaces array is added.
expect(vi.mocked(fs.remove)).toHaveBeenCalledWith(
path.join("/test/my-app", "pnpm-workspace.yaml")
)
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()
expect(written.workspaces).toBeUndefined()
})
it("should rewrite workspace: protocol refs to * for npm monorepo", async () => { it("should rewrite workspace: protocol refs to * for npm monorepo", async () => {
vi.mocked(fs.existsSync).mockImplementation((p: any) => { vi.mocked(fs.existsSync).mockImplementation((p: any) => {
const s = p.toString() const s = p.toString()

View File

@@ -1,5 +1,6 @@
packages: packages: []
- "."
ignoredBuiltDependencies: allowBuilds:
- esbuild esbuild: true
sharp: true
msw: false

View File

@@ -2,6 +2,7 @@ packages:
- "apps/*" - "apps/*"
- "packages/*" - "packages/*"
ignoredBuiltDependencies: allowBuilds:
- esbuild esbuild: true
sharp: true
msw: false

View File

@@ -1,6 +1,6 @@
packages: packages: []
- "."
ignoredBuiltDependencies: allowBuilds:
- sharp sharp: true
- unrs-resolver unrs-resolver: true
msw: false

View File

@@ -2,7 +2,7 @@ packages:
- "apps/*" - "apps/*"
- "packages/*" - "packages/*"
ignoredBuiltDependencies: allowBuilds:
- sharp sharp: true
- unrs-resolver unrs-resolver: true
msw: false

View File

@@ -1,5 +1,5 @@
packages: packages: []
- "."
ignoredBuiltDependencies: allowBuilds:
- esbuild esbuild: true
msw: false

View File

@@ -2,6 +2,6 @@ packages:
- "apps/*" - "apps/*"
- "packages/*" - "packages/*"
ignoredBuiltDependencies: allowBuilds:
- esbuild esbuild: true
msw: false

View File

@@ -1,6 +1,7 @@
packages: packages: []
- "."
onlyBuiltDependencies: allowBuilds:
- esbuild esbuild: true
- lightningcss lightningcss: true
unrs-resolver: true
msw: false

View File

@@ -1,3 +1,9 @@
packages: packages:
- "apps/*" - "apps/*"
- "packages/*" - "packages/*"
allowBuilds:
esbuild: true
lightningcss: true
unrs-resolver: true
msw: false

View File

@@ -1,5 +1,5 @@
packages: packages: []
- "."
ignoredBuiltDependencies: allowBuilds:
- esbuild esbuild: true
msw: false

View File

@@ -2,6 +2,6 @@ packages:
- "apps/*" - "apps/*"
- "packages/*" - "packages/*"
ignoredBuiltDependencies: allowBuilds:
- esbuild esbuild: true
msw: false