tailwindcss 0.0.0-insiders.7c337f2 → 0.0.0-insiders.7d8eb21

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 (161) hide show
  1. package/CHANGELOG.md +246 -6
  2. package/README.md +13 -9
  3. package/defaultTheme.d.ts +2 -1
  4. package/lib/cli/build/deps.js +54 -0
  5. package/lib/cli/build/index.js +48 -0
  6. package/lib/cli/build/plugin.js +362 -0
  7. package/lib/cli/build/utils.js +78 -0
  8. package/lib/cli/build/watching.js +178 -0
  9. package/lib/cli/help/index.js +71 -0
  10. package/lib/cli/index.js +18 -0
  11. package/lib/cli/init/index.js +46 -0
  12. package/lib/cli/shared.js +12 -0
  13. package/lib/cli-peer-dependencies.js +18 -5
  14. package/lib/cli.js +62 -592
  15. package/lib/constants.js +19 -12
  16. package/lib/corePluginList.js +5 -3
  17. package/lib/corePlugins.js +619 -347
  18. package/lib/css/preflight.css +7 -0
  19. package/lib/featureFlags.js +34 -22
  20. package/lib/index.js +16 -9
  21. package/lib/lib/cacheInvalidation.js +42 -21
  22. package/lib/lib/collapseAdjacentRules.js +16 -14
  23. package/lib/lib/collapseDuplicateDeclarations.js +13 -10
  24. package/lib/lib/content.js +177 -0
  25. package/lib/lib/defaultExtractor.js +230 -33
  26. package/lib/lib/detectNesting.js +13 -4
  27. package/lib/lib/evaluateTailwindFunctions.js +105 -42
  28. package/lib/lib/expandApplyAtRules.js +151 -92
  29. package/lib/lib/expandTailwindAtRules.js +134 -149
  30. package/lib/lib/findAtConfigPath.js +44 -0
  31. package/lib/lib/generateRules.js +404 -185
  32. package/lib/lib/getModuleDependencies.js +21 -18
  33. package/lib/lib/normalizeTailwindDirectives.js +11 -8
  34. package/lib/lib/offsets.js +285 -0
  35. package/lib/lib/partitionApplyAtRules.js +14 -11
  36. package/lib/lib/regex.js +60 -0
  37. package/lib/lib/remap-bitfield.js +87 -0
  38. package/lib/lib/resolveDefaultsAtRules.js +97 -79
  39. package/lib/lib/setupContextUtils.js +567 -242
  40. package/lib/lib/setupTrackingContext.js +77 -106
  41. package/lib/lib/sharedState.js +18 -10
  42. package/lib/lib/substituteScreenAtRules.js +9 -7
  43. package/lib/postcss-plugins/nesting/index.js +7 -5
  44. package/lib/postcss-plugins/nesting/plugin.js +10 -8
  45. package/lib/processTailwindFeatures.js +34 -31
  46. package/lib/public/colors.js +7 -5
  47. package/lib/public/create-plugin.js +6 -4
  48. package/lib/public/default-config.js +7 -5
  49. package/lib/public/default-theme.js +7 -5
  50. package/lib/public/resolve-config.js +13 -10
  51. package/lib/util/bigSign.js +4 -1
  52. package/lib/util/buildMediaQuery.js +10 -5
  53. package/lib/util/cloneDeep.js +6 -5
  54. package/lib/util/cloneNodes.js +9 -2
  55. package/lib/util/color.js +20 -17
  56. package/lib/util/configurePlugins.js +5 -2
  57. package/lib/util/createPlugin.js +6 -5
  58. package/lib/util/createUtilityPlugin.js +15 -13
  59. package/lib/util/dataTypes.js +75 -51
  60. package/lib/util/defaults.js +8 -5
  61. package/lib/util/escapeClassName.js +13 -10
  62. package/lib/util/escapeCommas.js +4 -1
  63. package/lib/util/flattenColorPalette.js +7 -8
  64. package/lib/util/formatVariantSelector.js +270 -103
  65. package/lib/util/getAllConfigs.js +34 -15
  66. package/lib/util/hashConfig.js +10 -7
  67. package/lib/util/isKeyframeRule.js +4 -1
  68. package/lib/util/isPlainObject.js +4 -1
  69. package/lib/util/{isValidArbitraryValue.js → isSyntacticallyValidPropertyValue.js} +34 -26
  70. package/lib/util/log.js +14 -8
  71. package/lib/util/nameClass.js +19 -8
  72. package/lib/util/negateValue.js +20 -5
  73. package/lib/util/normalizeConfig.js +94 -53
  74. package/lib/util/normalizeScreens.js +115 -4
  75. package/lib/util/parseAnimationValue.js +43 -41
  76. package/lib/util/parseBoxShadowValue.js +12 -45
  77. package/lib/util/parseDependency.js +37 -55
  78. package/lib/util/parseGlob.js +34 -0
  79. package/lib/util/parseObjectStyles.js +15 -12
  80. package/lib/util/pluginUtils.js +146 -42
  81. package/lib/util/prefixSelector.js +26 -9
  82. package/lib/util/removeAlphaVariables.js +29 -0
  83. package/lib/util/resolveConfig.js +104 -105
  84. package/lib/util/resolveConfigPath.js +20 -17
  85. package/lib/util/responsive.js +12 -9
  86. package/lib/util/splitAtTopLevelOnly.js +43 -0
  87. package/lib/util/tap.js +4 -1
  88. package/lib/util/toColorValue.js +4 -1
  89. package/lib/util/toPath.js +18 -2
  90. package/lib/util/transformThemeValue.js +23 -11
  91. package/lib/util/validateConfig.js +24 -0
  92. package/lib/util/validateFormalSyntax.js +24 -0
  93. package/lib/util/withAlphaVariable.js +32 -24
  94. package/package.json +32 -29
  95. package/peers/index.js +6693 -3036
  96. package/plugin.d.ts +10 -5
  97. package/resolveConfig.d.ts +12 -0
  98. package/scripts/generate-types.js +53 -0
  99. package/scripts/release-channel.js +18 -0
  100. package/scripts/release-notes.js +21 -0
  101. package/scripts/type-utils.js +27 -0
  102. package/src/cli/build/deps.js +56 -0
  103. package/src/cli/build/index.js +49 -0
  104. package/src/cli/build/plugin.js +434 -0
  105. package/src/cli/build/utils.js +76 -0
  106. package/src/cli/build/watching.js +227 -0
  107. package/src/cli/help/index.js +70 -0
  108. package/src/cli/index.js +3 -0
  109. package/src/cli/init/index.js +50 -0
  110. package/src/cli/shared.js +5 -0
  111. package/src/cli-peer-dependencies.js +7 -1
  112. package/src/cli.js +46 -659
  113. package/src/corePlugins.js +346 -79
  114. package/src/css/preflight.css +7 -0
  115. package/src/featureFlags.js +12 -2
  116. package/src/index.js +5 -0
  117. package/src/lib/content.js +213 -0
  118. package/src/lib/defaultExtractor.js +202 -35
  119. package/src/lib/detectNesting.js +9 -1
  120. package/src/lib/evaluateTailwindFunctions.js +78 -7
  121. package/src/lib/expandApplyAtRules.js +129 -34
  122. package/src/lib/expandTailwindAtRules.js +12 -50
  123. package/src/lib/findAtConfigPath.js +48 -0
  124. package/src/lib/generateRules.js +379 -126
  125. package/src/lib/offsets.js +351 -0
  126. package/src/lib/regex.js +74 -0
  127. package/src/lib/remap-bitfield.js +82 -0
  128. package/src/lib/resolveDefaultsAtRules.js +53 -36
  129. package/src/lib/setupContextUtils.js +510 -120
  130. package/src/lib/setupTrackingContext.js +8 -45
  131. package/src/lib/sharedState.js +2 -0
  132. package/src/util/buildMediaQuery.js +5 -3
  133. package/src/util/cloneNodes.js +5 -1
  134. package/src/util/dataTypes.js +51 -26
  135. package/src/util/formatVariantSelector.js +317 -101
  136. package/src/util/getAllConfigs.js +19 -0
  137. package/src/util/{isValidArbitraryValue.js → isSyntacticallyValidPropertyValue.js} +1 -1
  138. package/src/util/log.js +1 -1
  139. package/src/util/nameClass.js +4 -0
  140. package/src/util/negateValue.js +10 -2
  141. package/src/util/normalizeConfig.js +40 -10
  142. package/src/util/normalizeScreens.js +99 -4
  143. package/src/util/parseBoxShadowValue.js +3 -50
  144. package/src/util/parseDependency.js +37 -42
  145. package/src/util/parseGlob.js +24 -0
  146. package/src/util/pluginUtils.js +132 -10
  147. package/src/util/prefixSelector.js +28 -10
  148. package/src/util/removeAlphaVariables.js +24 -0
  149. package/src/util/resolveConfig.js +70 -62
  150. package/src/util/splitAtTopLevelOnly.js +45 -0
  151. package/src/util/toPath.js +1 -1
  152. package/src/util/transformThemeValue.js +13 -3
  153. package/src/util/validateConfig.js +13 -0
  154. package/src/util/validateFormalSyntax.js +34 -0
  155. package/src/util/withAlphaVariable.js +1 -1
  156. package/stubs/defaultConfig.stub.js +165 -164
  157. package/stubs/simpleConfig.stub.js +1 -0
  158. package/types/config.d.ts +77 -37
  159. package/types/generated/default-theme.d.ts +342 -0
  160. package/types/index.d.ts +7 -1
  161. package/types.d.ts +0 -1
@@ -3,19 +3,22 @@ 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, filterSelectorsForClass, getMatchingTypes } from '../util/pluginUtils'
7
7
  import log from '../util/log'
8
8
  import * as sharedState from './sharedState'
9
9
  import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
10
10
  import { asClass } from '../util/nameClass'
11
11
  import { normalize } from '../util/dataTypes'
12
- import isValidArbitraryValue from '../util/isValidArbitraryValue'
12
+ import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
13
+ import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
14
+ import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
15
+ import { flagEnabled } from '../featureFlags'
13
16
 
14
17
  let classNameParser = selectorParser((selectors) => {
15
18
  return selectors.first.filter(({ type }) => type === 'class').pop().value
16
19
  })
17
20
 
18
- function getClassNameFromSelector(selector) {
21
+ export function getClassNameFromSelector(selector) {
19
22
  return classNameParser.transformSync(selector)
20
23
  }
21
24
 
@@ -31,13 +34,24 @@ function* candidatePermutations(candidate) {
31
34
 
32
35
  while (lastIndex >= 0) {
33
36
  let dashIdx
37
+ let wasSlash = false
34
38
 
35
39
  if (lastIndex === Infinity && candidate.endsWith(']')) {
36
40
  let bracketIdx = candidate.indexOf('[')
37
41
 
38
42
  // If character before `[` isn't a dash or a slash, this isn't a dynamic class
39
43
  // eg. string[]
40
- dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
44
+ if (candidate[bracketIdx - 1] === '-') {
45
+ dashIdx = bracketIdx - 1
46
+ } else if (candidate[bracketIdx - 1] === '/') {
47
+ dashIdx = bracketIdx - 1
48
+ wasSlash = true
49
+ } else {
50
+ dashIdx = -1
51
+ }
52
+ } else if (lastIndex === Infinity && candidate.includes('/')) {
53
+ dashIdx = candidate.lastIndexOf('/')
54
+ wasSlash = true
41
55
  } else {
42
56
  dashIdx = candidate.lastIndexOf('-', lastIndex)
43
57
  }
@@ -47,11 +61,16 @@ function* candidatePermutations(candidate) {
47
61
  }
48
62
 
49
63
  let prefix = candidate.slice(0, dashIdx)
50
- let modifier = candidate.slice(dashIdx + 1)
51
-
52
- yield [prefix, modifier]
64
+ let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1)
53
65
 
54
66
  lastIndex = dashIdx - 1
67
+
68
+ // TODO: This feels a bit hacky
69
+ if (prefix === '' || modifier === '/') {
70
+ continue
71
+ }
72
+
73
+ yield [prefix, modifier]
55
74
  }
56
75
  }
57
76
 
@@ -97,12 +116,15 @@ function applyImportant(matches, classCandidate) {
97
116
  for (let [meta, rule] of matches) {
98
117
  let container = postcss.root({ nodes: [rule.clone()] })
99
118
  container.walkRules((r) => {
100
- r.selector = updateAllClasses(r.selector, (className) => {
101
- if (className === classCandidate) {
102
- return `!${className}`
119
+ r.selector = updateAllClasses(
120
+ filterSelectorsForClass(r.selector, classCandidate),
121
+ (className) => {
122
+ if (className === classCandidate) {
123
+ return `!${className}`
124
+ }
125
+ return className
103
126
  }
104
- return className
105
- })
127
+ )
106
128
  r.walkDecls((d) => (d.important = true))
107
129
  })
108
130
  result.push([{ ...meta, important: true }, container.nodes[0]])
@@ -125,8 +147,62 @@ function applyVariant(variant, matches, context) {
125
147
  return matches
126
148
  }
127
149
 
150
+ /** @type {{modifier: string | null, value: string | null}} */
151
+ let args = { modifier: null, value: sharedState.NONE }
152
+
153
+ // Retrieve "modifier"
154
+ {
155
+ let match = /(.*)\/(.*)$/g.exec(variant)
156
+ if (match) {
157
+ variant = match[1]
158
+ args.modifier = match[2]
159
+
160
+ if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
161
+ return []
162
+ }
163
+ }
164
+ }
165
+
166
+ // Retrieve "arbitrary value"
167
+ if (variant.endsWith(']') && !variant.startsWith('[')) {
168
+ // We either have:
169
+ // @[200px]
170
+ // group-[:hover]
171
+ //
172
+ // But we don't want:
173
+ // @-[200px] (`-` is incorrect)
174
+ // group[:hover] (`-` is missing)
175
+ let match = /(.)(-?)\[(.*)\]/g.exec(variant)
176
+ if (match) {
177
+ let [, char, seperator, value] = match
178
+ // @-[200px] case
179
+ if (char === '@' && seperator === '-') return []
180
+ // group[:hover] case
181
+ if (char !== '@' && seperator === '') return []
182
+
183
+ variant = variant.replace(`${seperator}[${value}]`, '')
184
+ args.value = value
185
+ }
186
+ }
187
+
188
+ // Register arbitrary variants
189
+ if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
190
+ let selector = normalize(variant.slice(1, -1))
191
+
192
+ if (!isValidVariantFormatString(selector)) {
193
+ return []
194
+ }
195
+
196
+ let fn = parseVariant(selector)
197
+
198
+ let sort = context.offsets.recordVariant(variant)
199
+
200
+ context.variantMap.set(variant, [[sort, fn]])
201
+ }
202
+
128
203
  if (context.variantMap.has(variant)) {
129
- let variantFunctionTuples = context.variantMap.get(variant)
204
+ let isArbitraryVariant = isArbitraryValue(variant)
205
+ let variantFunctionTuples = context.variantMap.get(variant).slice()
130
206
  let result = []
131
207
 
132
208
  for (let [meta, rule] of matches) {
@@ -137,15 +213,17 @@ function applyVariant(variant, matches, context) {
137
213
 
138
214
  let container = postcss.root({ nodes: [rule.clone()] })
139
215
 
140
- for (let [variantSort, variantFunction] of variantFunctionTuples) {
141
- let clone = container.clone()
216
+ for (let [variantSort, variantFunction, containerFromArray] of variantFunctionTuples) {
217
+ let clone = (containerFromArray ?? container).clone()
142
218
  let collectedFormats = []
143
219
 
144
- let originals = new Map()
145
-
146
220
  function prepareBackup() {
147
- if (originals.size > 0) return // Already prepared, chicken out
148
- clone.walkRules((rule) => originals.set(rule, rule.selector))
221
+ // Already prepared, chicken out
222
+ if (clone.raws.neededBackup) {
223
+ return
224
+ }
225
+ clone.raws.neededBackup = true
226
+ clone.walkRules((rule) => (rule.raws.originalSelector = rule.selector))
149
227
  }
150
228
 
151
229
  function modifySelectors(modifierFunction) {
@@ -185,25 +263,54 @@ function applyVariant(variant, matches, context) {
185
263
  clone.append(wrapper)
186
264
  },
187
265
  format(selectorFormat) {
188
- collectedFormats.push(selectorFormat)
266
+ collectedFormats.push({
267
+ format: selectorFormat,
268
+ isArbitraryVariant,
269
+ })
189
270
  },
271
+ args,
190
272
  })
191
273
 
274
+ // It can happen that a list of format strings is returned from within the function. In that
275
+ // case, we have to process them as well. We can use the existing `variantSort`.
276
+ if (Array.isArray(ruleWithVariant)) {
277
+ for (let [idx, variantFunction] of ruleWithVariant.entries()) {
278
+ // This is a little bit scary since we are pushing to an array of items that we are
279
+ // currently looping over. However, you can also think of it like a processing queue
280
+ // where you keep handling jobs until everything is done and each job can queue more
281
+ // jobs if needed.
282
+ variantFunctionTuples.push([
283
+ context.offsets.applyParallelOffset(variantSort, idx),
284
+ variantFunction,
285
+
286
+ // If the clone has been modified we have to pass that back
287
+ // though so each rule can use the modified container
288
+ clone.clone(),
289
+ ])
290
+ }
291
+ continue
292
+ }
293
+
192
294
  if (typeof ruleWithVariant === 'string') {
193
- collectedFormats.push(ruleWithVariant)
295
+ collectedFormats.push({
296
+ format: ruleWithVariant,
297
+ isArbitraryVariant,
298
+ })
194
299
  }
195
300
 
196
301
  if (ruleWithVariant === null) {
197
302
  continue
198
303
  }
199
304
 
200
- // We filled the `originals`, therefore we assume that somebody touched
305
+ // We had to backup selectors, therefore we assume that somebody touched
201
306
  // `container` or `modifySelectors`. Let's see if they did, so that we
202
307
  // can restore the selectors, and collect the format strings.
203
- if (originals.size > 0) {
308
+ if (clone.raws.neededBackup) {
309
+ delete clone.raws.neededBackup
204
310
  clone.walkRules((rule) => {
205
- if (!originals.has(rule)) return
206
- let before = originals.get(rule)
311
+ let before = rule.raws.originalSelector
312
+ if (!before) return
313
+ delete rule.raws.originalSelector
207
314
  if (before === rule.selector) return // No mutation happened
208
315
 
209
316
  let modified = rule.selector
@@ -229,7 +336,10 @@ function applyVariant(variant, matches, context) {
229
336
  // modified (by plugin): .foo .foo\\:markdown > p
230
337
  // rebuiltBase (internal): .foo\\:markdown > p
231
338
  // format: .foo &
232
- collectedFormats.push(modified.replace(rebuiltBase, '&'))
339
+ collectedFormats.push({
340
+ format: modified.replace(rebuiltBase, '&'),
341
+ isArbitraryVariant,
342
+ })
233
343
  rule.selector = before
234
344
  })
235
345
  }
@@ -243,7 +353,11 @@ function applyVariant(variant, matches, context) {
243
353
  let withOffset = [
244
354
  {
245
355
  ...meta,
246
- sort: variantSort | meta.sort,
356
+ sort: context.offsets.applyVariantOffset(
357
+ meta.sort,
358
+ variantSort,
359
+ Object.assign(args, context.variantOptions.get(variant))
360
+ ),
247
361
  collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
248
362
  },
249
363
  clone.nodes[0],
@@ -307,7 +421,7 @@ function isParsableNode(node) {
307
421
  let isParsable = true
308
422
 
309
423
  node.walkDecls((decl) => {
310
- if (!isParsableCssValue(decl.name, decl.value)) {
424
+ if (!isParsableCssValue(decl.prop, decl.value)) {
311
425
  isParsable = false
312
426
  return false
313
427
  }
@@ -353,9 +467,11 @@ function extractArbitraryProperty(classCandidate, context) {
353
467
  return null
354
468
  }
355
469
 
470
+ let sort = context.offsets.arbitraryProperty()
471
+
356
472
  return [
357
473
  [
358
- { sort: context.arbitraryPropertiesSort, layer: 'utilities' },
474
+ { sort, layer: 'utilities' },
359
475
  () => ({
360
476
  [asClass(classCandidate)]: {
361
477
  [property]: normalized,
@@ -407,18 +523,22 @@ function splitWithSeparator(input, separator) {
407
523
  return [sharedState.NOT_ON_DEMAND]
408
524
  }
409
525
 
410
- return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
526
+ return splitAtTopLevelOnly(input, separator)
411
527
  }
412
528
 
413
529
  function* recordCandidates(matches, classCandidate) {
414
530
  for (const match of matches) {
415
- match[1].raws.tailwind = { ...match[1].raws.tailwind, classCandidate }
531
+ match[1].raws.tailwind = {
532
+ ...match[1].raws.tailwind,
533
+ classCandidate,
534
+ preserveSource: match[0].options?.preserveSource ?? false,
535
+ }
416
536
 
417
537
  yield match
418
538
  }
419
539
  }
420
540
 
421
- function* resolveMatches(candidate, context) {
541
+ function* resolveMatches(candidate, context, original = candidate) {
422
542
  let separator = context.tailwindConfig.separator
423
543
  let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
424
544
  let important = false
@@ -428,6 +548,15 @@ function* resolveMatches(candidate, context) {
428
548
  classCandidate = classCandidate.slice(1)
429
549
  }
430
550
 
551
+ if (flagEnabled(context.tailwindConfig, 'variantGrouping')) {
552
+ if (classCandidate.startsWith('(') && classCandidate.endsWith(')')) {
553
+ let base = variants.slice().reverse().join(separator)
554
+ for (let part of splitAtTopLevelOnly(classCandidate.slice(1, -1), ',')) {
555
+ yield* resolveMatches(base + separator + part, context, original)
556
+ }
557
+ }
558
+ }
559
+
431
560
  // TODO: Reintroduce this in ways that doesn't break on false positives
432
561
  // function sortAgainst(toSort, against) {
433
562
  // return toSort.slice().sort((a, z) => {
@@ -468,68 +597,133 @@ function* resolveMatches(candidate, context) {
468
597
  }
469
598
 
470
599
  if (matchesPerPlugin.length > 0) {
471
- typesByMatches.set(matchesPerPlugin, sort.options?.type)
600
+ let matchingTypes = Array.from(
601
+ getMatchingTypes(
602
+ sort.options?.types ?? [],
603
+ modifier,
604
+ sort.options ?? {},
605
+ context.tailwindConfig
606
+ )
607
+ ).map(([_, type]) => type)
608
+
609
+ if (matchingTypes.length > 0) {
610
+ typesByMatches.set(matchesPerPlugin, matchingTypes)
611
+ }
612
+
472
613
  matches.push(matchesPerPlugin)
473
614
  }
474
615
  }
475
616
 
476
617
  if (isArbitraryValue(modifier)) {
477
- // When generated arbitrary values are ambiguous, we can't know
478
- // which to pick so don't generate any utilities for them
479
618
  if (matches.length > 1) {
480
- let typesPerPlugin = matches.map((match) => new Set([...(typesByMatches.get(match) ?? [])]))
619
+ // Partition plugins in 2 categories so that we can start searching in the plugins that
620
+ // don't have `any` as a type first.
621
+ let [withAny, withoutAny] = matches.reduce(
622
+ (group, plugin) => {
623
+ let hasAnyType = plugin.some(([{ options }]) =>
624
+ options.types.some(({ type }) => type === 'any')
625
+ )
481
626
 
482
- // Remove duplicates, so that we can detect proper unique types for each plugin.
483
- for (let pluginTypes of typesPerPlugin) {
484
- for (let type of pluginTypes) {
485
- let removeFromOwnGroup = false
627
+ if (hasAnyType) {
628
+ group[0].push(plugin)
629
+ } else {
630
+ group[1].push(plugin)
631
+ }
632
+ return group
633
+ },
634
+ [[], []]
635
+ )
486
636
 
487
- for (let otherGroup of typesPerPlugin) {
488
- if (pluginTypes === otherGroup) continue
637
+ function findFallback(matches) {
638
+ // If only a single plugin matches, let's take that one
639
+ if (matches.length === 1) {
640
+ return matches[0]
641
+ }
489
642
 
490
- if (otherGroup.has(type)) {
491
- otherGroup.delete(type)
492
- removeFromOwnGroup = true
643
+ // Otherwise, find the plugin that creates a valid rule given the arbitrary value, and
644
+ // also has the correct type which preferOnConflicts the plugin in case of clashes.
645
+ return matches.find((rules) => {
646
+ let matchingTypes = typesByMatches.get(rules)
647
+ return rules.some(([{ options }, rule]) => {
648
+ if (!isParsableNode(rule)) {
649
+ return false
493
650
  }
494
- }
495
651
 
496
- if (removeFromOwnGroup) pluginTypes.delete(type)
497
- }
652
+ return options.types.some(
653
+ ({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict
654
+ )
655
+ })
656
+ })
498
657
  }
499
658
 
500
- let messages = []
501
-
502
- for (let [idx, group] of typesPerPlugin.entries()) {
503
- for (let type of group) {
504
- let rules = matches[idx]
505
- .map(([, rule]) => rule)
506
- .flat()
507
- .map((rule) =>
508
- rule
509
- .toString()
510
- .split('\n')
511
- .slice(1, -1) // Remove selector and closing '}'
512
- .map((line) => line.trim())
513
- .map((x) => ` ${x}`) // Re-indent
514
- .join('\n')
515
- )
516
- .join('\n\n')
659
+ // Try to find a fallback plugin, because we already know that multiple plugins matched for
660
+ // the given arbitrary value.
661
+ let fallback = findFallback(withoutAny) ?? findFallback(withAny)
662
+ if (fallback) {
663
+ matches = [fallback]
664
+ }
517
665
 
518
- messages.push(
519
- ` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
520
- )
521
- break
666
+ // We couldn't find a fallback plugin which means that there are now multiple plugins that
667
+ // generated css for the current candidate. This means that the result is ambiguous and this
668
+ // should not happen. We won't generate anything right now, so let's report this to the user
669
+ // by logging some options about what they can do.
670
+ else {
671
+ let typesPerPlugin = matches.map(
672
+ (match) => new Set([...(typesByMatches.get(match) ?? [])])
673
+ )
674
+
675
+ // Remove duplicates, so that we can detect proper unique types for each plugin.
676
+ for (let pluginTypes of typesPerPlugin) {
677
+ for (let type of pluginTypes) {
678
+ let removeFromOwnGroup = false
679
+
680
+ for (let otherGroup of typesPerPlugin) {
681
+ if (pluginTypes === otherGroup) continue
682
+
683
+ if (otherGroup.has(type)) {
684
+ otherGroup.delete(type)
685
+ removeFromOwnGroup = true
686
+ }
687
+ }
688
+
689
+ if (removeFromOwnGroup) pluginTypes.delete(type)
690
+ }
522
691
  }
523
- }
524
692
 
525
- log.warn([
526
- `The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
527
- ...messages,
528
- `If this is content and not a class, replace it with \`${candidate
529
- .replace('[', '[')
530
- .replace(']', ']')}\` to silence this warning.`,
531
- ])
532
- continue
693
+ let messages = []
694
+
695
+ for (let [idx, group] of typesPerPlugin.entries()) {
696
+ for (let type of group) {
697
+ let rules = matches[idx]
698
+ .map(([, rule]) => rule)
699
+ .flat()
700
+ .map((rule) =>
701
+ rule
702
+ .toString()
703
+ .split('\n')
704
+ .slice(1, -1) // Remove selector and closing '}'
705
+ .map((line) => line.trim())
706
+ .map((x) => ` ${x}`) // Re-indent
707
+ .join('\n')
708
+ )
709
+ .join('\n\n')
710
+
711
+ messages.push(
712
+ ` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
713
+ )
714
+ break
715
+ }
716
+ }
717
+
718
+ log.warn([
719
+ `The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
720
+ ...messages,
721
+ `If this is content and not a class, replace it with \`${candidate
722
+ .replace('[', '[')
723
+ .replace(']', ']')}\` to silence this warning.`,
724
+ ])
725
+ continue
726
+ }
533
727
  }
534
728
 
535
729
  matches = matches.map((list) => list.filter((match) => isParsableNode(match[1])))
@@ -551,19 +745,13 @@ function* resolveMatches(candidate, context) {
551
745
  match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
552
746
 
553
747
  // Apply final format selector
554
- if (match[0].collectedFormats) {
555
- let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)
556
- let container = postcss.root({ nodes: [match[1].clone()] })
557
- container.walkRules((rule) => {
558
- if (inKeyframes(rule)) return
559
-
560
- rule.selector = finalizeSelector(finalFormat, {
561
- selector: rule.selector,
562
- candidate,
563
- context,
564
- })
565
- })
566
- match[1] = container.nodes[0]
748
+ match = applyFinalFormat(match, { context, candidate, original })
749
+
750
+ // Skip rules with invalid selectors
751
+ // This will cause the candidate to be added to the "not class"
752
+ // cache skipping it entirely for future builds
753
+ if (match === null) {
754
+ continue
567
755
  }
568
756
 
569
757
  yield match
@@ -571,20 +759,105 @@ function* resolveMatches(candidate, context) {
571
759
  }
572
760
  }
573
761
 
762
+ function applyFinalFormat(match, { context, candidate, original }) {
763
+ if (!match[0].collectedFormats) {
764
+ return match
765
+ }
766
+
767
+ let isValid = true
768
+ let finalFormat
769
+
770
+ try {
771
+ finalFormat = formatVariantSelector(match[0].collectedFormats, {
772
+ context,
773
+ candidate,
774
+ })
775
+ } catch {
776
+ // The format selector we produced is invalid
777
+ // This could be because:
778
+ // - A bug exists
779
+ // - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`)
780
+ // - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`)
781
+ // Either way the build will fail because of this
782
+ // We would rather that the build pass "silently" given that this could
783
+ // happen because of picking up invalid things when scanning content
784
+ // So we'll throw out the candidate instead
785
+
786
+ return null
787
+ }
788
+
789
+ let container = postcss.root({ nodes: [match[1].clone()] })
790
+
791
+ container.walkRules((rule) => {
792
+ if (inKeyframes(rule)) {
793
+ return
794
+ }
795
+
796
+ try {
797
+ rule.selector = finalizeSelector(rule.selector, finalFormat, {
798
+ candidate: original,
799
+ context,
800
+ })
801
+ } catch {
802
+ // If this selector is invalid we also want to skip it
803
+ // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content
804
+ isValid = false
805
+ return false
806
+ }
807
+ })
808
+
809
+ if (!isValid) {
810
+ return null
811
+ }
812
+
813
+ match[1] = container.nodes[0]
814
+
815
+ return match
816
+ }
817
+
574
818
  function inKeyframes(rule) {
575
819
  return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
576
820
  }
577
821
 
822
+ function getImportantStrategy(important) {
823
+ if (important === true) {
824
+ return (rule) => {
825
+ if (inKeyframes(rule)) {
826
+ return
827
+ }
828
+
829
+ rule.walkDecls((d) => {
830
+ if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
831
+ d.important = true
832
+ }
833
+ })
834
+ }
835
+ }
836
+
837
+ if (typeof important === 'string') {
838
+ return (rule) => {
839
+ if (inKeyframes(rule)) {
840
+ return
841
+ }
842
+
843
+ rule.selectors = rule.selectors.map((selector) => {
844
+ return `${important} ${selector}`
845
+ })
846
+ }
847
+ }
848
+ }
849
+
578
850
  function generateRules(candidates, context) {
579
851
  let allRules = []
852
+ let strategy = getImportantStrategy(context.tailwindConfig.important)
580
853
 
581
854
  for (let candidate of candidates) {
582
855
  if (context.notClassCache.has(candidate)) {
583
856
  continue
584
857
  }
585
858
 
586
- if (context.classCache.has(candidate)) {
587
- allRules.push(context.classCache.get(candidate))
859
+ if (context.candidateRuleCache.has(candidate)) {
860
+ allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)))
588
861
  continue
589
862
  }
590
863
 
@@ -596,47 +869,27 @@ function generateRules(candidates, context) {
596
869
  }
597
870
 
598
871
  context.classCache.set(candidate, matches)
599
- allRules.push(matches)
600
- }
601
872
 
602
- // Strategy based on `tailwindConfig.important`
603
- let strategy = ((important) => {
604
- if (important === true) {
605
- return (rule) => {
606
- rule.walkDecls((d) => {
607
- if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
608
- d.important = true
609
- }
610
- })
611
- }
612
- }
873
+ let rules = context.candidateRuleCache.get(candidate) ?? new Set()
874
+ context.candidateRuleCache.set(candidate, rules)
613
875
 
614
- if (typeof important === 'string') {
615
- return (rule) => {
616
- rule.selectors = rule.selectors.map((selector) => {
617
- return `${important} ${selector}`
618
- })
619
- }
620
- }
621
- })(context.tailwindConfig.important)
876
+ for (const match of matches) {
877
+ let [{ sort, options }, rule] = match
622
878
 
623
- return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
624
- if (options.respectImportant) {
625
- if (strategy) {
879
+ if (options.respectImportant && strategy) {
626
880
  let container = postcss.root({ nodes: [rule.clone()] })
627
- container.walkRules((r) => {
628
- if (inKeyframes(r)) {
629
- return
630
- }
631
-
632
- strategy(r)
633
- })
881
+ container.walkRules(strategy)
634
882
  rule = container.nodes[0]
635
883
  }
884
+
885
+ let newEntry = [sort, rule]
886
+ rules.add(newEntry)
887
+ context.ruleCache.add(newEntry)
888
+ allRules.push(newEntry)
636
889
  }
890
+ }
637
891
 
638
- return [sort | context.layerOrder[layer], rule]
639
- })
892
+ return allRules
640
893
  }
641
894
 
642
895
  function isArbitraryValue(input) {