tailwindcss 0.0.0-insiders.ea139f2 → 0.0.0-insiders.ea4e1cd

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/LICENSE +1 -2
  2. package/README.md +15 -7
  3. package/colors.d.ts +3 -0
  4. package/colors.js +2 -1
  5. package/defaultConfig.d.ts +3 -0
  6. package/defaultConfig.js +2 -1
  7. package/defaultTheme.d.ts +4 -0
  8. package/defaultTheme.js +2 -1
  9. package/lib/cli/build/deps.js +62 -0
  10. package/lib/cli/build/index.js +54 -0
  11. package/lib/cli/build/plugin.js +378 -0
  12. package/lib/cli/build/utils.js +88 -0
  13. package/lib/cli/build/watching.js +182 -0
  14. package/lib/cli/help/index.js +73 -0
  15. package/lib/cli/index.js +230 -0
  16. package/lib/cli/init/index.js +63 -0
  17. package/lib/cli-peer-dependencies.js +28 -7
  18. package/lib/cli.js +4 -703
  19. package/lib/corePluginList.js +12 -3
  20. package/lib/corePlugins.js +2373 -1863
  21. package/lib/css/preflight.css +10 -8
  22. package/lib/featureFlags.js +49 -26
  23. package/lib/index.js +1 -31
  24. package/lib/lib/cacheInvalidation.js +92 -0
  25. package/lib/lib/collapseAdjacentRules.js +30 -10
  26. package/lib/lib/collapseDuplicateDeclarations.js +60 -4
  27. package/lib/lib/content.js +181 -0
  28. package/lib/lib/defaultExtractor.js +243 -0
  29. package/lib/lib/detectNesting.js +21 -10
  30. package/lib/lib/evaluateTailwindFunctions.js +115 -50
  31. package/lib/lib/expandApplyAtRules.js +467 -161
  32. package/lib/lib/expandTailwindAtRules.js +160 -133
  33. package/lib/lib/findAtConfigPath.js +46 -0
  34. package/lib/lib/generateRules.js +553 -200
  35. package/lib/lib/getModuleDependencies.js +88 -37
  36. package/lib/lib/load-config.js +42 -0
  37. package/lib/lib/normalizeTailwindDirectives.js +46 -33
  38. package/lib/lib/offsets.js +306 -0
  39. package/lib/lib/partitionApplyAtRules.js +58 -0
  40. package/lib/lib/regex.js +74 -0
  41. package/lib/lib/remap-bitfield.js +89 -0
  42. package/lib/lib/resolveDefaultsAtRules.js +98 -58
  43. package/lib/lib/setupContextUtils.js +773 -321
  44. package/lib/lib/setupTrackingContext.js +70 -75
  45. package/lib/lib/sharedState.js +78 -10
  46. package/lib/lib/substituteScreenAtRules.js +14 -10
  47. package/lib/oxide/cli/build/deps.js +89 -0
  48. package/lib/oxide/cli/build/index.js +53 -0
  49. package/lib/oxide/cli/build/plugin.js +375 -0
  50. package/lib/oxide/cli/build/utils.js +87 -0
  51. package/lib/oxide/cli/build/watching.js +179 -0
  52. package/lib/oxide/cli/help/index.js +72 -0
  53. package/lib/oxide/cli/index.js +214 -0
  54. package/lib/oxide/cli/init/index.js +52 -0
  55. package/lib/oxide/cli.js +5 -0
  56. package/lib/oxide/postcss-plugin.js +2 -0
  57. package/lib/plugin.js +98 -0
  58. package/{nesting → lib/postcss-plugins/nesting}/README.md +2 -2
  59. package/lib/postcss-plugins/nesting/index.js +21 -0
  60. package/lib/postcss-plugins/nesting/plugin.js +89 -0
  61. package/lib/processTailwindFeatures.js +39 -26
  62. package/lib/public/colors.js +272 -246
  63. package/lib/public/create-plugin.js +9 -5
  64. package/lib/public/default-config.js +10 -6
  65. package/lib/public/default-theme.js +10 -6
  66. package/lib/public/load-config.js +12 -0
  67. package/lib/public/resolve-config.js +11 -6
  68. package/lib/util/applyImportantSelector.js +36 -0
  69. package/lib/util/bigSign.js +6 -1
  70. package/lib/util/buildMediaQuery.js +13 -6
  71. package/lib/util/cloneDeep.js +9 -6
  72. package/lib/util/cloneNodes.js +23 -3
  73. package/lib/util/color.js +70 -38
  74. package/lib/util/colorNames.js +752 -0
  75. package/lib/util/configurePlugins.js +7 -2
  76. package/lib/util/createPlugin.js +8 -6
  77. package/lib/util/createUtilityPlugin.js +16 -16
  78. package/lib/util/dataTypes.js +173 -108
  79. package/lib/util/defaults.js +14 -3
  80. package/lib/util/escapeClassName.js +13 -8
  81. package/lib/util/escapeCommas.js +7 -2
  82. package/lib/util/flattenColorPalette.js +11 -12
  83. package/lib/util/formatVariantSelector.js +228 -151
  84. package/lib/util/getAllConfigs.js +33 -12
  85. package/lib/util/hashConfig.js +9 -4
  86. package/lib/util/isKeyframeRule.js +7 -2
  87. package/lib/util/isPlainObject.js +7 -2
  88. package/lib/util/{isValidArbitraryValue.js → isSyntacticallyValidPropertyValue.js} +25 -15
  89. package/lib/util/log.js +27 -13
  90. package/lib/util/nameClass.js +27 -10
  91. package/lib/util/negateValue.js +25 -8
  92. package/lib/util/normalizeConfig.js +139 -65
  93. package/lib/util/normalizeScreens.js +131 -11
  94. package/lib/util/parseAnimationValue.js +44 -40
  95. package/lib/util/parseBoxShadowValue.js +34 -23
  96. package/lib/util/parseDependency.js +39 -55
  97. package/lib/util/parseGlob.js +36 -0
  98. package/lib/util/parseObjectStyles.js +15 -10
  99. package/lib/util/pluginUtils.js +159 -69
  100. package/lib/util/prefixSelector.js +30 -12
  101. package/lib/util/pseudoElements.js +229 -0
  102. package/lib/util/removeAlphaVariables.js +31 -0
  103. package/lib/util/resolveConfig.js +97 -75
  104. package/lib/util/resolveConfigPath.js +30 -12
  105. package/lib/util/responsive.js +11 -6
  106. package/lib/util/splitAtTopLevelOnly.js +51 -0
  107. package/lib/util/tap.js +6 -1
  108. package/lib/util/toColorValue.js +7 -3
  109. package/lib/util/toPath.js +26 -3
  110. package/lib/util/transformThemeValue.js +40 -30
  111. package/lib/util/validateConfig.js +37 -0
  112. package/lib/util/validateFormalSyntax.js +26 -0
  113. package/lib/util/withAlphaVariable.js +27 -15
  114. package/loadConfig.d.ts +4 -0
  115. package/loadConfig.js +2 -0
  116. package/nesting/index.js +2 -12
  117. package/package.json +66 -57
  118. package/peers/index.js +75964 -55560
  119. package/plugin.d.ts +11 -0
  120. package/plugin.js +2 -1
  121. package/resolveConfig.d.ts +12 -0
  122. package/resolveConfig.js +2 -1
  123. package/scripts/generate-types.js +105 -0
  124. package/scripts/release-channel.js +18 -0
  125. package/scripts/release-notes.js +21 -0
  126. package/scripts/swap-engines.js +40 -0
  127. package/scripts/type-utils.js +27 -0
  128. package/src/cli/build/deps.js +56 -0
  129. package/src/cli/build/index.js +49 -0
  130. package/src/cli/build/plugin.js +444 -0
  131. package/src/cli/build/utils.js +76 -0
  132. package/src/cli/build/watching.js +229 -0
  133. package/src/cli/help/index.js +70 -0
  134. package/src/cli/index.js +216 -0
  135. package/src/cli/init/index.js +79 -0
  136. package/src/cli-peer-dependencies.js +7 -1
  137. package/src/cli.js +4 -765
  138. package/src/corePluginList.js +1 -1
  139. package/src/corePlugins.js +786 -306
  140. package/src/css/preflight.css +10 -8
  141. package/src/featureFlags.js +21 -5
  142. package/src/index.js +1 -34
  143. package/src/lib/cacheInvalidation.js +52 -0
  144. package/src/lib/collapseAdjacentRules.js +21 -2
  145. package/src/lib/collapseDuplicateDeclarations.js +66 -1
  146. package/src/lib/content.js +208 -0
  147. package/src/lib/defaultExtractor.js +217 -0
  148. package/src/lib/detectNesting.js +9 -1
  149. package/src/lib/evaluateTailwindFunctions.js +79 -8
  150. package/src/lib/expandApplyAtRules.js +515 -153
  151. package/src/lib/expandTailwindAtRules.js +115 -86
  152. package/src/lib/findAtConfigPath.js +48 -0
  153. package/src/lib/generateRules.js +545 -147
  154. package/src/lib/getModuleDependencies.js +70 -30
  155. package/src/lib/load-config.ts +31 -0
  156. package/src/lib/normalizeTailwindDirectives.js +7 -1
  157. package/src/lib/offsets.js +373 -0
  158. package/src/lib/partitionApplyAtRules.js +52 -0
  159. package/src/lib/regex.js +74 -0
  160. package/src/lib/remap-bitfield.js +82 -0
  161. package/src/lib/resolveDefaultsAtRules.js +59 -17
  162. package/src/lib/setupContextUtils.js +701 -175
  163. package/src/lib/setupTrackingContext.js +51 -62
  164. package/src/lib/sharedState.js +58 -7
  165. package/src/oxide/cli/build/deps.ts +91 -0
  166. package/src/oxide/cli/build/index.ts +47 -0
  167. package/src/oxide/cli/build/plugin.ts +442 -0
  168. package/src/oxide/cli/build/utils.ts +74 -0
  169. package/src/oxide/cli/build/watching.ts +225 -0
  170. package/src/oxide/cli/help/index.ts +69 -0
  171. package/src/oxide/cli/index.ts +204 -0
  172. package/src/oxide/cli/init/index.ts +59 -0
  173. package/src/oxide/cli.ts +1 -0
  174. package/src/oxide/postcss-plugin.ts +1 -0
  175. package/src/plugin.js +107 -0
  176. package/src/postcss-plugins/nesting/README.md +42 -0
  177. package/src/postcss-plugins/nesting/index.js +13 -0
  178. package/src/postcss-plugins/nesting/plugin.js +80 -0
  179. package/src/processTailwindFeatures.js +12 -2
  180. package/src/public/colors.js +22 -0
  181. package/src/public/default-config.js +1 -1
  182. package/src/public/default-theme.js +2 -2
  183. package/src/public/load-config.js +2 -0
  184. package/src/util/applyImportantSelector.js +27 -0
  185. package/src/util/buildMediaQuery.js +5 -3
  186. package/src/util/cloneNodes.js +19 -2
  187. package/src/util/color.js +44 -12
  188. package/src/util/colorNames.js +150 -0
  189. package/src/util/dataTypes.js +51 -16
  190. package/src/util/defaults.js +6 -0
  191. package/src/util/formatVariantSelector.js +264 -144
  192. package/src/util/getAllConfigs.js +21 -2
  193. package/src/util/{isValidArbitraryValue.js → isSyntacticallyValidPropertyValue.js} +1 -1
  194. package/src/util/log.js +11 -7
  195. package/src/util/nameClass.js +4 -0
  196. package/src/util/negateValue.js +11 -3
  197. package/src/util/normalizeConfig.js +57 -5
  198. package/src/util/normalizeScreens.js +105 -7
  199. package/src/util/parseBoxShadowValue.js +4 -3
  200. package/src/util/parseDependency.js +37 -42
  201. package/src/util/parseGlob.js +24 -0
  202. package/src/util/pluginUtils.js +123 -24
  203. package/src/util/prefixSelector.js +30 -10
  204. package/src/util/pseudoElements.js +170 -0
  205. package/src/util/removeAlphaVariables.js +24 -0
  206. package/src/util/resolveConfig.js +74 -26
  207. package/src/util/resolveConfigPath.js +12 -1
  208. package/src/util/splitAtTopLevelOnly.js +52 -0
  209. package/src/util/toPath.js +23 -1
  210. package/src/util/transformThemeValue.js +13 -3
  211. package/src/util/validateConfig.js +26 -0
  212. package/src/util/validateFormalSyntax.js +34 -0
  213. package/src/util/withAlphaVariable.js +1 -1
  214. package/stubs/.gitignore +1 -0
  215. package/stubs/.prettierrc.json +6 -0
  216. package/stubs/{defaultConfig.stub.js → config.full.js} +206 -166
  217. package/stubs/postcss.config.js +6 -0
  218. package/stubs/tailwind.config.cjs +2 -0
  219. package/stubs/tailwind.config.js +2 -0
  220. package/stubs/tailwind.config.ts +3 -0
  221. package/types/config.d.ts +368 -0
  222. package/types/generated/.gitkeep +0 -0
  223. package/types/generated/colors.d.ts +298 -0
  224. package/types/generated/corePluginList.d.ts +1 -0
  225. package/types/generated/default-theme.d.ts +371 -0
  226. package/types/index.d.ts +7 -0
  227. package/CHANGELOG.md +0 -1843
  228. package/lib/constants.js +0 -37
  229. package/lib/lib/setupWatchingContext.js +0 -288
  230. package/nesting/plugin.js +0 -41
  231. package/scripts/install-integrations.js +0 -27
  232. package/scripts/rebuildFixtures.js +0 -68
  233. package/src/constants.js +0 -17
  234. package/src/lib/setupWatchingContext.js +0 -311
  235. /package/stubs/{simpleConfig.stub.js → config.simple.js} +0 -0
  236. /package/stubs/{defaultPostCssConfig.stub.js → postcss.config.cjs} +0 -0
@@ -3,18 +3,27 @@ import selectorParser from 'postcss-selector-parser'
3
3
  import parseObjectStyles from '../util/parseObjectStyles'
4
4
  import isPlainObject from '../util/isPlainObject'
5
5
  import prefixSelector from '../util/prefixSelector'
6
- import { updateAllClasses } from '../util/pluginUtils'
6
+ import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
7
7
  import log from '../util/log'
8
- import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
8
+ import * as sharedState from './sharedState'
9
+ import {
10
+ formatVariantSelector,
11
+ finalizeSelector,
12
+ eliminateIrrelevantSelectors,
13
+ } from '../util/formatVariantSelector'
9
14
  import { asClass } from '../util/nameClass'
10
15
  import { normalize } from '../util/dataTypes'
11
- import isValidArbitraryValue from '../util/isValidArbitraryValue'
16
+ import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
17
+ import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
18
+ import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
19
+ import { flagEnabled } from '../featureFlags'
20
+ import { applyImportantSelector } from '../util/applyImportantSelector'
12
21
 
13
22
  let classNameParser = selectorParser((selectors) => {
14
23
  return selectors.first.filter(({ type }) => type === 'class').pop().value
15
24
  })
16
25
 
17
- function getClassNameFromSelector(selector) {
26
+ export function getClassNameFromSelector(selector) {
18
27
  return classNameParser.transformSync(selector)
19
28
  }
20
29
 
@@ -25,33 +34,49 @@ function getClassNameFromSelector(selector) {
25
34
  // Example with dynamic classes:
26
35
  // ['grid-cols', '[[linename],1fr,auto]']
27
36
  // ['grid', 'cols-[[linename],1fr,auto]']
28
- function* candidatePermutations(candidate, lastIndex = Infinity) {
29
- if (lastIndex < 0) {
30
- return
31
- }
32
-
33
- let dashIdx
34
-
35
- if (lastIndex === Infinity && candidate.endsWith(']')) {
36
- let bracketIdx = candidate.indexOf('[')
37
+ function* candidatePermutations(candidate) {
38
+ let lastIndex = Infinity
39
+
40
+ while (lastIndex >= 0) {
41
+ let dashIdx
42
+ let wasSlash = false
43
+
44
+ if (lastIndex === Infinity && candidate.endsWith(']')) {
45
+ let bracketIdx = candidate.indexOf('[')
46
+
47
+ // If character before `[` isn't a dash or a slash, this isn't a dynamic class
48
+ // eg. string[]
49
+ if (candidate[bracketIdx - 1] === '-') {
50
+ dashIdx = bracketIdx - 1
51
+ } else if (candidate[bracketIdx - 1] === '/') {
52
+ dashIdx = bracketIdx - 1
53
+ wasSlash = true
54
+ } else {
55
+ dashIdx = -1
56
+ }
57
+ } else if (lastIndex === Infinity && candidate.includes('/')) {
58
+ dashIdx = candidate.lastIndexOf('/')
59
+ wasSlash = true
60
+ } else {
61
+ dashIdx = candidate.lastIndexOf('-', lastIndex)
62
+ }
37
63
 
38
- // If character before `[` isn't a dash or a slash, this isn't a dynamic class
39
- // eg. string[]
40
- dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
41
- } else {
42
- dashIdx = candidate.lastIndexOf('-', lastIndex)
43
- }
64
+ if (dashIdx < 0) {
65
+ break
66
+ }
44
67
 
45
- if (dashIdx < 0) {
46
- return
47
- }
68
+ let prefix = candidate.slice(0, dashIdx)
69
+ let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1)
48
70
 
49
- let prefix = candidate.slice(0, dashIdx)
50
- let modifier = candidate.slice(dashIdx + 1)
71
+ lastIndex = dashIdx - 1
51
72
 
52
- yield [prefix, modifier]
73
+ // TODO: This feels a bit hacky
74
+ if (prefix === '' || modifier === '/') {
75
+ continue
76
+ }
53
77
 
54
- yield* candidatePermutations(candidate, dashIdx - 1)
78
+ yield [prefix, modifier]
79
+ }
55
80
  }
56
81
 
57
82
  function applyPrefix(matches, context) {
@@ -63,9 +88,23 @@ function applyPrefix(matches, context) {
63
88
  let [meta] = match
64
89
  if (meta.options.respectPrefix) {
65
90
  let container = postcss.root({ nodes: [match[1].clone()] })
91
+ let classCandidate = match[1].raws.tailwind.classCandidate
92
+
66
93
  container.walkRules((r) => {
67
- r.selector = prefixSelector(context.tailwindConfig.prefix, r.selector)
94
+ // If this is a negative utility with a dash *before* the prefix we
95
+ // have to ensure that the generated selector matches the candidate
96
+
97
+ // Not doing this will cause `-tw-top-1` to generate the class `.tw--top-1`
98
+ // The disconnect between candidate <-> class can cause @apply to hard crash.
99
+ let shouldPrependNegative = classCandidate.startsWith('-')
100
+
101
+ r.selector = prefixSelector(
102
+ context.tailwindConfig.prefix,
103
+ r.selector,
104
+ shouldPrependNegative
105
+ )
68
106
  })
107
+
69
108
  match[1] = container.nodes[0]
70
109
  }
71
110
  }
@@ -73,20 +112,32 @@ function applyPrefix(matches, context) {
73
112
  return matches
74
113
  }
75
114
 
76
- function applyImportant(matches) {
115
+ function applyImportant(matches, classCandidate) {
77
116
  if (matches.length === 0) {
78
117
  return matches
79
118
  }
119
+
80
120
  let result = []
81
121
 
82
122
  for (let [meta, rule] of matches) {
83
123
  let container = postcss.root({ nodes: [rule.clone()] })
124
+
84
125
  container.walkRules((r) => {
85
- r.selector = updateAllClasses(r.selector, (className) => {
86
- return `!${className}`
87
- })
126
+ let ast = selectorParser().astSync(r.selector)
127
+
128
+ // Remove extraneous selectors that do not include the base candidate
129
+ ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate))
130
+
131
+ // Update all instances of the base candidate to include the important marker
132
+ updateAllClasses(ast, (className) =>
133
+ className === classCandidate ? `!${className}` : className
134
+ )
135
+
136
+ r.selector = ast.toString()
137
+
88
138
  r.walkDecls((d) => (d.important = true))
89
139
  })
140
+
90
141
  result.push([{ ...meta, important: true }, container.nodes[0]])
91
142
  }
92
143
 
@@ -107,22 +158,100 @@ function applyVariant(variant, matches, context) {
107
158
  return matches
108
159
  }
109
160
 
161
+ /** @type {{modifier: string | null, value: string | null}} */
162
+ let args = { modifier: null, value: sharedState.NONE }
163
+
164
+ // Retrieve "modifier"
165
+ {
166
+ let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/')
167
+
168
+ // This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500`
169
+ // In this case 1/10 is a value but /20 is a modifier
170
+ if (modifiers.length > 1) {
171
+ baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/')
172
+ modifiers = modifiers.slice(-1)
173
+ }
174
+
175
+ if (modifiers.length && !context.variantMap.has(variant)) {
176
+ variant = baseVariant
177
+ args.modifier = modifiers[0]
178
+
179
+ if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
180
+ return []
181
+ }
182
+ }
183
+ }
184
+
185
+ // Retrieve "arbitrary value"
186
+ if (variant.endsWith(']') && !variant.startsWith('[')) {
187
+ // We either have:
188
+ // @[200px]
189
+ // group-[:hover]
190
+ //
191
+ // But we don't want:
192
+ // @-[200px] (`-` is incorrect)
193
+ // group[:hover] (`-` is missing)
194
+ let match = /(.)(-?)\[(.*)\]/g.exec(variant)
195
+ if (match) {
196
+ let [, char, seperator, value] = match
197
+ // @-[200px] case
198
+ if (char === '@' && seperator === '-') return []
199
+ // group[:hover] case
200
+ if (char !== '@' && seperator === '') return []
201
+
202
+ variant = variant.replace(`${seperator}[${value}]`, '')
203
+ args.value = value
204
+ }
205
+ }
206
+
207
+ // Register arbitrary variants
208
+ if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
209
+ let sort = context.offsets.recordVariant(variant)
210
+
211
+ let selector = normalize(variant.slice(1, -1))
212
+ let selectors = splitAtTopLevelOnly(selector, ',')
213
+
214
+ // We do not support multiple selectors for arbitrary variants
215
+ if (selectors.length > 1) {
216
+ return []
217
+ }
218
+
219
+ if (!selectors.every(isValidVariantFormatString)) {
220
+ return []
221
+ }
222
+
223
+ let records = selectors.map((sel, idx) => [
224
+ context.offsets.applyParallelOffset(sort, idx),
225
+ parseVariant(sel.trim()),
226
+ ])
227
+
228
+ context.variantMap.set(variant, records)
229
+ }
230
+
110
231
  if (context.variantMap.has(variant)) {
111
- let variantFunctionTuples = context.variantMap.get(variant)
232
+ let isArbitraryVariant = isArbitraryValue(variant)
233
+ let variantFunctionTuples = context.variantMap.get(variant).slice()
112
234
  let result = []
113
235
 
114
236
  for (let [meta, rule] of matches) {
237
+ // Don't generate variants for user css
238
+ if (meta.layer === 'user') {
239
+ continue
240
+ }
241
+
115
242
  let container = postcss.root({ nodes: [rule.clone()] })
116
243
 
117
- for (let [variantSort, variantFunction] of variantFunctionTuples) {
118
- let clone = container.clone()
244
+ for (let [variantSort, variantFunction, containerFromArray] of variantFunctionTuples) {
245
+ let clone = (containerFromArray ?? container).clone()
119
246
  let collectedFormats = []
120
247
 
121
- let originals = new Map()
122
-
123
248
  function prepareBackup() {
124
- if (originals.size > 0) return // Already prepared, chicken out
125
- clone.walkRules((rule) => originals.set(rule, rule.selector))
249
+ // Already prepared, chicken out
250
+ if (clone.raws.neededBackup) {
251
+ return
252
+ }
253
+ clone.raws.neededBackup = true
254
+ clone.walkRules((rule) => (rule.raws.originalSelector = rule.selector))
126
255
  }
127
256
 
128
257
  function modifySelectors(modifierFunction) {
@@ -162,25 +291,54 @@ function applyVariant(variant, matches, context) {
162
291
  clone.append(wrapper)
163
292
  },
164
293
  format(selectorFormat) {
165
- collectedFormats.push(selectorFormat)
294
+ collectedFormats.push({
295
+ format: selectorFormat,
296
+ isArbitraryVariant,
297
+ })
166
298
  },
299
+ args,
167
300
  })
168
301
 
302
+ // It can happen that a list of format strings is returned from within the function. In that
303
+ // case, we have to process them as well. We can use the existing `variantSort`.
304
+ if (Array.isArray(ruleWithVariant)) {
305
+ for (let [idx, variantFunction] of ruleWithVariant.entries()) {
306
+ // This is a little bit scary since we are pushing to an array of items that we are
307
+ // currently looping over. However, you can also think of it like a processing queue
308
+ // where you keep handling jobs until everything is done and each job can queue more
309
+ // jobs if needed.
310
+ variantFunctionTuples.push([
311
+ context.offsets.applyParallelOffset(variantSort, idx),
312
+ variantFunction,
313
+
314
+ // If the clone has been modified we have to pass that back
315
+ // though so each rule can use the modified container
316
+ clone.clone(),
317
+ ])
318
+ }
319
+ continue
320
+ }
321
+
169
322
  if (typeof ruleWithVariant === 'string') {
170
- collectedFormats.push(ruleWithVariant)
323
+ collectedFormats.push({
324
+ format: ruleWithVariant,
325
+ isArbitraryVariant,
326
+ })
171
327
  }
172
328
 
173
329
  if (ruleWithVariant === null) {
174
330
  continue
175
331
  }
176
332
 
177
- // We filled the `originals`, therefore we assume that somebody touched
333
+ // We had to backup selectors, therefore we assume that somebody touched
178
334
  // `container` or `modifySelectors`. Let's see if they did, so that we
179
335
  // can restore the selectors, and collect the format strings.
180
- if (originals.size > 0) {
336
+ if (clone.raws.neededBackup) {
337
+ delete clone.raws.neededBackup
181
338
  clone.walkRules((rule) => {
182
- if (!originals.has(rule)) return
183
- let before = originals.get(rule)
339
+ let before = rule.raws.originalSelector
340
+ if (!before) return
341
+ delete rule.raws.originalSelector
184
342
  if (before === rule.selector) return // No mutation happened
185
343
 
186
344
  let modified = rule.selector
@@ -206,15 +364,28 @@ function applyVariant(variant, matches, context) {
206
364
  // modified (by plugin): .foo .foo\\:markdown > p
207
365
  // rebuiltBase (internal): .foo\\:markdown > p
208
366
  // format: .foo &
209
- collectedFormats.push(modified.replace(rebuiltBase, '&'))
367
+ collectedFormats.push({
368
+ format: modified.replace(rebuiltBase, '&'),
369
+ isArbitraryVariant,
370
+ })
210
371
  rule.selector = before
211
372
  })
212
373
  }
213
374
 
375
+ // This tracks the originating layer for the variant
376
+ // For example:
377
+ // .sm:underline {} is a variant of something in the utilities layer
378
+ // .sm:container {} is a variant of the container component
379
+ clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer }
380
+
214
381
  let withOffset = [
215
382
  {
216
383
  ...meta,
217
- sort: variantSort | meta.sort,
384
+ sort: context.offsets.applyVariantOffset(
385
+ meta.sort,
386
+ variantSort,
387
+ Object.assign(args, context.variantOptions.get(variant))
388
+ ),
218
389
  collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
219
390
  },
220
391
  clone.nodes[0],
@@ -248,6 +419,61 @@ function parseRules(rule, cache, options = {}) {
248
419
  return [cache.get(rule), options]
249
420
  }
250
421
 
422
+ const IS_VALID_PROPERTY_NAME = /^[a-z_-]/
423
+
424
+ function isValidPropName(name) {
425
+ return IS_VALID_PROPERTY_NAME.test(name)
426
+ }
427
+
428
+ /**
429
+ * @param {string} declaration
430
+ * @returns {boolean}
431
+ */
432
+ function looksLikeUri(declaration) {
433
+ // Quick bailout for obvious non-urls
434
+ // This doesn't support schemes that don't use a leading // but that's unlikely to be a problem
435
+ if (!declaration.includes('://')) {
436
+ return false
437
+ }
438
+
439
+ try {
440
+ const url = new URL(declaration)
441
+ return url.scheme !== '' && url.host !== ''
442
+ } catch (err) {
443
+ // Definitely not a valid url
444
+ return false
445
+ }
446
+ }
447
+
448
+ function isParsableNode(node) {
449
+ let isParsable = true
450
+
451
+ node.walkDecls((decl) => {
452
+ if (!isParsableCssValue(decl.prop, decl.value)) {
453
+ isParsable = false
454
+ return false
455
+ }
456
+ })
457
+
458
+ return isParsable
459
+ }
460
+
461
+ function isParsableCssValue(property, value) {
462
+ // We don't want to to treat [https://example.com] as a custom property
463
+ // Even though, according to the CSS grammar, it's a totally valid CSS declaration
464
+ // So we short-circuit here by checking if the custom property looks like a url
465
+ if (looksLikeUri(`${property}:${value}`)) {
466
+ return false
467
+ }
468
+
469
+ try {
470
+ postcss.parse(`a{${property}:${value}}`).toResult()
471
+ return true
472
+ } catch (err) {
473
+ return false
474
+ }
475
+ }
476
+
251
477
  function extractArbitraryProperty(classCandidate, context) {
252
478
  let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? []
253
479
 
@@ -255,15 +481,25 @@ function extractArbitraryProperty(classCandidate, context) {
255
481
  return null
256
482
  }
257
483
 
484
+ if (!isValidPropName(property)) {
485
+ return null
486
+ }
487
+
488
+ if (!isValidArbitraryValue(value)) {
489
+ return null
490
+ }
491
+
258
492
  let normalized = normalize(value)
259
493
 
260
- if (!isValidArbitraryValue(normalized)) {
494
+ if (!isParsableCssValue(property, normalized)) {
261
495
  return null
262
496
  }
263
497
 
498
+ let sort = context.offsets.arbitraryProperty()
499
+
264
500
  return [
265
501
  [
266
- { sort: context.arbitraryPropertiesSort, layer: 'utilities' },
502
+ { sort, layer: 'utilities' },
267
503
  () => ({
268
504
  [asClass(classCandidate)]: {
269
505
  [property]: normalized,
@@ -290,7 +526,11 @@ function* resolveMatchedPlugins(classCandidate, context) {
290
526
  const twConfigPrefix = context.tailwindConfig.prefix
291
527
 
292
528
  const twConfigPrefixLen = twConfigPrefix.length
293
- if (candidatePrefix[twConfigPrefixLen] === '-') {
529
+
530
+ const hasMatchingPrefix =
531
+ candidatePrefix.startsWith(twConfigPrefix) || candidatePrefix.startsWith(`-${twConfigPrefix}`)
532
+
533
+ if (candidatePrefix[twConfigPrefixLen] === '-' && hasMatchingPrefix) {
294
534
  negative = true
295
535
  candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1)
296
536
  }
@@ -302,16 +542,31 @@ function* resolveMatchedPlugins(classCandidate, context) {
302
542
  for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) {
303
543
  if (context.candidateRuleMap.has(prefix)) {
304
544
  yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier]
305
- return
306
545
  }
307
546
  }
308
547
  }
309
548
 
310
549
  function splitWithSeparator(input, separator) {
311
- return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
550
+ if (input === sharedState.NOT_ON_DEMAND) {
551
+ return [sharedState.NOT_ON_DEMAND]
552
+ }
553
+
554
+ return splitAtTopLevelOnly(input, separator)
555
+ }
556
+
557
+ function* recordCandidates(matches, classCandidate) {
558
+ for (const match of matches) {
559
+ match[1].raws.tailwind = {
560
+ ...match[1].raws.tailwind,
561
+ classCandidate,
562
+ preserveSource: match[0].options?.preserveSource ?? false,
563
+ }
564
+
565
+ yield match
566
+ }
312
567
  }
313
568
 
314
- function* resolveMatches(candidate, context) {
569
+ function* resolveMatches(candidate, context, original = candidate) {
315
570
  let separator = context.tailwindConfig.separator
316
571
  let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
317
572
  let important = false
@@ -321,6 +576,15 @@ function* resolveMatches(candidate, context) {
321
576
  classCandidate = classCandidate.slice(1)
322
577
  }
323
578
 
579
+ if (flagEnabled(context.tailwindConfig, 'variantGrouping')) {
580
+ if (classCandidate.startsWith('(') && classCandidate.endsWith(')')) {
581
+ let base = variants.slice().reverse().join(separator)
582
+ for (let part of splitAtTopLevelOnly(classCandidate.slice(1, -1), ',')) {
583
+ yield* resolveMatches(base + separator + part, context, original)
584
+ }
585
+ }
586
+ }
587
+
324
588
  // TODO: Reintroduce this in ways that doesn't break on false positives
325
589
  // function sortAgainst(toSort, against) {
326
590
  // return toSort.slice().sort((a, z) => {
@@ -361,71 +625,144 @@ function* resolveMatches(candidate, context) {
361
625
  }
362
626
 
363
627
  if (matchesPerPlugin.length > 0) {
364
- typesByMatches.set(matchesPerPlugin, sort.options?.type)
628
+ let matchingTypes = Array.from(
629
+ getMatchingTypes(
630
+ sort.options?.types ?? [],
631
+ modifier,
632
+ sort.options ?? {},
633
+ context.tailwindConfig
634
+ )
635
+ ).map(([_, type]) => type)
636
+
637
+ if (matchingTypes.length > 0) {
638
+ typesByMatches.set(matchesPerPlugin, matchingTypes)
639
+ }
640
+
365
641
  matches.push(matchesPerPlugin)
366
642
  }
367
643
  }
368
644
 
369
- // Only keep the result of the very first plugin if we are dealing with
370
- // arbitrary values, to protect against ambiguity.
371
- if (isArbitraryValue(modifier) && matches.length > 1) {
372
- let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))
373
-
374
- // Remove duplicates, so that we can detect proper unique types for each plugin.
375
- for (let pluginTypes of typesPerPlugin) {
376
- for (let type of pluginTypes) {
377
- let removeFromOwnGroup = false
378
-
379
- for (let otherGroup of typesPerPlugin) {
380
- if (pluginTypes === otherGroup) continue
645
+ if (isArbitraryValue(modifier)) {
646
+ if (matches.length > 1) {
647
+ // Partition plugins in 2 categories so that we can start searching in the plugins that
648
+ // don't have `any` as a type first.
649
+ let [withAny, withoutAny] = matches.reduce(
650
+ (group, plugin) => {
651
+ let hasAnyType = plugin.some(([{ options }]) =>
652
+ options.types.some(({ type }) => type === 'any')
653
+ )
381
654
 
382
- if (otherGroup.has(type)) {
383
- otherGroup.delete(type)
384
- removeFromOwnGroup = true
655
+ if (hasAnyType) {
656
+ group[0].push(plugin)
657
+ } else {
658
+ group[1].push(plugin)
385
659
  }
660
+ return group
661
+ },
662
+ [[], []]
663
+ )
664
+
665
+ function findFallback(matches) {
666
+ // If only a single plugin matches, let's take that one
667
+ if (matches.length === 1) {
668
+ return matches[0]
386
669
  }
387
670
 
388
- if (removeFromOwnGroup) pluginTypes.delete(type)
671
+ // Otherwise, find the plugin that creates a valid rule given the arbitrary value, and
672
+ // also has the correct type which preferOnConflicts the plugin in case of clashes.
673
+ return matches.find((rules) => {
674
+ let matchingTypes = typesByMatches.get(rules)
675
+ return rules.some(([{ options }, rule]) => {
676
+ if (!isParsableNode(rule)) {
677
+ return false
678
+ }
679
+
680
+ return options.types.some(
681
+ ({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict
682
+ )
683
+ })
684
+ })
389
685
  }
390
- }
391
686
 
392
- let messages = []
393
-
394
- for (let [idx, group] of typesPerPlugin.entries()) {
395
- for (let type of group) {
396
- let rules = matches[idx]
397
- .map(([, rule]) => rule)
398
- .flat()
399
- .map((rule) =>
400
- rule
401
- .toString()
402
- .split('\n')
403
- .slice(1, -1) // Remove selector and closing '}'
404
- .map((line) => line.trim())
405
- .map((x) => ` ${x}`) // Re-indent
406
- .join('\n')
407
- )
408
- .join('\n\n')
687
+ // Try to find a fallback plugin, because we already know that multiple plugins matched for
688
+ // the given arbitrary value.
689
+ let fallback = findFallback(withoutAny) ?? findFallback(withAny)
690
+ if (fallback) {
691
+ matches = [fallback]
692
+ }
409
693
 
410
- messages.push(` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``)
411
- break
694
+ // We couldn't find a fallback plugin which means that there are now multiple plugins that
695
+ // generated css for the current candidate. This means that the result is ambiguous and this
696
+ // should not happen. We won't generate anything right now, so let's report this to the user
697
+ // by logging some options about what they can do.
698
+ else {
699
+ let typesPerPlugin = matches.map(
700
+ (match) => new Set([...(typesByMatches.get(match) ?? [])])
701
+ )
702
+
703
+ // Remove duplicates, so that we can detect proper unique types for each plugin.
704
+ for (let pluginTypes of typesPerPlugin) {
705
+ for (let type of pluginTypes) {
706
+ let removeFromOwnGroup = false
707
+
708
+ for (let otherGroup of typesPerPlugin) {
709
+ if (pluginTypes === otherGroup) continue
710
+
711
+ if (otherGroup.has(type)) {
712
+ otherGroup.delete(type)
713
+ removeFromOwnGroup = true
714
+ }
715
+ }
716
+
717
+ if (removeFromOwnGroup) pluginTypes.delete(type)
718
+ }
719
+ }
720
+
721
+ let messages = []
722
+
723
+ for (let [idx, group] of typesPerPlugin.entries()) {
724
+ for (let type of group) {
725
+ let rules = matches[idx]
726
+ .map(([, rule]) => rule)
727
+ .flat()
728
+ .map((rule) =>
729
+ rule
730
+ .toString()
731
+ .split('\n')
732
+ .slice(1, -1) // Remove selector and closing '}'
733
+ .map((line) => line.trim())
734
+ .map((x) => ` ${x}`) // Re-indent
735
+ .join('\n')
736
+ )
737
+ .join('\n\n')
738
+
739
+ messages.push(
740
+ ` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
741
+ )
742
+ break
743
+ }
744
+ }
745
+
746
+ log.warn([
747
+ `The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
748
+ ...messages,
749
+ `If this is content and not a class, replace it with \`${candidate
750
+ .replace('[', '&lsqb;')
751
+ .replace(']', '&rsqb;')}\` to silence this warning.`,
752
+ ])
753
+ continue
412
754
  }
413
755
  }
414
756
 
415
- log.warn([
416
- `The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
417
- ...messages,
418
- `If this is content and not a class, replace it with \`${candidate
419
- .replace('[', '&lsqb;')
420
- .replace(']', '&rsqb;')}\` to silence this warning.`,
421
- ])
422
- continue
757
+ matches = matches.map((list) => list.filter((match) => isParsableNode(match[1])))
423
758
  }
424
759
 
425
- matches = applyPrefix(matches.flat(), context)
760
+ matches = matches.flat()
761
+ matches = Array.from(recordCandidates(matches, classCandidate))
762
+ matches = applyPrefix(matches, context)
426
763
 
427
764
  if (important) {
428
- matches = applyImportant(matches, context)
765
+ matches = applyImportant(matches, classCandidate)
429
766
  }
430
767
 
431
768
  for (let variant of variants) {
@@ -433,20 +770,16 @@ function* resolveMatches(candidate, context) {
433
770
  }
434
771
 
435
772
  for (let match of matches) {
773
+ match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
774
+
436
775
  // Apply final format selector
437
- if (match[0].collectedFormats) {
438
- let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)
439
- let container = postcss.root({ nodes: [match[1].clone()] })
440
- container.walkRules((rule) => {
441
- if (inKeyframes(rule)) return
442
-
443
- rule.selector = finalizeSelector(finalFormat, {
444
- selector: rule.selector,
445
- candidate,
446
- context,
447
- })
448
- })
449
- match[1] = container.nodes[0]
776
+ match = applyFinalFormat(match, { context, candidate, original })
777
+
778
+ // Skip rules with invalid selectors
779
+ // This will cause the candidate to be added to the "not class"
780
+ // cache skipping it entirely for future builds
781
+ if (match === null) {
782
+ continue
450
783
  }
451
784
 
452
785
  yield match
@@ -454,20 +787,105 @@ function* resolveMatches(candidate, context) {
454
787
  }
455
788
  }
456
789
 
790
+ function applyFinalFormat(match, { context, candidate, original }) {
791
+ if (!match[0].collectedFormats) {
792
+ return match
793
+ }
794
+
795
+ let isValid = true
796
+ let finalFormat
797
+
798
+ try {
799
+ finalFormat = formatVariantSelector(match[0].collectedFormats, {
800
+ context,
801
+ candidate,
802
+ })
803
+ } catch {
804
+ // The format selector we produced is invalid
805
+ // This could be because:
806
+ // - A bug exists
807
+ // - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`)
808
+ // - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`)
809
+ // Either way the build will fail because of this
810
+ // We would rather that the build pass "silently" given that this could
811
+ // happen because of picking up invalid things when scanning content
812
+ // So we'll throw out the candidate instead
813
+
814
+ return null
815
+ }
816
+
817
+ let container = postcss.root({ nodes: [match[1].clone()] })
818
+
819
+ container.walkRules((rule) => {
820
+ if (inKeyframes(rule)) {
821
+ return
822
+ }
823
+
824
+ try {
825
+ rule.selector = finalizeSelector(rule.selector, finalFormat, {
826
+ candidate: original,
827
+ context,
828
+ })
829
+ } catch {
830
+ // If this selector is invalid we also want to skip it
831
+ // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content
832
+ isValid = false
833
+ return false
834
+ }
835
+ })
836
+
837
+ if (!isValid) {
838
+ return null
839
+ }
840
+
841
+ match[1] = container.nodes[0]
842
+
843
+ return match
844
+ }
845
+
457
846
  function inKeyframes(rule) {
458
847
  return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
459
848
  }
460
849
 
850
+ function getImportantStrategy(important) {
851
+ if (important === true) {
852
+ return (rule) => {
853
+ if (inKeyframes(rule)) {
854
+ return
855
+ }
856
+
857
+ rule.walkDecls((d) => {
858
+ if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
859
+ d.important = true
860
+ }
861
+ })
862
+ }
863
+ }
864
+
865
+ if (typeof important === 'string') {
866
+ return (rule) => {
867
+ if (inKeyframes(rule)) {
868
+ return
869
+ }
870
+
871
+ rule.selectors = rule.selectors.map((selector) => {
872
+ return applyImportantSelector(selector, important)
873
+ })
874
+ }
875
+ }
876
+ }
877
+
461
878
  function generateRules(candidates, context) {
462
879
  let allRules = []
880
+ let strategy = getImportantStrategy(context.tailwindConfig.important)
463
881
 
464
882
  for (let candidate of candidates) {
465
883
  if (context.notClassCache.has(candidate)) {
466
884
  continue
467
885
  }
468
886
 
469
- if (context.classCache.has(candidate)) {
470
- allRules.push(context.classCache.get(candidate))
887
+ if (context.candidateRuleCache.has(candidate)) {
888
+ allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)))
471
889
  continue
472
890
  }
473
891
 
@@ -479,47 +897,27 @@ function generateRules(candidates, context) {
479
897
  }
480
898
 
481
899
  context.classCache.set(candidate, matches)
482
- allRules.push(matches)
483
- }
484
900
 
485
- // Strategy based on `tailwindConfig.important`
486
- let strategy = ((important) => {
487
- if (important === true) {
488
- return (rule) => {
489
- rule.walkDecls((d) => {
490
- if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
491
- d.important = true
492
- }
493
- })
494
- }
495
- }
901
+ let rules = context.candidateRuleCache.get(candidate) ?? new Set()
902
+ context.candidateRuleCache.set(candidate, rules)
496
903
 
497
- if (typeof important === 'string') {
498
- return (rule) => {
499
- rule.selectors = rule.selectors.map((selector) => {
500
- return `${important} ${selector}`
501
- })
502
- }
503
- }
504
- })(context.tailwindConfig.important)
904
+ for (const match of matches) {
905
+ let [{ sort, options }, rule] = match
505
906
 
506
- return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
507
- if (options.respectImportant) {
508
- if (strategy) {
907
+ if (options.respectImportant && strategy) {
509
908
  let container = postcss.root({ nodes: [rule.clone()] })
510
- container.walkRules((r) => {
511
- if (inKeyframes(r)) {
512
- return
513
- }
514
-
515
- strategy(r)
516
- })
909
+ container.walkRules(strategy)
517
910
  rule = container.nodes[0]
518
911
  }
912
+
913
+ let newEntry = [sort, rule]
914
+ rules.add(newEntry)
915
+ context.ruleCache.add(newEntry)
916
+ allRules.push(newEntry)
519
917
  }
918
+ }
520
919
 
521
- return [sort | context.layerOrder[layer], rule]
522
- })
920
+ return allRules
523
921
  }
524
922
 
525
923
  function isArbitraryValue(input) {