@@ -31,7 +31,13 @@ const canonicalizeCandidateCache = new DefaultMap((ds: DesignSystem) => {
31
31
} )
32
32
} )
33
33
34
- const CANONICALIZATIONS = [ bgGradientToLinear , themeToVar , arbitraryUtilities , print ]
34
+ const CANONICALIZATIONS = [
35
+ bgGradientToLinear ,
36
+ themeToVar ,
37
+ arbitraryUtilities ,
38
+ bareValueUtilities ,
39
+ print ,
40
+ ]
35
41
36
42
function print ( designSystem : DesignSystem , rawCandidate : string ) : string {
37
43
for ( let candidate of designSystem . parseCandidate ( rawCandidate ) ) {
@@ -696,3 +702,112 @@ function allVariablesAreUsed(
696
702
697
703
return isSafeMigration
698
704
}
705
+
706
+ // ----
707
+
708
+ function bareValueUtilities ( designSystem : DesignSystem , rawCandidate : string ) : string {
709
+ let utilities = preComputedUtilities . get ( designSystem )
710
+ let signatures = computeUtilitySignature . get ( designSystem )
711
+
712
+ for ( let readonlyCandidate of designSystem . parseCandidate ( rawCandidate ) ) {
713
+ // We are only interested in bare value utilities
714
+ if ( readonlyCandidate . kind !== 'functional' || readonlyCandidate . value ?. kind !== 'named' ) {
715
+ continue
716
+ }
717
+
718
+ // The below logic makes use of mutation. Since candidates in the
719
+ // DesignSystem are cached, we can't mutate them directly.
720
+ let candidate = structuredClone ( readonlyCandidate ) as Writable < typeof readonlyCandidate >
721
+
722
+ // Create a basic stripped candidate without variants or important flag. We
723
+ // will re-add those later but they are irrelevant for what we are trying to
724
+ // do here (and will increase cache hits because we only have to deal with
725
+ // the base utility, nothing more).
726
+ let targetCandidate = baseCandidate ( candidate )
727
+
728
+ let targetCandidateString = designSystem . printCandidate ( targetCandidate )
729
+ if ( baseReplacementsCache . get ( designSystem ) . has ( targetCandidateString ) ) {
730
+ let target = structuredClone (
731
+ baseReplacementsCache . get ( designSystem ) . get ( targetCandidateString ) ! ,
732
+ )
733
+ // Re-add the variants and important flag from the original candidate
734
+ target . variants = candidate . variants
735
+ target . important = candidate . important
736
+
737
+ return designSystem . printCandidate ( target )
738
+ }
739
+
740
+ // Compute the signature for the target candidate
741
+ let targetSignature = signatures . get ( targetCandidateString )
742
+ if ( typeof targetSignature !== 'string' ) continue
743
+
744
+ // Try a few options to find a suitable replacement utility
745
+ for ( let replacementCandidate of tryReplacements ( targetSignature , targetCandidate ) ) {
746
+ let replacementString = designSystem . printCandidate ( replacementCandidate )
747
+ let replacementSignature = signatures . get ( replacementString )
748
+ if ( replacementSignature !== targetSignature ) {
749
+ continue
750
+ }
751
+
752
+ replacementCandidate = structuredClone ( replacementCandidate )
753
+
754
+ // Cache the result so we can re-use this work later
755
+ baseReplacementsCache . get ( designSystem ) . set ( targetCandidateString , replacementCandidate )
756
+
757
+ // Re-add the variants and important flag from the original candidate
758
+ replacementCandidate . variants = candidate . variants
759
+ replacementCandidate . important = candidate . important
760
+
761
+ // Update the candidate with the new value
762
+ Object . assign ( candidate , replacementCandidate )
763
+
764
+ // We will re-print the candidate to get the migrated candidate out
765
+ return designSystem . printCandidate ( candidate )
766
+ }
767
+ }
768
+
769
+ return rawCandidate
770
+
771
+ function * tryReplacements (
772
+ targetSignature : string ,
773
+ candidate : Extract < Candidate , { kind : 'functional' } > ,
774
+ ) : Generator < Candidate > {
775
+ // Find a corresponding utility for the same signature
776
+ let replacements = utilities . get ( targetSignature )
777
+
778
+ // Multiple utilities can map to the same signature. Not sure how to migrate
779
+ // this one so let's just skip it for now.
780
+ //
781
+ // TODO: Do we just migrate to the first one?
782
+ if ( replacements . length > 1 ) return
783
+
784
+ // If we didn't find any replacement utilities, let's try to strip the
785
+ // modifier and find a replacement then. If we do, we can try to re-add the
786
+ // modifier later and verify if we have a valid migration.
787
+ //
788
+ // This is necessary because `text-red-500/50` will not be pre-computed,
789
+ // only `text-red-500` will.
790
+ if ( replacements . length === 0 && candidate . modifier ) {
791
+ let candidateWithoutModifier = { ...candidate , modifier : null }
792
+ let targetSignatureWithoutModifier = signatures . get (
793
+ designSystem . printCandidate ( candidateWithoutModifier ) ,
794
+ )
795
+ if ( typeof targetSignatureWithoutModifier === 'string' ) {
796
+ for ( let replacementCandidate of tryReplacements (
797
+ targetSignatureWithoutModifier ,
798
+ candidateWithoutModifier ,
799
+ ) ) {
800
+ yield Object . assign ( { } , replacementCandidate , { modifier : candidate . modifier } )
801
+ }
802
+ }
803
+ }
804
+
805
+ // If only a single utility maps to the signature, we can use that as the
806
+ // replacement.
807
+ if ( replacements . length === 1 ) {
808
+ for ( let replacementCandidate of parseCandidate ( designSystem , replacements [ 0 ] ) ) {
809
+ yield replacementCandidate
810
+ }
811
+ }
812
+ }
813
+ }
0 commit comments