@@ -484,6 +484,10 @@ function isObject(value) {
484
484
return typeof value === 'object' && value !== null
485
485
}
486
486
487
+ function isPlainObject ( value ) {
488
+ return isObject ( value ) && ! Array . isArray ( value )
489
+ }
490
+
487
491
function isEmpty ( obj ) {
488
492
return Object . keys ( obj ) . length === 0
489
493
}
@@ -687,6 +691,8 @@ module.exports = (pluginOptions = {}) => {
687
691
return postcss ( [
688
692
// substituteTailwindAtRules
689
693
function ( root ) {
694
+ let applyCandidates = new Set ( )
695
+
690
696
// Make sure this file contains Tailwind directives. If not, we can save
691
697
// a lot of work and bail early. Also we don't have to register our touch
692
698
// file as a dependency since the output of this CSS does not depend on
@@ -719,6 +725,15 @@ module.exports = (pluginOptions = {}) => {
719
725
}
720
726
} )
721
727
728
+ // Collect all @apply rules and candidates
729
+ let applies = [ ]
730
+ root . walkAtRules ( 'apply' , ( rule ) => {
731
+ for ( let util of rule . params . split ( / [ \s \t \n ] + / g) ) {
732
+ applyCandidates . add ( util )
733
+ }
734
+ applies . push ( rule )
735
+ } )
736
+
722
737
if ( ! foundTailwind ) {
723
738
return root
724
739
}
@@ -767,6 +782,113 @@ module.exports = (pluginOptions = {}) => {
767
782
candidates ,
768
783
context
769
784
)
785
+
786
+ // Start the @apply process if we have rules with @apply in them
787
+ if ( applies . length > 0 ) {
788
+ // Fill up some caches!
789
+ generateRules ( context . tailwindConfig , applyCandidates , context )
790
+
791
+ /**
792
+ * When we have an apply like this:
793
+ *
794
+ * .abc {
795
+ * @apply hover:font-bold;
796
+ * }
797
+ *
798
+ * What we essentially will do is resolve to this:
799
+ *
800
+ * .abc {
801
+ * @apply .hover\:font-bold:hover {
802
+ * font-weight: 500;
803
+ * }
804
+ * }
805
+ *
806
+ * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
807
+ * What happens in this function is that we prepend a `.` and escape the candidate.
808
+ * This will result in `.hover\:font-bold`
809
+ * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
810
+ */
811
+ // TODO: Should we use postcss-selector-parser for this instead?
812
+ function replaceSelector ( selector , utilitySelector , candidate ) {
813
+ return selector
814
+ . split ( / \s * , \s * / g)
815
+ . map ( ( s ) => utilitySelector . replace ( `.${ escape ( candidate ) } ` , s ) )
816
+ . join ( ', ' )
817
+ }
818
+
819
+ function updateSelectors ( rule , apply , candidate ) {
820
+ return rule . map ( ( [ selector , rule ] ) => {
821
+ if ( ! isPlainObject ( rule ) ) {
822
+ return [ selector , updateSelectors ( rule , apply , candidate ) ]
823
+ }
824
+ return [ replaceSelector ( apply . parent . selector , selector , candidate ) , rule ]
825
+ } )
826
+ }
827
+
828
+ for ( let apply of applies ) {
829
+ let siblings = [ ]
830
+ let applyCandidates = apply . params . split ( / [ \s \t \n ] + / g)
831
+ for ( let applyCandidate of applyCandidates ) {
832
+ // TODO: Check for user css rules?
833
+ if ( ! context . classCache . has ( applyCandidate ) ) {
834
+ throw new Error ( 'Utility does not exist!' )
835
+ }
836
+
837
+ let [ layerName , rules ] = context . classCache . get ( applyCandidate )
838
+ for ( let [ sort , [ selector , rule ] ] of rules ) {
839
+ // Nested rules...
840
+ if ( ! isPlainObject ( rule ) ) {
841
+ siblings . push ( [
842
+ sort ,
843
+ toPostCssNode (
844
+ [ selector , updateSelectors ( rule , apply , applyCandidate ) ] ,
845
+ context . postCssNodeCache
846
+ ) ,
847
+ ] )
848
+ } else {
849
+ let appliedSelector = replaceSelector (
850
+ apply . parent . selector ,
851
+ selector ,
852
+ applyCandidate
853
+ )
854
+
855
+ if ( appliedSelector !== apply . parent . selector ) {
856
+ siblings . push ( [
857
+ sort ,
858
+ toPostCssNode (
859
+ [
860
+ replaceSelector ( apply . parent . selector , selector , applyCandidate ) ,
861
+ rule ,
862
+ ] ,
863
+ context . postCssNodeCache
864
+ ) ,
865
+ ] )
866
+ continue
867
+ }
868
+
869
+ // Add declarations directly
870
+ for ( let property in rule ) {
871
+ apply . before ( postcss . decl ( { prop : property , value : rule [ property ] } ) )
872
+ }
873
+ }
874
+ }
875
+ }
876
+
877
+ // Inject the rules, sorted, correctly
878
+ for ( let [ sort , sibling ] of siblings . sort ( ( [ a ] , [ z ] ) => Math . sign ( Number ( z - a ) ) ) ) {
879
+ // `apply.parent` is refering to the node at `.abc` in: .abc { @apply mt-2 }
880
+ apply . parent . after ( sibling )
881
+ }
882
+
883
+ // If there are left-over declarations, just remove the @apply
884
+ if ( apply . parent . nodes . length > 1 ) {
885
+ apply . remove ( )
886
+ } else {
887
+ // The node is empty, drop the full node
888
+ apply . parent . remove ( )
889
+ }
890
+ }
891
+ }
770
892
env . DEBUG && console . timeEnd ( 'Generate rules' )
771
893
772
894
// We only ever add to the classCache, so if it didn't grow, there is nothing new.
0 commit comments