Skip to content
Closed
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
84 changes: 84 additions & 0 deletions packages/@tailwindcss-upgrade/src/mock-design-system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
parseCandidate,
parseVariant,
type Candidate,
type Variant,
} from '../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../tailwindcss/src/design-system'
import { convertUnderscoresToWhitespace } from '../../tailwindcss/src/utils/decode-arbitrary-value'
import * as ValueParser from '../../tailwindcss/src/value-parser'

export function mockDesignSystem(designSystem: DesignSystem): DesignSystem {
// Custom `parseCandidate` implementation that does two things:
// 1. Has custom `decodeArbitraryValue` that does not add whitespace around
// operators.
// 2. Does not cache the parsing of candidates
designSystem.parseCandidate = (candidate) => {
return Array.from(parseCandidate(candidate, designSystem, { decodeArbitraryValue })).map(
(candidate) => {
// We inject `&:is(…)` into arbitrary variants `[p]` becomes `[&:is(p)]`.
//
// In this case, for the migrations we don't care about this outer
// `&:is(…)` so we replace it with the contents of the `&:is(…)` instead.
//
// We could have a false positive here _if_ the user added the `&:is()`
// themselves. But if it's not there then we would inject it so the
// behavior should be the exact same.
for (let variant of variants(candidate)) {
if (variant.kind === 'arbitrary') {
let ast = ValueParser.parse(variant.selector)

// &:is(…)
if (
ast.length === 3 &&
// &
ast[0].kind === 'word' &&
ast[0].value === '&' &&
// :
ast[1].kind === 'separator' &&
ast[1].value === ':' &&
// is(…)
ast[2].kind === 'function' &&
ast[2].value === 'is'
) {
variant.selector = ValueParser.toCss(ast[2].nodes)
}
}
}

return candidate
},
)
}

designSystem.parseVariant = (variant) => {
return parseVariant(variant, designSystem, { decodeArbitraryValue })
}

return designSystem
}

function decodeArbitraryValue(input: string) {
// We do not want to normalize anything inside of a url() because if we
// replace `_` with ` `, then it will very likely break the url.
if (input.startsWith('url(')) {
return input
}

input = convertUnderscoresToWhitespace(input)

return input
}

function* variants(candidate: Candidate) {
function* inner(variant: Variant): Iterable<Variant> {
yield variant
if (variant.kind === 'compound') {
yield* inner(variant.variant)
}
}

for (let variant of candidate.variants) {
yield* inner(variant)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test } from 'vitest'
import { mockDesignSystem } from '../../mock-design-system'
import { arbitraryValueToBareValue } from './arbitrary-value-to-bare-value'

test.each([
Expand Down Expand Up @@ -45,9 +46,11 @@ test.each([
'data-selected:aria-selected:aspect-12/34',
],
])('%s => %s', async (candidate, result) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
let designSystem = mockDesignSystem(
await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
}),
)

expect(arbitraryValueToBareValue(designSystem, {}, candidate)).toEqual(result)
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Config } from 'tailwindcss'
import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
import { type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type'
import { segment } from '../../../../tailwindcss/src/utils/segment'
Expand All @@ -10,24 +10,26 @@ export function arbitraryValueToBareValue(
_userConfig: Config,
rawCandidate: string,
): string {
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
let clone = structuredClone(candidate)
for (let candidate of designSystem.parseCandidate(rawCandidate) as Candidate[]) {
let changed = false

// Convert font-stretch-* utilities
if (
clone.kind === 'functional' &&
clone.value?.kind === 'arbitrary' &&
clone.value.dataType === null &&
clone.root === 'font-stretch'
candidate.kind === 'functional' &&
candidate.value?.kind === 'arbitrary' &&
candidate.value.dataType === null &&
candidate.root === 'font-stretch'
) {
if (clone.value.value.endsWith('%') && isPositiveInteger(clone.value.value.slice(0, -1))) {
let percentage = parseInt(clone.value.value)
if (
candidate.value.value.endsWith('%') &&
isPositiveInteger(candidate.value.value.slice(0, -1))
) {
let percentage = parseInt(candidate.value.value)
if (percentage >= 50 && percentage <= 200) {
changed = true
clone.value = {
candidate.value = {
kind: 'named',
value: clone.value.value,
value: candidate.value.value,
fraction: null,
}
}
Expand All @@ -37,51 +39,51 @@ export function arbitraryValueToBareValue(
// Convert arbitrary values with positive integers to bare values
// Convert arbitrary values with fractions to bare values
else if (
clone.kind === 'functional' &&
clone.value?.kind === 'arbitrary' &&
clone.value.dataType === null
candidate.kind === 'functional' &&
candidate.value?.kind === 'arbitrary' &&
candidate.value.dataType === null
) {
let parts = segment(clone.value.value, '/')
let parts = segment(candidate.value.value, '/')
if (parts.every((part) => isPositiveInteger(part))) {
changed = true

let currentValue = clone.value
let currentModifier = clone.modifier
let currentValue = candidate.value
let currentModifier = candidate.modifier

// E.g.: `col-start-[12]`
// ^^
if (parts.length === 1) {
clone.value = {
candidate.value = {
kind: 'named',
value: clone.value.value,
value: candidate.value.value,
fraction: null,
}
}

// E.g.: `aspect-[12/34]`
// ^^ ^^
else {
clone.value = {
candidate.value = {
kind: 'named',
value: parts[0],
fraction: clone.value.value,
fraction: candidate.value.value,
}
clone.modifier = {
candidate.modifier = {
kind: 'named',
value: parts[1],
}
}

// Double check that the new value compiles correctly
if (designSystem.compileAstNodes(clone).length === 0) {
clone.value = currentValue
clone.modifier = currentModifier
if (designSystem.compileAstNodes(candidate).length === 0) {
candidate.value = currentValue
candidate.modifier = currentModifier
changed = false
}
}
}

for (let variant of variants(clone)) {
for (let variant of variants(candidate)) {
// Convert `data-[selected]` to `data-selected`
if (
variant.kind === 'functional' &&
Expand Down Expand Up @@ -143,7 +145,7 @@ export function arbitraryValueToBareValue(
}
}

return changed ? printCandidate(designSystem, clone) : rawCandidate
return changed ? printCandidate(designSystem, candidate) : rawCandidate
}

return rawCandidate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test } from 'vitest'
import { mockDesignSystem } from '../../mock-design-system'
import { automaticVarInjection } from './automatic-var-injection'

test.each([
Expand Down Expand Up @@ -50,9 +51,11 @@ test.each([
['[view-timeline:--myTimeline]', '[view-timeline:--myTimeline]'],
['[position-try:--myAnchor]', '[position-try:--myAnchor]'],
])('%s => %s', async (candidate, result) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
let designSystem = mockDesignSystem(
await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
}),
)

let migrated = automaticVarInjection(designSystem, {}, candidate)
expect(migrated).toEqual(result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ export function automaticVarInjection(
_userConfig: Config,
rawCandidate: string,
): string {
for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
// The below logic makes extended use of mutation. Since candidates in the
// DesignSystem are cached, we can't mutate them directly.
let candidate = structuredClone(readonlyCandidate) as Candidate

for (let candidate of designSystem.parseCandidate(rawCandidate) as Candidate[]) {
let didChange = false

// Add `var(…)` in modifier position, e.g.:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test } from 'vitest'
import { mockDesignSystem } from '../../mock-design-system'
import { bgGradient } from './bg-gradient'

test.each([
Expand All @@ -14,9 +15,11 @@ test.each([

['max-lg:hover:bg-gradient-to-t', 'max-lg:hover:bg-linear-to-t'],
])('%s => %s', async (candidate, result) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
let designSystem = mockDesignSystem(
await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
}),
)

expect(bgGradient(designSystem, {}, candidate)).toEqual(result)
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Config } from 'tailwindcss'
import type { Candidate } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { printCandidate } from '../candidates'

Expand All @@ -9,18 +10,17 @@ export function bgGradient(
_userConfig: Config,
rawCandidate: string,
): string {
for (let candidate of designSystem.parseCandidate(rawCandidate)) {
for (let candidate of designSystem.parseCandidate(rawCandidate) as Candidate[]) {
if (candidate.kind === 'static' && candidate.root.startsWith('bg-gradient-to-')) {
let direction = candidate.root.slice(15)

if (!DIRECTIONS.includes(direction)) {
continue
}

return printCandidate(designSystem, {
...candidate,
root: `bg-linear-to-${direction}`,
})
candidate.root = `bg-linear-to-${direction}`

return printCandidate(designSystem, candidate)
}
}
return rawCandidate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test } from 'vitest'
import { mockDesignSystem } from '../../mock-design-system'
import { important } from './important'

test.each([
['!flex', 'flex!'],
['min-[calc(1000px+12em)]:!flex', 'min-[calc(1000px_+_12em)]:flex!'],
['min-[calc(1000px+12em)]:!flex', 'min-[calc(1000px+12em)]:flex!'],
['md:!block', 'md:block!'],

// Maintains the original arbitrary value contents
['has-[[data-foo]]:!flex', 'has-[[data-foo]]:flex!'],
['!px-[calc(var(--spacing-1)-1px)]', 'px-[calc(var(--spacing-1)-1px)]!'],

// 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,
})
let designSystem = mockDesignSystem(
await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
}),
)

expect(important(designSystem, {}, candidate)).toEqual(result)
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Config } from 'tailwindcss'
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { printCandidate } from '../candidates'

Expand All @@ -20,7 +19,7 @@ export function important(
_userConfig: Config,
rawCandidate: string,
): string {
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
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
Expand Down
Loading