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(
-
+
`, 'node_modules/my-external-lib/src/template.html': html`
@@ -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')