-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Migrate bare values to named values #18000
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
Merged
Merged
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
666337c
allow migrating functional utilities
RobinMalfait a75205b
move pre computed maps to the `signatures`
RobinMalfait 5526698
extract `baseCandidate`
RobinMalfait 83f9a1c
add `migrateBareValueUtilities` step
RobinMalfait 9591df7
remove bare value handling from arbitrary migration
RobinMalfait 61eba77
update changelog
RobinMalfait 98a7483
move prefix-aware `parseCandidate` to `candidates.ts` file
RobinMalfait File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 2 additions & 21 deletions
23
packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 72 additions & 0 deletions
72
packages/@tailwindcss-upgrade/src/codemods/template/migrate-bare-utilities.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { __unstable__loadDesignSystem } from '@tailwindcss/node' | ||
import { describe, expect, test } from 'vitest' | ||
import type { UserConfig } from '../../../../tailwindcss/src/compat/config/types' | ||
import type { DesignSystem } from '../../../../tailwindcss/src/design-system' | ||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' | ||
import { migrateBareValueUtilities } from './migrate-bare-utilities' | ||
|
||
const css = String.raw | ||
|
||
const designSystems = new DefaultMap((base: string) => { | ||
return new DefaultMap((input: string) => { | ||
return __unstable__loadDesignSystem(input, { base }) | ||
}) | ||
}) | ||
|
||
function migrate(designSystem: DesignSystem, userConfig: UserConfig | null, rawCandidate: string) { | ||
for (let migration of [migrateBareValueUtilities]) { | ||
rawCandidate = migration(designSystem, userConfig, rawCandidate) | ||
} | ||
return rawCandidate | ||
} | ||
|
||
describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', (strategy) => { | ||
let testName = '%s => %s (%#)' | ||
if (strategy === 'with-variant') { | ||
testName = testName.replaceAll('%s', 'focus:%s') | ||
} else if (strategy === 'important') { | ||
testName = testName.replaceAll('%s', '%s!') | ||
} else if (strategy === 'prefix') { | ||
testName = testName.replaceAll('%s', 'tw:%s') | ||
} | ||
|
||
// Basic input with minimal design system to keep the tests fast | ||
let input = css` | ||
@import 'tailwindcss' ${strategy === 'prefix' ? 'prefix(tw)' : ''}; | ||
@theme { | ||
--*: initial; | ||
--spacing: 0.25rem; | ||
--aspect-video: 16 / 9; | ||
--tab-size-github: 8; | ||
} | ||
|
||
@utility tab-* { | ||
tab-size: --value(--tab-size, integer); | ||
} | ||
` | ||
|
||
test.each([ | ||
// Built-in utility with bare value fraction | ||
['aspect-16/9', 'aspect-video'], | ||
|
||
// Custom utility with bare value integer | ||
['tab-8', 'tab-github'], | ||
])(testName, async (candidate, result) => { | ||
if (strategy === 'with-variant') { | ||
candidate = `focus:${candidate}` | ||
result = `focus:${result}` | ||
} else if (strategy === 'important') { | ||
candidate = `${candidate}!` | ||
result = `${result}!` | ||
} else if (strategy === 'prefix') { | ||
// Not only do we need to prefix the candidate, we also have to make | ||
// sure that we prefix all CSS variables. | ||
candidate = `tw:${candidate.replaceAll('var(--', 'var(--tw-')}` | ||
result = `tw:${result.replaceAll('var(--', 'var(--tw-')}` | ||
} | ||
|
||
let designSystem = await designSystems.get(__dirname).get(input) | ||
let migrated = migrate(designSystem, {}, candidate) | ||
expect(migrated).toEqual(result) | ||
}) | ||
}) |
130 changes: 130 additions & 0 deletions
130
packages/@tailwindcss-upgrade/src/codemods/template/migrate-bare-utilities.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { type Candidate } 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 type { Writable } from '../../utils/types' | ||
import { baseCandidate } from './candidates' | ||
import { computeUtilitySignature, preComputedUtilities } from './signatures' | ||
|
||
const baseReplacementsCache = new DefaultMap<DesignSystem, Map<string, Candidate>>( | ||
() => new Map<string, Candidate>(), | ||
) | ||
|
||
export function migrateBareValueUtilities( | ||
designSystem: DesignSystem, | ||
_userConfig: Config | null, | ||
rawCandidate: string, | ||
): string { | ||
let utilities = preComputedUtilities.get(designSystem) | ||
let signatures = computeUtilitySignature.get(designSystem) | ||
|
||
for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { | ||
// We are only interested in bare value utilities | ||
if (readonlyCandidate.kind !== 'functional' || readonlyCandidate.value?.kind !== 'named') { | ||
continue | ||
} | ||
|
||
// 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<typeof readonlyCandidate> | ||
|
||
// Create a basic stripped candidate without variants or important flag. We | ||
// will re-add those later but they are irrelevant for what we are trying to | ||
// do here (and will increase cache hits because we only have to deal with | ||
// the base utility, nothing more). | ||
let targetCandidate = baseCandidate(candidate) | ||
|
||
let targetCandidateString = designSystem.printCandidate(targetCandidate) | ||
if (baseReplacementsCache.get(designSystem).has(targetCandidateString)) { | ||
let target = structuredClone( | ||
baseReplacementsCache.get(designSystem).get(targetCandidateString)!, | ||
) | ||
// Re-add the variants and important flag from the original candidate | ||
target.variants = candidate.variants | ||
target.important = candidate.important | ||
|
||
return designSystem.printCandidate(target) | ||
} | ||
|
||
// Compute the signature for the target candidate | ||
let targetSignature = signatures.get(targetCandidateString) | ||
if (typeof targetSignature !== 'string') continue | ||
|
||
// Try a few options to find a suitable replacement utility | ||
for (let replacementCandidate of tryReplacements(targetSignature, targetCandidate)) { | ||
let replacementString = designSystem.printCandidate(replacementCandidate) | ||
let replacementSignature = signatures.get(replacementString) | ||
if (replacementSignature !== targetSignature) { | ||
continue | ||
} | ||
|
||
replacementCandidate = structuredClone(replacementCandidate) | ||
|
||
// Cache the result so we can re-use this work later | ||
baseReplacementsCache.get(designSystem).set(targetCandidateString, replacementCandidate) | ||
|
||
// Re-add the variants and important flag from the original candidate | ||
replacementCandidate.variants = candidate.variants | ||
replacementCandidate.important = candidate.important | ||
|
||
// Update the candidate with the new value | ||
Object.assign(candidate, replacementCandidate) | ||
|
||
// We will re-print the candidate to get the migrated candidate out | ||
return designSystem.printCandidate(candidate) | ||
} | ||
} | ||
|
||
return rawCandidate | ||
|
||
function* tryReplacements( | ||
targetSignature: string, | ||
candidate: Extract<Candidate, { kind: 'functional' }>, | ||
): Generator<Candidate> { | ||
// Find a corresponding utility for the same signature | ||
let replacements = utilities.get(targetSignature) | ||
|
||
// Multiple utilities can map to the same signature. Not sure how to migrate | ||
// this one so let's just skip it for now. | ||
// | ||
// TODO: Do we just migrate to the first one? | ||
if (replacements.length > 1) return | ||
|
||
// If we didn't find any replacement utilities, let's try to strip the | ||
// modifier and find a replacement then. If we do, we can try to re-add the | ||
// modifier later and verify if we have a valid migration. | ||
// | ||
// This is necessary because `text-red-500/50` will not be pre-computed, | ||
// only `text-red-500` will. | ||
if (replacements.length === 0 && candidate.modifier) { | ||
let candidateWithoutModifier = { ...candidate, modifier: null } | ||
let targetSignatureWithoutModifier = signatures.get( | ||
designSystem.printCandidate(candidateWithoutModifier), | ||
) | ||
if (typeof targetSignatureWithoutModifier === 'string') { | ||
for (let replacementCandidate of tryReplacements( | ||
targetSignatureWithoutModifier, | ||
candidateWithoutModifier, | ||
)) { | ||
yield Object.assign({}, replacementCandidate, { modifier: candidate.modifier }) | ||
} | ||
} | ||
} | ||
|
||
// If only a single utility maps to the signature, we can use that as the | ||
// replacement. | ||
if (replacements.length === 1) { | ||
for (let replacementCandidate of parseCandidate(designSystem, replacements[0])) { | ||
yield replacementCandidate | ||
} | ||
} | ||
} | ||
} | ||
|
||
function parseCandidate(designSystem: DesignSystem, input: string) { | ||
return designSystem.parseCandidate( | ||
designSystem.theme.prefix && !input.startsWith(`${designSystem.theme.prefix}:`) | ||
? `${designSystem.theme.prefix}:${input}` | ||
: input, | ||
) | ||
} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we have exactly this implementation of parseCandidate in some other migration? I feel like I've seen this before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, I copied this whole file and deleted what I didn't need. Can move this to the
candidates.ts
file but should we just call itparseCandidate
or something like prefixAwareParseCandidate`There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just
parseCandidate
is fine for now. Can improve the name if we feel it's necessary later.