do not migrate a variant literal behind a conditional

the shadcn variant guard only looked at the text right before the literal so
any ternary or nullish branch between variant and the string defeated it and
the upgrade rewrote a react prop value as if it were a class

braces and commas stay excluded from the new part so the match cannot run past
its own value into a neighbouring prop like className
This commit is contained in:
Huzaifa Iftikhar 2026-08-28 04:05:14 +05:00 • committed by HuzaifaChaudary
parent f723e834ad
commit 64201c1f3a
2 changed files with 17 additions and 2 deletions

View file

@ -119,6 +119,10 @@ describe('is-safe-migration', async () => {
[`function Button({ variant='outline' }) {}`, 'outline'],
[`Button({ variant: "outline" })`, 'outline'],
[`Button({ variant: 'outline' })`, 'outline'],
[`Button({ variant: isActive ? "outline" : "ghost" })`, 'outline'],
[`<Button variant={first ? "default" : "outline"} />`, 'outline'],
[`<Button variant={variant ?? "outline"} />`, 'outline'],
[`<Button variant={required ? 'secondary' : 'outline'} />`, 'outline'],
])('does not replace classes in invalid positions #%#', async (example, candidate) => {
expect(
await migrateCandidate(designSystem, {}, candidate, {
@ -142,6 +146,15 @@ describe('is-safe-migration', async () => {
// Preact-style
[`<div enterClass="shadow"></div>`, 'shadow', 'shadow-sm'],
// A conditional class is still a class, even next to a `variant` prop
[`<div className={active ? "shadow" : "none"}></div>`, 'shadow', 'shadow-sm'],
[`<Button variant="ghost" className={active ? "shadow" : "none"} />`, 'shadow', 'shadow-sm'],
[
`Button({ variant: a ? "ghost" : "x", className: b ? "shadow" : "y" })`,
'shadow',
'shadow-sm',
],
])('replaces classes in valid positions #%#', async (example, candidate, expected) => {
expect(
await migrateCandidate(designSystem, {}, candidate, {

View file

@ -17,8 +17,10 @@ const CONDITIONAL_TEMPLATE_SYNTAX = [
// Alpine
/wire:[^\s]*?$/,
// shadcn/ui variants
/variant\s*[:=]\s*\{?['"`]$/,
// shadcn/ui variants, including a ternary or nullish branch between the prop
// and the literal. Braces and commas are excluded so the match cannot reach
// out of its own value into a neighbouring prop such as `className`.
/variant\s*[:=]\s*\{?(?:[^{},]*?(?:\?\?|\?|:)\s*)?['"`]$/,
]
const NEXT_PLACEHOLDER_PROP = /placeholder=\{?['"`]$/
const VUE_3_EMIT = /\b\$?emit\(['"`]$/