review found two more, both an object literal inside the condition. a vue
binding calling isActive({ x: 1 }) and a jsx expression starting with an
inline object
an object group is now allowed as a unit the same way a parenthesised group
already was, so the braces inside it do not read as the end of the value
177 lines
7.5 KiB
TypeScript
177 lines
7.5 KiB
TypeScript
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
|
|
import { describe, expect, test, vi } from 'vitest'
|
|
import * as versions from '../../utils/version'
|
|
import { migrateCandidate } from './migrate'
|
|
vi.spyOn(versions, 'isMajor').mockReturnValue(true)
|
|
|
|
const css = String.raw
|
|
|
|
describe('is-safe-migration', async () => {
|
|
let designSystem = await __unstable__loadDesignSystem(
|
|
css`
|
|
@import 'tailwindcss';
|
|
|
|
/* TODO(perf): Only here to speed up the tests */
|
|
@theme {
|
|
--*: initial;
|
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
}
|
|
`,
|
|
{ base: __dirname },
|
|
)
|
|
|
|
test.each([
|
|
[`let notBorder = !border \n`, '!border'],
|
|
[`{ "foo": !border.something + ""}\n`, '!border'],
|
|
[`<div v-if="something && !border"></div>\n`, '!border'],
|
|
[`<div v-else-if="something && !border"></div>\n`, '!border'],
|
|
[`<div v-show="something && !border"></div>\n`, '!border'],
|
|
[`<div v-if="!border || !border"></div>\n`, '!border'],
|
|
[`<div v-else-if="!border || !border"></div>\n`, '!border'],
|
|
[`<div v-show="!border || !border"></div>\n`, '!border'],
|
|
[`<div v-if="!border"></div>\n`, '!border'],
|
|
[`<div v-else-if="!border"></div>\n`, '!border'],
|
|
[`<div v-show="!border"></div>\n`, '!border'],
|
|
[`<div x-if="!border"></div>\n`, '!border'],
|
|
|
|
[`let notShadow = shadow \n`, 'shadow'],
|
|
[`{ "foo": shadow.something + ""}\n`, 'shadow'],
|
|
[`<div v-if="something && shadow"></div>\n`, 'shadow'],
|
|
[`<div v-else-if="something && shadow"></div>\n`, 'shadow'],
|
|
[`<div v-show="something && shadow"></div>\n`, 'shadow'],
|
|
[`<div v-if="shadow || shadow"></div>\n`, 'shadow'],
|
|
[`<div v-else-if="shadow || shadow"></div>\n`, 'shadow'],
|
|
[`<div v-show="shadow || shadow"></div>\n`, 'shadow'],
|
|
[`<div v-if="shadow"></div>\n`, 'shadow'],
|
|
[`<div v-else-if="shadow"></div>\n`, 'shadow'],
|
|
[`<div v-show="shadow"></div>\n`, 'shadow'],
|
|
[`<div x-if="shadow"></div>\n`, 'shadow'],
|
|
[`<div style={{filter: 'drop-shadow(30px 10px 4px #4444dd)'}}/>\n`, 'shadow'],
|
|
[`<div style="flex-grow: 1"></div>\n`, 'flex-grow'],
|
|
[`<div style='flex-shrink: 0'></div>\n`, 'flex-shrink'],
|
|
[`<div style=" flex-shrink: 0"></div>\n`, 'flex-shrink'],
|
|
[`<div style="\nflex-shrink: 0\n"></div>\n`, 'flex-shrink'],
|
|
|
|
// Next.js Image placeholder cases
|
|
[`<Image placeholder="blur" src="/image.jpg" />`, 'blur'],
|
|
[`<Image placeholder={'blur'} src="/image.jpg" />`, 'blur'],
|
|
[`<Image placeholder={blur} src="/image.jpg" />`, 'blur'],
|
|
|
|
// https://github.com/tailwindlabs/tailwindcss/issues/17974
|
|
['<div v-if="!duration">', '!duration'],
|
|
['<div :active="!duration">', '!duration'],
|
|
['<div :active="!visible">', '!visible'],
|
|
|
|
// Alpine/Livewire wire:…
|
|
['<x-input.number required="foo" wire:model.blur="coins" />', 'blur'],
|
|
|
|
// Vue 3 events
|
|
[`emit('blur', props.modelValue)\n`, 'blur'],
|
|
[`$emit('blur', props.modelValue)\n`, 'blur'],
|
|
|
|
// JavaScript / TypeScript
|
|
[`document.addEventListener('blur',handleBlur)`, 'blur'],
|
|
[`document.addEventListener('blur', handleBlur)`, 'blur'],
|
|
|
|
[`function foo({ outline = true })`, 'outline'],
|
|
[`function foo({ before = false, outline = true })`, 'outline'],
|
|
[`function foo({before=false,outline=true })`, 'outline'],
|
|
[`function foo({outline=true })`, 'outline'],
|
|
// https://github.com/tailwindlabs/tailwindcss/issues/18675
|
|
[
|
|
// With default value
|
|
`function foo({ size = "1.25rem", digit, outline = true, textClass = "", className = "" })`,
|
|
'outline',
|
|
],
|
|
[
|
|
// Without default value
|
|
`function foo({ size = "1.25rem", digit, outline, textClass = "", className = "" })`,
|
|
'outline',
|
|
],
|
|
[
|
|
// As the last argument
|
|
`function foo({ size = "1.25rem", digit, outline })`,
|
|
'outline',
|
|
],
|
|
[
|
|
// As the last argument, but there is technically another `"` on the same line
|
|
`function foo({ size = "1.25rem", digit, outline }): { return "foo" }`,
|
|
'outline',
|
|
],
|
|
[
|
|
// Tricky quote balancing
|
|
`function foo({ before = "'", outline, after = "'" }): { return "foo" }`,
|
|
'outline',
|
|
],
|
|
|
|
[`function foo(blur, foo)`, 'blur'],
|
|
[`function foo(blur,foo)`, 'blur'],
|
|
|
|
// shadcn/ui variants
|
|
[`<Button variant="outline" />`, 'outline'],
|
|
[`<Button variant='outline' />`, 'outline'],
|
|
[`<Button variant={"outline"} />`, 'outline'],
|
|
[`<Button variant={'outline'} />`, 'outline'],
|
|
[`function Button({ variant = "outline" }) {}`, 'outline'],
|
|
[`function Button({ variant = 'outline' }) {}`, 'outline'],
|
|
[`function Button({ variant="outline" }) {}`, 'outline'],
|
|
[`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'],
|
|
])('does not replace classes in invalid positions #%#', async (example, candidate) => {
|
|
expect(
|
|
await migrateCandidate(designSystem, {}, candidate, {
|
|
contents: example,
|
|
start: example.indexOf(candidate),
|
|
end: example.indexOf(candidate) + candidate.length,
|
|
}),
|
|
).toEqual(candidate)
|
|
})
|
|
|
|
test.each([
|
|
// Vue classes
|
|
[`<div class="shadow"></div>`, 'shadow', 'shadow-sm'],
|
|
[`<div :class="{ shadow: true }"></div>`, 'shadow', 'shadow-sm'],
|
|
[`<div enter-class="shadow"></div>`, 'shadow', 'shadow-sm'],
|
|
[`<div :enter-class="{ shadow: true }"></div>`, 'shadow', 'shadow-sm'],
|
|
|
|
// React classes
|
|
[`<div className="shadow"></div>`, 'shadow', 'shadow-sm'],
|
|
[`<div enterClassName="shadow"></div>`, 'shadow', 'shadow-sm'],
|
|
|
|
// 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'],
|
|
])('replaces classes in valid positions #%#', async (example, candidate, expected) => {
|
|
expect(
|
|
await migrateCandidate(designSystem, {}, candidate, {
|
|
contents: example,
|
|
start: example.indexOf(candidate),
|
|
end: example.indexOf(candidate) + candidate.length,
|
|
}),
|
|
).toEqual(expected)
|
|
})
|
|
})
|