tailwindcss 3.0.0-alpha.1 → 3.0.2

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 (73) hide show
  1. package/colors.js +2 -1
  2. package/defaultConfig.js +2 -1
  3. package/defaultTheme.js +2 -1
  4. package/lib/cli.js +39 -35
  5. package/lib/constants.js +1 -1
  6. package/lib/corePluginList.js +10 -1
  7. package/lib/corePlugins.js +393 -259
  8. package/lib/css/preflight.css +14 -1
  9. package/lib/featureFlags.js +12 -7
  10. package/lib/lib/collapseDuplicateDeclarations.js +29 -0
  11. package/lib/lib/detectNesting.js +17 -2
  12. package/lib/lib/evaluateTailwindFunctions.js +9 -5
  13. package/lib/lib/expandApplyAtRules.js +26 -9
  14. package/lib/lib/expandTailwindAtRules.js +4 -1
  15. package/lib/lib/generateRules.js +151 -19
  16. package/lib/lib/resolveDefaultsAtRules.js +67 -56
  17. package/lib/lib/setupContextUtils.js +80 -80
  18. package/lib/lib/setupWatchingContext.js +5 -1
  19. package/lib/lib/sharedState.js +2 -2
  20. package/lib/lib/substituteScreenAtRules.js +7 -4
  21. package/lib/processTailwindFeatures.js +4 -0
  22. package/lib/util/buildMediaQuery.js +13 -24
  23. package/lib/util/createUtilityPlugin.js +5 -5
  24. package/lib/util/dataTypes.js +38 -7
  25. package/lib/util/formatVariantSelector.js +186 -0
  26. package/lib/util/isValidArbitraryValue.js +64 -0
  27. package/lib/util/nameClass.js +2 -1
  28. package/lib/util/negateValue.js +3 -1
  29. package/lib/util/normalizeConfig.js +22 -8
  30. package/lib/util/normalizeScreens.js +61 -0
  31. package/lib/util/parseBoxShadowValue.js +77 -0
  32. package/lib/util/pluginUtils.js +62 -158
  33. package/lib/util/prefixSelector.js +1 -3
  34. package/lib/util/resolveConfig.js +17 -13
  35. package/lib/util/transformThemeValue.js +23 -13
  36. package/package.json +15 -15
  37. package/peers/index.js +4456 -5450
  38. package/plugin.js +2 -1
  39. package/resolveConfig.js +2 -1
  40. package/src/.DS_Store +0 -0
  41. package/src/cli.js +9 -2
  42. package/src/corePluginList.js +1 -1
  43. package/src/corePlugins.js +392 -404
  44. package/src/css/preflight.css +14 -1
  45. package/src/featureFlags.js +14 -4
  46. package/src/lib/collapseDuplicateDeclarations.js +28 -0
  47. package/src/lib/detectNesting.js +22 -3
  48. package/src/lib/evaluateTailwindFunctions.js +5 -2
  49. package/src/lib/expandApplyAtRules.js +29 -2
  50. package/src/lib/expandTailwindAtRules.js +5 -2
  51. package/src/lib/generateRules.js +155 -11
  52. package/src/lib/resolveDefaultsAtRules.js +67 -50
  53. package/src/lib/setupContextUtils.js +77 -67
  54. package/src/lib/setupWatchingContext.js +7 -0
  55. package/src/lib/sharedState.js +1 -1
  56. package/src/lib/substituteScreenAtRules.js +6 -3
  57. package/src/processTailwindFeatures.js +5 -0
  58. package/src/util/buildMediaQuery.js +14 -18
  59. package/src/util/createUtilityPlugin.js +2 -2
  60. package/src/util/dataTypes.js +43 -11
  61. package/src/util/formatVariantSelector.js +196 -0
  62. package/src/util/isValidArbitraryValue.js +61 -0
  63. package/src/util/nameClass.js +2 -2
  64. package/src/util/negateValue.js +4 -2
  65. package/src/util/normalizeConfig.js +17 -1
  66. package/src/util/normalizeScreens.js +45 -0
  67. package/src/util/parseBoxShadowValue.js +71 -0
  68. package/src/util/pluginUtils.js +50 -146
  69. package/src/util/prefixSelector.js +1 -4
  70. package/src/util/resolveConfig.js +7 -1
  71. package/src/util/transformThemeValue.js +22 -7
  72. package/stubs/defaultConfig.stub.js +118 -58
  73. package/CHANGELOG.md +0 -1759
@@ -12,6 +12,11 @@
12
12
  border-color: currentColor; /* 2 */
13
13
  }
14
14
 
15
+ ::before,
16
+ ::after {
17
+ --tw-content: '';
18
+ }
19
+
15
20
  /*
16
21
  1. Use a consistent sensible line-height in all browsers.
17
22
  2. Prevent adjustments of font size after orientation changes in iOS.
@@ -283,7 +288,8 @@ legend {
283
288
  }
284
289
 
285
290
  ol,
286
- ul {
291
+ ul,
292
+ menu {
287
293
  list-style: none;
288
294
  margin: 0;
289
295
  padding: 0;
@@ -317,6 +323,13 @@ button,
317
323
  cursor: pointer;
318
324
  }
319
325
 
326
+ /*
327
+ Make sure disabled buttons don't get the pointer cursor.
328
+ */
329
+ :disabled {
330
+ cursor: default;
331
+ }
332
+
320
333
  /*
321
334
  1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
322
335
  2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
@@ -1,18 +1,28 @@
1
1
  import chalk from 'chalk'
2
2
  import log from './util/log'
3
3
 
4
- const featureFlags = {
4
+ let defaults = {
5
+ // TODO: Drop this once we can safely rely on optimizeUniversalDefaults being
6
+ // the default.
7
+ optimizeUniversalDefaults: process.env.NODE_ENV === 'test' ? true : false,
8
+
9
+ // optimizeUniversalDefaults: true
10
+ }
11
+
12
+ let featureFlags = {
5
13
  future: [],
6
14
  experimental: ['optimizeUniversalDefaults'],
7
15
  }
8
16
 
9
17
  export function flagEnabled(config, flag) {
10
18
  if (featureFlags.future.includes(flag)) {
11
- return config.future === 'all' || (config?.future?.[flag] ?? false)
19
+ return config.future === 'all' || (config?.future?.[flag] ?? defaults[flag] ?? false)
12
20
  }
13
21
 
14
22
  if (featureFlags.experimental.includes(flag)) {
15
- return config.experimental === 'all' || (config?.experimental?.[flag] ?? false)
23
+ return (
24
+ config.experimental === 'all' || (config?.experimental?.[flag] ?? defaults[flag] ?? false)
25
+ )
16
26
  }
17
27
 
18
28
  return false
@@ -34,7 +44,7 @@ export function issueFlagNotices(config) {
34
44
  }
35
45
 
36
46
  if (experimentalFlagsEnabled(config).length > 0) {
37
- const changes = experimentalFlagsEnabled(config)
47
+ let changes = experimentalFlagsEnabled(config)
38
48
  .map((s) => chalk.yellow(s))
39
49
  .join(', ')
40
50
 
@@ -0,0 +1,28 @@
1
+ export default function collapseDuplicateDeclarations() {
2
+ return (root) => {
3
+ root.walkRules((node) => {
4
+ let seen = new Map()
5
+ let droppable = new Set([])
6
+
7
+ node.walkDecls((decl) => {
8
+ // This could happen if we have nested selectors. In that case the
9
+ // parent will loop over all its declarations but also the declarations
10
+ // of nested rules. With this we ensure that we are shallowly checking
11
+ // declarations.
12
+ if (decl.parent !== node) {
13
+ return
14
+ }
15
+
16
+ if (seen.has(decl.prop)) {
17
+ droppable.add(seen.get(decl.prop))
18
+ }
19
+
20
+ seen.set(decl.prop, decl)
21
+ })
22
+
23
+ for (let decl of droppable) {
24
+ decl.remove()
25
+ }
26
+ })
27
+ }
28
+ }
@@ -2,6 +2,23 @@ export default function (_context) {
2
2
  return (root, result) => {
3
3
  let found = false
4
4
 
5
+ root.walkAtRules('tailwind', (node) => {
6
+ if (found) return false
7
+
8
+ if (node.parent && node.parent.type !== 'root') {
9
+ found = true
10
+ node.warn(
11
+ result,
12
+ [
13
+ 'Nested @tailwind rules were detected, but are not supported.',
14
+ "Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix",
15
+ 'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy',
16
+ ].join('\n')
17
+ )
18
+ return false
19
+ }
20
+ })
21
+
5
22
  root.walkRules((rule) => {
6
23
  if (found) return false
7
24
 
@@ -9,10 +26,12 @@ export default function (_context) {
9
26
  found = true
10
27
  nestedRule.warn(
11
28
  result,
12
- // TODO: Improve this warning message
13
- 'Nested CSS detected, checkout the docs on how to support nesting: https://tailwindcss.com/docs/using-with-preprocessors#nesting'
29
+ [
30
+ 'Nested CSS was detected, but CSS nesting has not been configured correctly.',
31
+ 'Please enable a CSS nesting plugin *before* Tailwind in your configuration.',
32
+ 'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting',
33
+ ].join('\n')
14
34
  )
15
-
16
35
  return false
17
36
  })
18
37
  })
@@ -2,6 +2,7 @@ import dlv from 'dlv'
2
2
  import didYouMean from 'didyoumean'
3
3
  import transformThemeValue from '../util/transformThemeValue'
4
4
  import parseValue from 'postcss-value-parser'
5
+ import { normalizeScreens } from '../util/normalizeScreens'
5
6
  import buildMediaQuery from '../util/buildMediaQuery'
6
7
  import { toPath } from '../util/toPath'
7
8
 
@@ -173,12 +174,14 @@ export default function ({ tailwindConfig: config }) {
173
174
  },
174
175
  screen: (node, screen) => {
175
176
  screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
177
+ let screens = normalizeScreens(config.theme.screens)
178
+ let screenDefinition = screens.find(({ name }) => name === screen)
176
179
 
177
- if (config.theme.screens[screen] === undefined) {
180
+ if (!screenDefinition) {
178
181
  throw node.error(`The '${screen}' screen does not exist in your theme.`)
179
182
  }
180
183
 
181
- return buildMediaQuery(config.theme.screens[screen])
184
+ return buildMediaQuery(screenDefinition)
182
185
  },
183
186
  }
184
187
  return (root) => {
@@ -1,8 +1,24 @@
1
1
  import postcss from 'postcss'
2
+ import parser from 'postcss-selector-parser'
2
3
  import { resolveMatches } from './generateRules'
3
4
  import bigSign from '../util/bigSign'
4
5
  import escapeClassName from '../util/escapeClassName'
5
6
 
7
+ function containsBase(selector, classCandidateBase, separator) {
8
+ return parser((selectors) => {
9
+ let contains = false
10
+
11
+ selectors.walkClasses((classSelector) => {
12
+ if (classSelector.value.split(separator).pop() === classCandidateBase) {
13
+ contains = true
14
+ return false
15
+ }
16
+ })
17
+
18
+ return contains
19
+ }).transformSync(selector)
20
+ }
21
+
6
22
  function prefix(context, selector) {
7
23
  let prefix = context.tailwindConfig.prefix
8
24
  return typeof prefix === 'function' ? prefix(selector) : prefix + selector
@@ -129,10 +145,10 @@ function processApply(root, context) {
129
145
  // TODO: Should we use postcss-selector-parser for this instead?
130
146
  function replaceSelector(selector, utilitySelectors, candidate) {
131
147
  let needle = `.${escapeClassName(candidate)}`
132
- let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g)
148
+ let utilitySelectorsList = utilitySelectors.split(/\s*\,(?![^(]*\))\s*/g)
133
149
 
134
150
  return selector
135
- .split(/\s*,\s*/g)
151
+ .split(/\s*\,(?![^(]*\))\s*/g)
136
152
  .map((s) => {
137
153
  let replaced = []
138
154
 
@@ -196,7 +212,18 @@ function processApply(root, context) {
196
212
  let siblings = []
197
213
 
198
214
  for (let [applyCandidate, important, rules] of candidates) {
215
+ let base = applyCandidate.split(context.tailwindConfig.separator).pop()
216
+
199
217
  for (let [meta, node] of rules) {
218
+ if (
219
+ containsBase(parent.selector, base, context.tailwindConfig.separator) &&
220
+ containsBase(node.selector, base, context.tailwindConfig.separator)
221
+ ) {
222
+ throw node.error(
223
+ `Circular dependency detected when using: \`@apply ${applyCandidate}\``
224
+ )
225
+ }
226
+
200
227
  let root = postcss.root({ nodes: [node.clone()] })
201
228
  let canRewriteSelector =
202
229
  node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
@@ -11,10 +11,14 @@ const PATTERNS = [
11
11
  /([^<>"'`\s]*\[\w*"[^"`\s]*"?\])/.source, // font-["some_font",sans-serif]
12
12
  /([^<>"'`\s]*\[\w*\('[^"'`\s]*'\)\])/.source, // bg-[url('...')]
13
13
  /([^<>"'`\s]*\[\w*\("[^"'`\s]*"\)\])/.source, // bg-[url("...")]
14
+ /([^<>"'`\s]*\[\w*\('[^"`\s]*'\)\])/.source, // bg-[url('...'),url('...')]
15
+ /([^<>"'`\s]*\[\w*\("[^'`\s]*"\)\])/.source, // bg-[url("..."),url("...")]
14
16
  /([^<>"'`\s]*\['[^"'`\s]*'\])/.source, // `content-['hello']` but not `content-['hello']']`
15
17
  /([^<>"'`\s]*\["[^"'`\s]*"\])/.source, // `content-["hello"]` but not `content-["hello"]"]`
18
+ /([^<>"'`\s]*\[[^<>"'`\s]*:'[^"'`\s]*'\])/.source, // `[content:'hello']` but not `[content:"hello"]`
19
+ /([^<>"'`\s]*\[[^<>"'`\s]*:"[^"'`\s]*"\])/.source, // `[content:"hello"]` but not `[content:'hello']`
16
20
  /([^<>"'`\s]*\[[^"'`\s]+\][^<>"'`\s]*)/.source, // `fill-[#bada55]`, `fill-[#bada55]/50`
17
- /([^<>"'`\s]*[^"'`\s:])/.source, // `px-1.5`, `uppercase` but not `uppercase:`].join('|')
21
+ /([^<>"'`\s]*[^"'`\s:])/.source, // `px-1.5`, `uppercase` but not `uppercase:`
18
22
  ].join('|')
19
23
  const BROAD_MATCH_GLOBAL_REGEXP = new RegExp(PATTERNS, 'g')
20
24
  const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g
@@ -235,7 +239,6 @@ export default function expandTailwindAtRules(context) {
235
239
  if (env.DEBUG) {
236
240
  console.log('Potential classes: ', candidates.size)
237
241
  console.log('Active contexts: ', sharedState.contextSourcesMap.size)
238
- console.log('Content match entries', contentMatchCache.size)
239
242
  }
240
243
 
241
244
  // Clear the cache for the changed files
@@ -5,6 +5,10 @@ import isPlainObject from '../util/isPlainObject'
5
5
  import prefixSelector from '../util/prefixSelector'
6
6
  import { updateAllClasses } from '../util/pluginUtils'
7
7
  import log from '../util/log'
8
+ import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
9
+ import { asClass } from '../util/nameClass'
10
+ import { normalize } from '../util/dataTypes'
11
+ import isValidArbitraryValue from '../util/isValidArbitraryValue'
8
12
 
9
13
  let classNameParser = selectorParser((selectors) => {
10
14
  return selectors.first.filter(({ type }) => type === 'class').pop().value
@@ -112,7 +116,17 @@ function applyVariant(variant, matches, context) {
112
116
 
113
117
  for (let [variantSort, variantFunction] of variantFunctionTuples) {
114
118
  let clone = container.clone()
119
+ let collectedFormats = []
120
+
121
+ let originals = new Map()
122
+
123
+ function prepareBackup() {
124
+ if (originals.size > 0) return // Already prepared, chicken out
125
+ clone.walkRules((rule) => originals.set(rule, rule.selector))
126
+ }
127
+
115
128
  function modifySelectors(modifierFunction) {
129
+ prepareBackup()
116
130
  clone.each((rule) => {
117
131
  if (rule.type !== 'rule') {
118
132
  return
@@ -127,20 +141,84 @@ function applyVariant(variant, matches, context) {
127
141
  })
128
142
  })
129
143
  })
144
+
130
145
  return clone
131
146
  }
132
147
 
133
148
  let ruleWithVariant = variantFunction({
134
- container: clone,
149
+ // Public API
150
+ get container() {
151
+ prepareBackup()
152
+ return clone
153
+ },
135
154
  separator: context.tailwindConfig.separator,
136
155
  modifySelectors,
156
+
157
+ // Private API for now
158
+ wrap(wrapper) {
159
+ let nodes = clone.nodes
160
+ clone.removeAll()
161
+ wrapper.append(nodes)
162
+ clone.append(wrapper)
163
+ },
164
+ format(selectorFormat) {
165
+ collectedFormats.push(selectorFormat)
166
+ },
137
167
  })
138
168
 
169
+ if (typeof ruleWithVariant === 'string') {
170
+ collectedFormats.push(ruleWithVariant)
171
+ }
172
+
139
173
  if (ruleWithVariant === null) {
140
174
  continue
141
175
  }
142
176
 
143
- let withOffset = [{ ...meta, sort: variantSort | meta.sort }, clone.nodes[0]]
177
+ // We filled the `originals`, therefore we assume that somebody touched
178
+ // `container` or `modifySelectors`. Let's see if they did, so that we
179
+ // can restore the selectors, and collect the format strings.
180
+ if (originals.size > 0) {
181
+ clone.walkRules((rule) => {
182
+ if (!originals.has(rule)) return
183
+ let before = originals.get(rule)
184
+ if (before === rule.selector) return // No mutation happened
185
+
186
+ let modified = rule.selector
187
+
188
+ // Rebuild the base selector, this is what plugin authors would do
189
+ // as well. E.g.: `${variant}${separator}${className}`.
190
+ // However, plugin authors probably also prepend or append certain
191
+ // classes, pseudos, ids, ...
192
+ let rebuiltBase = selectorParser((selectors) => {
193
+ selectors.walkClasses((classNode) => {
194
+ classNode.value = `${variant}${context.tailwindConfig.separator}${classNode.value}`
195
+ })
196
+ }).processSync(before)
197
+
198
+ // Now that we know the original selector, the new selector, and
199
+ // the rebuild part in between, we can replace the part that plugin
200
+ // authors need to rebuild with `&`, and eventually store it in the
201
+ // collectedFormats. Similar to what `format('...')` would do.
202
+ //
203
+ // E.g.:
204
+ // variant: foo
205
+ // selector: .markdown > p
206
+ // modified (by plugin): .foo .foo\\:markdown > p
207
+ // rebuiltBase (internal): .foo\\:markdown > p
208
+ // format: .foo &
209
+ collectedFormats.push(modified.replace(rebuiltBase, '&'))
210
+ rule.selector = before
211
+ })
212
+ }
213
+
214
+ let withOffset = [
215
+ {
216
+ ...meta,
217
+ sort: variantSort | meta.sort,
218
+ collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
219
+ },
220
+ clone.nodes[0],
221
+ ]
144
222
  result.push(withOffset)
145
223
  }
146
224
  }
@@ -170,21 +248,57 @@ function parseRules(rule, cache, options = {}) {
170
248
  return [cache.get(rule), options]
171
249
  }
172
250
 
251
+ function extractArbitraryProperty(classCandidate, context) {
252
+ let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? []
253
+
254
+ if (value === undefined) {
255
+ return null
256
+ }
257
+
258
+ let normalized = normalize(value)
259
+
260
+ if (!isValidArbitraryValue(normalized)) {
261
+ return null
262
+ }
263
+
264
+ return [
265
+ [
266
+ { sort: context.arbitraryPropertiesSort, layer: 'utilities' },
267
+ () => ({
268
+ [asClass(classCandidate)]: {
269
+ [property]: normalized,
270
+ },
271
+ }),
272
+ ],
273
+ ]
274
+ }
275
+
173
276
  function* resolveMatchedPlugins(classCandidate, context) {
174
277
  if (context.candidateRuleMap.has(classCandidate)) {
175
278
  yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT']
176
279
  }
177
280
 
281
+ yield* (function* (arbitraryPropertyRule) {
282
+ if (arbitraryPropertyRule !== null) {
283
+ yield [arbitraryPropertyRule, 'DEFAULT']
284
+ }
285
+ })(extractArbitraryProperty(classCandidate, context))
286
+
178
287
  let candidatePrefix = classCandidate
179
288
  let negative = false
180
289
 
181
- const twConfigPrefix = context.tailwindConfig.prefix || ''
290
+ const twConfigPrefix = context.tailwindConfig.prefix
291
+
182
292
  const twConfigPrefixLen = twConfigPrefix.length
183
293
  if (candidatePrefix[twConfigPrefixLen] === '-') {
184
294
  negative = true
185
295
  candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1)
186
296
  }
187
297
 
298
+ if (negative && context.candidateRuleMap.has(candidatePrefix)) {
299
+ yield [context.candidateRuleMap.get(candidatePrefix), '-DEFAULT']
300
+ }
301
+
188
302
  for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) {
189
303
  if (context.candidateRuleMap.has(prefix)) {
190
304
  yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier]
@@ -238,7 +352,7 @@ function* resolveMatches(candidate, context) {
238
352
  }
239
353
  }
240
354
  // Only process static plugins on exact matches
241
- else if (modifier === 'DEFAULT') {
355
+ else if (modifier === 'DEFAULT' || modifier === '-DEFAULT') {
242
356
  let ruleSet = plugin
243
357
  let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
244
358
  for (let rule of rules) {
@@ -319,6 +433,22 @@ function* resolveMatches(candidate, context) {
319
433
  }
320
434
 
321
435
  for (let match of matches) {
436
+ // Apply final format selector
437
+ if (match[0].collectedFormats) {
438
+ let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)
439
+ let container = postcss.root({ nodes: [match[1].clone()] })
440
+ container.walkRules((rule) => {
441
+ if (inKeyframes(rule)) return
442
+
443
+ rule.selector = finalizeSelector(finalFormat, {
444
+ selector: rule.selector,
445
+ candidate,
446
+ context,
447
+ })
448
+ })
449
+ match[1] = container.nodes[0]
450
+ }
451
+
322
452
  yield match
323
453
  }
324
454
  }
@@ -352,28 +482,42 @@ function generateRules(candidates, context) {
352
482
  allRules.push(matches)
353
483
  }
354
484
 
355
- return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
356
- if (options.respectImportant) {
357
- if (context.tailwindConfig.important === true) {
485
+ // Strategy based on `tailwindConfig.important`
486
+ let strategy = ((important) => {
487
+ if (important === true) {
488
+ return (rule) => {
358
489
  rule.walkDecls((d) => {
359
490
  if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
360
491
  d.important = true
361
492
  }
362
493
  })
363
- } else if (typeof context.tailwindConfig.important === 'string') {
494
+ }
495
+ }
496
+
497
+ if (typeof important === 'string') {
498
+ return (rule) => {
499
+ rule.selectors = rule.selectors.map((selector) => {
500
+ return `${important} ${selector}`
501
+ })
502
+ }
503
+ }
504
+ })(context.tailwindConfig.important)
505
+
506
+ return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
507
+ if (options.respectImportant) {
508
+ if (strategy) {
364
509
  let container = postcss.root({ nodes: [rule.clone()] })
365
510
  container.walkRules((r) => {
366
511
  if (inKeyframes(r)) {
367
512
  return
368
513
  }
369
514
 
370
- r.selectors = r.selectors.map((selector) => {
371
- return `${context.tailwindConfig.important} ${selector}`
372
- })
515
+ strategy(r)
373
516
  })
374
517
  rule = container.nodes[0]
375
518
  }
376
519
  }
520
+
377
521
  return [sort | context.layerOrder[layer], rule]
378
522
  })
379
523
  }
@@ -2,60 +2,56 @@ import postcss from 'postcss'
2
2
  import selectorParser from 'postcss-selector-parser'
3
3
  import { flagEnabled } from '../featureFlags'
4
4
 
5
- function isPseudoElement(n) {
6
- if (n.type !== 'pseudo') {
7
- return false
8
- }
9
-
10
- return (
11
- n.value.startsWith('::') ||
12
- [':before', ':after', ':first-line', ':first-letter'].includes(n.value)
13
- )
5
+ let getNode = {
6
+ id(node) {
7
+ return selectorParser.attribute({
8
+ attribute: 'id',
9
+ operator: '=',
10
+ value: node.value,
11
+ quoteMark: '"',
12
+ })
13
+ },
14
14
  }
15
15
 
16
16
  function minimumImpactSelector(nodes) {
17
17
  let rest = nodes
18
- // Keep all pseudo & combinator types (:not([hidden]) ~ :not([hidden]))
19
- .filter((n) => n.type === 'pseudo' || n.type === 'combinator')
20
- // Remove leading pseudo's (:hover, :focus, ...)
21
- .filter((n, idx, all) => {
22
- // Keep pseudo elements
23
- if (isPseudoElement(n)) return true
18
+ .filter((node) => {
19
+ // Keep non-pseudo nodes
20
+ if (node.type !== 'pseudo') return true
21
+
22
+ // Keep pseudo nodes that have subnodes
23
+ // E.g.: `:not()` contains subnodes inside the parentheses
24
+ if (node.nodes.length > 0) return true
25
+
26
+ // Keep pseudo `elements`
27
+ // This implicitly means that we ignore pseudo `classes`
28
+ return (
29
+ node.value.startsWith('::') ||
30
+ [':before', ':after', ':first-line', ':first-letter'].includes(node.value)
31
+ )
32
+ })
33
+ .reverse()
24
34
 
25
- if (idx === 0 && n.type === 'pseudo') return false
26
- if (idx > 0 && n.type === 'pseudo' && all[idx - 1].type === 'pseudo') return false
35
+ let searchFor = new Set(['tag', 'class', 'id', 'attribute'])
27
36
 
28
- return true
29
- })
37
+ let splitPointIdx = rest.findIndex((n) => searchFor.has(n.type))
38
+ if (splitPointIdx === -1) return rest.reverse().join('').trim()
30
39
 
31
- let [bestNode] = nodes
32
-
33
- for (let [type, getNode = (n) => n] of [
34
- ['class'],
35
- [
36
- 'id',
37
- (n) =>
38
- selectorParser.attribute({
39
- attribute: 'id',
40
- operator: '=',
41
- value: n.value,
42
- quoteMark: '"',
43
- }),
44
- ],
45
- ['attribute'],
46
- ]) {
47
- let match = nodes.find((n) => n.type === type)
48
-
49
- if (match) {
50
- bestNode = getNode(match)
51
- break
52
- }
40
+ let node = rest[splitPointIdx]
41
+ let bestNode = getNode[node.type] ? getNode[node.type](node) : node
42
+
43
+ rest = rest.slice(0, splitPointIdx)
44
+
45
+ let combinatorIdx = rest.findIndex((n) => n.type === 'combinator' && n.value === '>')
46
+ if (combinatorIdx !== -1) {
47
+ rest.splice(0, combinatorIdx)
48
+ rest.unshift(selectorParser.universal())
53
49
  }
54
50
 
55
- return [bestNode, ...rest].join('').trim()
51
+ return [bestNode, ...rest.reverse()].join('').trim()
56
52
  }
57
53
 
58
- let elementSelectorParser = selectorParser((selectors) => {
54
+ export let elementSelectorParser = selectorParser((selectors) => {
59
55
  return selectors.map((s) => {
60
56
  let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop()
61
57
  return minimumImpactSelector(nodes)
@@ -75,6 +71,8 @@ function extractElementSelector(selector) {
75
71
  export default function resolveDefaultsAtRules({ tailwindConfig }) {
76
72
  return (root) => {
77
73
  let variableNodeMap = new Map()
74
+
75
+ /** @type {Set<import('postcss').AtRule>} */
78
76
  let universals = new Set()
79
77
 
80
78
  root.walkAtRules('defaults', (rule) => {
@@ -94,31 +92,50 @@ export default function resolveDefaultsAtRules({ tailwindConfig }) {
94
92
  })
95
93
 
96
94
  for (let universal of universals) {
97
- let selectors = new Set()
95
+ /** @type {Map<string, Set<string>>} */
96
+ let selectorGroups = new Map()
98
97
 
99
98
  let rules = variableNodeMap.get(universal.params) ?? []
100
99
 
101
100
  for (let rule of rules) {
102
101
  for (let selector of extractElementSelector(rule.selector)) {
102
+ // If selector contains a vendor prefix after a pseudo element or class,
103
+ // we consider them separately because merging the declarations into
104
+ // a single rule will cause browsers that do not understand the
105
+ // vendor prefix to throw out the whole rule
106
+ let selectorGroupName =
107
+ selector.includes(':-') || selector.includes('::-') ? selector : '__DEFAULT__'
108
+
109
+ let selectors = selectorGroups.get(selectorGroupName) ?? new Set()
110
+ selectorGroups.set(selectorGroupName, selectors)
111
+
103
112
  selectors.add(selector)
104
113
  }
105
114
  }
106
115
 
107
- if (selectors.size === 0) {
116
+ if (selectorGroups.size === 0) {
108
117
  universal.remove()
109
118
  continue
110
119
  }
111
120
 
112
- let universalRule = postcss.rule()
113
-
114
121
  if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) {
115
- universalRule.selectors = [...selectors]
122
+ for (let [, selectors] of selectorGroups) {
123
+ let universalRule = postcss.rule()
124
+
125
+ universalRule.selectors = [...selectors]
126
+
127
+ universalRule.append(universal.nodes.map((node) => node.clone()))
128
+ universal.before(universalRule)
129
+ }
116
130
  } else {
131
+ let universalRule = postcss.rule()
132
+
117
133
  universalRule.selectors = ['*', '::before', '::after']
134
+
135
+ universalRule.append(universal.nodes)
136
+ universal.before(universalRule)
118
137
  }
119
138
 
120
- universalRule.append(universal.nodes)
121
- universal.before(universalRule)
122
139
  universal.remove()
123
140
  }
124
141
  }