diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.test.ts b/packages/@tailwindcss-upgrade/src/template/candidates.test.ts index 634cbe5e92e4..e03439ce4adf 100644 --- a/packages/@tailwindcss-upgrade/src/template/candidates.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/candidates.test.ts @@ -1,6 +1,6 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import { describe, expect, test } from 'vitest' -import { extractCandidates, printCandidate, replaceCandidateInContent } from './candidates' +import { extractRawCandidates, printCandidate, replaceCandidateInContent } from './candidates' let html = String.raw @@ -10,107 +10,40 @@ test('extracts candidates with positions from a template', async () => { ` - let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { base: __dirname, }) - expect(extractCandidates(designSystem, content)).resolves.toMatchInlineSnapshot(` + let candidates = await extractRawCandidates(content) + let validCandidates = candidates.filter( + ({ rawCandidate }) => designSystem.parseCandidate(rawCandidate).length > 0, + ) + + expect(validCandidates).toMatchInlineSnapshot(` [ { - "candidate": { - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "raw": "bg-blue-500", - "root": "bg", - "value": { - "fraction": null, - "kind": "named", - "value": "blue-500", - }, - "variants": [], - }, "end": 28, + "rawCandidate": "bg-blue-500", "start": 17, }, { - "candidate": { - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "raw": "hover:focus:text-white", - "root": "text", - "value": { - "fraction": null, - "kind": "named", - "value": "white", - }, - "variants": [ - { - "compounds": true, - "kind": "static", - "root": "focus", - }, - { - "compounds": true, - "kind": "static", - "root": "hover", - }, - ], - }, "end": 51, + "rawCandidate": "hover:focus:text-white", "start": 29, }, { - "candidate": { - "important": false, - "kind": "arbitrary", - "modifier": null, - "property": "color", - "raw": "[color:red]", - "value": "red", - "variants": [], - }, "end": 63, + "rawCandidate": "[color:red]", "start": 52, }, { - "candidate": { - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "raw": "bg-blue-500", - "root": "bg", - "value": { - "fraction": null, - "kind": "named", - "value": "blue-500", - }, - "variants": [], - }, "end": 98, + "rawCandidate": "bg-blue-500", "start": 87, }, { - "candidate": { - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "raw": "text-white", - "root": "text", - "value": { - "fraction": null, - "kind": "named", - "value": "white", - }, - "variants": [], - }, "end": 109, + "rawCandidate": "text-white", "start": 99, }, ] @@ -127,7 +60,11 @@ test('replaces the right positions for a candidate', async () => { base: __dirname, }) - let candidate = (await extractCandidates(designSystem, content))[0] + let candidates = await extractRawCandidates(content) + + let candidate = candidates.find( + ({ rawCandidate }) => designSystem.parseCandidate(rawCandidate).length > 0, + )! expect(replaceCandidateInContent(content, 'flex', candidate.start, candidate.end)) .toMatchInlineSnapshot(` diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.ts b/packages/@tailwindcss-upgrade/src/template/candidates.ts index 19b61d996217..a36cc50b07c5 100644 --- a/packages/@tailwindcss-upgrade/src/template/candidates.ts +++ b/packages/@tailwindcss-upgrade/src/template/candidates.ts @@ -1,20 +1,16 @@ import { Scanner } from '@tailwindcss/oxide' import stringByteSlice from 'string-byte-slice' import type { Candidate, Variant } from '../../../tailwindcss/src/candidate' -import type { DesignSystem } from '../../../tailwindcss/src/design-system' -export async function extractCandidates( - designSystem: DesignSystem, +export async function extractRawCandidates( content: string, -): Promise<{ candidate: Candidate; start: number; end: number }[]> { +): Promise<{ rawCandidate: string; start: number; end: number }[]> { let scanner = new Scanner({}) let result = scanner.getCandidatesWithPositions({ content, extension: 'html' }) - let candidates: { candidate: Candidate; start: number; end: number }[] = [] + let candidates: { rawCandidate: string; start: number; end: number }[] = [] for (let { candidate: rawCandidate, position: start } of result) { - for (let candidate of designSystem.parseCandidate(rawCandidate)) { - candidates.push({ candidate, start, end: start + rawCandidate.length }) - } + candidates.push({ rawCandidate, start, end: start + rawCandidate.length }) } return candidates } diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.test.ts index 8ea6572ad13d..c5811eb8134e 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.test.ts @@ -1,6 +1,5 @@ import { __unstable__loadDesignSystem } from '@tailwindcss/node' import { expect, test } from 'vitest' -import { printCandidate } from '../candidates' import { bgGradient } from './bg-gradient' test.each([ @@ -19,6 +18,5 @@ test.each([ base: __dirname, }) - let migrated = bgGradient(designSystem.parseCandidate(candidate)[0]!) - expect(migrated ? printCandidate(migrated) : migrated).toEqual(result) + expect(bgGradient(designSystem, candidate)).toEqual(result) }) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.ts b/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.ts index 8b926a8b20be..bf871e24a4cb 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.ts @@ -1,17 +1,20 @@ -import type { Candidate } from '../../../../tailwindcss/src/candidate' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { printCandidate } from '../candidates' const DIRECTIONS = ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] -export function bgGradient(candidate: Candidate): Candidate | null { - if (candidate.kind === 'static' && candidate.root.startsWith('bg-gradient-to-')) { - let direction = candidate.root.slice(15) +export function bgGradient(designSystem: DesignSystem, rawCandidate: string): string { + for (let candidate of designSystem.parseCandidate(rawCandidate)) { + if (candidate.kind === 'static' && candidate.root.startsWith('bg-gradient-to-')) { + let direction = candidate.root.slice(15) - if (!DIRECTIONS.includes(direction)) { - return null - } + if (!DIRECTIONS.includes(direction)) { + continue + } - candidate.root = `bg-linear-to-${direction}` - return candidate + candidate.root = `bg-linear-to-${direction}` + return printCandidate(candidate) + } } - return null + return rawCandidate } diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts new file mode 100644 index 000000000000..fd568c500718 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts @@ -0,0 +1,22 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import dedent from 'dedent' +import { expect, test } from 'vitest' +import { important } from './important' + +let html = dedent + +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(important(designSystem, candidate)).toEqual(result) +}) diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/important.ts b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts new file mode 100644 index 000000000000..d6c6fc2db7fe --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/template/codemods/important.ts @@ -0,0 +1,27 @@ +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { printCandidate } from '../candidates' + +// 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 important(designSystem: DesignSystem, rawCandidate: string): string { + for (let candidate of designSystem.parseCandidate(rawCandidate)) { + if (candidate.important && candidate.raw[candidate.raw.length - 1] !== '!') { + // 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 printCandidate(candidate) + } + } + + return rawCandidate +} diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/migrate-important.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/migrate-important.test.ts deleted file mode 100644 index 0755dd41a808..000000000000 --- a/packages/@tailwindcss-upgrade/src/template/codemods/migrate-important.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { __unstable__loadDesignSystem } from '@tailwindcss/node' -import dedent from 'dedent' -import { expect, test } from 'vitest' -import migrate from '../migrate' -import { migrateImportant } from './migrate-important' - -let html = dedent - -test('applies the migration', async () => { - let content = html` -