Merge 36917ce30c into fa81d697fe
This commit is contained in:
commit
3122bc2b44
2 changed files with 97 additions and 0 deletions
|
|
@ -119,6 +119,23 @@ 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'],
|
||||
[`<Button variant={isActive(foo, bar) ? "outline" : "ghost"} />`, 'outline'],
|
||||
[`<Button variant={isActive(getState(foo, bar), x) ? "outline" : "ghost"} />`, 'outline'],
|
||||
[`<div :variant="active ? 'ghost' : 'outline'"></div>`, 'outline'],
|
||||
[`Button({ variant: (value ?? "outline") })`, 'outline'],
|
||||
[`Button({ variant: theme === "dark" ? "outline" : "ghost" })`, 'outline'],
|
||||
[`<div :variant="isActive({ x: 1 }) ? 'ghost' : 'outline'"></div>`, 'outline'],
|
||||
[`<Button variant={{ tone: "dark" }.tone === "dark" ? "outline" : "ghost"} />`, 'outline'],
|
||||
[`<Button variant={cond ? ("outline") : "ghost"} />`, 'outline'],
|
||||
[`<Button variant={cond ? "ghost" : ("outline")} />`, 'outline'],
|
||||
[`<Button variant={{ tone: "}" }.tone === "}" ? "outline" : "ghost"} />`, 'outline'],
|
||||
[`<Button variant={{ a: { b: { c: 1 } } }.a ? "outline" : "ghost"} />`, 'outline'],
|
||||
[`Button({ variant: { a: { b: 1 } }.a ? "outline" : "ghost" })`, 'outline'],
|
||||
[`<Button variant={obj["state"] ? "outline" : "ghost"} />`, 'outline'],
|
||||
])('does not replace classes in invalid positions #%#', async (example, candidate) => {
|
||||
expect(
|
||||
await migrateCandidate(designSystem, {}, candidate, {
|
||||
|
|
@ -142,6 +159,23 @@ 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',
|
||||
],
|
||||
[`<div variant="ghost" :class="active ? 'shadow' : 'none'"></div>`, 'shadow', 'shadow-sm'],
|
||||
[`<Button variant={{ a: 1 }} className={b ? "shadow" : "none"} />`, 'shadow', 'shadow-sm'],
|
||||
[`<Button variant={cn("ghost", active ? "shadow" : "none")} />`, 'shadow', 'shadow-sm'],
|
||||
[
|
||||
`<div title="variant: outline" className={b ? "shadow" : "none"}></div>`,
|
||||
'shadow',
|
||||
'shadow-sm',
|
||||
],
|
||||
])('replaces classes in valid positions #%#', async (example, candidate, expected) => {
|
||||
expect(
|
||||
await migrateCandidate(designSystem, {}, candidate, {
|
||||
|
|
|
|||
|
|
@ -189,6 +189,14 @@ export function isSafeMigration(
|
|||
}
|
||||
}
|
||||
|
||||
// Heuristic: Disallow anything inside a shadcn/ui `variant` prop, because a
|
||||
// conditional can sit between the prop and the string
|
||||
//
|
||||
// E.g.: `<Button variant={active ? "outline" : "ghost"} />`
|
||||
if (isInsideVariantValue(currentLineBeforeCandidate)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Heuristic: Disallow Next.js Image `placeholder` prop
|
||||
if (NEXT_PLACEHOLDER_PROP.test(currentLineBeforeCandidate)) {
|
||||
return false
|
||||
|
|
@ -202,6 +210,61 @@ export function isSafeMigration(
|
|||
return true
|
||||
}
|
||||
|
||||
const VARIANT_PROP = /variant\s*([:=])\s*/g
|
||||
const CALLEE = /[\w$.)\]]/
|
||||
|
||||
// The candidate is the last thing on the line, so the `variant` value it sits in
|
||||
// is the one that has not ended yet. Each form ends somewhere different:
|
||||
// `variant={…}` at its matching brace, `variant="…"` at its own quote, and
|
||||
// `{ variant: … }` at a comma or at the brace around it.
|
||||
function isInsideVariantValue(line: string): boolean {
|
||||
for (let match of line.matchAll(VARIANT_PROP)) {
|
||||
// The prop itself is never inside a string, so a `variant:` that is only
|
||||
// mentioned in some other attribute value does not count
|
||||
if (isMiddleOfString(line.slice(0, match.index))) continue
|
||||
|
||||
let start = match.index + match[0].length
|
||||
let char = line[start]
|
||||
|
||||
if (match[1] === ':') {
|
||||
if (isOpenValue(line, start)) return true
|
||||
} else if (char === '{') {
|
||||
if (isOpenValue(line, start + 1)) return true
|
||||
} else if (char === '"' || char === "'" || char === '`') {
|
||||
if (!line.includes(char, start + 1)) return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isOpenValue(line: string, start: number): boolean {
|
||||
// Whether each open group is the argument list of a call, because an argument
|
||||
// is passed to that call and is not the value itself
|
||||
let groups: boolean[] = []
|
||||
let quote: string | null = null
|
||||
|
||||
for (let i = start; i < line.length; i++) {
|
||||
let char = line[i]
|
||||
|
||||
if (quote !== null) {
|
||||
if (char === '\\') i++
|
||||
else if (char === quote) quote = null
|
||||
} else if (char === '"' || char === "'" || char === '`') {
|
||||
quote = char
|
||||
} else if (char === '(' || char === '[' || char === '{') {
|
||||
groups.push(char === '(' && CALLEE.test(line[i - 1] ?? ''))
|
||||
} else if (char === ')' || char === ']' || char === '}') {
|
||||
if (groups.length === 0) return false
|
||||
groups.pop()
|
||||
} else if (char === ',' && groups.length === 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return !groups.includes(true)
|
||||
}
|
||||
|
||||
// Assumptions:
|
||||
// - All `<style` tags appear before the next `</style>` tag
|
||||
// - All `<style` tags are closed with `</style>`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue