Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update template migration interface #14539

Merged
merged 2 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 17 additions & 80 deletions packages/@tailwindcss-upgrade/src/template/candidates.test.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -10,107 +10,40 @@ test('extracts candidates with positions from a template', async () => {
<button class="bg-blue-500 text-white">My button</button>
</div>
`

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,
},
]
Expand All @@ -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(`
Expand Down
12 changes: 4 additions & 8 deletions packages/@tailwindcss-upgrade/src/template/candidates.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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([
Expand All @@ -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)
})
23 changes: 13 additions & 10 deletions packages/@tailwindcss-upgrade/src/template/codemods/bg-gradient.ts
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrated this test to what I found more useful in other migrations. Basically it only tests the migration function and not the text replace logic (we have other tests for this incl. integration tests)

27 changes: 27 additions & 0 deletions packages/@tailwindcss-upgrade/src/template/codemods/important.ts
Original file line number Diff line number Diff line change
@@ -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
}

This file was deleted.

This file was deleted.

21 changes: 10 additions & 11 deletions packages/@tailwindcss-upgrade/src/template/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import type { Candidate } from '../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
import { extractCandidates, printCandidate, replaceCandidateInContent } from './candidates'
import { extractRawCandidates, replaceCandidateInContent } from './candidates'
import { bgGradient } from './codemods/bg-gradient'
import { migrateImportant } from './codemods/migrate-important'
import { important } from './codemods/important'

export type Migration = (candidate: Candidate) => Candidate | null
export type Migration = (designSystem: DesignSystem, rawCandidate: string) => string

export default async function migrateContents(
designSystem: DesignSystem,
contents: string,
migrations: Migration[] = [migrateImportant, bgGradient],
migrations: Migration[] = [important, bgGradient],
): Promise<string> {
let candidates = await extractCandidates(designSystem, contents)
let candidates = await extractRawCandidates(contents)

// Sort candidates by starting position desc
candidates.sort((a, z) => z.start - a.start)

let output = contents
for (let { candidate, start, end } of candidates) {
for (let { rawCandidate, start, end } of candidates) {
let needsMigration = false
for (let migration of migrations) {
let migrated = migration(candidate)
if (migrated) {
candidate = migrated
let candidate = migration(designSystem, rawCandidate)
if (rawCandidate !== candidate) {
rawCandidate = candidate
needsMigration = true
}
}

if (needsMigration) {
output = replaceCandidateInContent(output, printCandidate(candidate), start, end)
output = replaceCandidateInContent(output, rawCandidate, start, end)
}
}

Expand Down