Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions[bot]
2baa86081d chore(release): version packages (#10791)
Co-authored-by: shadcn <m@shadcn.com>
2026-05-29 12:10:45 +04:00
shadcn
980f288149 ci(templates): test pnpm 11 (#10790) 2026-05-29 11:15:40 +04:00
Raashish Aggarwal
07900769d9 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>
2026-05-29 08:24:31 +04:00
Artyom Konoplyov
360e8a19c3 fix(transform-rtl): preserve quotes in transformed className literals (#10495)
* fix(transform-rtl): preserve string literal escapes

* chore(changeset): add rtl quote preservation note

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-05-27 22:29:44 +04:00
19 changed files with 250 additions and 73 deletions

View File

@@ -19,7 +19,7 @@ on:
jobs:
validate:
runs-on: ubuntu-latest
name: ${{ matrix.package-manager }} ${{ matrix.template }}
name: ${{ matrix.package-manager == 'pnpm' && format('pnpm {0}', matrix.pnpm-version) || matrix.package-manager }} ${{ matrix.template }}
permissions:
contents: read
timeout-minutes: 45
@@ -28,11 +28,20 @@ jobs:
matrix:
template: [next, vite, astro, start, react-router]
package-manager: [pnpm, bun, npm, yarn]
pnpm-version: [10.33.4, 11]
exclude:
- package-manager: bun
pnpm-version: 11
- package-manager: npm
pnpm-version: 11
- package-manager: yarn
pnpm-version: 11
env:
NEXT_PUBLIC_APP_URL: http://localhost:4000
NEXT_PUBLIC_V0_URL: https://v0.dev
REGISTRY_URL: http://localhost:4000/r
TEMPLATE_PNPM_VERSION: 10.33.4
ROOT_PNPM_VERSION: 10.33.4
TEMPLATE_PNPM_VERSION: ${{ matrix.pnpm-version }}
steps:
- uses: actions/checkout@v4
with:
@@ -48,7 +57,7 @@ jobs:
name: Install pnpm
id: pnpm-install
with:
version: 10.33.4
version: ${{ env.ROOT_PNPM_VERSION }}
run_install: false
- name: Install Bun
@@ -131,6 +140,7 @@ jobs:
local package_manager="$1"
local project_path="$2"
local check_workspace_protocol="$3"
local is_monorepo="$4"
cd "$project_path"
test ! -f pnpm-workspace.yaml
@@ -138,6 +148,7 @@ jobs:
EXPECTED_PACKAGE_MANAGER="$package_manager" \
CHECK_WORKSPACE_PROTOCOL="$check_workspace_protocol" \
IS_MONOREPO="$is_monorepo" \
node <<'NODE'
const fs = require("node:fs")
const path = require("node:path")
@@ -145,27 +156,41 @@ jobs:
const expectedPackageManager = process.env.EXPECTED_PACKAGE_MANAGER
const checkWorkspaceProtocol =
process.env.CHECK_WORKSPACE_PROTOCOL === "true"
const isMonorepo = process.env.IS_MONOREPO === "true"
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
const workspaces = pkg.workspaces ?? []
if (!Array.isArray(workspaces)) {
throw new Error("Expected package.json workspaces to be an array.")
}
if (isMonorepo) {
const workspaces = pkg.workspaces ?? []
if (workspaces.length === 0) {
throw new Error("Expected package.json workspaces to have entries.")
}
for (const workspace of ["sharp", "unrs-resolver", "esbuild"]) {
if (workspaces.includes(workspace)) {
throw new Error(`Unexpected workspace entry: ${workspace}`)
if (!Array.isArray(workspaces)) {
throw new Error("Expected package.json workspaces to be an array.")
}
}
if (!pkg.packageManager?.startsWith(`${expectedPackageManager}@`)) {
throw new Error(
`Expected packageManager to use ${expectedPackageManager}, got ${pkg.packageManager}`
)
if (workspaces.length === 0) {
throw new Error("Expected package.json workspaces to have entries.")
}
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) {
@@ -213,8 +238,10 @@ jobs:
if [ "$mode" = "monorepo" ]; then
args+=(--monorepo)
is_monorepo="true"
else
args+=(--no-monorepo)
is_monorepo="false"
fi
case "$TEMPLATE_PACKAGE_MANAGER" in
@@ -238,7 +265,11 @@ jobs:
bunx --bun --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}"
)
validate_non_pnpm_project "bun" "$project_path" "false"
validate_non_pnpm_project \
"bun" \
"$project_path" \
"false" \
"$is_monorepo"
;;
npm)
(
@@ -249,7 +280,11 @@ jobs:
npx --yes --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}"
)
validate_non_pnpm_project "npm" "$project_path" "true"
validate_non_pnpm_project \
"npm" \
"$project_path" \
"true" \
"$is_monorepo"
;;
yarn)
(
@@ -261,7 +296,11 @@ jobs:
yarn dlx --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}"
)
validate_non_pnpm_project "yarn" "$project_path" "false"
validate_non_pnpm_project \
"yarn" \
"$project_path" \
"false" \
"$is_monorepo"
;;
esac

View File

@@ -77,7 +77,7 @@
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"server-only": "^0.0.1",
"shadcn": "4.8.2",
"shadcn": "4.8.3",
"shiki": "^1.10.1",
"sonner": "^2.0.0",
"swr": "^2.3.6",

View File

@@ -1,5 +1,13 @@
# @shadcn/ui
## 4.8.3
### Patch Changes
- [#10659](https://github.com/shadcn-ui/ui/pull/10659) [`07900769d91b09def00e68179bcb7a821f59b954`](https://github.com/shadcn-ui/ui/commit/07900769d91b09def00e68179bcb7a821f59b954) Thanks [@raashish1601](https://github.com/raashish1601)! - update template handling
- [#10495](https://github.com/shadcn-ui/ui/pull/10495) [`360e8a19c3ee13ac78b656027462007c8bdaa6d5`](https://github.com/shadcn-ui/ui/commit/360e8a19c3ee13ac78b656027462007c8bdaa6d5) Thanks [@artemxknpv](https://github.com/artemxknpv)! - Preserve quotes in className literals when applying RTL transforms.
## 4.8.2
### Patch Changes

View File

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

View File

@@ -127,7 +127,11 @@ async function adaptWorkspaceConfig(
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
// target package manager, and add "workspaces" for npm/bun/yarn.
@@ -145,12 +149,7 @@ async function adaptWorkspaceConfig(
}
if (isMonorepo) {
// Read workspace patterns from pnpm-workspace.yaml.
const workspaceContent = await fs.readFile(pnpmWorkspacePath, "utf8")
const patterns = parsePnpmWorkspacePackages(workspaceContent)
packageJson.workspaces = patterns
await fs.remove(pnpmWorkspacePath)
packageJson.workspaces = workspacePatterns
}
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.
// npm does not support workspace: protocol; bun and yarn do, so only
// rewrite for npm monorepo templates.

View File

@@ -338,6 +338,84 @@ describe("defaultScaffold", () => {
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 () => {
vi.mocked(fs.existsSync).mockImplementation((p: any) => {
const s = p.toString()

View File

@@ -1,5 +1,6 @@
import { Transformer } from "@/src/utils/transformers"
import { Project, ScriptKind, SourceFile, SyntaxKind } from "ts-morph"
import type { StringLiteral } from "ts-morph"
import { splitClassName } from "./transform-css-vars"
@@ -130,12 +131,9 @@ function stripQuotes(str: string) {
}
// Transforms a string literal node by applying RTL mappings.
function transformStringLiteralNode(node: {
getText(): string
replaceWithText(text: string): void
}) {
const text = stripQuotes(node.getText() ?? "")
node.replaceWithText(`"${applyRtlMapping(text)}"`)
function transformStringLiteralNode(node: StringLiteral) {
const text = node.getLiteralText()
node.setLiteralValue(applyRtlMapping(text))
}
export function applyRtlMapping(input: string) {

View File

@@ -1,4 +1,5 @@
import { describe, expect, test } from "vitest"
import ts from "typescript"
import { transform } from "../../src/utils/transformers"
import { applyRtlMapping } from "../../src/utils/transformers/transform-rtl"
@@ -349,6 +350,47 @@ export function Foo() {
expect(result).toContain("text-start")
})
test("escapes transformed string literals that contain double quotes", async () => {
const result = await transform({
filename: "test.tsx",
raw: String.raw`import * as React from "react"
export function Foo() {
return <div className='ml-1 after:content-["\""]' />
}
`,
config: {
rtl: true,
tailwind: {
baseColor: "neutral",
},
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
},
})
const sourceFile = ts.createSourceFile(
"test.tsx",
result,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX
)
const stringLiterals: string[] = []
function visit(node: ts.Node) {
if (ts.isStringLiteral(node)) {
stringLiterals.push(node.text)
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
expect(sourceFile.parseDiagnostics).toHaveLength(0)
expect(stringLiterals).toContain('ms-1 after:content-["\\""]')
})
test("does not transform when rtl is false", async () => {
const result = await transform({
filename: "test.tsx",

2
pnpm-lock.yaml generated
View File

@@ -290,7 +290,7 @@ importers:
specifier: ^0.0.1
version: 0.0.1
shadcn:
specifier: 4.8.2
specifier: 4.8.3
version: link:../../packages/shadcn
shiki:
specifier: ^1.10.1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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