tailwindcss 0.0.0-insiders.fda68f7 → 0.0.0-oxide.6bf5e56

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 (195) hide show
  1. package/CHANGELOG.md +603 -2
  2. package/LICENSE +1 -2
  3. package/README.md +14 -6
  4. package/colors.d.ts +3 -0
  5. package/colors.js +2 -304
  6. package/defaultConfig.d.ts +3 -0
  7. package/defaultConfig.js +2 -4
  8. package/defaultTheme.d.ts +4 -0
  9. package/defaultTheme.js +2 -4
  10. package/lib/cli/build/deps.js +54 -0
  11. package/lib/cli/build/index.js +48 -0
  12. package/lib/cli/build/plugin.js +367 -0
  13. package/lib/cli/build/utils.js +78 -0
  14. package/lib/cli/build/watching.js +178 -0
  15. package/lib/cli/help/index.js +71 -0
  16. package/lib/cli/index.js +18 -0
  17. package/lib/cli/init/index.js +46 -0
  18. package/lib/cli/shared.js +13 -0
  19. package/lib/cli-peer-dependencies.js +22 -14
  20. package/lib/cli.js +217 -743
  21. package/lib/constants.js +41 -34
  22. package/lib/corePluginList.js +178 -5
  23. package/lib/corePlugins.js +3879 -2941
  24. package/lib/css/preflight.css +22 -9
  25. package/lib/featureFlags.js +61 -50
  26. package/lib/index.js +45 -28
  27. package/lib/lib/cacheInvalidation.js +90 -0
  28. package/lib/lib/collapseAdjacentRules.js +52 -36
  29. package/lib/lib/collapseDuplicateDeclarations.js +83 -0
  30. package/lib/lib/content.js +176 -0
  31. package/lib/lib/defaultExtractor.js +236 -0
  32. package/lib/lib/detectNesting.js +37 -0
  33. package/lib/lib/evaluateTailwindFunctions.js +203 -161
  34. package/lib/lib/expandApplyAtRules.js +502 -221
  35. package/lib/lib/expandTailwindAtRules.js +258 -243
  36. package/lib/lib/findAtConfigPath.js +44 -0
  37. package/lib/lib/generateRules.js +775 -320
  38. package/lib/lib/getModuleDependencies.js +44 -46
  39. package/lib/lib/normalizeTailwindDirectives.js +79 -60
  40. package/lib/lib/offsets.js +217 -0
  41. package/lib/lib/partitionApplyAtRules.js +56 -0
  42. package/lib/lib/regex.js +60 -0
  43. package/lib/lib/resolveDefaultsAtRules.js +150 -94
  44. package/lib/lib/setupContextUtils.js +1146 -599
  45. package/lib/lib/setupTrackingContext.js +129 -177
  46. package/lib/lib/sharedState.js +53 -21
  47. package/lib/lib/substituteScreenAtRules.js +26 -28
  48. package/{nesting → lib/postcss-plugins/nesting}/README.md +2 -2
  49. package/lib/postcss-plugins/nesting/index.js +19 -0
  50. package/lib/postcss-plugins/nesting/plugin.js +87 -0
  51. package/lib/processTailwindFeatures.js +58 -53
  52. package/lib/public/colors.js +331 -0
  53. package/lib/public/create-plugin.js +15 -0
  54. package/lib/public/default-config.js +16 -0
  55. package/lib/public/default-theme.js +16 -0
  56. package/lib/public/resolve-config.js +22 -0
  57. package/lib/util/bigSign.js +7 -6
  58. package/lib/util/buildMediaQuery.js +21 -32
  59. package/lib/util/cloneDeep.js +16 -14
  60. package/lib/util/cloneNodes.js +29 -15
  61. package/lib/util/color.js +90 -66
  62. package/lib/util/configurePlugins.js +17 -15
  63. package/lib/util/createPlugin.js +23 -26
  64. package/lib/util/createUtilityPlugin.js +46 -46
  65. package/lib/util/dataTypes.js +242 -0
  66. package/lib/util/defaults.js +20 -15
  67. package/lib/util/escapeClassName.js +18 -17
  68. package/lib/util/escapeCommas.js +7 -6
  69. package/lib/util/flattenColorPalette.js +13 -12
  70. package/lib/util/formatVariantSelector.js +285 -0
  71. package/lib/util/getAllConfigs.js +44 -18
  72. package/lib/util/hashConfig.js +15 -12
  73. package/lib/util/isKeyframeRule.js +7 -6
  74. package/lib/util/isPlainObject.js +11 -11
  75. package/lib/util/isSyntacticallyValidPropertyValue.js +72 -0
  76. package/lib/util/log.js +52 -33
  77. package/lib/util/nameClass.js +37 -26
  78. package/lib/util/negateValue.js +31 -17
  79. package/lib/util/normalizeConfig.js +281 -0
  80. package/lib/util/normalizeScreens.js +170 -0
  81. package/lib/util/parseAnimationValue.js +85 -54
  82. package/lib/util/parseBoxShadowValue.js +84 -0
  83. package/lib/util/parseDependency.js +41 -70
  84. package/lib/util/parseGlob.js +34 -0
  85. package/lib/util/parseObjectStyles.js +30 -24
  86. package/lib/util/pluginUtils.js +252 -287
  87. package/lib/util/prefixSelector.js +20 -20
  88. package/lib/util/removeAlphaVariables.js +29 -0
  89. package/lib/util/resolveConfig.js +221 -256
  90. package/lib/util/resolveConfigPath.js +43 -48
  91. package/lib/util/responsive.js +18 -14
  92. package/lib/util/splitAtTopLevelOnly.js +43 -0
  93. package/lib/util/tap.js +8 -7
  94. package/lib/util/toColorValue.js +7 -6
  95. package/lib/util/toPath.js +27 -8
  96. package/lib/util/transformThemeValue.js +67 -28
  97. package/lib/util/validateConfig.js +24 -0
  98. package/lib/util/validateFormalSyntax.js +24 -0
  99. package/lib/util/withAlphaVariable.js +67 -57
  100. package/nesting/index.js +2 -12
  101. package/package.json +60 -65
  102. package/peers/index.js +76445 -84221
  103. package/plugin.d.ts +11 -0
  104. package/plugin.js +1 -2
  105. package/resolveConfig.d.ts +12 -0
  106. package/resolveConfig.js +2 -7
  107. package/scripts/create-plugin-list.js +2 -2
  108. package/scripts/generate-types.js +105 -0
  109. package/scripts/release-channel.js +18 -0
  110. package/scripts/release-notes.js +21 -0
  111. package/scripts/type-utils.js +27 -0
  112. package/src/cli/build/deps.js +56 -0
  113. package/src/cli/build/index.js +49 -0
  114. package/src/cli/build/plugin.js +439 -0
  115. package/src/cli/build/utils.js +76 -0
  116. package/src/cli/build/watching.js +227 -0
  117. package/src/cli/help/index.js +70 -0
  118. package/src/cli/index.js +3 -0
  119. package/src/cli/init/index.js +50 -0
  120. package/src/cli/shared.js +6 -0
  121. package/src/cli-peer-dependencies.js +7 -1
  122. package/src/cli.js +50 -575
  123. package/src/corePluginList.js +1 -1
  124. package/src/corePlugins.js +2405 -1948
  125. package/src/css/preflight.css +22 -9
  126. package/src/featureFlags.js +26 -10
  127. package/src/index.js +19 -6
  128. package/src/lib/cacheInvalidation.js +52 -0
  129. package/src/lib/collapseAdjacentRules.js +21 -2
  130. package/src/lib/collapseDuplicateDeclarations.js +93 -0
  131. package/src/lib/content.js +212 -0
  132. package/src/lib/defaultExtractor.js +211 -0
  133. package/src/lib/detectNesting.js +39 -0
  134. package/src/lib/evaluateTailwindFunctions.js +84 -10
  135. package/src/lib/expandApplyAtRules.js +508 -153
  136. package/src/lib/expandTailwindAtRules.js +130 -104
  137. package/src/lib/findAtConfigPath.js +48 -0
  138. package/src/lib/generateRules.js +596 -70
  139. package/src/lib/normalizeTailwindDirectives.js +10 -3
  140. package/src/lib/offsets.js +270 -0
  141. package/src/lib/partitionApplyAtRules.js +52 -0
  142. package/src/lib/regex.js +74 -0
  143. package/src/lib/resolveDefaultsAtRules.js +105 -47
  144. package/src/lib/setupContextUtils.js +828 -196
  145. package/src/lib/setupTrackingContext.js +19 -54
  146. package/src/lib/sharedState.js +45 -7
  147. package/src/lib/substituteScreenAtRules.js +6 -3
  148. package/src/postcss-plugins/nesting/README.md +42 -0
  149. package/src/postcss-plugins/nesting/index.js +13 -0
  150. package/src/postcss-plugins/nesting/plugin.js +80 -0
  151. package/src/processTailwindFeatures.js +19 -2
  152. package/src/public/colors.js +300 -0
  153. package/src/public/create-plugin.js +2 -0
  154. package/src/public/default-config.js +4 -0
  155. package/src/public/default-theme.js +4 -0
  156. package/src/public/resolve-config.js +7 -0
  157. package/src/util/buildMediaQuery.js +14 -16
  158. package/src/util/cloneNodes.js +19 -2
  159. package/src/util/color.js +31 -14
  160. package/src/util/createUtilityPlugin.js +2 -11
  161. package/src/util/dataTypes.js +256 -0
  162. package/src/util/defaults.js +6 -0
  163. package/src/util/formatVariantSelector.js +319 -0
  164. package/src/util/getAllConfigs.js +19 -0
  165. package/src/util/isSyntacticallyValidPropertyValue.js +61 -0
  166. package/src/util/log.js +23 -22
  167. package/src/util/nameClass.js +14 -6
  168. package/src/util/negateValue.js +15 -5
  169. package/src/util/normalizeConfig.js +300 -0
  170. package/src/util/normalizeScreens.js +140 -0
  171. package/src/util/parseAnimationValue.js +7 -1
  172. package/src/util/parseBoxShadowValue.js +72 -0
  173. package/src/util/parseDependency.js +37 -38
  174. package/src/util/parseGlob.js +24 -0
  175. package/src/util/pluginUtils.js +216 -197
  176. package/src/util/prefixSelector.js +7 -8
  177. package/src/util/removeAlphaVariables.js +24 -0
  178. package/src/util/resolveConfig.js +86 -91
  179. package/src/util/splitAtTopLevelOnly.js +45 -0
  180. package/src/util/toPath.js +23 -1
  181. package/src/util/transformThemeValue.js +33 -8
  182. package/src/util/validateConfig.js +13 -0
  183. package/src/util/validateFormalSyntax.js +34 -0
  184. package/src/util/withAlphaVariable.js +14 -9
  185. package/stubs/defaultConfig.stub.js +186 -117
  186. package/stubs/simpleConfig.stub.js +1 -1
  187. package/types/config.d.ts +362 -0
  188. package/types/generated/.gitkeep +0 -0
  189. package/types/generated/colors.d.ts +276 -0
  190. package/types/generated/corePluginList.d.ts +1 -0
  191. package/types/generated/default-theme.d.ts +342 -0
  192. package/types/index.d.ts +7 -0
  193. package/lib/lib/setupWatchingContext.js +0 -331
  194. package/nesting/plugin.js +0 -41
  195. package/src/lib/setupWatchingContext.js +0 -306
@@ -3,13 +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
+ import log from '../util/log'
8
+ import * as sharedState from './sharedState'
9
+ import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
10
+ import { asClass } from '../util/nameClass'
11
+ import { normalize } from '../util/dataTypes'
12
+ import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
13
+ import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
14
+ import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
15
+ import { flagEnabled } from '../featureFlags'
7
16
 
8
17
  let classNameParser = selectorParser((selectors) => {
9
18
  return selectors.first.filter(({ type }) => type === 'class').pop().value
10
19
  })
11
20
 
12
- function getClassNameFromSelector(selector) {
21
+ export function getClassNameFromSelector(selector) {
13
22
  return classNameParser.transformSync(selector)
14
23
  }
15
24
 
@@ -20,33 +29,49 @@ function getClassNameFromSelector(selector) {
20
29
  // Example with dynamic classes:
21
30
  // ['grid-cols', '[[linename],1fr,auto]']
22
31
  // ['grid', 'cols-[[linename],1fr,auto]']
23
- function* candidatePermutations(candidate, lastIndex = Infinity) {
24
- if (lastIndex < 0) {
25
- return
26
- }
27
-
28
- let dashIdx
29
-
30
- if (lastIndex === Infinity && candidate.endsWith(']')) {
31
- let bracketIdx = candidate.indexOf('[')
32
+ function* candidatePermutations(candidate) {
33
+ let lastIndex = Infinity
34
+
35
+ while (lastIndex >= 0) {
36
+ let dashIdx
37
+ let wasSlash = false
38
+
39
+ if (lastIndex === Infinity && candidate.endsWith(']')) {
40
+ let bracketIdx = candidate.indexOf('[')
41
+
42
+ // If character before `[` isn't a dash or a slash, this isn't a dynamic class
43
+ // eg. string[]
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
55
+ } else {
56
+ dashIdx = candidate.lastIndexOf('-', lastIndex)
57
+ }
32
58
 
33
- // If character before `[` isn't a dash or a slash, this isn't a dynamic class
34
- // eg. string[]
35
- dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
36
- } else {
37
- dashIdx = candidate.lastIndexOf('-', lastIndex)
38
- }
59
+ if (dashIdx < 0) {
60
+ break
61
+ }
39
62
 
40
- if (dashIdx < 0) {
41
- return
42
- }
63
+ let prefix = candidate.slice(0, dashIdx)
64
+ let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1)
43
65
 
44
- let prefix = candidate.slice(0, dashIdx)
45
- let modifier = candidate.slice(dashIdx + 1)
66
+ lastIndex = dashIdx - 1
46
67
 
47
- yield [prefix, modifier]
68
+ // TODO: This feels a bit hacky
69
+ if (prefix === '' || modifier === '/') {
70
+ continue
71
+ }
48
72
 
49
- yield* candidatePermutations(candidate, dashIdx - 1)
73
+ yield [prefix, modifier]
74
+ }
50
75
  }
51
76
 
52
77
  function applyPrefix(matches, context) {
@@ -58,9 +83,23 @@ function applyPrefix(matches, context) {
58
83
  let [meta] = match
59
84
  if (meta.options.respectPrefix) {
60
85
  let container = postcss.root({ nodes: [match[1].clone()] })
86
+ let classCandidate = match[1].raws.tailwind.classCandidate
87
+
61
88
  container.walkRules((r) => {
62
- r.selector = prefixSelector(context.tailwindConfig.prefix, r.selector)
89
+ // If this is a negative utility with a dash *before* the prefix we
90
+ // have to ensure that the generated selector matches the candidate
91
+
92
+ // Not doing this will cause `-tw-top-1` to generate the class `.tw--top-1`
93
+ // The disconnect between candidate <-> class can cause @apply to hard crash.
94
+ let shouldPrependNegative = classCandidate.startsWith('-')
95
+
96
+ r.selector = prefixSelector(
97
+ context.tailwindConfig.prefix,
98
+ r.selector,
99
+ shouldPrependNegative
100
+ )
63
101
  })
102
+
64
103
  match[1] = container.nodes[0]
65
104
  }
66
105
  }
@@ -68,7 +107,7 @@ function applyPrefix(matches, context) {
68
107
  return matches
69
108
  }
70
109
 
71
- function applyImportant(matches) {
110
+ function applyImportant(matches, classCandidate) {
72
111
  if (matches.length === 0) {
73
112
  return matches
74
113
  }
@@ -77,9 +116,15 @@ function applyImportant(matches) {
77
116
  for (let [meta, rule] of matches) {
78
117
  let container = postcss.root({ nodes: [rule.clone()] })
79
118
  container.walkRules((r) => {
80
- r.selector = updateAllClasses(r.selector, (className) => {
81
- return `!${className}`
82
- })
119
+ r.selector = updateAllClasses(
120
+ filterSelectorsForClass(r.selector, classCandidate),
121
+ (className) => {
122
+ if (className === classCandidate) {
123
+ return `!${className}`
124
+ }
125
+ return className
126
+ }
127
+ )
83
128
  r.walkDecls((d) => (d.important = true))
84
129
  })
85
130
  result.push([{ ...meta, important: true }, container.nodes[0]])
@@ -102,21 +147,86 @@ function applyVariant(variant, matches, context) {
102
147
  return matches
103
148
  }
104
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
+
105
203
  if (context.variantMap.has(variant)) {
106
- let variantFunctionTuples = context.variantMap.get(variant)
204
+ let variantFunctionTuples = context.variantMap.get(variant).slice()
107
205
  let result = []
108
206
 
109
207
  for (let [meta, rule] of matches) {
110
- if (meta.options.respectVariants === false) {
111
- result.push([meta, rule])
208
+ // Don't generate variants for user css
209
+ if (meta.layer === 'user') {
112
210
  continue
113
211
  }
114
212
 
115
213
  let container = postcss.root({ nodes: [rule.clone()] })
116
214
 
117
- for (let [variantSort, variantFunction] of variantFunctionTuples) {
118
- let clone = container.clone()
215
+ for (let [variantSort, variantFunction, containerFromArray] of variantFunctionTuples) {
216
+ let clone = (containerFromArray ?? container).clone()
217
+ let collectedFormats = []
218
+
219
+ function prepareBackup() {
220
+ // Already prepared, chicken out
221
+ if (clone.raws.neededBackup) {
222
+ return
223
+ }
224
+ clone.raws.neededBackup = true
225
+ clone.walkRules((rule) => (rule.raws.originalSelector = rule.selector))
226
+ }
227
+
119
228
  function modifySelectors(modifierFunction) {
229
+ prepareBackup()
120
230
  clone.each((rule) => {
121
231
  if (rule.type !== 'rule') {
122
232
  return
@@ -131,20 +241,118 @@ function applyVariant(variant, matches, context) {
131
241
  })
132
242
  })
133
243
  })
244
+
134
245
  return clone
135
246
  }
136
247
 
137
248
  let ruleWithVariant = variantFunction({
138
- container: clone,
249
+ // Public API
250
+ get container() {
251
+ prepareBackup()
252
+ return clone
253
+ },
139
254
  separator: context.tailwindConfig.separator,
140
255
  modifySelectors,
256
+
257
+ // Private API for now
258
+ wrap(wrapper) {
259
+ let nodes = clone.nodes
260
+ clone.removeAll()
261
+ wrapper.append(nodes)
262
+ clone.append(wrapper)
263
+ },
264
+ format(selectorFormat) {
265
+ collectedFormats.push(selectorFormat)
266
+ },
267
+ args,
141
268
  })
142
269
 
270
+ // It can happen that a list of format strings is returned from within the function. In that
271
+ // case, we have to process them as well. We can use the existing `variantSort`.
272
+ if (Array.isArray(ruleWithVariant)) {
273
+ for (let [idx, variantFunction] of ruleWithVariant.entries()) {
274
+ // This is a little bit scary since we are pushing to an array of items that we are
275
+ // currently looping over. However, you can also think of it like a processing queue
276
+ // where you keep handling jobs until everything is done and each job can queue more
277
+ // jobs if needed.
278
+ variantFunctionTuples.push([
279
+ context.offsets.applyParallelOffset(variantSort, idx),
280
+ variantFunction,
281
+
282
+ // If the clone has been modified we have to pass that back
283
+ // though so each rule can use the modified container
284
+ clone.clone(),
285
+ ])
286
+ }
287
+ continue
288
+ }
289
+
290
+ if (typeof ruleWithVariant === 'string') {
291
+ collectedFormats.push(ruleWithVariant)
292
+ }
293
+
143
294
  if (ruleWithVariant === null) {
144
295
  continue
145
296
  }
146
297
 
147
- let withOffset = [{ ...meta, sort: variantSort | meta.sort }, clone.nodes[0]]
298
+ // We had to backup selectors, therefore we assume that somebody touched
299
+ // `container` or `modifySelectors`. Let's see if they did, so that we
300
+ // can restore the selectors, and collect the format strings.
301
+ if (clone.raws.neededBackup) {
302
+ delete clone.raws.neededBackup
303
+ clone.walkRules((rule) => {
304
+ let before = rule.raws.originalSelector
305
+ if (!before) return
306
+ delete rule.raws.originalSelector
307
+ if (before === rule.selector) return // No mutation happened
308
+
309
+ let modified = rule.selector
310
+
311
+ // Rebuild the base selector, this is what plugin authors would do
312
+ // as well. E.g.: `${variant}${separator}${className}`.
313
+ // However, plugin authors probably also prepend or append certain
314
+ // classes, pseudos, ids, ...
315
+ let rebuiltBase = selectorParser((selectors) => {
316
+ selectors.walkClasses((classNode) => {
317
+ classNode.value = `${variant}${context.tailwindConfig.separator}${classNode.value}`
318
+ })
319
+ }).processSync(before)
320
+
321
+ // Now that we know the original selector, the new selector, and
322
+ // the rebuild part in between, we can replace the part that plugin
323
+ // authors need to rebuild with `&`, and eventually store it in the
324
+ // collectedFormats. Similar to what `format('...')` would do.
325
+ //
326
+ // E.g.:
327
+ // variant: foo
328
+ // selector: .markdown > p
329
+ // modified (by plugin): .foo .foo\\:markdown > p
330
+ // rebuiltBase (internal): .foo\\:markdown > p
331
+ // format: .foo &
332
+ collectedFormats.push(modified.replace(rebuiltBase, '&'))
333
+ rule.selector = before
334
+ })
335
+ }
336
+
337
+ // This tracks the originating layer for the variant
338
+ // For example:
339
+ // .sm:underline {} is a variant of something in the utilities layer
340
+ // .sm:container {} is a variant of the container component
341
+ clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer }
342
+
343
+ let withOffset = [
344
+ {
345
+ ...meta,
346
+ sort: context.offsets.applyVariantOffset(
347
+ meta.sort,
348
+ variantSort,
349
+ Object.assign(args, context.variantOptions.get(variant))
350
+ ),
351
+ collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
352
+ isArbitraryVariant: isArbitraryValue(variant),
353
+ },
354
+ clone.nodes[0],
355
+ ]
148
356
  result.push(withOffset)
149
357
  }
150
358
  }
@@ -174,34 +382,154 @@ function parseRules(rule, cache, options = {}) {
174
382
  return [cache.get(rule), options]
175
383
  }
176
384
 
385
+ const IS_VALID_PROPERTY_NAME = /^[a-z_-]/
386
+
387
+ function isValidPropName(name) {
388
+ return IS_VALID_PROPERTY_NAME.test(name)
389
+ }
390
+
391
+ /**
392
+ * @param {string} declaration
393
+ * @returns {boolean}
394
+ */
395
+ function looksLikeUri(declaration) {
396
+ // Quick bailout for obvious non-urls
397
+ // This doesn't support schemes that don't use a leading // but that's unlikely to be a problem
398
+ if (!declaration.includes('://')) {
399
+ return false
400
+ }
401
+
402
+ try {
403
+ const url = new URL(declaration)
404
+ return url.scheme !== '' && url.host !== ''
405
+ } catch (err) {
406
+ // Definitely not a valid url
407
+ return false
408
+ }
409
+ }
410
+
411
+ function isParsableNode(node) {
412
+ let isParsable = true
413
+
414
+ node.walkDecls((decl) => {
415
+ if (!isParsableCssValue(decl.name, decl.value)) {
416
+ isParsable = false
417
+ return false
418
+ }
419
+ })
420
+
421
+ return isParsable
422
+ }
423
+
424
+ function isParsableCssValue(property, value) {
425
+ // We don't want to to treat [https://example.com] as a custom property
426
+ // Even though, according to the CSS grammar, it's a totally valid CSS declaration
427
+ // So we short-circuit here by checking if the custom property looks like a url
428
+ if (looksLikeUri(`${property}:${value}`)) {
429
+ return false
430
+ }
431
+
432
+ try {
433
+ postcss.parse(`a{${property}:${value}}`).toResult()
434
+ return true
435
+ } catch (err) {
436
+ return false
437
+ }
438
+ }
439
+
440
+ function extractArbitraryProperty(classCandidate, context) {
441
+ let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? []
442
+
443
+ if (value === undefined) {
444
+ return null
445
+ }
446
+
447
+ if (!isValidPropName(property)) {
448
+ return null
449
+ }
450
+
451
+ if (!isValidArbitraryValue(value)) {
452
+ return null
453
+ }
454
+
455
+ let normalized = normalize(value)
456
+
457
+ if (!isParsableCssValue(property, normalized)) {
458
+ return null
459
+ }
460
+
461
+ let sort = context.offsets.arbitraryProperty()
462
+
463
+ return [
464
+ [
465
+ { sort, layer: 'utilities' },
466
+ () => ({
467
+ [asClass(classCandidate)]: {
468
+ [property]: normalized,
469
+ },
470
+ }),
471
+ ],
472
+ ]
473
+ }
474
+
177
475
  function* resolveMatchedPlugins(classCandidate, context) {
178
476
  if (context.candidateRuleMap.has(classCandidate)) {
179
477
  yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT']
180
478
  }
181
479
 
480
+ yield* (function* (arbitraryPropertyRule) {
481
+ if (arbitraryPropertyRule !== null) {
482
+ yield [arbitraryPropertyRule, 'DEFAULT']
483
+ }
484
+ })(extractArbitraryProperty(classCandidate, context))
485
+
182
486
  let candidatePrefix = classCandidate
183
487
  let negative = false
184
488
 
185
- const twConfigPrefix = context.tailwindConfig.prefix || ''
489
+ const twConfigPrefix = context.tailwindConfig.prefix
490
+
186
491
  const twConfigPrefixLen = twConfigPrefix.length
187
- if (candidatePrefix[twConfigPrefixLen] === '-') {
492
+
493
+ const hasMatchingPrefix =
494
+ candidatePrefix.startsWith(twConfigPrefix) || candidatePrefix.startsWith(`-${twConfigPrefix}`)
495
+
496
+ if (candidatePrefix[twConfigPrefixLen] === '-' && hasMatchingPrefix) {
188
497
  negative = true
189
498
  candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1)
190
499
  }
191
500
 
501
+ if (negative && context.candidateRuleMap.has(candidatePrefix)) {
502
+ yield [context.candidateRuleMap.get(candidatePrefix), '-DEFAULT']
503
+ }
504
+
192
505
  for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) {
193
506
  if (context.candidateRuleMap.has(prefix)) {
194
507
  yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier]
195
- return
196
508
  }
197
509
  }
198
510
  }
199
511
 
200
512
  function splitWithSeparator(input, separator) {
201
- return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
513
+ if (input === sharedState.NOT_ON_DEMAND) {
514
+ return [sharedState.NOT_ON_DEMAND]
515
+ }
516
+
517
+ return splitAtTopLevelOnly(input, separator)
518
+ }
519
+
520
+ function* recordCandidates(matches, classCandidate) {
521
+ for (const match of matches) {
522
+ match[1].raws.tailwind = {
523
+ ...match[1].raws.tailwind,
524
+ classCandidate,
525
+ preserveSource: match[0].options?.preserveSource ?? false,
526
+ }
527
+
528
+ yield match
529
+ }
202
530
  }
203
531
 
204
- function* resolveMatches(candidate, context) {
532
+ function* resolveMatches(candidate, context, original = candidate) {
205
533
  let separator = context.tailwindConfig.separator
206
534
  let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
207
535
  let important = false
@@ -211,6 +539,15 @@ function* resolveMatches(candidate, context) {
211
539
  classCandidate = classCandidate.slice(1)
212
540
  }
213
541
 
542
+ if (flagEnabled(context.tailwindConfig, 'variantGrouping')) {
543
+ if (classCandidate.startsWith('(') && classCandidate.endsWith(')')) {
544
+ let base = variants.slice().reverse().join(separator)
545
+ for (let part of splitAtTopLevelOnly(classCandidate.slice(1, -1), ',')) {
546
+ yield* resolveMatches(base + separator + part, context, original)
547
+ }
548
+ }
549
+ }
550
+
214
551
  // TODO: Reintroduce this in ways that doesn't break on false positives
215
552
  // function sortAgainst(toSort, against) {
216
553
  // return toSort.slice().sort((a, z) => {
@@ -225,31 +562,170 @@ function* resolveMatches(candidate, context) {
225
562
 
226
563
  for (let matchedPlugins of resolveMatchedPlugins(classCandidate, context)) {
227
564
  let matches = []
565
+ let typesByMatches = new Map()
566
+
228
567
  let [plugins, modifier] = matchedPlugins
568
+ let isOnlyPlugin = plugins.length === 1
229
569
 
230
570
  for (let [sort, plugin] of plugins) {
571
+ let matchesPerPlugin = []
572
+
231
573
  if (typeof plugin === 'function') {
232
- for (let ruleSet of [].concat(plugin(modifier))) {
574
+ for (let ruleSet of [].concat(plugin(modifier, { isOnlyPlugin }))) {
233
575
  let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
234
576
  for (let rule of rules) {
235
- matches.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
577
+ matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
236
578
  }
237
579
  }
238
580
  }
239
581
  // Only process static plugins on exact matches
240
- else if (modifier === 'DEFAULT') {
582
+ else if (modifier === 'DEFAULT' || modifier === '-DEFAULT') {
241
583
  let ruleSet = plugin
242
584
  let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
243
585
  for (let rule of rules) {
244
- matches.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
586
+ matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
245
587
  }
246
588
  }
589
+
590
+ if (matchesPerPlugin.length > 0) {
591
+ let matchingTypes = Array.from(
592
+ getMatchingTypes(
593
+ sort.options?.types ?? [],
594
+ modifier,
595
+ sort.options ?? {},
596
+ context.tailwindConfig
597
+ )
598
+ ).map(([_, type]) => type)
599
+
600
+ if (matchingTypes.length > 0) {
601
+ typesByMatches.set(matchesPerPlugin, matchingTypes)
602
+ }
603
+
604
+ matches.push(matchesPerPlugin)
605
+ }
247
606
  }
248
607
 
608
+ if (isArbitraryValue(modifier)) {
609
+ if (matches.length > 1) {
610
+ // Partition plugins in 2 categories so that we can start searching in the plugins that
611
+ // don't have `any` as a type first.
612
+ let [withAny, withoutAny] = matches.reduce(
613
+ (group, plugin) => {
614
+ let hasAnyType = plugin.some(([{ options }]) =>
615
+ options.types.some(({ type }) => type === 'any')
616
+ )
617
+
618
+ if (hasAnyType) {
619
+ group[0].push(plugin)
620
+ } else {
621
+ group[1].push(plugin)
622
+ }
623
+ return group
624
+ },
625
+ [[], []]
626
+ )
627
+
628
+ function findFallback(matches) {
629
+ // If only a single plugin matches, let's take that one
630
+ if (matches.length === 1) {
631
+ return matches[0]
632
+ }
633
+
634
+ // Otherwise, find the plugin that creates a valid rule given the arbitrary value, and
635
+ // also has the correct type which preferOnConflicts the plugin in case of clashes.
636
+ return matches.find((rules) => {
637
+ let matchingTypes = typesByMatches.get(rules)
638
+ return rules.some(([{ options }, rule]) => {
639
+ if (!isParsableNode(rule)) {
640
+ return false
641
+ }
642
+
643
+ return options.types.some(
644
+ ({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict
645
+ )
646
+ })
647
+ })
648
+ }
649
+
650
+ // Try to find a fallback plugin, because we already know that multiple plugins matched for
651
+ // the given arbitrary value.
652
+ let fallback = findFallback(withoutAny) ?? findFallback(withAny)
653
+ if (fallback) {
654
+ matches = [fallback]
655
+ }
656
+
657
+ // We couldn't find a fallback plugin which means that there are now multiple plugins that
658
+ // generated css for the current candidate. This means that the result is ambiguous and this
659
+ // should not happen. We won't generate anything right now, so let's report this to the user
660
+ // by logging some options about what they can do.
661
+ else {
662
+ let typesPerPlugin = matches.map(
663
+ (match) => new Set([...(typesByMatches.get(match) ?? [])])
664
+ )
665
+
666
+ // Remove duplicates, so that we can detect proper unique types for each plugin.
667
+ for (let pluginTypes of typesPerPlugin) {
668
+ for (let type of pluginTypes) {
669
+ let removeFromOwnGroup = false
670
+
671
+ for (let otherGroup of typesPerPlugin) {
672
+ if (pluginTypes === otherGroup) continue
673
+
674
+ if (otherGroup.has(type)) {
675
+ otherGroup.delete(type)
676
+ removeFromOwnGroup = true
677
+ }
678
+ }
679
+
680
+ if (removeFromOwnGroup) pluginTypes.delete(type)
681
+ }
682
+ }
683
+
684
+ let messages = []
685
+
686
+ for (let [idx, group] of typesPerPlugin.entries()) {
687
+ for (let type of group) {
688
+ let rules = matches[idx]
689
+ .map(([, rule]) => rule)
690
+ .flat()
691
+ .map((rule) =>
692
+ rule
693
+ .toString()
694
+ .split('\n')
695
+ .slice(1, -1) // Remove selector and closing '}'
696
+ .map((line) => line.trim())
697
+ .map((x) => ` ${x}`) // Re-indent
698
+ .join('\n')
699
+ )
700
+ .join('\n\n')
701
+
702
+ messages.push(
703
+ ` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
704
+ )
705
+ break
706
+ }
707
+ }
708
+
709
+ log.warn([
710
+ `The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
711
+ ...messages,
712
+ `If this is content and not a class, replace it with \`${candidate
713
+ .replace('[', '&lsqb;')
714
+ .replace(']', '&rsqb;')}\` to silence this warning.`,
715
+ ])
716
+ continue
717
+ }
718
+ }
719
+
720
+ matches = matches.map((list) => list.filter((match) => isParsableNode(match[1])))
721
+ }
722
+
723
+ matches = matches.flat()
724
+ matches = Array.from(recordCandidates(matches, classCandidate))
249
725
  matches = applyPrefix(matches, context)
250
726
 
251
727
  if (important) {
252
- matches = applyImportant(matches, context)
728
+ matches = applyImportant(matches, classCandidate)
253
729
  }
254
730
 
255
731
  for (let variant of variants) {
@@ -257,6 +733,29 @@ function* resolveMatches(candidate, context) {
257
733
  }
258
734
 
259
735
  for (let match of matches) {
736
+ match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
737
+
738
+ // Apply final format selector
739
+ if (match[0].collectedFormats) {
740
+ let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)
741
+ let container = postcss.root({ nodes: [match[1].clone()] })
742
+ container.walkRules((rule) => {
743
+ if (inKeyframes(rule)) return
744
+
745
+ rule.selector = finalizeSelector(finalFormat, {
746
+ selector: rule.selector,
747
+ candidate: original,
748
+ base: candidate
749
+ .split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
750
+ .pop(),
751
+ isArbitraryVariant: match[0].isArbitraryVariant,
752
+
753
+ context,
754
+ })
755
+ })
756
+ match[1] = container.nodes[0]
757
+ }
758
+
260
759
  yield match
261
760
  }
262
761
  }
@@ -266,16 +765,45 @@ function inKeyframes(rule) {
266
765
  return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
267
766
  }
268
767
 
768
+ function getImportantStrategy(important) {
769
+ if (important === true) {
770
+ return (rule) => {
771
+ if (inKeyframes(rule)) {
772
+ return
773
+ }
774
+
775
+ rule.walkDecls((d) => {
776
+ if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
777
+ d.important = true
778
+ }
779
+ })
780
+ }
781
+ }
782
+
783
+ if (typeof important === 'string') {
784
+ return (rule) => {
785
+ if (inKeyframes(rule)) {
786
+ return
787
+ }
788
+
789
+ rule.selectors = rule.selectors.map((selector) => {
790
+ return `${important} ${selector}`
791
+ })
792
+ }
793
+ }
794
+ }
795
+
269
796
  function generateRules(candidates, context) {
270
797
  let allRules = []
798
+ let strategy = getImportantStrategy(context.tailwindConfig.important)
271
799
 
272
800
  for (let candidate of candidates) {
273
801
  if (context.notClassCache.has(candidate)) {
274
802
  continue
275
803
  }
276
804
 
277
- if (context.classCache.has(candidate)) {
278
- allRules.push(context.classCache.get(candidate))
805
+ if (context.candidateRuleCache.has(candidate)) {
806
+ allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)))
279
807
  continue
280
808
  }
281
809
 
@@ -287,33 +815,31 @@ function generateRules(candidates, context) {
287
815
  }
288
816
 
289
817
  context.classCache.set(candidate, matches)
290
- allRules.push(matches)
291
- }
292
818
 
293
- return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
294
- if (options.respectImportant) {
295
- if (context.tailwindConfig.important === true) {
296
- rule.walkDecls((d) => {
297
- if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
298
- d.important = true
299
- }
300
- })
301
- } else if (typeof context.tailwindConfig.important === 'string') {
302
- let container = postcss.root({ nodes: [rule.clone()] })
303
- container.walkRules((r) => {
304
- if (inKeyframes(r)) {
305
- return
306
- }
819
+ let rules = context.candidateRuleCache.get(candidate) ?? new Set()
820
+ context.candidateRuleCache.set(candidate, rules)
307
821
 
308
- r.selectors = r.selectors.map((selector) => {
309
- return `${context.tailwindConfig.important} ${selector}`
310
- })
311
- })
822
+ for (const match of matches) {
823
+ let [{ sort, options }, rule] = match
824
+
825
+ if (options.respectImportant && strategy) {
826
+ let container = postcss.root({ nodes: [rule.clone()] })
827
+ container.walkRules(strategy)
312
828
  rule = container.nodes[0]
313
829
  }
830
+
831
+ let newEntry = [sort, rule]
832
+ rules.add(newEntry)
833
+ context.ruleCache.add(newEntry)
834
+ allRules.push(newEntry)
314
835
  }
315
- return [sort | context.layerOrder[layer], rule]
316
- })
836
+ }
837
+
838
+ return allRules
839
+ }
840
+
841
+ function isArbitraryValue(input) {
842
+ return input.startsWith('[') && input.endsWith(']')
317
843
  }
318
844
 
319
845
  export { resolveMatches, generateRules }