Fix: Preserve 'use client' directive in universal registry items (#8798)

* fix: preserve 'use client' directive in universal registry items

Universal items (registry:file and registry:item) are framework-agnostic
components that can be installed without shadcn project initialization.
However, the RSC transformer was incorrectly removing 'use client'
directives from these files when config.rsc was false/undefined, breaking
client-side functionality.

This fix ensures transformers are skipped for universal items, preserving
their original content including 'use client' directives, while regular
shadcn components continue to have transformers applied as expected.

Changes:
- Skip all transformers for registry:file and registry:item types
- Add tests to verify 'use client' preservation in universal items
- Ensure regular components still have transformers applied

Fixes issue where universal items would lose 'use client' directives when
copied without a full shadcn project setup.

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
This commit is contained in:
Паламар Роман
2026-01-17 11:12:01 +02:00
committed by GitHub
parent 1e9e337923
commit 2acaf954d7
3 changed files with 139 additions and 24 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
Fix: skip all transforms for universal registry items

View File

@@ -124,30 +124,35 @@ export async function updateFiles(
// Run our transformers.
// Skip transformers for .env files to preserve exact content
const content = isEnvFile(filePath)
? file.content
: await transform(
{
filename: file.path,
raw: file.content,
config,
baseColor,
transformJsx: !config.tsx,
isRemote: options.isRemote,
},
[
transformImport,
transformRsc,
transformCssVars,
transformTwPrefixes,
transformIcons,
transformMenu,
transformAsChild,
...(_isNext16Middleware(filePath, projectInfo, config)
? [transformNext]
: []),
]
)
// Skip transformers for universal item files (registry:file and registry:item)
// to preserve their original content as they're meant to be framework-agnostic
const isUniversalItemFile =
file.type === "registry:file" || file.type === "registry:item"
const content =
isEnvFile(filePath) || isUniversalItemFile
? file.content
: await transform(
{
filename: file.path,
raw: file.content,
config,
baseColor,
transformJsx: !config.tsx,
isRemote: options.isRemote,
},
[
transformImport,
transformRsc,
transformCssVars,
transformTwPrefixes,
transformIcons,
transformMenu,
transformAsChild,
...(_isNext16Middleware(filePath, projectInfo, config)
? [transformNext]
: []),
]
)
// Skip the file if it already exists and the content is the same.
// Exception: Don't skip .env files as we merge content instead of replacing

View File

@@ -1526,6 +1526,111 @@ DATABASE_URL=postgres://localhost:5432/mydb`,
}
`)
})
test("should preserve 'use client' directive for universal item files (registry:file)", async () => {
const config = await getConfig(
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
)
const result = await updateFiles(
[
{
path: "custom-component.tsx",
type: "registry:file",
target: "~/custom-component.tsx",
content: `"use client"
export function CustomComponent() {
return <div>Custom Component</div>
}`,
},
],
config,
{
overwrite: true,
silent: true,
}
)
// Verify that the file was created
expect(result.filesCreated).toContain("custom-component.tsx")
// Read the written file and check if 'use client' is preserved
const writtenContent = (fs.writeFile as any).mock.calls.find((call: any) =>
call[0].endsWith("custom-component.tsx")
)?.[1]
expect(writtenContent).toContain('"use client"')
})
test("should preserve 'use client' directive for universal item files (registry:item)", async () => {
const config = await getConfig(
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
)
const result = await updateFiles(
[
{
path: "universal-widget.tsx",
type: "registry:item",
target: "~/universal-widget.tsx",
content: `'use client'
export function UniversalWidget() {
return <div>Universal Widget</div>
}`,
},
],
config,
{
overwrite: true,
silent: true,
}
)
// Verify that the file was created
expect(result.filesCreated).toContain("universal-widget.tsx")
// Read the written file and check if 'use client' is preserved
const writtenContent = (fs.writeFile as any).mock.calls.find((call: any) =>
call[0].endsWith("universal-widget.tsx")
)?.[1]
expect(writtenContent).toContain("'use client'")
})
test("should remove 'use client' directive for non-universal item files when rsc is false", async () => {
const config = await getConfig(
path.resolve(__dirname, "../../fixtures/vite-with-tailwind")
)
const result = await updateFiles(
[
{
path: "registry/default/ui/regular-component.tsx",
type: "registry:ui",
content: `"use client"
export function RegularComponent() {
return <div>Regular Component</div>
}`,
},
],
config,
{
overwrite: true,
silent: true,
}
)
// Verify that the file was created (filesCreated contains relative paths)
expect(result.filesCreated.length).toBeGreaterThan(0)
// Read the written file and check if 'use client' was removed
const writtenContent = (fs.writeFile as any).mock.calls.find((call: any) =>
call[0].endsWith("regular-component.tsx")
)?.[1]
// The 'use client' should be removed by the RSC transformer
expect(writtenContent).not.toContain('"use client"')
})
})
describe("resolveModuleByProbablePath", () => {