|
| 1 | +import { type Candidate } from '../../../../tailwindcss/src/candidate' |
| 2 | +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' |
| 3 | +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' |
| 4 | +import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' |
| 5 | +import type { Writable } from '../../utils/types' |
| 6 | +import { baseCandidate, parseCandidate } from './candidates' |
| 7 | +import { computeUtilitySignature, preComputedUtilities } from './signatures' |
| 8 | + |
| 9 | +const baseReplacementsCache = new DefaultMap<DesignSystem, Map<string, Candidate>>( |
| 10 | + () => new Map<string, Candidate>(), |
| 11 | +) |
| 12 | + |
| 13 | +export function migrateBareValueUtilities( |
| 14 | + designSystem: DesignSystem, |
| 15 | + _userConfig: Config | null, |
| 16 | + rawCandidate: string, |
| 17 | +): string { |
| 18 | + let utilities = preComputedUtilities.get(designSystem) |
| 19 | + let signatures = computeUtilitySignature.get(designSystem) |
| 20 | + |
| 21 | + for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) { |
| 22 | + // We are only interested in bare value utilities |
| 23 | + if (readonlyCandidate.kind !== 'functional' || readonlyCandidate.value?.kind !== 'named') { |
| 24 | + continue |
| 25 | + } |
| 26 | + |
| 27 | + // The below logic makes use of mutation. Since candidates in the |
| 28 | + // DesignSystem are cached, we can't mutate them directly. |
| 29 | + let candidate = structuredClone(readonlyCandidate) as Writable<typeof readonlyCandidate> |
| 30 | + |
| 31 | + // Create a basic stripped candidate without variants or important flag. We |
| 32 | + // will re-add those later but they are irrelevant for what we are trying to |
| 33 | + // do here (and will increase cache hits because we only have to deal with |
| 34 | + // the base utility, nothing more). |
| 35 | + let targetCandidate = baseCandidate(candidate) |
| 36 | + |
| 37 | + let targetCandidateString = designSystem.printCandidate(targetCandidate) |
| 38 | + if (baseReplacementsCache.get(designSystem).has(targetCandidateString)) { |
| 39 | + let target = structuredClone( |
| 40 | + baseReplacementsCache.get(designSystem).get(targetCandidateString)!, |
| 41 | + ) |
| 42 | + // Re-add the variants and important flag from the original candidate |
| 43 | + target.variants = candidate.variants |
| 44 | + target.important = candidate.important |
| 45 | + |
| 46 | + return designSystem.printCandidate(target) |
| 47 | + } |
| 48 | + |
| 49 | + // Compute the signature for the target candidate |
| 50 | + let targetSignature = signatures.get(targetCandidateString) |
| 51 | + if (typeof targetSignature !== 'string') continue |
| 52 | + |
| 53 | + // Try a few options to find a suitable replacement utility |
| 54 | + for (let replacementCandidate of tryReplacements(targetSignature, targetCandidate)) { |
| 55 | + let replacementString = designSystem.printCandidate(replacementCandidate) |
| 56 | + let replacementSignature = signatures.get(replacementString) |
| 57 | + if (replacementSignature !== targetSignature) { |
| 58 | + continue |
| 59 | + } |
| 60 | + |
| 61 | + replacementCandidate = structuredClone(replacementCandidate) |
| 62 | + |
| 63 | + // Cache the result so we can re-use this work later |
| 64 | + baseReplacementsCache.get(designSystem).set(targetCandidateString, replacementCandidate) |
| 65 | + |
| 66 | + // Re-add the variants and important flag from the original candidate |
| 67 | + replacementCandidate.variants = candidate.variants |
| 68 | + replacementCandidate.important = candidate.important |
| 69 | + |
| 70 | + // Update the candidate with the new value |
| 71 | + Object.assign(candidate, replacementCandidate) |
| 72 | + |
| 73 | + // We will re-print the candidate to get the migrated candidate out |
| 74 | + return designSystem.printCandidate(candidate) |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + return rawCandidate |
| 79 | + |
| 80 | + function* tryReplacements( |
| 81 | + targetSignature: string, |
| 82 | + candidate: Extract<Candidate, { kind: 'functional' }>, |
| 83 | + ): Generator<Candidate> { |
| 84 | + // Find a corresponding utility for the same signature |
| 85 | + let replacements = utilities.get(targetSignature) |
| 86 | + |
| 87 | + // Multiple utilities can map to the same signature. Not sure how to migrate |
| 88 | + // this one so let's just skip it for now. |
| 89 | + // |
| 90 | + // TODO: Do we just migrate to the first one? |
| 91 | + if (replacements.length > 1) return |
| 92 | + |
| 93 | + // If we didn't find any replacement utilities, let's try to strip the |
| 94 | + // modifier and find a replacement then. If we do, we can try to re-add the |
| 95 | + // modifier later and verify if we have a valid migration. |
| 96 | + // |
| 97 | + // This is necessary because `text-red-500/50` will not be pre-computed, |
| 98 | + // only `text-red-500` will. |
| 99 | + if (replacements.length === 0 && candidate.modifier) { |
| 100 | + let candidateWithoutModifier = { ...candidate, modifier: null } |
| 101 | + let targetSignatureWithoutModifier = signatures.get( |
| 102 | + designSystem.printCandidate(candidateWithoutModifier), |
| 103 | + ) |
| 104 | + if (typeof targetSignatureWithoutModifier === 'string') { |
| 105 | + for (let replacementCandidate of tryReplacements( |
| 106 | + targetSignatureWithoutModifier, |
| 107 | + candidateWithoutModifier, |
| 108 | + )) { |
| 109 | + yield Object.assign({}, replacementCandidate, { modifier: candidate.modifier }) |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + // If only a single utility maps to the signature, we can use that as the |
| 115 | + // replacement. |
| 116 | + if (replacements.length === 1) { |
| 117 | + for (let replacementCandidate of parseCandidate(designSystem, replacements[0])) { |
| 118 | + yield replacementCandidate |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | +} |
0 commit comments