tailwindcss 3.4.0 → 3.4.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 (69) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +2 -3
  3. package/lib/cli/build/plugin.js +4 -11
  4. package/lib/cli.js +1 -5
  5. package/lib/corePluginList.js +1 -0
  6. package/lib/corePlugins.js +92 -25
  7. package/lib/css/preflight.css +4 -3
  8. package/lib/featureFlags.js +2 -6
  9. package/lib/lib/content.js +36 -3
  10. package/lib/lib/defaultExtractor.js +2 -2
  11. package/lib/lib/expandApplyAtRules.js +13 -0
  12. package/lib/lib/expandTailwindAtRules.js +20 -32
  13. package/lib/lib/generateRules.js +13 -2
  14. package/lib/lib/load-config.js +4 -0
  15. package/lib/lib/offsets.js +51 -2
  16. package/lib/lib/resolveDefaultsAtRules.js +3 -1
  17. package/lib/lib/setupContextUtils.js +23 -3
  18. package/lib/lib/sharedState.js +2 -10
  19. package/lib/plugin.js +0 -50
  20. package/lib/processTailwindFeatures.js +0 -2
  21. package/lib/util/dataTypes.js +12 -2
  22. package/lib/util/pseudoElements.js +3 -0
  23. package/package.json +5 -8
  24. package/peers/index.js +61 -61
  25. package/src/cli/build/plugin.js +4 -11
  26. package/src/cli.js +1 -5
  27. package/src/corePluginList.js +1 -1
  28. package/src/corePlugins.js +88 -27
  29. package/src/css/preflight.css +4 -3
  30. package/src/featureFlags.js +2 -6
  31. package/src/lib/content.js +42 -1
  32. package/src/lib/defaultExtractor.js +2 -2
  33. package/src/lib/expandApplyAtRules.js +17 -0
  34. package/src/lib/expandTailwindAtRules.js +23 -41
  35. package/src/lib/generateRules.js +12 -2
  36. package/src/lib/load-config.ts +5 -0
  37. package/src/lib/offsets.js +61 -2
  38. package/src/lib/resolveDefaultsAtRules.js +5 -1
  39. package/src/lib/setupContextUtils.js +28 -2
  40. package/src/lib/sharedState.js +0 -4
  41. package/src/plugin.js +0 -60
  42. package/src/processTailwindFeatures.js +0 -3
  43. package/src/util/dataTypes.js +13 -1
  44. package/src/util/pseudoElements.js +4 -0
  45. package/types/config.d.ts +7 -0
  46. package/types/generated/corePluginList.d.ts +1 -1
  47. package/lib/lib/detectNesting.js +0 -45
  48. package/lib/oxide/cli/build/deps.js +0 -89
  49. package/lib/oxide/cli/build/index.js +0 -53
  50. package/lib/oxide/cli/build/plugin.js +0 -375
  51. package/lib/oxide/cli/build/utils.js +0 -87
  52. package/lib/oxide/cli/build/watching.js +0 -179
  53. package/lib/oxide/cli/help/index.js +0 -72
  54. package/lib/oxide/cli/index.js +0 -214
  55. package/lib/oxide/cli/init/index.js +0 -52
  56. package/lib/oxide/cli.js +0 -5
  57. package/lib/oxide/postcss-plugin.js +0 -2
  58. package/scripts/swap-engines.js +0 -40
  59. package/src/lib/detectNesting.js +0 -47
  60. package/src/oxide/cli/build/deps.ts +0 -91
  61. package/src/oxide/cli/build/index.ts +0 -47
  62. package/src/oxide/cli/build/plugin.ts +0 -442
  63. package/src/oxide/cli/build/utils.ts +0 -74
  64. package/src/oxide/cli/build/watching.ts +0 -225
  65. package/src/oxide/cli/help/index.ts +0 -69
  66. package/src/oxide/cli/index.ts +0 -204
  67. package/src/oxide/cli/init/index.ts +0 -59
  68. package/src/oxide/cli.ts +0 -1
  69. package/src/oxide/postcss-plugin.ts +0 -1
@@ -207,8 +207,8 @@ export let variantPlugins = {
207
207
  },
208
208
 
209
209
  directionVariants: ({ addVariant }) => {
210
- addVariant('ltr', ':is(:where([dir="ltr"]) &)')
211
- addVariant('rtl', ':is(:where([dir="rtl"]) &)')
210
+ addVariant('ltr', '&:where([dir="ltr"], [dir="ltr"] *)')
211
+ addVariant('rtl', '&:where([dir="rtl"], [dir="rtl"] *)')
212
212
  },
213
213
 
214
214
  reducedMotionVariants: ({ addVariant }) => {
@@ -217,7 +217,7 @@ export let variantPlugins = {
217
217
  },
218
218
 
219
219
  darkVariants: ({ config, addVariant }) => {
220
- let [mode, className = '.dark'] = [].concat(config('darkMode', 'media'))
220
+ let [mode, selector = '.dark'] = [].concat(config('darkMode', 'media'))
221
221
 
222
222
  if (mode === false) {
223
223
  mode = 'media'
@@ -228,10 +228,49 @@ export let variantPlugins = {
228
228
  ])
229
229
  }
230
230
 
231
- if (mode === 'class') {
232
- addVariant('dark', `:is(:where(${className}) &)`)
231
+ if (mode === 'variant') {
232
+ let formats
233
+ if (Array.isArray(selector)) {
234
+ formats = selector
235
+ } else if (typeof selector === 'function') {
236
+ formats = selector
237
+ } else if (typeof selector === 'string') {
238
+ formats = [selector]
239
+ }
240
+
241
+ // TODO: We could also add these warnings if the user passes a function that returns string | string[]
242
+ // But this is an advanced enough use case that it's probably not necessary
243
+ if (Array.isArray(formats)) {
244
+ for (let format of formats) {
245
+ if (format === '.dark') {
246
+ mode = false
247
+ log.warn('darkmode-variant-without-selector', [
248
+ 'When using `variant` for `darkMode`, you must provide a selector.',
249
+ 'Example: `darkMode: ["variant", ".your-selector &"]`',
250
+ ])
251
+ } else if (!format.includes('&')) {
252
+ mode = false
253
+ log.warn('darkmode-variant-without-ampersand', [
254
+ 'When using `variant` for `darkMode`, your selector must contain `&`.',
255
+ 'Example `darkMode: ["variant", ".your-selector &"]`',
256
+ ])
257
+ }
258
+ }
259
+ }
260
+
261
+ selector = formats
262
+ }
263
+
264
+ if (mode === 'selector') {
265
+ // New preferred behavior
266
+ addVariant('dark', `&:where(${selector}, ${selector} *)`)
233
267
  } else if (mode === 'media') {
234
268
  addVariant('dark', '@media (prefers-color-scheme: dark)')
269
+ } else if (mode === 'variant') {
270
+ addVariant('dark', selector)
271
+ } else if (mode === 'class') {
272
+ // Old behavior
273
+ addVariant('dark', `&:is(${selector} *)`)
235
274
  }
236
275
  },
237
276
 
@@ -1301,16 +1340,6 @@ export let corePlugins = {
1301
1340
  'space-x': (value) => {
1302
1341
  value = value === '0' ? '0px' : value
1303
1342
 
1304
- if (__OXIDE__) {
1305
- return {
1306
- '& > :not([hidden]) ~ :not([hidden])': {
1307
- '--tw-space-x-reverse': '0',
1308
- 'margin-inline-end': `calc(${value} * var(--tw-space-x-reverse))`,
1309
- 'margin-inline-start': `calc(${value} * calc(1 - var(--tw-space-x-reverse)))`,
1310
- },
1311
- }
1312
- }
1313
-
1314
1343
  return {
1315
1344
  '& > :not([hidden]) ~ :not([hidden])': {
1316
1345
  '--tw-space-x-reverse': '0',
@@ -1346,17 +1375,6 @@ export let corePlugins = {
1346
1375
  'divide-x': (value) => {
1347
1376
  value = value === '0' ? '0px' : value
1348
1377
 
1349
- if (__OXIDE__) {
1350
- return {
1351
- '& > :not([hidden]) ~ :not([hidden])': {
1352
- '@defaults border-width': {},
1353
- '--tw-divide-x-reverse': '0',
1354
- 'border-inline-end-width': `calc(${value} * var(--tw-divide-x-reverse))`,
1355
- 'border-inline-start-width': `calc(${value} * calc(1 - var(--tw-divide-x-reverse)))`,
1356
- },
1357
- }
1358
- }
1359
-
1360
1378
  return {
1361
1379
  '& > :not([hidden]) ~ :not([hidden])': {
1362
1380
  '@defaults border-width': {},
@@ -2345,6 +2363,7 @@ export let corePlugins = {
2345
2363
  '.mix-blend-saturation': { 'mix-blend-mode': 'saturation' },
2346
2364
  '.mix-blend-color': { 'mix-blend-mode': 'color' },
2347
2365
  '.mix-blend-luminosity': { 'mix-blend-mode': 'luminosity' },
2366
+ '.mix-blend-plus-darker': { 'mix-blend-mode': 'plus-darker' },
2348
2367
  '.mix-blend-plus-lighter': { 'mix-blend-mode': 'plus-lighter' },
2349
2368
  })
2350
2369
  },
@@ -2358,7 +2377,7 @@ export let corePlugins = {
2358
2377
  ].join(', ')
2359
2378
 
2360
2379
  return function ({ matchUtilities, addDefaults, theme }) {
2361
- addDefaults(' box-shadow', {
2380
+ addDefaults('box-shadow', {
2362
2381
  '--tw-ring-offset-shadow': '0 0 #0000',
2363
2382
  '--tw-ring-shadow': '0 0 #0000',
2364
2383
  '--tw-shadow': '0 0 #0000',
@@ -2892,6 +2911,48 @@ export let corePlugins = {
2892
2911
  { filterDefault: true }
2893
2912
  ),
2894
2913
  willChange: createUtilityPlugin('willChange', [['will-change', ['will-change']]]),
2914
+ contain: ({ addDefaults, addUtilities }) => {
2915
+ let cssContainValue =
2916
+ 'var(--tw-contain-size) var(--tw-contain-layout) var(--tw-contain-paint) var(--tw-contain-style)'
2917
+
2918
+ addDefaults('contain', {
2919
+ '--tw-contain-size': ' ',
2920
+ '--tw-contain-layout': ' ',
2921
+ '--tw-contain-paint': ' ',
2922
+ '--tw-contain-style': ' ',
2923
+ })
2924
+
2925
+ addUtilities({
2926
+ '.contain-none': { contain: 'none' },
2927
+ '.contain-content': { contain: 'content' },
2928
+ '.contain-strict': { contain: 'strict' },
2929
+ '.contain-size': {
2930
+ '@defaults contain': {},
2931
+ '--tw-contain-size': 'size',
2932
+ contain: cssContainValue,
2933
+ },
2934
+ '.contain-inline-size': {
2935
+ '@defaults contain': {},
2936
+ '--tw-contain-size': 'inline-size',
2937
+ contain: cssContainValue,
2938
+ },
2939
+ '.contain-layout': {
2940
+ '@defaults contain': {},
2941
+ '--tw-contain-layout': 'layout',
2942
+ contain: cssContainValue,
2943
+ },
2944
+ '.contain-paint': {
2945
+ '@defaults contain': {},
2946
+ '--tw-contain-paint': 'paint',
2947
+ contain: cssContainValue,
2948
+ },
2949
+ '.contain-style': {
2950
+ '@defaults contain': {},
2951
+ '--tw-contain-style': 'style',
2952
+ contain: cssContainValue,
2953
+ },
2954
+ })
2955
+ },
2895
2956
  content: createUtilityPlugin('content', [
2896
2957
  ['content', ['--tw-content', ['content', 'var(--tw-content)']]],
2897
2958
  ]),
@@ -175,6 +175,7 @@ textarea {
175
175
  font-size: 100%; /* 1 */
176
176
  font-weight: inherit; /* 1 */
177
177
  line-height: inherit; /* 1 */
178
+ letter-spacing: inherit; /* 1 */
178
179
  color: inherit; /* 1 */
179
180
  margin: 0; /* 2 */
180
181
  padding: 0; /* 3 */
@@ -195,9 +196,9 @@ select {
195
196
  */
196
197
 
197
198
  button,
198
- [type='button'],
199
- [type='reset'],
200
- [type='submit'] {
199
+ input:where([type='button']),
200
+ input:where([type='reset']),
201
+ input:where([type='submit']) {
201
202
  -webkit-appearance: button; /* 1 */
202
203
  background-color: transparent; /* 2 */
203
204
  background-image: none; /* 2 */
@@ -4,12 +4,8 @@ import log from './util/log'
4
4
  let defaults = {
5
5
  optimizeUniversalDefaults: false,
6
6
  generalizedModifiers: true,
7
- get disableColorOpacityUtilitiesByDefault() {
8
- return __OXIDE__
9
- },
10
- get relativeContentPathsByDefault() {
11
- return __OXIDE__
12
- },
7
+ disableColorOpacityUtilitiesByDefault: false,
8
+ relativeContentPathsByDefault: false,
13
9
  }
14
10
 
15
11
  let featureFlags = {
@@ -4,10 +4,47 @@ import fs from 'fs'
4
4
  import path from 'path'
5
5
  import isGlob from 'is-glob'
6
6
  import fastGlob from 'fast-glob'
7
- import normalizePath from 'normalize-path'
8
7
  import { parseGlob } from '../util/parseGlob'
9
8
  import { env } from './sharedState'
10
9
 
10
+ /*!
11
+ * Modified version of normalize-path, original license below
12
+ *
13
+ * normalize-path <https://github.com/jonschlinkert/normalize-path>
14
+ *
15
+ * Copyright (c) 2014-2018, Jon Schlinkert.
16
+ * Released under the MIT License.
17
+ */
18
+
19
+ function normalizePath(path) {
20
+ if (typeof path !== 'string') {
21
+ throw new TypeError('expected path to be a string')
22
+ }
23
+
24
+ if (path === '\\' || path === '/') return '/'
25
+
26
+ var len = path.length
27
+ if (len <= 1) return path
28
+
29
+ // ensure that win32 namespaces has two leading slashes, so that the path is
30
+ // handled properly by the win32 version of path.parse() after being normalized
31
+ // https://msdn.microsoft.com/library/windows/desktop/aa365247(v=vs.85).aspx#namespaces
32
+ var prefix = ''
33
+ if (len > 4 && path[3] === '\\') {
34
+ var ch = path[2]
35
+ if ((ch === '?' || ch === '.') && path.slice(0, 2) === '\\\\') {
36
+ path = path.slice(2)
37
+ prefix = '//'
38
+ }
39
+ }
40
+
41
+ // Modified part: instead of purely splitting on `\\` and `/`, we split on
42
+ // `/` and `\\` that is _not_ followed by any of the following characters: ()[]
43
+ // This is to ensure that we keep the escaping of brackets and parentheses
44
+ let segs = path.split(/[/\\]+(?![\(\)\[\]])/)
45
+ return prefix + segs.join('/')
46
+ }
47
+
11
48
  /** @typedef {import('../../types/config.js').RawFile} RawFile */
12
49
  /** @typedef {import('../../types/config.js').FilePath} FilePath */
13
50
 
@@ -73,6 +110,10 @@ export function parseCandidateFiles(context, tailwindConfig) {
73
110
  * @returns {ContentPath}
74
111
  */
75
112
  function parseFilePath(filePath, ignore) {
113
+ // Escape special characters in the file path such as: ()[]
114
+ // But only if the special character isn't already escaped
115
+ filePath = filePath.replace(/(?<!\\)([\[\]\(\)])/g, '\\$1')
116
+
76
117
  let contentPath = {
77
118
  original: filePath,
78
119
  base: filePath,
@@ -96,7 +96,7 @@ function* buildRegExps(context) {
96
96
  regex.pattern([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/, separator]),
97
97
 
98
98
  // With variant modifier (e.g.: group-[..]/modifier)
99
- regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/\w+/, separator]),
99
+ regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/[\w_-]+/, separator]),
100
100
 
101
101
  regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, separator]),
102
102
  regex.pattern([/[^\s"'`\[\\]+/, separator]),
@@ -105,7 +105,7 @@ function* buildRegExps(context) {
105
105
  // With quotes allowed
106
106
  regex.any([
107
107
  // With variant modifier (e.g.: group-[..]/modifier)
108
- regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/\w+/, separator]),
108
+ regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/[\w_-]+/, separator]),
109
109
 
110
110
  regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, separator]),
111
111
  regex.pattern([/[^\s`\[\\]+/, separator]),
@@ -432,6 +432,23 @@ function processApply(root, context, localCache) {
432
432
 
433
433
  let rules = applyClassCache.get(applyCandidate)
434
434
 
435
+ // Verify that we can apply the class
436
+ for (let [, rule] of rules) {
437
+ if (rule.type === 'atrule') {
438
+ continue
439
+ }
440
+
441
+ rule.walkRules(() => {
442
+ throw apply.error(
443
+ [
444
+ `The \`${applyCandidate}\` class cannot be used with \`@apply\` because \`@apply\` does not currently support nested CSS.`,
445
+ 'Rewrite the selector without nesting or configure the `tailwindcss/nesting` plugin:',
446
+ 'https://tailwindcss.com/docs/using-with-preprocessors#nesting',
447
+ ].join('\n')
448
+ )
449
+ })
450
+ }
451
+
435
452
  candidates.push([applyCandidate, important, rules])
436
453
  }
437
454
  }
@@ -130,41 +130,25 @@ export default function expandTailwindAtRules(context) {
130
130
 
131
131
  env.DEBUG && console.time('Reading changed files')
132
132
 
133
- if (__OXIDE__) {
134
- // TODO: Pass through or implement `extractor`
135
- for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles(
136
- context.changedContent
137
- // Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform)
138
- )) {
139
- candidates.add(candidate)
140
- }
141
-
142
- // for (let { file, content, extension } of context.changedContent) {
143
- // let transformer = getTransformer(context.tailwindConfig, extension)
144
- // let extractor = getExtractor(context, extension)
145
- // getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen)
146
- // }
147
- } else {
148
- /** @type {[item: {file?: string, content?: string}, meta: {transformer: any, extractor: any}][]} */
149
- let regexParserContent = []
133
+ /** @type {[item: {file?: string, content?: string}, meta: {transformer: any, extractor: any}][]} */
134
+ let regexParserContent = []
150
135
 
151
- for (let item of context.changedContent) {
152
- let transformer = getTransformer(context.tailwindConfig, item.extension)
153
- let extractor = getExtractor(context, item.extension)
154
- regexParserContent.push([item, { transformer, extractor }])
155
- }
136
+ for (let item of context.changedContent) {
137
+ let transformer = getTransformer(context.tailwindConfig, item.extension)
138
+ let extractor = getExtractor(context, item.extension)
139
+ regexParserContent.push([item, { transformer, extractor }])
140
+ }
156
141
 
157
- const BATCH_SIZE = 500
142
+ const BATCH_SIZE = 500
158
143
 
159
- for (let i = 0; i < regexParserContent.length; i += BATCH_SIZE) {
160
- let batch = regexParserContent.slice(i, i + BATCH_SIZE)
161
- await Promise.all(
162
- batch.map(async ([{ file, content }, { transformer, extractor }]) => {
163
- content = file ? await fs.promises.readFile(file, 'utf8') : content
164
- getClassCandidates(transformer(content), extractor, candidates, seen)
165
- })
166
- )
167
- }
144
+ for (let i = 0; i < regexParserContent.length; i += BATCH_SIZE) {
145
+ let batch = regexParserContent.slice(i, i + BATCH_SIZE)
146
+ await Promise.all(
147
+ batch.map(async ([{ file, content }, { transformer, extractor }]) => {
148
+ content = file ? await fs.promises.readFile(file, 'utf8') : content
149
+ getClassCandidates(transformer(content), extractor, candidates, seen)
150
+ })
151
+ )
168
152
  }
169
153
 
170
154
  env.DEBUG && console.timeEnd('Reading changed files')
@@ -176,15 +160,13 @@ export default function expandTailwindAtRules(context) {
176
160
 
177
161
  env.DEBUG && console.time('Generate rules')
178
162
  env.DEBUG && console.time('Sorting candidates')
179
- let sortedCandidates = __OXIDE__
180
- ? candidates
181
- : new Set(
182
- [...candidates].sort((a, z) => {
183
- if (a === z) return 0
184
- if (a < z) return -1
185
- return 1
186
- })
187
- )
163
+ let sortedCandidates = new Set(
164
+ [...candidates].sort((a, z) => {
165
+ if (a === z) return 0
166
+ if (a < z) return -1
167
+ return 1
168
+ })
169
+ )
188
170
  env.DEBUG && console.timeEnd('Sorting candidates')
189
171
  generateRules(sortedCandidates, context)
190
172
  env.DEBUG && console.timeEnd('Generate rules')
@@ -119,10 +119,20 @@ function applyImportant(matches, classCandidate) {
119
119
 
120
120
  let result = []
121
121
 
122
+ function isInKeyframes(rule) {
123
+ return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
124
+ }
125
+
122
126
  for (let [meta, rule] of matches) {
123
127
  let container = postcss.root({ nodes: [rule.clone()] })
124
128
 
125
129
  container.walkRules((r) => {
130
+ // Declarations inside keyframes cannot be marked as important
131
+ // They will be ignored by the browser
132
+ if (isInKeyframes(r)) {
133
+ return
134
+ }
135
+
126
136
  let ast = selectorParser().astSync(r.selector)
127
137
 
128
138
  // Remove extraneous selectors that do not include the base candidate
@@ -502,11 +512,11 @@ function extractArbitraryProperty(classCandidate, context) {
502
512
  return null
503
513
  }
504
514
 
505
- let sort = context.offsets.arbitraryProperty()
515
+ let sort = context.offsets.arbitraryProperty(classCandidate)
506
516
 
507
517
  return [
508
518
  [
509
- { sort, layer: 'utilities' },
519
+ { sort, layer: 'utilities', options: { respectImportant: true } },
510
520
  () => ({
511
521
  [asClass(classCandidate)]: {
512
522
  [property]: normalized,
@@ -18,6 +18,11 @@ function lazyJiti() {
18
18
  (jiti = jitiFactory(__filename, {
19
19
  interopDefault: true,
20
20
  transform: (opts) => {
21
+ // Sucrase can't transform import.meta so we have to use Babel
22
+ if (opts.source.includes('import.meta')) {
23
+ return require('jiti/dist/babel.js')(opts)
24
+ }
25
+
21
26
  return transform(opts.source, {
22
27
  transforms: ['typescript', 'imports'],
23
28
  })
@@ -23,7 +23,9 @@ import { remapBitfield } from './remap-bitfield.js'
23
23
  * @property {bigint} arbitrary 0n if false, 1n if true
24
24
  * @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants
25
25
  * @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable.
26
- * @property {bigint} index Index of the rule / utility in it's given *parent* layer. Monotonically increasing.
26
+ * @property {bigint} index Index of the rule / utility in its given *parent* layer. Monotonically increasing.
27
+ * @property {bigint} propertyOffset Offset for the arbitrary property. Only valid after sorting.
28
+ * @property {string} property Name/Value of the arbitrary property.
27
29
  * @property {VariantOption[]} options Some information on how we can sort arbitrary variants
28
30
  */
29
31
 
@@ -88,17 +90,21 @@ export class Offsets {
88
90
  variants: 0n,
89
91
  parallelIndex: 0n,
90
92
  index: this.offsets[layer]++,
93
+ propertyOffset: 0n,
94
+ property: '',
91
95
  options: [],
92
96
  }
93
97
  }
94
98
 
95
99
  /**
100
+ * @param {string} name
96
101
  * @returns {RuleOffset}
97
102
  */
98
- arbitraryProperty() {
103
+ arbitraryProperty(name) {
99
104
  return {
100
105
  ...this.create('utilities'),
101
106
  arbitrary: 1n,
107
+ property: name,
102
108
  }
103
109
  }
104
110
 
@@ -262,6 +268,11 @@ export class Offsets {
262
268
  return a.arbitrary - b.arbitrary
263
269
  }
264
270
 
271
+ // Always sort arbitrary properties alphabetically
272
+ if (a.propertyOffset !== b.propertyOffset) {
273
+ return a.propertyOffset - b.propertyOffset
274
+ }
275
+
265
276
  // Sort utilities, components, etc… in the order they were registered
266
277
  return a.index - b.index
267
278
  }
@@ -320,14 +331,62 @@ export class Offsets {
320
331
  })
321
332
  }
322
333
 
334
+ /**
335
+ * @template T
336
+ * @param {[RuleOffset, T][]} list
337
+ * @returns {[RuleOffset, T][]}
338
+ */
339
+ sortArbitraryProperties(list) {
340
+ // Collect all known arbitrary properties
341
+ let known = new Set()
342
+
343
+ for (let [offset] of list) {
344
+ if (offset.arbitrary === 1n) {
345
+ known.add(offset.property)
346
+ }
347
+ }
348
+
349
+ // No arbitrary properties? Nothing to do.
350
+ if (known.size === 0) {
351
+ return list
352
+ }
353
+
354
+ // Sort the properties alphabetically
355
+ let properties = Array.from(known).sort()
356
+
357
+ // Create a map from the property name to its offset
358
+ let offsets = new Map()
359
+
360
+ let offset = 1n
361
+ for (let property of properties) {
362
+ offsets.set(property, offset++)
363
+ }
364
+
365
+ // Apply the sorted offsets to the list
366
+ return list.map((item) => {
367
+ let [offset, rule] = item
368
+
369
+ offset = {
370
+ ...offset,
371
+ propertyOffset: offsets.get(offset.property) ?? 0n,
372
+ }
373
+
374
+ return [offset, rule]
375
+ })
376
+ }
377
+
323
378
  /**
324
379
  * @template T
325
380
  * @param {[RuleOffset, T][]} list
326
381
  * @returns {[RuleOffset, T][]}
327
382
  */
328
383
  sort(list) {
384
+ // Sort arbitrary variants so they're in alphabetical order
329
385
  list = this.remapArbitraryVariantOffsets(list)
330
386
 
387
+ // Sort arbitrary properties so they're in alphabetical order
388
+ list = this.sortArbitraryProperties(list)
389
+
331
390
  return list.sort(([a], [b]) => bigSign(this.compare(a, b)))
332
391
  }
333
392
  }
@@ -104,8 +104,12 @@ export default function resolveDefaultsAtRules({ tailwindConfig }) {
104
104
  // we consider them separately because merging the declarations into
105
105
  // a single rule will cause browsers that do not understand the
106
106
  // vendor prefix to throw out the whole rule
107
+ // Additionally if a selector contains `:has` we also consider
108
+ // it separately because FF only recently gained support for it
107
109
  let selectorGroupName =
108
- selector.includes(':-') || selector.includes('::-') ? selector : '__DEFAULT__'
110
+ selector.includes(':-') || selector.includes('::-') || selector.includes(':has')
111
+ ? selector
112
+ : '__DEFAULT__'
109
113
 
110
114
  let selectors = selectorGroups.get(selectorGroupName) ?? new Set()
111
115
  selectorGroups.set(selectorGroupName, selectors)
@@ -767,14 +767,35 @@ function resolvePlugins(context, root) {
767
767
  variantPlugins['supportsVariants'],
768
768
  variantPlugins['reducedMotionVariants'],
769
769
  variantPlugins['prefersContrastVariants'],
770
- variantPlugins['printVariant'],
771
770
  variantPlugins['screenVariants'],
772
771
  variantPlugins['orientationVariants'],
773
772
  variantPlugins['directionVariants'],
774
773
  variantPlugins['darkVariants'],
775
774
  variantPlugins['forcedColorsVariants'],
775
+ variantPlugins['printVariant'],
776
776
  ]
777
777
 
778
+ // This is a compatibility fix for the pre 3.4 dark mode behavior
779
+ // `class` retains the old behavior, but `selector` keeps the new behavior
780
+ let isLegacyDarkMode =
781
+ context.tailwindConfig.darkMode === 'class' ||
782
+ (Array.isArray(context.tailwindConfig.darkMode) &&
783
+ context.tailwindConfig.darkMode[0] === 'class')
784
+
785
+ if (isLegacyDarkMode) {
786
+ afterVariants = [
787
+ variantPlugins['supportsVariants'],
788
+ variantPlugins['reducedMotionVariants'],
789
+ variantPlugins['prefersContrastVariants'],
790
+ variantPlugins['darkVariants'],
791
+ variantPlugins['screenVariants'],
792
+ variantPlugins['orientationVariants'],
793
+ variantPlugins['directionVariants'],
794
+ variantPlugins['forcedColorsVariants'],
795
+ variantPlugins['printVariant'],
796
+ ]
797
+ }
798
+
778
799
  return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
779
800
  }
780
801
 
@@ -1023,6 +1044,11 @@ function registerPlugins(plugins, context) {
1023
1044
 
1024
1045
  // Generate a list of available variants with meta information of the type of variant.
1025
1046
  context.getVariants = function getVariants() {
1047
+ // We use a unique, random ID for candidate names to avoid conflicts
1048
+ // We can't use characters like `_`, `:`, `@` or `.` because they might
1049
+ // be used as a separator
1050
+ let id = Math.random().toString(36).substring(7).toUpperCase()
1051
+
1026
1052
  let result = []
1027
1053
  for (let [name, options] of context.variantOptions.entries()) {
1028
1054
  if (options.variantInfo === VARIANT_INFO.Base) continue
@@ -1033,7 +1059,7 @@ function registerPlugins(plugins, context) {
1033
1059
  values: Object.keys(options.values ?? {}),
1034
1060
  hasDash: name !== '@',
1035
1061
  selectors({ modifier, value } = {}) {
1036
- let candidate = '__TAILWIND_PLACEHOLDER__'
1062
+ let candidate = `TAILWINDPLACEHOLDER${id}`
1037
1063
 
1038
1064
  let rule = postcss.rule({ selector: `.${candidate}` })
1039
1065
  let container = postcss.root({ nodes: [rule.clone()] })
@@ -1,16 +1,12 @@
1
- import pkg from '../../package.json'
2
-
3
1
  export const env =
4
2
  typeof process !== 'undefined'
5
3
  ? {
6
4
  NODE_ENV: process.env.NODE_ENV,
7
5
  DEBUG: resolveDebug(process.env.DEBUG),
8
- ENGINE: pkg.tailwindcss.engine,
9
6
  }
10
7
  : {
11
8
  NODE_ENV: 'production',
12
9
  DEBUG: false,
13
- ENGINE: pkg.tailwindcss.engine,
14
10
  }
15
11
 
16
12
  export const contextMap = new Map()