tailwindcss 3.0.24 → 3.1.0

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 (88) hide show
  1. package/CHANGELOG.md +57 -3
  2. package/colors.d.ts +3 -0
  3. package/defaultConfig.d.ts +3 -0
  4. package/defaultTheme.d.ts +3 -0
  5. package/lib/cli-peer-dependencies.js +8 -3
  6. package/lib/cli.js +118 -77
  7. package/lib/corePluginList.js +1 -0
  8. package/lib/corePlugins.js +146 -117
  9. package/lib/css/preflight.css +1 -8
  10. package/lib/featureFlags.js +8 -6
  11. package/lib/index.js +10 -13
  12. package/lib/lib/cacheInvalidation.js +32 -14
  13. package/lib/lib/collapseAdjacentRules.js +5 -3
  14. package/lib/lib/defaultExtractor.js +191 -32
  15. package/lib/lib/evaluateTailwindFunctions.js +22 -13
  16. package/lib/lib/expandApplyAtRules.js +232 -195
  17. package/lib/lib/expandTailwindAtRules.js +40 -26
  18. package/lib/lib/generateRules.js +106 -42
  19. package/lib/lib/regex.js +52 -0
  20. package/lib/lib/resolveDefaultsAtRules.js +6 -9
  21. package/lib/lib/setupContextUtils.js +131 -79
  22. package/lib/lib/setupTrackingContext.js +7 -9
  23. package/lib/lib/sharedState.js +1 -2
  24. package/lib/lib/substituteScreenAtRules.js +1 -2
  25. package/lib/postcss-plugins/nesting/plugin.js +1 -2
  26. package/lib/util/buildMediaQuery.js +1 -2
  27. package/lib/util/cloneDeep.js +2 -4
  28. package/lib/util/color.js +26 -36
  29. package/lib/util/createPlugin.js +1 -2
  30. package/lib/util/createUtilityPlugin.js +1 -2
  31. package/lib/util/dataTypes.js +14 -12
  32. package/lib/util/flattenColorPalette.js +2 -5
  33. package/lib/util/formatVariantSelector.js +64 -57
  34. package/lib/util/getAllConfigs.js +10 -5
  35. package/lib/util/isValidArbitraryValue.js +1 -2
  36. package/lib/util/log.js +2 -3
  37. package/lib/util/negateValue.js +1 -2
  38. package/lib/util/normalizeConfig.js +33 -23
  39. package/lib/util/normalizeScreens.js +1 -2
  40. package/lib/util/parseAnimationValue.js +1 -2
  41. package/lib/util/parseBoxShadowValue.js +2 -43
  42. package/lib/util/pluginUtils.js +11 -3
  43. package/lib/util/resolveConfig.js +57 -34
  44. package/lib/util/splitAtTopLevelOnly.js +90 -0
  45. package/lib/util/transformThemeValue.js +4 -2
  46. package/lib/util/validateConfig.js +21 -0
  47. package/lib/util/withAlphaVariable.js +5 -5
  48. package/package.json +21 -16
  49. package/peers/index.js +3264 -1330
  50. package/plugin.d.ts +11 -0
  51. package/src/cli-peer-dependencies.js +7 -1
  52. package/src/cli.js +97 -33
  53. package/src/corePluginList.js +1 -1
  54. package/src/corePlugins.js +57 -40
  55. package/src/css/preflight.css +1 -8
  56. package/src/featureFlags.js +2 -2
  57. package/src/index.js +0 -2
  58. package/src/lib/collapseAdjacentRules.js +5 -1
  59. package/src/lib/defaultExtractor.js +177 -35
  60. package/src/lib/evaluateTailwindFunctions.js +20 -4
  61. package/src/lib/expandApplyAtRules.js +247 -188
  62. package/src/lib/expandTailwindAtRules.js +4 -4
  63. package/src/lib/generateRules.js +69 -5
  64. package/src/lib/regex.js +74 -0
  65. package/src/lib/resolveDefaultsAtRules.js +1 -1
  66. package/src/lib/setupContextUtils.js +103 -39
  67. package/src/lib/setupTrackingContext.js +4 -0
  68. package/src/util/color.js +20 -18
  69. package/src/util/dataTypes.js +11 -5
  70. package/src/util/formatVariantSelector.js +79 -62
  71. package/src/util/getAllConfigs.js +7 -0
  72. package/src/util/log.js +1 -1
  73. package/src/util/normalizeConfig.js +0 -8
  74. package/src/util/parseBoxShadowValue.js +3 -50
  75. package/src/util/pluginUtils.js +13 -1
  76. package/src/util/resolveConfig.js +66 -54
  77. package/src/util/splitAtTopLevelOnly.js +71 -0
  78. package/src/util/toPath.js +1 -1
  79. package/src/util/transformThemeValue.js +4 -2
  80. package/src/util/validateConfig.js +13 -0
  81. package/src/util/withAlphaVariable.js +1 -1
  82. package/stubs/defaultConfig.stub.js +2 -3
  83. package/stubs/simpleConfig.stub.js +1 -0
  84. package/types/config.d.ts +325 -0
  85. package/types/generated/.gitkeep +0 -0
  86. package/types/generated/colors.d.ts +276 -0
  87. package/types/generated/corePluginList.d.ts +1 -0
  88. package/types/index.d.ts +1 -0
@@ -8,18 +8,39 @@ import escapeClassName from '../util/escapeClassName'
8
8
  /** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */
9
9
 
10
10
  function extractClasses(node) {
11
- let classes = new Set()
11
+ /** @type {Map<string, Set<string>>} */
12
+ let groups = new Map()
13
+
12
14
  let container = postcss.root({ nodes: [node.clone()] })
13
15
 
14
16
  container.walkRules((rule) => {
15
17
  parser((selectors) => {
16
18
  selectors.walkClasses((classSelector) => {
19
+ let parentSelector = classSelector.parent.toString()
20
+
21
+ let classes = groups.get(parentSelector)
22
+ if (!classes) {
23
+ groups.set(parentSelector, (classes = new Set()))
24
+ }
25
+
17
26
  classes.add(classSelector.value)
18
27
  })
19
28
  }).processSync(rule.selector)
20
29
  })
21
30
 
22
- return Array.from(classes)
31
+ let normalizedGroups = Array.from(groups.values(), (classes) => Array.from(classes))
32
+ let classes = normalizedGroups.flat()
33
+
34
+ return Object.assign(classes, { groups: normalizedGroups })
35
+ }
36
+
37
+ let selectorExtractor = parser((root) => root.nodes.map((node) => node.toString()))
38
+
39
+ /**
40
+ * @param {string} ruleSelectors
41
+ */
42
+ function extractSelectors(ruleSelectors) {
43
+ return selectorExtractor.transformSync(ruleSelectors)
23
44
  }
24
45
 
25
46
  function extractBaseCandidates(candidates, separator) {
@@ -252,222 +273,260 @@ function processApply(root, context, localCache) {
252
273
  })
253
274
 
254
275
  // Start the @apply process if we have rules with @apply in them
255
- if (applies.length > 0) {
256
- // Fill up some caches!
257
- let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)])
258
-
259
- /**
260
- * When we have an apply like this:
261
- *
262
- * .abc {
263
- * @apply hover:font-bold;
264
- * }
265
- *
266
- * What we essentially will do is resolve to this:
267
- *
268
- * .abc {
269
- * @apply .hover\:font-bold:hover {
270
- * font-weight: 500;
271
- * }
272
- * }
273
- *
274
- * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
275
- * What happens in this function is that we prepend a `.` and escape the candidate.
276
- * This will result in `.hover\:font-bold`
277
- * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
278
- */
279
- // TODO: Should we use postcss-selector-parser for this instead?
280
- function replaceSelector(selector, utilitySelectors, candidate) {
281
- let needle = `.${escapeClassName(candidate)}`
282
- let utilitySelectorsList = utilitySelectors.split(/\s*\,(?![^(]*\))\s*/g)
283
-
284
- return selector
285
- .split(/\s*\,(?![^(]*\))\s*/g)
286
- .map((s) => {
287
- let replaced = []
288
-
289
- for (let utilitySelector of utilitySelectorsList) {
290
- let replacedSelector = utilitySelector.replace(needle, s)
291
- if (replacedSelector === utilitySelector) {
292
- continue
293
- }
294
- replaced.push(replacedSelector)
276
+ if (applies.length === 0) {
277
+ return
278
+ }
279
+
280
+ // Fill up some caches!
281
+ let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)])
282
+
283
+ /**
284
+ * When we have an apply like this:
285
+ *
286
+ * .abc {
287
+ * @apply hover:font-bold;
288
+ * }
289
+ *
290
+ * What we essentially will do is resolve to this:
291
+ *
292
+ * .abc {
293
+ * @apply .hover\:font-bold:hover {
294
+ * font-weight: 500;
295
+ * }
296
+ * }
297
+ *
298
+ * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
299
+ * What happens in this function is that we prepend a `.` and escape the candidate.
300
+ * This will result in `.hover\:font-bold`
301
+ * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
302
+ */
303
+ // TODO: Should we use postcss-selector-parser for this instead?
304
+ function replaceSelector(selector, utilitySelectors, candidate) {
305
+ let needle = `.${escapeClassName(candidate)}`
306
+ let needles = [...new Set([needle, needle.replace(/\\2c /g, '\\,')])]
307
+ let utilitySelectorsList = extractSelectors(utilitySelectors)
308
+
309
+ return extractSelectors(selector)
310
+ .map((s) => {
311
+ let replaced = []
312
+
313
+ for (let utilitySelector of utilitySelectorsList) {
314
+ let replacedSelector = utilitySelector
315
+ for (const needle of needles) {
316
+ replacedSelector = replacedSelector.replace(needle, s)
295
317
  }
296
- return replaced.join(', ')
297
- })
298
- .join(', ')
299
- }
318
+ if (replacedSelector === utilitySelector) {
319
+ continue
320
+ }
321
+ replaced.push(replacedSelector)
322
+ }
323
+ return replaced.join(', ')
324
+ })
325
+ .join(', ')
326
+ }
300
327
 
301
- let perParentApplies = new Map()
328
+ let perParentApplies = new Map()
302
329
 
303
- // Collect all apply candidates and their rules
304
- for (let apply of applies) {
305
- let candidates = perParentApplies.get(apply.parent) || []
330
+ // Collect all apply candidates and their rules
331
+ for (let apply of applies) {
332
+ let [candidates] = perParentApplies.get(apply.parent) || [[], apply.source]
306
333
 
307
- perParentApplies.set(apply.parent, [candidates, apply.source])
334
+ perParentApplies.set(apply.parent, [candidates, apply.source])
308
335
 
309
- let [applyCandidates, important] = extractApplyCandidates(apply.params)
336
+ let [applyCandidates, important] = extractApplyCandidates(apply.params)
310
337
 
311
- if (apply.parent.type === 'atrule') {
312
- if (apply.parent.name === 'screen') {
313
- const screenType = apply.parent.params
338
+ if (apply.parent.type === 'atrule') {
339
+ if (apply.parent.name === 'screen') {
340
+ const screenType = apply.parent.params
314
341
 
315
- throw apply.error(
316
- `@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates
317
- .map((c) => `${screenType}:${c}`)
318
- .join(' ')} instead.`
319
- )
320
- }
342
+ throw apply.error(
343
+ `@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates
344
+ .map((c) => `${screenType}:${c}`)
345
+ .join(' ')} instead.`
346
+ )
347
+ }
348
+
349
+ throw apply.error(
350
+ `@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.`
351
+ )
352
+ }
321
353
 
354
+ for (let applyCandidate of applyCandidates) {
355
+ if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
356
+ // TODO: Link to specific documentation page with error code.
357
+ throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
358
+ }
359
+
360
+ if (!applyClassCache.has(applyCandidate)) {
322
361
  throw apply.error(
323
- `@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.`
362
+ `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
324
363
  )
325
364
  }
326
365
 
327
- for (let applyCandidate of applyCandidates) {
328
- if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
329
- // TODO: Link to specific documentation page with error code.
330
- throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
331
- }
366
+ let rules = applyClassCache.get(applyCandidate)
332
367
 
333
- if (!applyClassCache.has(applyCandidate)) {
334
- throw apply.error(
335
- `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
368
+ candidates.push([applyCandidate, important, rules])
369
+ }
370
+ }
371
+
372
+ for (const [parent, [candidates, atApplySource]] of perParentApplies) {
373
+ let siblings = []
374
+
375
+ for (let [applyCandidate, important, rules] of candidates) {
376
+ let potentialApplyCandidates = [
377
+ applyCandidate,
378
+ ...extractBaseCandidates([applyCandidate], context.tailwindConfig.separator),
379
+ ]
380
+
381
+ for (let [meta, node] of rules) {
382
+ let parentClasses = extractClasses(parent)
383
+ let nodeClasses = extractClasses(node)
384
+
385
+ // When we encounter a rule like `.dark .a, .b { … }` we only want to be left with `[.dark, .a]` if the base applyCandidate is `.a` or with `[.b]` if the base applyCandidate is `.b`
386
+ // So we've split them into groups
387
+ nodeClasses = nodeClasses.groups
388
+ .filter((classList) =>
389
+ classList.some((className) => potentialApplyCandidates.includes(className))
390
+ )
391
+ .flat()
392
+
393
+ // Add base utility classes from the @apply node to the list of
394
+ // classes to check whether it intersects and therefore results in a
395
+ // circular dependency or not.
396
+ //
397
+ // E.g.:
398
+ // .foo {
399
+ // @apply hover:a; // This applies "a" but with a modifier
400
+ // }
401
+ //
402
+ // We only have to do that with base classes of the `node`, not of the `parent`
403
+ // E.g.:
404
+ // .hover\:foo {
405
+ // @apply bar;
406
+ // }
407
+ // .bar {
408
+ // @apply foo;
409
+ // }
410
+ //
411
+ // This should not result in a circular dependency because we are
412
+ // just applying `.foo` and the rule above is `.hover\:foo` which is
413
+ // unrelated. However, if we were to apply `hover:foo` then we _did_
414
+ // have to include this one.
415
+ nodeClasses = nodeClasses.concat(
416
+ extractBaseCandidates(nodeClasses, context.tailwindConfig.separator)
417
+ )
418
+
419
+ let intersects = parentClasses.some((selector) => nodeClasses.includes(selector))
420
+ if (intersects) {
421
+ throw node.error(
422
+ `You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.`
336
423
  )
337
424
  }
338
425
 
339
- let rules = applyClassCache.get(applyCandidate)
426
+ let root = postcss.root({ nodes: [node.clone()] })
340
427
 
341
- candidates.push([applyCandidate, important, rules])
342
- }
343
- }
428
+ // Make sure every node in the entire tree points back at the @apply rule that generated it
429
+ root.walk((node) => {
430
+ node.source = atApplySource
431
+ })
344
432
 
345
- for (const [parent, [candidates, atApplySource]] of perParentApplies) {
346
- let siblings = []
347
-
348
- for (let [applyCandidate, important, rules] of candidates) {
349
- for (let [meta, node] of rules) {
350
- let parentClasses = extractClasses(parent)
351
- let nodeClasses = extractClasses(node)
352
-
353
- // Add base utility classes from the @apply node to the list of
354
- // classes to check whether it intersects and therefore results in a
355
- // circular dependency or not.
356
- //
357
- // E.g.:
358
- // .foo {
359
- // @apply hover:a; // This applies "a" but with a modifier
360
- // }
361
- //
362
- // We only have to do that with base classes of the `node`, not of the `parent`
363
- // E.g.:
364
- // .hover\:foo {
365
- // @apply bar;
366
- // }
367
- // .bar {
368
- // @apply foo;
369
- // }
370
- //
371
- // This should not result in a circular dependency because we are
372
- // just applying `.foo` and the rule above is `.hover\:foo` which is
373
- // unrelated. However, if we were to apply `hover:foo` then we _did_
374
- // have to include this one.
375
- nodeClasses = nodeClasses.concat(
376
- extractBaseCandidates(nodeClasses, context.tailwindConfig.separator)
377
- )
433
+ let canRewriteSelector =
434
+ node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
435
+
436
+ if (canRewriteSelector) {
437
+ root.walkRules((rule) => {
438
+ // Let's imagine you have the following structure:
439
+ //
440
+ // .foo {
441
+ // @apply bar;
442
+ // }
443
+ //
444
+ // @supports (a: b) {
445
+ // .bar {
446
+ // color: blue
447
+ // }
448
+ //
449
+ // .something-unrelated {}
450
+ // }
451
+ //
452
+ // In this case we want to apply `.bar` but it happens to be in
453
+ // an atrule node. We clone that node instead of the nested one
454
+ // because we still want that @supports rule to be there once we
455
+ // applied everything.
456
+ //
457
+ // However it happens to be that the `.something-unrelated` is
458
+ // also in that same shared @supports atrule. This is not good,
459
+ // and this should not be there. The good part is that this is
460
+ // a clone already and it can be safely removed. The question is
461
+ // how do we know we can remove it. Basically what we can do is
462
+ // match it against the applyCandidate that you want to apply. If
463
+ // it doesn't match the we can safely delete it.
464
+ //
465
+ // If we didn't do this, then the `replaceSelector` function
466
+ // would have replaced this with something that didn't exist and
467
+ // therefore it removed the selector altogether. In this specific
468
+ // case it would result in `{}` instead of `.something-unrelated {}`
469
+ if (!extractClasses(rule).some((candidate) => candidate === applyCandidate)) {
470
+ rule.remove()
471
+ return
472
+ }
378
473
 
379
- let intersects = parentClasses.some((selector) => nodeClasses.includes(selector))
380
- if (intersects) {
381
- throw node.error(
382
- `You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.`
383
- )
384
- }
474
+ // Strip the important selector from the parent selector if at the beginning
475
+ let importantSelector =
476
+ typeof context.tailwindConfig.important === 'string'
477
+ ? context.tailwindConfig.important
478
+ : null
385
479
 
386
- let root = postcss.root({ nodes: [node.clone()] })
480
+ // We only want to move the "important" selector if this is a Tailwind-generated utility
481
+ // We do *not* want to do this for user CSS that happens to be structured the same
482
+ let isGenerated = parent.raws.tailwind !== undefined
387
483
 
388
- // Make sure every node in the entire tree points back at the @apply rule that generated it
389
- root.walk((node) => {
390
- node.source = atApplySource
391
- })
484
+ let parentSelector =
485
+ isGenerated && importantSelector && parent.selector.indexOf(importantSelector) === 0
486
+ ? parent.selector.slice(importantSelector.length)
487
+ : parent.selector
392
488
 
393
- let canRewriteSelector =
394
- node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
395
-
396
- if (canRewriteSelector) {
397
- root.walkRules((rule) => {
398
- // Let's imagine you have the following structure:
399
- //
400
- // .foo {
401
- // @apply bar;
402
- // }
403
- //
404
- // @supports (a: b) {
405
- // .bar {
406
- // color: blue
407
- // }
408
- //
409
- // .something-unrelated {}
410
- // }
411
- //
412
- // In this case we want to apply `.bar` but it happens to be in
413
- // an atrule node. We clone that node instead of the nested one
414
- // because we still want that @supports rule to be there once we
415
- // applied everything.
416
- //
417
- // However it happens to be that the `.something-unrelated` is
418
- // also in that same shared @supports atrule. This is not good,
419
- // and this should not be there. The good part is that this is
420
- // a clone already and it can be safely removed. The question is
421
- // how do we know we can remove it. Basically what we can do is
422
- // match it against the applyCandidate that you want to apply. If
423
- // it doesn't match the we can safely delete it.
424
- //
425
- // If we didn't do this, then the `replaceSelector` function
426
- // would have replaced this with something that didn't exist and
427
- // therefore it removed the selector altogether. In this specific
428
- // case it would result in `{}` instead of `.something-unrelated {}`
429
- if (!extractClasses(rule).some((candidate) => candidate === applyCandidate)) {
430
- rule.remove()
431
- return
432
- }
433
-
434
- rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)
435
-
436
- rule.walkDecls((d) => {
437
- d.important = meta.important || important
438
- })
439
- })
440
- }
489
+ rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate)
490
+
491
+ // And then re-add it if it was removed
492
+ if (importantSelector && parentSelector !== parent.selector) {
493
+ rule.selector = `${importantSelector} ${rule.selector}`
494
+ }
441
495
 
442
- // Insert it
443
- siblings.push([
444
- // Ensure that when we are sorting, that we take the layer order into account
445
- { ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
446
- root.nodes[0],
447
- ])
496
+ rule.walkDecls((d) => {
497
+ d.important = meta.important || important
498
+ })
499
+ })
448
500
  }
501
+
502
+ // Insert it
503
+ siblings.push([
504
+ // Ensure that when we are sorting, that we take the layer order into account
505
+ { ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
506
+ root.nodes[0],
507
+ ])
449
508
  }
509
+ }
450
510
 
451
- // Inject the rules, sorted, correctly
452
- let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
511
+ // Inject the rules, sorted, correctly
512
+ let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
453
513
 
454
- // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
455
- parent.after(nodes)
456
- }
514
+ // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
515
+ parent.after(nodes)
516
+ }
457
517
 
458
- for (let apply of applies) {
459
- // If there are left-over declarations, just remove the @apply
460
- if (apply.parent.nodes.length > 1) {
461
- apply.remove()
462
- } else {
463
- // The node is empty, drop the full node
464
- apply.parent.remove()
465
- }
518
+ for (let apply of applies) {
519
+ // If there are left-over declarations, just remove the @apply
520
+ if (apply.parent.nodes.length > 1) {
521
+ apply.remove()
522
+ } else {
523
+ // The node is empty, drop the full node
524
+ apply.parent.remove()
466
525
  }
467
-
468
- // Do it again, in case we have other `@apply` rules
469
- processApply(root, context, localCache)
470
526
  }
527
+
528
+ // Do it again, in case we have other `@apply` rules
529
+ processApply(root, context, localCache)
471
530
  }
472
531
 
473
532
  export default function expandApplyAtRules(context) {
@@ -17,14 +17,14 @@ const builtInTransformers = {
17
17
  svelte: (content) => content.replace(/(?:^|\s)class:/g, ' '),
18
18
  }
19
19
 
20
- function getExtractor(tailwindConfig, fileExtension) {
21
- let extractors = tailwindConfig.content.extract
20
+ function getExtractor(context, fileExtension) {
21
+ let extractors = context.tailwindConfig.content.extract
22
22
 
23
23
  return (
24
24
  extractors[fileExtension] ||
25
25
  extractors.DEFAULT ||
26
26
  builtInExtractors[fileExtension] ||
27
- builtInExtractors.DEFAULT
27
+ builtInExtractors.DEFAULT(context)
28
28
  )
29
29
  }
30
30
 
@@ -165,7 +165,7 @@ export default function expandTailwindAtRules(context) {
165
165
 
166
166
  for (let { content, extension } of context.changedContent) {
167
167
  let transformer = getTransformer(context.tailwindConfig, extension)
168
- let extractor = getExtractor(context.tailwindConfig, extension)
168
+ let extractor = getExtractor(context, extension)
169
169
  getClassCandidates(transformer(content), extractor, candidates, seen)
170
170
  }
171
171
 
@@ -9,7 +9,10 @@ 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 { isValidVariantFormatString, parseVariant } from './setupContextUtils'
12
13
  import isValidArbitraryValue from '../util/isValidArbitraryValue'
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
@@ -125,8 +128,31 @@ function applyVariant(variant, matches, context) {
125
128
  return matches
126
129
  }
127
130
 
131
+ let args
132
+
133
+ // Find partial arbitrary variants
134
+ if (variant.endsWith(']') && !variant.startsWith('[')) {
135
+ args = variant.slice(variant.lastIndexOf('[') + 1, -1)
136
+ variant = variant.slice(0, variant.indexOf(args) - 1 /* - */ - 1 /* [ */)
137
+ }
138
+
139
+ // Register arbitrary variants
140
+ if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
141
+ let selector = normalize(variant.slice(1, -1))
142
+
143
+ if (!isValidVariantFormatString(selector)) {
144
+ return []
145
+ }
146
+
147
+ let fn = parseVariant(selector)
148
+
149
+ let sort = Array.from(context.variantOrder.values()).pop() << 1n
150
+ context.variantMap.set(variant, [[sort, fn]])
151
+ context.variantOrder.set(variant, sort)
152
+ }
153
+
128
154
  if (context.variantMap.has(variant)) {
129
- let variantFunctionTuples = context.variantMap.get(variant)
155
+ let variantFunctionTuples = context.variantMap.get(variant).slice()
130
156
  let result = []
131
157
 
132
158
  for (let [meta, rule] of matches) {
@@ -187,8 +213,29 @@ function applyVariant(variant, matches, context) {
187
213
  format(selectorFormat) {
188
214
  collectedFormats.push(selectorFormat)
189
215
  },
216
+ args,
190
217
  })
191
218
 
219
+ // It can happen that a list of format strings is returned from within the function. In that
220
+ // case, we have to process them as well. We can use the existing `variantSort`.
221
+ if (Array.isArray(ruleWithVariant)) {
222
+ for (let [idx, variantFunction] of ruleWithVariant.entries()) {
223
+ // This is a little bit scary since we are pushing to an array of items that we are
224
+ // currently looping over. However, you can also think of it like a processing queue
225
+ // where you keep handling jobs until everything is done and each job can queue more
226
+ // jobs if needed.
227
+ variantFunctionTuples.push([
228
+ // TODO: This could have potential bugs if we shift the sort order from variant A far
229
+ // enough into the sort space of variant B. The chances are low, but if this happens
230
+ // then this might be the place too look at. One potential solution to this problem is
231
+ // reserving additional X places for these 'unknown' variants in between.
232
+ variantSort | BigInt(idx << ruleWithVariant.length),
233
+ variantFunction,
234
+ ])
235
+ }
236
+ continue
237
+ }
238
+
192
239
  if (typeof ruleWithVariant === 'string') {
193
240
  collectedFormats.push(ruleWithVariant)
194
241
  }
@@ -382,7 +429,11 @@ function* resolveMatchedPlugins(classCandidate, context) {
382
429
  const twConfigPrefix = context.tailwindConfig.prefix
383
430
 
384
431
  const twConfigPrefixLen = twConfigPrefix.length
385
- if (candidatePrefix[twConfigPrefixLen] === '-') {
432
+
433
+ const hasMatchingPrefix =
434
+ candidatePrefix.startsWith(twConfigPrefix) || candidatePrefix.startsWith(`-${twConfigPrefix}`)
435
+
436
+ if (candidatePrefix[twConfigPrefixLen] === '-' && hasMatchingPrefix) {
386
437
  negative = true
387
438
  candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1)
388
439
  }
@@ -403,7 +454,7 @@ function splitWithSeparator(input, separator) {
403
454
  return [sharedState.NOT_ON_DEMAND]
404
455
  }
405
456
 
406
- return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
457
+ return Array.from(splitAtTopLevelOnly(input, separator))
407
458
  }
408
459
 
409
460
  function* recordCandidates(matches, classCandidate) {
@@ -414,7 +465,7 @@ function* recordCandidates(matches, classCandidate) {
414
465
  }
415
466
  }
416
467
 
417
- function* resolveMatches(candidate, context) {
468
+ function* resolveMatches(candidate, context, original = candidate) {
418
469
  let separator = context.tailwindConfig.separator
419
470
  let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
420
471
  let important = false
@@ -424,6 +475,15 @@ function* resolveMatches(candidate, context) {
424
475
  classCandidate = classCandidate.slice(1)
425
476
  }
426
477
 
478
+ if (flagEnabled(context.tailwindConfig, 'variantGrouping')) {
479
+ if (classCandidate.startsWith('(') && classCandidate.endsWith(')')) {
480
+ let base = variants.slice().reverse().join(separator)
481
+ for (let part of splitAtTopLevelOnly(classCandidate.slice(1, -1), ',')) {
482
+ yield* resolveMatches(base + separator + part, context, original)
483
+ }
484
+ }
485
+ }
486
+
427
487
  // TODO: Reintroduce this in ways that doesn't break on false positives
428
488
  // function sortAgainst(toSort, against) {
429
489
  // return toSort.slice().sort((a, z) => {
@@ -555,7 +615,11 @@ function* resolveMatches(candidate, context) {
555
615
 
556
616
  rule.selector = finalizeSelector(finalFormat, {
557
617
  selector: rule.selector,
558
- candidate,
618
+ candidate: original,
619
+ base: candidate
620
+ .split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
621
+ .pop(),
622
+
559
623
  context,
560
624
  })
561
625
  })