Skip to content

Commit d3b4361

Browse files
fix: support for v3.1.0
- Drop support for `officialSorting` & `prependCustom` (`classnames-order`) - New features from v3.1.0 - Tests adapted - New tests - README - Include fix from #144 (by @mpsijm)
1 parent 75e0b68 commit d3b4361

15 files changed

+3923
-486
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ If you enjoy my work you can:
3737

3838
## Latest changelog
3939

40+
- ADD: support for [new features from Tailwind CSS v3.1.0](https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.1.0)
41+
> Custom `dark` class, `.grid-flow-dense`, `.text-start`, `.text-end`, `.mix-blend-plus-lighter`, `.border-spacing...`
42+
- BREAKING CHANGE: `groupByResponsive`, `officialSorting` and `prependCustom` are deprecated ☠️. The official sorting is always used for `classnames-order`.
43+
> This was required in order to support classnames generated by plugins.
44+
- FIX: [Many fixes](https://github.com/francoismassart/eslint-plugin-tailwindcss/pull/132) including support for classnames generated by plugins.
4045
- FIX: [speeds up `enforces-shorthand` and `classnames-order`](https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/136) with `officialSorting: true` by introducing WeakMap caches to reduce duplicate calculations (by [mpsijm](https://github.com/mpsijm) 🙏)
4146
- New strategy for whitespaces and linebreaks: the plugin will attempt to leave them intact
4247
- New option `officialSorting` for [`classnames-order`](docs/rules/classnames-order.md#officialsorting-default-false) can be set to `true` in order to use the same ordering order as the official [`prettier-plugin-tailwindcss`](https://www.npmjs.com/package/prettier-plugin-tailwindcss)
@@ -159,10 +164,7 @@ All these settings have nice default values that are explained in each rules' do
159164
"config": "tailwind.config.js",
160165
"cssFiles": ["**/*.css", "!**/node_modules", "!**/.*", "!**/dist", "!**/build"],
161166
"cssFilesRefreshRate": 5_000,
162-
"groupByResponsive": true,
163167
"groups": defaultGroups, // imported from groups.js
164-
"officialSorting": false,
165-
"prependCustom": false,
166168
"removeDuplicates": true,
167169
"whitelist": []
168170
}

docs/rules/classnames-order.md

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ Examples of **correct** code for this rule:
2525
"tailwindcss/classnames-order": [<enabled>, {
2626
"callees": Array<string>,
2727
"config": <string>|<object>,
28-
"groupByResponsive": <boolean>,
2928
"groups": Array<object>,
30-
"officialSorting": <boolean>,
31-
"prependCustom": <boolean>,
3229
"removeDuplicates": <boolean>,
3330
"tags": Array<string>,
3431
}]
@@ -55,25 +52,6 @@ It is also possible to directly inject a configuration as plain `object` like `{
5552

5653
Finally, the plugin will [merge the provided configuration](https://tailwindcss.com/docs/configuration#referencing-in-java-script) with [Tailwind CSS's default configuration](https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js).
5754

58-
### `groupByResponsive` (default: `true`)
59-
60-
When this option was introduced in version 2.x.x of the plugin, this setting was set to `false` to avoid a tsunami of reorder in the classnames.
61-
You had to set it to `true` intentionally.
62-
63-
Since version 3 of the plugin, the default value is now `true`, grouping by responsive modifier in priority vs. grouping by property.
64-
65-
Linting this code:
66-
67-
`<div class="rounded sm:rounded-lg lg:rounded-2xl p-4 sm:p-6 lg:p-8">...</div>`
68-
69-
By default, the ordering process will group the classnames by variants then by properties:
70-
71-
`<div class="p-4 rounded sm:p-6 sm:rounded-lg lg:p-8 lg:rounded-2xl">...</div>`
72-
73-
Set `groupByResponsive` to `false` and the ordering will work by properties, then by variants:
74-
75-
`<div class="p-4 sm:p-6 lg:p-8 rounded sm:rounded-lg lg:rounded-2xl">...</div>`
76-
7755
### `groups` (default defined in [groups.js](../../lib/config/groups.js))
7856

7957
If you really need to, you can write your own configuration.
@@ -116,14 +94,6 @@ const customGroups = require('custom-groups').groups;
11694
...
11795
```
11896

119-
### `officialSorting` (default: `false`)
120-
121-
Set `officialSorting` to `true` if you want to use the same ordering rules as the official plugin `prettier-plugin-tailwindcss`. Enabling this settings will cause `groupByResponsive`, `groups`, `prependCustom` and `removeDuplicates` options to be ignored.
122-
123-
### `prependCustom` (default: `false`)
124-
125-
By default, classnames which doesn't belong to Tailwind CSS will be pushed at the end. Set `prependCustom` to `true` if you prefer to move them at the beginning.
126-
12797
### `removeDuplicates` (default: `true`)
12898

12999
Duplicate classnames are automatically removed but you can always disable this behavior by setting `removeDuplicates` to `false`.

lib/config/groups.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ module.exports.groups = [
289289
},
290290
{
291291
type: 'Grid Auto Flow',
292-
members: 'grid\\-flow\\-(row|col)(\\-dense)?',
292+
members: 'grid\\-flow\\-(dense|(row|col)(\\-dense)?)',
293293
},
294294
{
295295
type: 'Grid Auto Columns',
@@ -555,7 +555,7 @@ module.exports.groups = [
555555
},
556556
{
557557
type: 'Text Alignment',
558-
members: 'text\\-(left|center|right|justify)',
558+
members: 'text\\-(left|center|right|justify|start|end)',
559559
},
560560
{
561561
type: 'Text Color',
@@ -940,7 +940,7 @@ module.exports.groups = [
940940
{
941941
type: 'Mix Blend Mode',
942942
members:
943-
'mix\\-blend\\-(normal|multiply|screen|overlay|darken|lighten|color\\-(burn|dodge)|(hard|soft)\\-light|difference|exclusion|hue|saturation|color|luminosity)',
943+
'mix\\-blend\\-(normal|multiply|screen|overlay|darken|lighten|color\\-(burn|dodge)|(hard|soft)\\-light|difference|exclusion|hue|saturation|color|luminosity|plus\\-lighter)',
944944
},
945945
{
946946
type: 'Background Blend Mode',
@@ -1039,6 +1039,29 @@ module.exports.groups = [
10391039
type: 'Border Collapse',
10401040
members: 'border\\-(collapse|separate)',
10411041
},
1042+
{
1043+
type: 'Border Spacing',
1044+
members: [
1045+
{
1046+
type: 'border-spacing',
1047+
members: 'border\\-spacing\\-(?<value>${borderSpacing})',
1048+
shorthand: 'all',
1049+
body: 'border-spacing',
1050+
},
1051+
{
1052+
type: 'border-spacing-x',
1053+
members: 'border\\-spacing\\-x\\-(?<value>${borderSpacing})',
1054+
shorthand: 'x',
1055+
body: 'border-spacing-x',
1056+
},
1057+
{
1058+
type: 'border-spacing-y',
1059+
members: 'border\\-spacing\\-y\\-(?<value>${borderSpacing})',
1060+
shorthand: 'y',
1061+
body: 'border-spacing-y',
1062+
},
1063+
],
1064+
},
10421065
{
10431066
type: 'Table Layout',
10441067
members: 'table\\-(auto|fixed)',

lib/rules/classnames-order.js

Lines changed: 19 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,10 @@ module.exports = {
4949
default: 'tailwind.config.js',
5050
type: ['string', 'object'],
5151
},
52-
groupByResponsive: {
53-
default: true,
54-
type: 'boolean',
55-
},
5652
groups: {
5753
type: 'array',
5854
items: { type: 'object' },
5955
},
60-
officialSorting: {
61-
default: false,
62-
type: 'boolean',
63-
},
64-
prependCustom: {
65-
default: false,
66-
type: 'boolean',
67-
},
6856
removeDuplicates: {
6957
default: true,
7058
type: 'boolean',
@@ -84,20 +72,15 @@ module.exports = {
8472
const tags = getOption(context, 'tags');
8573
const twConfig = getOption(context, 'config');
8674
const groupsConfig = getOption(context, 'groups');
87-
const groupByResponsive = getOption(context, 'groupByResponsive');
88-
const officialSorting = getOption(context, 'officialSorting');
89-
const prependCustom = getOption(context, 'prependCustom');
9075
const removeDuplicates = getOption(context, 'removeDuplicates');
9176

9277
const mergedConfig = customConfig.resolve(twConfig);
93-
const contextFallback = officialSorting
94-
? (
95-
// Set the created contextFallback in the cache if it does not exist yet.
96-
contextFallbackCache.has(mergedConfig)
97-
? contextFallbackCache
98-
: contextFallbackCache.set(mergedConfig, createContextFallback(mergedConfig))
99-
).get(mergedConfig)
100-
: null;
78+
const contextFallback = // Set the created contextFallback in the cache if it does not exist yet.
79+
(
80+
contextFallbackCache.has(mergedConfig)
81+
? contextFallbackCache
82+
: contextFallbackCache.set(mergedConfig, createContextFallback(mergedConfig))
83+
).get(mergedConfig);
10184

10285
//----------------------------------------------------------------------
10386
// Helpers
@@ -309,52 +292,23 @@ module.exports = {
309292
return;
310293
}
311294

312-
let orderedClassNames;
313-
let validatedClassNamesValue = '';
295+
let orderedClassNames = order(classNames, contextFallback);
314296

315-
if (officialSorting) {
316-
orderedClassNames = order(classNames, contextFallback);
317-
for (let i = 0; i < orderedClassNames.length; i++) {
318-
const w = whitespaces[i] ?? '';
319-
const cls = orderedClassNames[i];
320-
validatedClassNamesValue += headSpace ? `${w}${cls}` : `${cls}${w}`;
321-
if (headSpace && tailSpace && i === orderedClassNames.length - 1) {
322-
validatedClassNamesValue += whitespaces[whitespaces.length - 1] ?? '';
323-
}
324-
}
325-
} else {
326-
if (removeDuplicates) {
327-
removeDuplicatesFromClassnamesAndWhitespaces(classNames, whitespaces, headSpace, tailSpace);
328-
}
329-
330-
// Sorting
331-
const mergedSorted = [];
332-
const mergedExtras = [];
333-
if (groupByResponsive) {
334-
const respGroups = getResponsiveGroups(classNames);
335-
respGroups.forEach((clsGroup) => {
336-
const { sorted, extras } = getSortedGroups(clsGroup);
337-
mergedSorted.push(...sorted);
338-
mergedExtras.push(...extras);
339-
});
340-
} else {
341-
const { sorted, extras } = getSortedGroups(classNames);
342-
mergedSorted.push(...sorted);
343-
mergedExtras.push(...extras);
344-
}
297+
if (removeDuplicates) {
298+
removeDuplicatesFromClassnamesAndWhitespaces(orderedClassNames, whitespaces, headSpace, tailSpace);
299+
}
345300

346-
// Generates the validated/sorted attribute value
347-
const flatted = mergedSorted.flat();
348-
const union = prependCustom ? [...mergedExtras, ...flatted] : [...flatted, ...mergedExtras];
349-
for (let i = 0; i < union.length; i++) {
350-
const w = whitespaces[i] ?? '';
351-
const cls = union[i];
352-
validatedClassNamesValue += headSpace ? `${w}${cls}` : `${cls}${w}`;
353-
if (headSpace && tailSpace && i === union.length - 1) {
354-
validatedClassNamesValue += whitespaces[whitespaces.length - 1] ?? '';
355-
}
301+
// Generates the validated/sorted attribute value
302+
let validatedClassNamesValue = '';
303+
for (let i = 0; i < orderedClassNames.length; i++) {
304+
const w = whitespaces[i] ?? '';
305+
const cls = orderedClassNames[i];
306+
validatedClassNamesValue += headSpace ? `${w}${cls}` : `${cls}${w}`;
307+
if (headSpace && tailSpace && i === orderedClassNames.length - 1) {
308+
validatedClassNamesValue += whitespaces[whitespaces.length - 1] ?? '';
356309
}
357310
}
311+
358312
if (originalClassNamesValue !== validatedClassNamesValue) {
359313
validatedClassNamesValue = prefix + validatedClassNamesValue + suffix;
360314
context.report({

lib/rules/enforces-shorthand.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ module.exports = {
7272
'Flexbox & Grid': ['Gap'],
7373
Spacing: ['Padding', 'Margin'],
7474
Borders: ['Border Radius', 'Border Width', 'Border Color'],
75+
Tables: ['Border Spacing'],
7576
Transforms: ['Scale'],
7677
};
7778
// We don't want to affect other rules by object reference

lib/util/groupMethods.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,22 @@ function generateOptions(propName, keys, config, isNegative = false) {
6464
switch (propName) {
6565
case 'dark':
6666
// Optional `dark` class
67-
return config.darkMode === 'class' ? 'dark' : '';
67+
if (config.darkMode === 'class') {
68+
return 'dark';
69+
} else if (Array.isArray(config.darkMode) && config.darkMode.length === 2 && config.darkMode[0] === 'class') {
70+
// https://tailwindcss.com/docs/dark-mode#customizing-the-class-name
71+
// For the sake of simplicity we only support a single class name
72+
let value = '';
73+
const res = /^\.(?<classnameValue>[A-Z0-9\:\-\_\[\d\]]*)$/gi.exec(config.darkMode[1]);
74+
if (res && res.groups) {
75+
if (res.groups.classnameValue) {
76+
value = res.groups.classnameValue;
77+
}
78+
}
79+
return value;
80+
} else {
81+
return '';
82+
}
6883
case 'arbitraryProperties':
6984
escapedKeys.push(genericArbitraryOption);
7085
return '(' + escapedKeys.join('|') + ')';
@@ -119,6 +134,7 @@ function generateOptions(propName, keys, config, isNegative = false) {
119134
}
120135
options.push(`\\[(${arbitraryColors.join('|')})\\]`);
121136
return '(' + options.join('|') + ')';
137+
case 'borderSpacing':
122138
case 'borderWidth':
123139
case 'divideWidth':
124140
case 'fontSize':
@@ -290,7 +306,7 @@ function patchRegex(re, config) {
290306
const resArray = [...res];
291307
const props = resArray.map((arr) => arr[1]);
292308
if (props.length === 0) {
293-
return cache[re] = `${patched}(${replaced})`;
309+
return (cache[re] = `${patched}(${replaced})`);
294310
}
295311
// e.g. backgroundColor, letterSpacing, -margin...
296312
props.forEach((prop) => {
@@ -345,7 +361,7 @@ function patchRegex(re, config) {
345361
const opts = generateOptions(absoluteProp, keys, config, isNegative);
346362
replaced = replaced.replace(token, opts);
347363
});
348-
return cache[re] = `${patched}(${replaced})`;
364+
return (cache[re] = `${patched}(${replaced})`);
349365
}
350366

351367
/**
Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
'use strict';
22

3-
function removeDuplicatesFromClassnamesAndWhitespaces(classNames, whitespaces, headSpace, tailSpace) {
4-
const uniqueSet = new Set(classNames);
5-
if (uniqueSet.size === classNames.length) {
6-
return;
7-
}
3+
function removeDuplicatesFromClassnamesAndWhitespaces(orderedClassNames, whitespaces, headSpace, tailSpace) {
4+
let previous = orderedClassNames[0];
85
const offset = (!headSpace && !tailSpace) || tailSpace ? -1 : 0;
9-
uniqueSet.forEach((cls) => {
10-
let duplicatedInstances = classNames.filter((el) => el === cls).length - 1;
11-
while (duplicatedInstances > 0) {
12-
const idx = classNames.lastIndexOf(cls);
13-
classNames.splice(idx, 1);
14-
whitespaces.splice(idx + offset, 1);
15-
duplicatedInstances--;
6+
for (let i = 1; i < orderedClassNames.length; i++) {
7+
const cls = orderedClassNames[i];
8+
// This function assumes that the list of classNames is ordered, so just comparing to the previous className is enough
9+
if (cls === previous) {
10+
orderedClassNames.splice(i, 1);
11+
whitespaces.splice(i + offset, 1);
12+
i--;
1613
}
17-
});
14+
previous = cls;
15+
}
1816
}
1917

2018
module.exports = removeDuplicatesFromClassnamesAndWhitespaces;

lib/util/settings.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,8 @@ function getOption(context, name) {
1919
return 'tailwind.config.js';
2020
case 'cssFiles':
2121
return ['**/*.css', '!**/node_modules', '!**/.*', '!**/dist', '!**/build'];
22-
case 'groupByResponsive':
23-
return true;
2422
case 'groups':
2523
return defaultGroups;
26-
case 'officialSorting':
27-
return false;
28-
case 'prependCustom':
29-
return false;
3024
case 'removeDuplicates':
3125
return true;
3226
case 'tags':

0 commit comments

Comments
 (0)