mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Do not migrate legacy classes with custom values (#14976)
This PR fixes an issue where we migrated classes such as `rounded` to `rounded-sm` (see: https://github.com/tailwindlabs/tailwindcss/pull/14875) However, if you override the values in your `tailwind.config.js` file, then the migration might not be correct. This PR makes sure to only migrate the classes if you haven't overridden the values in your `tailwind.config.js` file. --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
parent
dd85aadc2c
commit
49484f0491
@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
|
||||
- Ensure that CSS inside Svelte `<style>` blocks always run the expected Svelte processors when using the Vite extension ([#14981](https://github.com/tailwindlabs/tailwindcss/pull/14981))
|
||||
- _Upgrade (experimental)_: Ensure it's safe to migrate `blur`, `rounded`, or `shadow` ([#14979](https://github.com/tailwindlabs/tailwindcss/pull/14979))
|
||||
- _Upgrade (experimental)_: Do not rename classes using custom defined theme values ([#14976](https://github.com/tailwindlabs/tailwindcss/pull/14976))
|
||||
|
||||
## [4.0.0-alpha.33] - 2024-11-11
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { expect } from 'vitest'
|
||||
import { candidate, css, html, js, json, test } from '../utils'
|
||||
import { candidate, css, html, js, json, test, ts } from '../utils'
|
||||
|
||||
test(
|
||||
'error when no CSS file with @tailwind is used',
|
||||
@ -1642,3 +1642,200 @@ test(
|
||||
expect(pkg.devDependencies['prettier-plugin-tailwindcss']).not.toEqual('0.5.0')
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'only migrate legacy classes when it is safe to do so',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3.4.14",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier-plugin-tailwindcss": "0.5.0"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
module.exports = {
|
||||
content: ['./*.html'],
|
||||
theme: {
|
||||
// Overrides the default boxShadow entirely so none of the
|
||||
// migrations are safe.
|
||||
boxShadow: {
|
||||
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
||||
},
|
||||
|
||||
extend: {
|
||||
// Changes the "before" class definition. 'blur' -> 'blur-sm' is
|
||||
// not safe because 'blur' has a custom value.
|
||||
//
|
||||
// But 'blur-sm' -> 'blur-xs' is safe because 'blur-xs' uses the
|
||||
// default value.
|
||||
blur: {
|
||||
DEFAULT: 'var(--custom-default-blur)',
|
||||
},
|
||||
|
||||
// Changes the "after" class definition. 'rounded' -> 'rounded-sm' is
|
||||
// not safe because 'rounded-sm' has a custom value.
|
||||
borderRadius: {
|
||||
sm: 'var(--custom-rounded-sm)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
'index.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
'index.html': html`
|
||||
<div>
|
||||
<div class="shadow shadow-sm shadow-xs"></div>
|
||||
<div class="blur blur-sm"></div>
|
||||
<div class="rounded rounded-sm"></div>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
// Files should not be modified
|
||||
expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- index.html ---
|
||||
<div>
|
||||
<div class="shadow shadow-sm shadow-xs"></div>
|
||||
<div class="blur blur-xs"></div>
|
||||
<div class="rounded rounded-sm"></div>
|
||||
</div>
|
||||
|
||||
--- index.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--shadow-*: initial;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
|
||||
--blur: var(--custom-default-blur);
|
||||
|
||||
--radius-sm: var(--custom-rounded-sm);
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'make suffix-less migrations safe (e.g.: `blur`, `rounded`, `shadow`)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3.4.14",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier-plugin-tailwindcss": "0.5.0"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
module.exports = {
|
||||
content: ['./*.{html,tsx}'],
|
||||
}
|
||||
`,
|
||||
'index.css': css`
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`,
|
||||
'index.html': html`
|
||||
<div class="rounded blur shadow"></div>
|
||||
`,
|
||||
'example-component.tsx': ts`
|
||||
type Star = [
|
||||
x: number,
|
||||
y: number,
|
||||
dim?: boolean,
|
||||
blur?: boolean,
|
||||
rounded?: boolean,
|
||||
shadow?: boolean,
|
||||
]
|
||||
|
||||
function Star({ point: [cx, cy, dim, blur, rounded, shadow] }: { point: Star }) {
|
||||
return <svg class="rounded shadow blur" filter={blur ? 'url(…)' : undefined} />
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
|
||||
// Files should not be modified
|
||||
expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- index.html ---
|
||||
<div class="rounded-sm blur-sm shadow-sm"></div>
|
||||
|
||||
--- index.css ---
|
||||
@import 'tailwindcss';
|
||||
|
||||
/*
|
||||
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
--- example-component.tsx ---
|
||||
type Star = [
|
||||
x: number,
|
||||
y: number,
|
||||
dim?: boolean,
|
||||
blur?: boolean,
|
||||
rounded?: boolean,
|
||||
shadow?: boolean,
|
||||
]
|
||||
|
||||
function Star({ point: [cx, cy, dim, blur, rounded, shadow] }: { point: Star }) {
|
||||
return <svg class="rounded-sm shadow-sm blur-sm" filter={blur ? 'url(…)' : undefined} />
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
@ -421,6 +421,20 @@ export function test(
|
||||
|
||||
options.onTestFinished(dispose)
|
||||
|
||||
// Make it a git repository, and commit all files
|
||||
if (only || debug) {
|
||||
try {
|
||||
await context.exec('git init', { cwd: root })
|
||||
await context.exec('git add --all', { cwd: root })
|
||||
await context.exec('git commit -m "before migration"', { cwd: root })
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
console.error(error.stdout?.toString())
|
||||
console.error(error.stderr?.toString())
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return await testCallback(context)
|
||||
},
|
||||
)
|
||||
|
||||
@ -34,17 +34,26 @@ export function migrateAtApply({
|
||||
return [...variants, utility].join(':')
|
||||
})
|
||||
|
||||
// If we have a valid designSystem and config setup, we can run all
|
||||
// candidate migrations on each utility
|
||||
params = params.map((param) => migrateCandidate(designSystem, userConfig, param))
|
||||
return async () => {
|
||||
// If we have a valid designSystem and config setup, we can run all
|
||||
// candidate migrations on each utility
|
||||
params = await Promise.all(
|
||||
params.map(async (param) => await migrateCandidate(designSystem, userConfig, param)),
|
||||
)
|
||||
|
||||
atRule.params = params.join('').trim()
|
||||
atRule.params = params.join('').trim()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply',
|
||||
OnceExit(root) {
|
||||
root.walkAtRules('apply', migrate)
|
||||
async OnceExit(root) {
|
||||
let migrations: (() => void)[] = []
|
||||
root.walkAtRules('apply', (atRule) => {
|
||||
migrations.push(migrate(atRule))
|
||||
})
|
||||
|
||||
await Promise.allSettled(migrations.map((m) => m()))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,19 +2,7 @@ import type { Config } from 'tailwindcss'
|
||||
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { printCandidate } from '../candidates'
|
||||
|
||||
const QUOTES = ['"', "'", '`']
|
||||
const LOGICAL_OPERATORS = ['&&', '||', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
|
||||
const CONDITIONAL_TEMPLATE_SYNTAX = [
|
||||
// Vue
|
||||
/v-else-if=['"]$/,
|
||||
/v-if=['"]$/,
|
||||
/v-show=['"]$/,
|
||||
|
||||
// Alpine
|
||||
/x-if=['"]$/,
|
||||
/x-show=['"]$/,
|
||||
]
|
||||
import { isSafeMigration } from '../is-safe-migration'
|
||||
|
||||
// In v3 the important modifier `!` sits in front of the utility itself, not
|
||||
// before any of the variants. In v4, we want it to be at the end of the utility
|
||||
@ -46,56 +34,8 @@ export function important(
|
||||
// with v3 in that it can read `!` in the front of the utility too, we err
|
||||
// on the side of caution and only migrate candidates that we are certain
|
||||
// are inside of a string.
|
||||
if (location) {
|
||||
let currentLineBeforeCandidate = ''
|
||||
for (let i = location.start - 1; i >= 0; i--) {
|
||||
let char = location.contents.at(i)!
|
||||
if (char === '\n') {
|
||||
break
|
||||
}
|
||||
currentLineBeforeCandidate = char + currentLineBeforeCandidate
|
||||
}
|
||||
let currentLineAfterCandidate = ''
|
||||
for (let i = location.end; i < location.contents.length; i++) {
|
||||
let char = location.contents.at(i)!
|
||||
if (char === '\n') {
|
||||
break
|
||||
}
|
||||
currentLineAfterCandidate += char
|
||||
}
|
||||
|
||||
// Heuristic 1: Require the candidate to be inside quotes
|
||||
let isQuoteBeforeCandidate = QUOTES.some((quote) =>
|
||||
currentLineBeforeCandidate.includes(quote),
|
||||
)
|
||||
let isQuoteAfterCandidate = QUOTES.some((quote) =>
|
||||
currentLineAfterCandidate.includes(quote),
|
||||
)
|
||||
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
|
||||
continue nextCandidate
|
||||
}
|
||||
|
||||
// Heuristic 2: Disallow object access immediately following the candidate
|
||||
if (currentLineAfterCandidate[0] === '.') {
|
||||
continue nextCandidate
|
||||
}
|
||||
|
||||
// Heuristic 3: Disallow logical operators preceding or following the candidate
|
||||
for (let operator of LOGICAL_OPERATORS) {
|
||||
if (
|
||||
currentLineAfterCandidate.trim().startsWith(operator) ||
|
||||
currentLineBeforeCandidate.trim().endsWith(operator)
|
||||
) {
|
||||
continue nextCandidate
|
||||
}
|
||||
}
|
||||
|
||||
// Heuristic 4: Disallow conditional template syntax
|
||||
for (let rule of CONDITIONAL_TEMPLATE_SYNTAX) {
|
||||
if (rule.test(currentLineBeforeCandidate)) {
|
||||
continue nextCandidate
|
||||
}
|
||||
}
|
||||
if (location && !isSafeMigration(location)) {
|
||||
continue nextCandidate
|
||||
}
|
||||
|
||||
// The printCandidate function will already put the exclamation mark in
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
|
||||
import { expect, test } from 'vitest'
|
||||
import { legacyClasses } from './legacy-classes'
|
||||
|
||||
test.each([
|
||||
['shadow', 'shadow-sm'],
|
||||
['shadow-sm', 'shadow-xs'],
|
||||
['shadow-xs', 'shadow-2xs'],
|
||||
|
||||
['inset-shadow', 'inset-shadow-sm'],
|
||||
['inset-shadow-sm', 'inset-shadow-xs'],
|
||||
['inset-shadow-xs', 'inset-shadow-2xs'],
|
||||
|
||||
['drop-shadow', 'drop-shadow-sm'],
|
||||
['drop-shadow-sm', 'drop-shadow-xs'],
|
||||
|
||||
['rounded', 'rounded-sm'],
|
||||
['rounded-sm', 'rounded-xs'],
|
||||
|
||||
['blur', 'blur-sm'],
|
||||
['blur-sm', 'blur-xs'],
|
||||
|
||||
['blur!', 'blur-sm!'],
|
||||
['hover:blur', 'hover:blur-sm'],
|
||||
['hover:blur!', 'hover:blur-sm!'],
|
||||
|
||||
['hover:blur-sm', 'hover:blur-xs'],
|
||||
['blur-sm!', 'blur-xs!'],
|
||||
['hover:blur-sm!', 'hover:blur-xs!'],
|
||||
])('%s => %s', async (candidate, result) => {
|
||||
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
|
||||
base: __dirname,
|
||||
})
|
||||
|
||||
expect(await legacyClasses(designSystem, {}, candidate)).toEqual(result)
|
||||
})
|
||||
@ -0,0 +1,156 @@
|
||||
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
|
||||
import path from 'node:path'
|
||||
import url from 'node:url'
|
||||
import type { Config } from 'tailwindcss'
|
||||
import type { Candidate } from '../../../../tailwindcss/src/candidate'
|
||||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
|
||||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import { printCandidate } from '../candidates'
|
||||
import { isSafeMigration } from '../is-safe-migration'
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const LEGACY_CLASS_MAP = new Map([
|
||||
['shadow', 'shadow-sm'],
|
||||
['shadow-sm', 'shadow-xs'],
|
||||
['shadow-xs', 'shadow-2xs'],
|
||||
|
||||
['inset-shadow', 'inset-shadow-sm'],
|
||||
['inset-shadow-sm', 'inset-shadow-xs'],
|
||||
['inset-shadow-xs', 'inset-shadow-2xs'],
|
||||
|
||||
['drop-shadow', 'drop-shadow-sm'],
|
||||
['drop-shadow-sm', 'drop-shadow-xs'],
|
||||
|
||||
['rounded', 'rounded-sm'],
|
||||
['rounded-sm', 'rounded-xs'],
|
||||
|
||||
['blur', 'blur-sm'],
|
||||
['blur-sm', 'blur-xs'],
|
||||
])
|
||||
|
||||
const THEME_KEYS = new Map([
|
||||
['shadow', '--shadow'],
|
||||
['shadow-sm', '--shadow-sm'],
|
||||
['shadow-xs', '--shadow-xs'],
|
||||
['shadow-2xs', '--shadow-2xs'],
|
||||
|
||||
['drop-shadow', '--drop-shadow'],
|
||||
['drop-shadow-sm', '--drop-shadow-sm'],
|
||||
['drop-shadow-xs', '--drop-shadow-xs'],
|
||||
|
||||
['rounded', '--radius'],
|
||||
['rounded-sm', '--radius-sm'],
|
||||
['rounded-xs', '--radius-xs'],
|
||||
|
||||
['blur', '--blur'],
|
||||
['blur-sm', '--blur-sm'],
|
||||
['blur-xs', '--blur-xs'],
|
||||
])
|
||||
|
||||
const DESIGN_SYSTEMS = new DefaultMap((base) => {
|
||||
return __unstable__loadDesignSystem('@import "tailwindcss";', { base })
|
||||
})
|
||||
|
||||
export async function legacyClasses(
|
||||
designSystem: DesignSystem,
|
||||
_userConfig: Config,
|
||||
rawCandidate: string,
|
||||
location?: {
|
||||
contents: string
|
||||
start: number
|
||||
end: number
|
||||
},
|
||||
): Promise<string> {
|
||||
let defaultDesignSystem = await DESIGN_SYSTEMS.get(__dirname)
|
||||
|
||||
function* migrate(rawCandidate: string) {
|
||||
for (let candidate of designSystem.parseCandidate(rawCandidate)) {
|
||||
// Create a base candidate string from the candidate.
|
||||
// E.g.: `hover:blur!` -> `blur`
|
||||
let baseCandidate = structuredClone(candidate) as Candidate
|
||||
baseCandidate.variants = []
|
||||
baseCandidate.important = false
|
||||
let baseCandidateString = printCandidate(designSystem, baseCandidate)
|
||||
|
||||
// Find the new base candidate string. `blur` -> `blur-sm`
|
||||
let newBaseCandidateString = LEGACY_CLASS_MAP.get(baseCandidateString)
|
||||
if (!newBaseCandidateString) continue
|
||||
|
||||
// Parse the new base candidate string into an actual candidate AST.
|
||||
let [newBaseCandidate] = designSystem.parseCandidate(newBaseCandidateString)
|
||||
if (!newBaseCandidate) continue
|
||||
|
||||
// Re-apply the variants and important flag from the original candidate.
|
||||
// E.g.: `hover:blur!` -> `blur` -> `blur-sm` -> `hover:blur-sm!`
|
||||
let newCandidate = structuredClone(newBaseCandidate) as Candidate
|
||||
newCandidate.variants = candidate.variants
|
||||
newCandidate.important = candidate.important
|
||||
|
||||
yield [
|
||||
candidate,
|
||||
newCandidate,
|
||||
THEME_KEYS.get(baseCandidateString),
|
||||
THEME_KEYS.get(newBaseCandidateString),
|
||||
] as const
|
||||
}
|
||||
}
|
||||
|
||||
for (let [fromCandidate, toCandidate, fromThemeKey, toThemeKey] of migrate(rawCandidate)) {
|
||||
// Every utility that has a simple representation (e.g.: `blur`, `radius`,
|
||||
// etc.`) without variants or special characters _could_ be a potential
|
||||
// problem during the migration.
|
||||
let isPotentialProblematicClass = (() => {
|
||||
if (fromCandidate.variants.length > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (fromCandidate.kind === 'arbitrary') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (fromCandidate.kind === 'static') {
|
||||
return !fromCandidate.root.includes('-')
|
||||
}
|
||||
|
||||
if (fromCandidate.kind === 'functional') {
|
||||
return fromCandidate.value === null || !fromCandidate.root.includes('-')
|
||||
}
|
||||
|
||||
return false
|
||||
})()
|
||||
|
||||
if (location && isPotentialProblematicClass && !isSafeMigration(location)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (fromThemeKey && toThemeKey) {
|
||||
// Migrating something that resolves to a value in the theme.
|
||||
let customFrom = designSystem.resolveThemeValue(fromThemeKey)
|
||||
let defaultFrom = defaultDesignSystem.resolveThemeValue(fromThemeKey)
|
||||
let customTo = designSystem.resolveThemeValue(toThemeKey)
|
||||
let defaultTo = defaultDesignSystem.resolveThemeValue(toThemeKey)
|
||||
|
||||
// The new theme value is not defined, which means we can't safely
|
||||
// migrate the utility.
|
||||
if (customTo === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
// The "from" theme value changed compared to the default theme value.
|
||||
if (customFrom !== defaultFrom) {
|
||||
continue
|
||||
}
|
||||
|
||||
// The "to" theme value changed compared to the default theme value.
|
||||
if (customTo !== defaultTo) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return printCandidate(designSystem, toCandidate)
|
||||
}
|
||||
|
||||
return rawCandidate
|
||||
}
|
||||
@ -16,23 +16,6 @@ test.each([
|
||||
['max-lg:hover:decoration-slice!', 'max-lg:hover:box-decoration-slice!'],
|
||||
['max-lg:hover:!decoration-slice', 'max-lg:hover:box-decoration-slice!'],
|
||||
|
||||
['shadow', 'shadow-sm'],
|
||||
['shadow-sm', 'shadow-xs'],
|
||||
['shadow-xs', 'shadow-2xs'],
|
||||
|
||||
['inset-shadow', 'inset-shadow-sm'],
|
||||
['inset-shadow-sm', 'inset-shadow-xs'],
|
||||
['inset-shadow-xs', 'inset-shadow-2xs'],
|
||||
|
||||
['drop-shadow', 'drop-shadow-sm'],
|
||||
['drop-shadow-sm', 'drop-shadow-xs'],
|
||||
|
||||
['rounded', 'rounded-sm'],
|
||||
['rounded-sm', 'rounded-xs'],
|
||||
|
||||
['blur', 'blur-sm'],
|
||||
['blur-sm', 'blur-xs'],
|
||||
|
||||
['focus:outline-none', 'focus:outline-hidden'],
|
||||
])('%s => %s', async (candidate, result) => {
|
||||
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
|
||||
|
||||
@ -7,30 +7,15 @@ import { printCandidate } from '../candidates'
|
||||
const LEGACY_CLASS_MAP = {
|
||||
'overflow-clip': 'text-clip',
|
||||
'overflow-ellipsis': 'text-ellipsis',
|
||||
|
||||
'flex-grow': 'grow',
|
||||
'flex-grow-0': 'grow-0',
|
||||
'flex-shrink': 'shrink',
|
||||
'flex-shrink-0': 'shrink-0',
|
||||
|
||||
'decoration-clone': 'box-decoration-clone',
|
||||
'decoration-slice': 'box-decoration-slice',
|
||||
|
||||
shadow: 'shadow-sm',
|
||||
'shadow-sm': 'shadow-xs',
|
||||
'shadow-xs': 'shadow-2xs',
|
||||
|
||||
'inset-shadow': 'inset-shadow-sm',
|
||||
'inset-shadow-sm': 'inset-shadow-xs',
|
||||
'inset-shadow-xs': 'inset-shadow-2xs',
|
||||
|
||||
'drop-shadow': 'drop-shadow-sm',
|
||||
'drop-shadow-sm': 'drop-shadow-xs',
|
||||
|
||||
rounded: 'rounded-sm',
|
||||
'rounded-sm': 'rounded-xs',
|
||||
|
||||
blur: 'blur-sm',
|
||||
'blur-sm': 'blur-xs',
|
||||
|
||||
'outline-none': 'outline-hidden',
|
||||
}
|
||||
|
||||
@ -41,7 +26,7 @@ export function simpleLegacyClasses(
|
||||
_userConfig: Config,
|
||||
rawCandidate: string,
|
||||
): string {
|
||||
// Prepare design system with the legacy classes
|
||||
// Prepare design system with the unknown legacy classes
|
||||
if (!SEEDED.has(designSystem)) {
|
||||
for (let old in LEGACY_CLASS_MAP) {
|
||||
designSystem.utilities.static(old, () => [])
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
const QUOTES = ['"', "'", '`']
|
||||
const LOGICAL_OPERATORS = ['&&', '||', '?', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
|
||||
const CONDITIONAL_TEMPLATE_SYNTAX = [
|
||||
// Vue
|
||||
/v-else-if=['"]$/,
|
||||
/v-if=['"]$/,
|
||||
/v-show=['"]$/,
|
||||
|
||||
// Alpine
|
||||
/x-if=['"]$/,
|
||||
/x-show=['"]$/,
|
||||
]
|
||||
|
||||
export function isSafeMigration(location: { contents: string; start: number; end: number }) {
|
||||
let currentLineBeforeCandidate = ''
|
||||
for (let i = location.start - 1; i >= 0; i--) {
|
||||
let char = location.contents.at(i)!
|
||||
if (char === '\n') {
|
||||
break
|
||||
}
|
||||
currentLineBeforeCandidate = char + currentLineBeforeCandidate
|
||||
}
|
||||
let currentLineAfterCandidate = ''
|
||||
for (let i = location.end; i < location.contents.length; i++) {
|
||||
let char = location.contents.at(i)!
|
||||
if (char === '\n') {
|
||||
break
|
||||
}
|
||||
currentLineAfterCandidate += char
|
||||
}
|
||||
|
||||
// Heuristic 1: Require the candidate to be inside quotes
|
||||
let isQuoteBeforeCandidate = QUOTES.some((quote) => currentLineBeforeCandidate.includes(quote))
|
||||
let isQuoteAfterCandidate = QUOTES.some((quote) => currentLineAfterCandidate.includes(quote))
|
||||
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Heuristic 2: Disallow object access immediately following the candidate
|
||||
if (currentLineAfterCandidate[0] === '.') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Heuristic 3: Disallow logical operators preceding or following the candidate
|
||||
for (let operator of LOGICAL_OPERATORS) {
|
||||
if (
|
||||
currentLineAfterCandidate.trim().startsWith(operator) ||
|
||||
currentLineBeforeCandidate.trim().endsWith(operator)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Heuristic 4: Disallow conditional template syntax
|
||||
for (let rule of CONDITIONAL_TEMPLATE_SYNTAX) {
|
||||
if (rule.test(currentLineBeforeCandidate)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { automaticVarInjection } from './codemods/automatic-var-injection'
|
||||
import { bgGradient } from './codemods/bg-gradient'
|
||||
import { important } from './codemods/important'
|
||||
import { legacyArbitraryValues } from './codemods/legacy-arbitrary-values'
|
||||
import { legacyClasses } from './codemods/legacy-classes'
|
||||
import { maxWidthScreen } from './codemods/max-width-screen'
|
||||
import { modernizeArbitraryValues } from './codemods/modernize-arbitrary-values'
|
||||
import { prefix } from './codemods/prefix'
|
||||
@ -25,13 +26,14 @@ export type Migration = (
|
||||
start: number
|
||||
end: number
|
||||
},
|
||||
) => string
|
||||
) => string | Promise<string>
|
||||
|
||||
export const DEFAULT_MIGRATIONS: Migration[] = [
|
||||
prefix,
|
||||
important,
|
||||
bgGradient,
|
||||
simpleLegacyClasses,
|
||||
legacyClasses,
|
||||
maxWidthScreen,
|
||||
themeToVar,
|
||||
variantOrder, // Has to happen before migrations that modify variants
|
||||
@ -41,7 +43,7 @@ export const DEFAULT_MIGRATIONS: Migration[] = [
|
||||
modernizeArbitraryValues,
|
||||
]
|
||||
|
||||
export function migrateCandidate(
|
||||
export async function migrateCandidate(
|
||||
designSystem: DesignSystem,
|
||||
userConfig: Config,
|
||||
rawCandidate: string,
|
||||
@ -51,9 +53,9 @@ export function migrateCandidate(
|
||||
start: number
|
||||
end: number
|
||||
},
|
||||
): string {
|
||||
): Promise<string> {
|
||||
for (let migration of DEFAULT_MIGRATIONS) {
|
||||
rawCandidate = migration(designSystem, userConfig, rawCandidate, location)
|
||||
rawCandidate = await migration(designSystem, userConfig, rawCandidate, location)
|
||||
}
|
||||
return rawCandidate
|
||||
}
|
||||
@ -69,7 +71,7 @@ export default async function migrateContents(
|
||||
let changes: StringChange[] = []
|
||||
|
||||
for (let { rawCandidate, start, end } of candidates) {
|
||||
let migratedCandidate = migrateCandidate(designSystem, userConfig, rawCandidate, {
|
||||
let migratedCandidate = await migrateCandidate(designSystem, userConfig, rawCandidate, {
|
||||
contents,
|
||||
start,
|
||||
end,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user