diff --git a/CHANGELOG.md b/CHANGELOG.md
index c161df301..0519e8e82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,11 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure that running the Standalone build does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981))
- Fix `-rotate-*` utilities with arbitrary values ([#18014](https://github.com/tailwindlabs/tailwindcss/pull/18014))
- Upgrade: Change casing of utilities with named values to kebab-case to match updated theme variables ([#18017](https://github.com/tailwindlabs/tailwindcss/pull/18017))
+- Upgrade: Fix unsafe migrations in Vue files ([#18025](https://github.com/tailwindlabs/tailwindcss/pull/18025))
- Ignore custom variants using `:merge(…)` selectors in legacy JS plugins ([#18020](https://github.com/tailwindlabs/tailwindcss/pull/18020))
### Added
- Upgrade: Migrate bare values to named values ([#18000](https://github.com/tailwindlabs/tailwindcss/pull/18000))
+- Upgrade: Make candidate template migrations faster using caching ([#18025](https://github.com/tailwindlabs/tailwindcss/pull/18025))
## [4.1.6] - 2025-05-09
diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts
index 93d239af0..7b539c4e4 100644
--- a/integrations/upgrade/js-config.test.ts
+++ b/integrations/upgrade/js-config.test.ts
@@ -151,7 +151,7 @@ test(
@@ -169,7 +169,7 @@ test(
-
+
--- src/input.css ---
@import 'tailwindcss';
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts
new file mode 100644
index 000000000..455b68660
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts
@@ -0,0 +1,61 @@
+import { __unstable__loadDesignSystem } from '@tailwindcss/node'
+import { expect, test, vi } from 'vitest'
+import * as versions from '../../utils/version'
+import { migrateCandidate } from './migrate'
+vi.spyOn(versions, 'isMajor').mockReturnValue(true)
+
+test('does not replace classes in invalid positions', async () => {
+ let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
+ base: __dirname,
+ })
+
+ async function shouldNotReplace(example: string, candidate = '!border') {
+ expect(
+ await migrateCandidate(designSystem, {}, candidate, {
+ contents: example,
+ start: example.indexOf(candidate),
+ end: example.indexOf(candidate) + candidate.length,
+ }),
+ ).toEqual(candidate)
+ }
+
+ await shouldNotReplace(`let notBorder = !border \n`)
+ await shouldNotReplace(`{ "foo": !border.something + ""}\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+ await shouldNotReplace(`
\n`)
+
+ await shouldNotReplace(`let notShadow = shadow \n`, 'shadow')
+ await shouldNotReplace(`{ "foo": shadow.something + ""}\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(`
\n`, 'shadow')
+ await shouldNotReplace(
+ `
\n`,
+ 'shadow',
+ )
+
+ // Next.js Image placeholder cases
+ await shouldNotReplace(`
`, 'blur')
+ await shouldNotReplace(`
`, 'blur')
+ await shouldNotReplace(`
`, 'blur')
+
+ // https://github.com/tailwindlabs/tailwindcss/issues/17974
+ await shouldNotReplace('
', '!duration')
+ await shouldNotReplace('
', '!duration')
+ await shouldNotReplace('
', '!visible')
+})
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
index dc84d3022..dd540d5c5 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
@@ -1,3 +1,7 @@
+import { parseCandidate } from '../../../../tailwindcss/src/candidate'
+import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
+import * as version from '../../utils/version'
+
const QUOTES = ['"', "'", '`']
const LOGICAL_OPERATORS = ['&&', '||', '?', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
const CONDITIONAL_TEMPLATE_SYNTAX = [
@@ -5,6 +9,7 @@ const CONDITIONAL_TEMPLATE_SYNTAX = [
/v-else-if=['"]$/,
/v-if=['"]$/,
/v-show=['"]$/,
+ /(? 0) {
+ return true
+ }
+
+ // When we have an arbitrary property, the candidate has such a particular
+ // structure it's very likely to be safe.
+ //
+ // E.g.: `[color:red]`
+ if (candidate.kind === 'arbitrary') {
+ return true
+ }
+
+ // A static candidate is very likely safe if it contains a dash.
+ //
+ // E.g.: `items-center`
+ if (candidate.kind === 'static' && candidate.root.includes('-')) {
+ return true
+ }
+
+ // A functional candidate is very likely safe if it contains a value (which
+ // implies a `-`). Or if the root contains a dash.
+ //
+ // E.g.: `bg-red-500`, `bg-position-20`
+ if (
+ (candidate.kind === 'functional' && candidate.value !== null) ||
+ (candidate.kind === 'functional' && candidate.root.includes('-'))
+ ) {
+ return true
+ }
+
+ // If the candidate contains a modifier, it's very likely to be safe because
+ // it implies that it contains a `/`.
+ //
+ // E.g.: `text-sm/7`
+ if (candidate.kind === 'functional' && candidate.modifier) {
+ return true
+ }
+ }
+
let currentLineBeforeCandidate = ''
for (let i = location.start - 1; i >= 0; i--) {
let char = location.contents.at(i)!
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-utilities.ts
index 1e71dbf93..c03100cdd 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-utilities.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-utilities.ts
@@ -51,27 +51,6 @@ export function migrateArbitraryUtilities(
continue
}
- // 1. Canonicalize the value. This might be a bit wasteful because it might
- // have been done by other migrations before, but essentially we want to
- // canonicalize the arbitrary value to its simplest canonical form. We
- // won't be constant folding `calc(…)` expressions (yet?), but we can
- // remove unnecessary whitespace (which the `printCandidate` already
- // handles for us).
- //
- // E.g.:
- //
- // ```
- // [display:_flex_] => [display:flex]
- // [display:_flex] => [display:flex]
- // [display:flex_] => [display:flex]
- // [display:flex] => [display:flex]
- // ```
- //
- let canonicalizedCandidate = designSystem.printCandidate(readonlyCandidate)
- if (canonicalizedCandidate !== rawCandidate) {
- return migrateArbitraryUtilities(designSystem, _userConfig, canonicalizedCandidate)
- }
-
// The below logic makes use of mutation. Since candidates in the
// DesignSystem are cached, we can't mutate them directly.
let candidate = structuredClone(readonlyCandidate) as Writable
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.test.ts
new file mode 100644
index 000000000..57fe80c22
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.test.ts
@@ -0,0 +1,27 @@
+import { __unstable__loadDesignSystem } from '@tailwindcss/node'
+import { expect, test, vi } from 'vitest'
+import * as versions from '../../utils/version'
+import { migrateCanonicalizeCandidate } from './migrate-canonicalize-candidate'
+vi.spyOn(versions, 'isMajor').mockReturnValue(true)
+
+test.each([
+ // Normalize whitespace in arbitrary properties
+ ['[display:flex]', '[display:flex]'],
+ ['[display:_flex]', '[display:flex]'],
+ ['[display:flex_]', '[display:flex]'],
+ ['[display:_flex_]', '[display:flex]'],
+
+ // Normalize whitespace in `calc` expressions
+ ['w-[calc(100%-2rem)]', 'w-[calc(100%-2rem)]'],
+ ['w-[calc(100%_-_2rem)]', 'w-[calc(100%-2rem)]'],
+
+ // Normalize the important modifier
+ ['!flex', 'flex!'],
+ ['flex!', 'flex!'],
+])('%s => %s', async (candidate, result) => {
+ let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
+ base: __dirname,
+ })
+
+ expect(migrateCanonicalizeCandidate(designSystem, {}, candidate)).toEqual(result)
+})
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.ts
new file mode 100644
index 000000000..32d126467
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-canonicalize-candidate.ts
@@ -0,0 +1,29 @@
+import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
+import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
+
+// Canonicalize the value to its minimal form. This will normalize whitespace,
+// and print the important modifier `!` in the correct place.
+//
+// E.g.:
+//
+// ```
+// [display:_flex_] => [display:flex]
+// [display:_flex] => [display:flex]
+// [display:flex_] => [display:flex]
+// [display:flex] => [display:flex]
+// ```
+//
+export function migrateCanonicalizeCandidate(
+ designSystem: DesignSystem,
+ _userConfig: Config | null,
+ rawCandidate: string,
+) {
+ for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
+ let canonicalizedCandidate = designSystem.printCandidate(readonlyCandidate)
+ if (canonicalizedCandidate !== rawCandidate) {
+ return canonicalizedCandidate
+ }
+ }
+
+ return rawCandidate
+}
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.test.ts
deleted file mode 100644
index bc84538aa..000000000
--- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.test.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { __unstable__loadDesignSystem } from '@tailwindcss/node'
-import { expect, test } from 'vitest'
-import { migrateImportant } from './migrate-important'
-
-test.each([
- ['!flex', 'flex!'],
- ['min-[calc(1000px+12em)]:!flex', 'min-[calc(1000px+12em)]:flex!'],
- ['md:!block', 'md:block!'],
-
- // Does not change non-important candidates
- ['bg-blue-500', 'bg-blue-500'],
- ['min-[calc(1000px+12em)]:flex', 'min-[calc(1000px+12em)]:flex'],
-])('%s => %s', async (candidate, result) => {
- let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
- base: __dirname,
- })
-
- expect(
- migrateImportant(designSystem, {}, candidate, {
- contents: `"${candidate}"`,
- start: 1,
- end: candidate.length + 1,
- }),
- ).toEqual(result)
-})
-
-test('does not match false positives', async () => {
- let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
- base: __dirname,
- })
-
- expect(
- migrateImportant(designSystem, {}, '!border', {
- contents: `let notBorder = !border\n`,
- start: 16,
- end: 16 + '!border'.length,
- }),
- ).toEqual('!border')
-})
-
-test('does not replace classes in invalid positions', async () => {
- let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
- base: __dirname,
- })
-
- function shouldNotReplace(example: string, candidate = '!border') {
- expect(
- migrateImportant(designSystem, {}, candidate, {
- contents: example,
- start: example.indexOf(candidate),
- end: example.indexOf(candidate) + candidate.length,
- }),
- ).toEqual(candidate)
- }
-
- shouldNotReplace(`let notBorder = !border \n`)
- shouldNotReplace(`{ "foo": !border.something + ""}\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
- shouldNotReplace(`\n`)
-})
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts
deleted file mode 100644
index 27a663f47..000000000
--- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-important.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { parseCandidate } from '../../../../tailwindcss/src/candidate'
-import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
-import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
-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
-// so that it's always in the same location regardless of whether you used
-// variants or not.
-//
-// So this:
-//
-// !flex md:!block
-//
-// Should turn into:
-//
-// flex! md:block!
-export function migrateImportant(
- designSystem: DesignSystem,
- _userConfig: Config | null,
- rawCandidate: string,
- location?: {
- contents: string
- start: number
- end: number
- },
-): string {
- nextCandidate: for (let candidate of parseCandidate(rawCandidate, designSystem)) {
- if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') {
- // The important migration is one of the most broad migrations with a high
- // potential of matching false positives since `!` is a valid character in
- // most programming languages. Since v4 is technically backward compatible
- // 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 && !isSafeMigration(location)) {
- continue nextCandidate
- }
-
- // The printCandidate function will already put the exclamation mark in
- // the right place, so we just need to mark this candidate as requiring a
- // migration.
- return designSystem.printCandidate(candidate)
- }
- }
-
- return rawCandidate
-}
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts
index a48834180..5b204386f 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.test.ts
@@ -43,38 +43,3 @@ test.each([
expect(await migrateLegacyClasses(designSystem, {}, candidate)).toEqual(result)
})
-
-test('does not replace classes in invalid positions', async () => {
- let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
- base: __dirname,
- })
-
- async function shouldNotReplace(example: string, candidate = 'shadow') {
- expect(
- await migrateLegacyClasses(designSystem, {}, candidate, {
- contents: example,
- start: example.indexOf(candidate),
- end: example.indexOf(candidate) + candidate.length,
- }),
- ).toEqual(candidate)
- }
-
- await shouldNotReplace(`let notShadow = shadow \n`)
- await shouldNotReplace(`{ "foo": shadow.something + ""}\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
- await shouldNotReplace(`\n`)
-
- // Next.js Image placeholder cases
- await shouldNotReplace(``, 'blur')
- await shouldNotReplace(``, 'blur')
- await shouldNotReplace(``, 'blur')
-})
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts
index 4fcb18c2f..fa425574c 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts
@@ -7,7 +7,6 @@ import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import * as version from '../../utils/version'
import { baseCandidate } from './candidates'
-import { isSafeMigration } from './is-safe-migration'
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
@@ -72,11 +71,6 @@ export async function migrateLegacyClasses(
designSystem: DesignSystem,
_userConfig: Config | null,
rawCandidate: string,
- location?: {
- contents: string
- start: number
- end: number
- },
): Promise {
// These migrations are only safe when migrating from v3 to v4.
//
@@ -111,7 +105,6 @@ export async function migrateLegacyClasses(
newCandidate.important = candidate.important
yield [
- candidate,
newCandidate,
THEME_KEYS.get(baseCandidateString),
THEME_KEYS.get(newBaseCandidateString),
@@ -119,34 +112,7 @@ export async function migrateLegacyClasses(
}
}
- 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
- }
-
+ for (let [toCandidate, fromThemeKey, toThemeKey] of migrate(rawCandidate)) {
if (fromThemeKey && toThemeKey) {
// Migrating something that resolves to a value in the theme.
let customFrom = designSystem.resolveThemeValue(fromThemeKey, true)
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts
index 3e014817c..776032850 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate-prefix.ts
@@ -1,3 +1,4 @@
+import { decl } from '../../../../tailwindcss/src/ast'
import { parseCandidate, type Candidate } from '../../../../tailwindcss/src/candidate'
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
@@ -16,8 +17,18 @@ export function migratePrefix(
if (!version.isMajor(3)) return rawCandidate
if (!seenDesignSystems.has(designSystem)) {
- designSystem.utilities.functional('group', () => null)
- designSystem.utilities.functional('peer', () => null)
+ designSystem.utilities.functional('group', (value) => [
+ // To ensure that `@apply group` works when computing a signature
+ decl('--phantom-class', 'group'),
+ // To ensure `group` and `group/foo` are considered different classes
+ decl('--phantom-modifier', value.modifier?.value),
+ ])
+ designSystem.utilities.functional('peer', (value) => [
+ // To ensure that `@apply peer` works when computing a signature
+ decl('--phantom-class', 'peer'),
+ // To ensure `peer` and `peer/foo` are considered different classes
+ decl('--phantom-modifier', value.modifier?.value),
+ ])
seenDesignSystems.add(designSystem)
}
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts
index fff859c0a..4c0a26a83 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts
@@ -1,10 +1,11 @@
import fs from 'node:fs/promises'
import path, { extname } from 'node:path'
-import { parseCandidate } from '../../../../tailwindcss/src/candidate'
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
+import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import { spliceChangesIntoString, type StringChange } from '../../utils/splice-changes-into-string'
import { extractRawCandidates } from './candidates'
+import { isSafeMigration } from './is-safe-migration'
import { migrateArbitraryUtilities } from './migrate-arbitrary-utilities'
import { migrateArbitraryValueToBareValue } from './migrate-arbitrary-value-to-bare-value'
import { migrateArbitraryVariants } from './migrate-arbitrary-variants'
@@ -12,9 +13,9 @@ import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection'
import { migrateBareValueUtilities } from './migrate-bare-utilities'
import { migrateBgGradient } from './migrate-bg-gradient'
import { migrateCamelcaseInNamedValue } from './migrate-camelcase-in-named-value'
+import { migrateCanonicalizeCandidate } from './migrate-canonicalize-candidate'
import { migrateDropUnnecessaryDataTypes } from './migrate-drop-unnecessary-data-types'
import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values'
-import { migrateImportant } from './migrate-important'
import { migrateLegacyArbitraryValues } from './migrate-legacy-arbitrary-values'
import { migrateLegacyClasses } from './migrate-legacy-classes'
import { migrateMaxWidthScreen } from './migrate-max-width-screen'
@@ -24,22 +25,18 @@ import { migratePrefix } from './migrate-prefix'
import { migrateSimpleLegacyClasses } from './migrate-simple-legacy-classes'
import { migrateThemeToVar } from './migrate-theme-to-var'
import { migrateVariantOrder } from './migrate-variant-order'
+import { computeUtilitySignature } from './signatures'
export type Migration = (
designSystem: DesignSystem,
userConfig: Config | null,
rawCandidate: string,
- location?: {
- contents: string
- start: number
- end: number
- },
) => string | Promise
export const DEFAULT_MIGRATIONS: Migration[] = [
migrateEmptyArbitraryValues,
migratePrefix,
- migrateImportant,
+ migrateCanonicalizeCandidate,
migrateBgGradient,
migrateSimpleLegacyClasses,
migrateCamelcaseInNamedValue,
@@ -58,6 +55,29 @@ export const DEFAULT_MIGRATIONS: Migration[] = [
migrateOptimizeModifier,
]
+let migrateCached = new DefaultMap<
+ DesignSystem,
+ DefaultMap>>
+>((designSystem) => {
+ return new DefaultMap((userConfig) => {
+ return new DefaultMap(async (rawCandidate) => {
+ let original = rawCandidate
+
+ for (let migration of DEFAULT_MIGRATIONS) {
+ rawCandidate = await migration(designSystem, userConfig, rawCandidate)
+ }
+
+ // Verify that the candidate actually makes sense at all. E.g.: `duration`
+ // is not a valid candidate, but it will parse because `duration-`
+ // exists.
+ let signature = computeUtilitySignature.get(designSystem).get(rawCandidate)
+ if (typeof signature !== 'string') return original
+
+ return rawCandidate
+ })
+ })
+})
+
export async function migrateCandidate(
designSystem: DesignSystem,
userConfig: Config | null,
@@ -69,23 +89,12 @@ export async function migrateCandidate(
end: number
},
): Promise {
- let original = rawCandidate
- for (let migration of DEFAULT_MIGRATIONS) {
- rawCandidate = await migration(designSystem, userConfig, rawCandidate, location)
+ // Skip this migration if we think that the migration is unsafe
+ if (location && !isSafeMigration(rawCandidate, location, designSystem)) {
+ return rawCandidate
}
- // If nothing changed, let's parse it again and re-print it. This will migrate
- // pretty print candidates to the new format. If it did change, we already had
- // to re-print it.
- //
- // E.g.: `bg-red-500/[var(--my-opacity)]` -> `bg-red-500/(--my-opacity)`
- if (rawCandidate === original) {
- for (let candidate of parseCandidate(rawCandidate, designSystem)) {
- return designSystem.printCandidate(candidate)
- }
- }
-
- return rawCandidate
+ return migrateCached.get(designSystem).get(userConfig).get(rawCandidate)
}
export default async function migrateContents(
diff --git a/packages/@tailwindcss-upgrade/src/utils/version.ts b/packages/@tailwindcss-upgrade/src/utils/version.ts
index e36ba06ba..6794b1a81 100644
--- a/packages/@tailwindcss-upgrade/src/utils/version.ts
+++ b/packages/@tailwindcss-upgrade/src/utils/version.ts
@@ -11,6 +11,15 @@ export function isMajor(version: number) {
return semver.satisfies(installedTailwindVersion(), `>=${version}.0.0 <${version + 1}.0.0`)
}
+/**
+ * Must be of greater than the current major version including minor and patch.
+ *
+ * E.g.: `isGreaterThan(3)`
+ */
+export function isGreaterThan(version: number) {
+ return semver.gte(installedTailwindVersion(), `${version + 1}.0.0`)
+}
+
let cache = new DefaultMap((base) => {
let tailwindVersion = getPackageVersionSync('tailwindcss', base)
if (!tailwindVersion) throw new Error('Tailwind CSS is not installed')