Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ If you enjoy my work you can:

## Latest changelog

- New strategy for whitespaces and linebreaks: the plugin will attempt to leave them intact
- 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)
- FIX: `enforces-shorthand` rule [fixer](https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/120) and [fix prefix](https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/121)
- FIX: [`enforces-shorthand` rule loses the importance flag](https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/114)
- New rule: [`enforces-negative-arbitrary-values`](docs/rules/enforces-negative-arbitrary-values.md): prefers `top-[-5px]` instead of `-top-[5px]`
Expand Down Expand Up @@ -158,6 +160,7 @@ All these settings have nice default values that are explained in each rules' do
"cssFilesRefreshRate": 5_000,
"groupByResponsive": true,
"groups": defaultGroups, // imported from groups.js
"officialSorting": false,
"prependCustom": false,
"removeDuplicates": true,
"whitelist": []
Expand Down
5 changes: 5 additions & 0 deletions docs/rules/classnames-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Examples of **correct** code for this rule:
"config": <string>|<object>,
"groupByResponsive": <boolean>,
"groups": Array<object>,
"officialSorting": <boolean>,
"prependCustom": <boolean>,
"removeDuplicates": <boolean>,
"tags": Array<string>,
Expand Down Expand Up @@ -115,6 +116,10 @@ const customGroups = require('custom-groups').groups;
...
```

### `officialSorting` (default: `false`)

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.

### `prependCustom` (default: `false`)

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.
Expand Down
111 changes: 63 additions & 48 deletions lib/rules/classnames-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
const docsUrl = require('../util/docsUrl');
const customConfig = require('../util/customConfig');
const astUtil = require('../util/ast');
const attrUtil = require('../util/attr');
const groupUtil = require('../util/groupMethods');
const removeDuplicatesFromArray = require('../util/removeDuplicatesFromArray');
const removeDuplicatesFromClassnamesAndWhitespaces = require('../util/removeDuplicatesFromClassnamesAndWhitespaces');
const getOption = require('../util/settings');
const parserUtil = require('../util/parser');
const order = require('../util/prettier/order');
const createContextFallback = require('tailwindcss/lib/lib/setupContextUtils').createContext;

//------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -54,6 +55,10 @@ module.exports = {
type: 'array',
items: { type: 'object' },
},
officialSorting: {
default: false,
type: 'boolean',
},
prependCustom: {
default: false,
type: 'boolean',
Expand All @@ -78,10 +83,12 @@ module.exports = {
const twConfig = getOption(context, 'config');
const groupsConfig = getOption(context, 'groups');
const groupByResponsive = getOption(context, 'groupByResponsive');
const officialSorting = getOption(context, 'officialSorting');
const prependCustom = getOption(context, 'prependCustom');
const removeDuplicates = getOption(context, 'removeDuplicates');

const mergedConfig = customConfig.resolve(twConfig);
const contextFallback = officialSorting ? createContextFallback(mergedConfig) : null;

//----------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -129,7 +136,7 @@ module.exports = {
classnamesByResponsive.push([]);
});
classNames.forEach((cls) => {
const idx = parseInt(getSpecificity(attrUtil.cleanClassname(cls), responsiveVariants, true), 10);
const idx = parseInt(getSpecificity(cls, responsiveVariants, true), 10);
classnamesByResponsive[idx].push(cls);
});
return classnamesByResponsive;
Expand Down Expand Up @@ -191,11 +198,11 @@ module.exports = {
// motion-safe/reduce are not present...
// TODO Check if already present due to custom config overwiting the default `variantOrder`
const stateVariants = [...mergedConfig.variantOrder, 'motion-safe', 'motion-reduce'];
const aIdxStr = `${getSpecificity(attrUtil.cleanClassname(a), responsiveVariants, true)}${getSpecificity(
const aIdxStr = `${getSpecificity(a, responsiveVariants, true)}${getSpecificity(
a,
themeVariants
)}${getSpecificity(a, stateVariants)}`;
const bIdxStr = `${getSpecificity(attrUtil.cleanClassname(b), responsiveVariants, true)}${getSpecificity(
const bIdxStr = `${getSpecificity(b, responsiveVariants, true)}${getSpecificity(
b,
themeVariants
)}${getSpecificity(b, stateVariants)}`;
Expand All @@ -222,7 +229,6 @@ module.exports = {
let end = null;
let prefix = '';
let suffix = '';
let trim = false;
if (arg === null) {
originalClassNamesValue = astUtil.extractValueFromNode(node);
const range = astUtil.extractRangeFromNode(node);
Expand All @@ -235,6 +241,8 @@ module.exports = {
}
} else {
switch (arg.type) {
case 'Identifier':
return;
case 'TemplateLiteral':
arg.expressions.forEach((exp) => {
sortNodeArgumentValue(node, exp);
Expand All @@ -261,13 +269,15 @@ module.exports = {
});
return;
case 'Literal':
trim = true;
originalClassNamesValue = arg.value;
start = arg.range[0] + 1;
end = arg.range[1] - 1;
break;
case 'TemplateElement':
originalClassNamesValue = arg.value.raw;
if (originalClassNamesValue === '') {
return;
}
start = arg.range[0];
end = arg.range[1];
// https://github.com/eslint/eslint/issues/13360
Expand All @@ -277,61 +287,66 @@ module.exports = {
const txt = context.getSourceCode().getText(arg);
prefix = astUtil.getTemplateElementPrefix(txt, originalClassNamesValue);
suffix = astUtil.getTemplateElementSuffix(txt, originalClassNamesValue);
originalClassNamesValue = astUtil.getTemplateElementBody(txt, prefix, suffix);
break;
}
}

let classNames = attrUtil.getClassNamesFromAttribute(originalClassNamesValue, trim);
const isSingleLine = attrUtil.isSingleLine(originalClassNamesValue);
let before = null;
let after = null;
let { classNames, whitespaces, headSpace, tailSpace } =
astUtil.extractClassnamesFromValue(originalClassNamesValue);

if (!isSingleLine) {
const spacesOnly = /^(\s)*$/;
if (spacesOnly.test(classNames[0])) {
before = classNames.shift();
}
if (spacesOnly.test(classNames[classNames.length - 1])) {
after = classNames.pop();
}
}

if (removeDuplicates) {
classNames = removeDuplicatesFromArray(classNames);
}
if (classNames.length <= 1) {
// Don't run sorting for a single or empty className
return;
}

// Sorting
const mergedSorted = [];
const mergedExtras = [];
if (groupByResponsive) {
const respGroups = getResponsiveGroups(classNames);
respGroups.forEach((clsGroup) => {
const { sorted, extras } = getSortedGroups(clsGroup);
let orderedClassNames;
let validatedClassNamesValue = '';

if (officialSorting) {
orderedClassNames = order(classNames, contextFallback);
for (let i = 0; i < orderedClassNames.length; i++) {
const w = whitespaces[i] ?? '';
const cls = orderedClassNames[i];
validatedClassNamesValue += headSpace ? `${w}${cls}` : `${cls}${w}`;
if (headSpace && tailSpace && i === orderedClassNames.length - 1) {
validatedClassNamesValue += whitespaces[whitespaces.length - 1] ?? '';
}
}
} else {
if (removeDuplicates) {
removeDuplicatesFromClassnamesAndWhitespaces(classNames, whitespaces, headSpace, tailSpace);
}

// Sorting
const mergedSorted = [];
const mergedExtras = [];
if (groupByResponsive) {
const respGroups = getResponsiveGroups(classNames);
respGroups.forEach((clsGroup) => {
const { sorted, extras } = getSortedGroups(clsGroup);
mergedSorted.push(...sorted);
mergedExtras.push(...extras);
});
} else {
const { sorted, extras } = getSortedGroups(classNames);
mergedSorted.push(...sorted);
mergedExtras.push(...extras);
});
} else {
const { sorted, extras } = getSortedGroups(classNames);
mergedSorted.push(...sorted);
mergedExtras.push(...extras);
}
}

// Generates the validated/sorted attribute value
const flatted = mergedSorted.flat();
const union = prependCustom ? [...mergedExtras, ...flatted] : [...flatted, ...mergedExtras];
if (before !== null) {
union.unshift(before);
}
if (after !== null) {
union.push(after);
// Generates the validated/sorted attribute value
const flatted = mergedSorted.flat();
const union = prependCustom ? [...mergedExtras, ...flatted] : [...flatted, ...mergedExtras];
for (let i = 0; i < union.length; i++) {
const w = whitespaces[i] ?? '';
const cls = union[i];
validatedClassNamesValue += headSpace ? `${w}${cls}` : `${cls}${w}`;
if (headSpace && tailSpace && i === union.length - 1) {
validatedClassNamesValue += whitespaces[whitespaces.length - 1] ?? '';
}
}
}
let validatedClassNamesValue = union.join(isSingleLine ? ' ' : '\n');
const originalPatched = isSingleLine ? originalClassNamesValue.trim() : originalClassNamesValue;
if (originalPatched !== validatedClassNamesValue) {
if (originalClassNamesValue !== validatedClassNamesValue) {
validatedClassNamesValue = prefix + validatedClassNamesValue + suffix;
context.report({
node: node,
Expand Down
16 changes: 7 additions & 9 deletions lib/rules/enforces-negative-arbitrary-values.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
const docsUrl = require('../util/docsUrl');
const customConfig = require('../util/customConfig');
const astUtil = require('../util/ast');
const attrUtil = require('../util/attr');
const groupUtil = require('../util/groupMethods');
const removeDuplicatesFromClassnamesAndWhitespaces = require('../util/removeDuplicatesFromClassnamesAndWhitespaces');
const getOption = require('../util/settings');
const parserUtil = require('../util/parser');

Expand Down Expand Up @@ -74,11 +74,12 @@ module.exports = {
*/
const parseForNegativeArbitraryClassNames = (node, arg = null) => {
let originalClassNamesValue = null;
let trim = false;
if (arg === null) {
originalClassNamesValue = astUtil.extractValueFromNode(node);
} else {
switch (arg.type) {
case 'Identifier':
return;
case 'TemplateLiteral':
arg.expressions.forEach((exp) => {
parseForNegativeArbitraryClassNames(node, exp);
Expand All @@ -105,21 +106,18 @@ module.exports = {
});
return;
case 'Literal':
trim = true;
originalClassNamesValue = arg.value;
break;
case 'TemplateElement':
originalClassNamesValue = arg.value.raw;
// https://github.com/eslint/eslint/issues/13360
// The problem is that range computation includes the backticks (`test`)
// but value.raw does not include them, so there is a mismatch.
// start/end does not include the backticks, therefore it matches value.raw.
const txt = context.getSourceCode().getText(arg);
if (originalClassNamesValue === '') {
return;
}
break;
}
}

let classNames = attrUtil.getClassNamesFromAttribute(originalClassNamesValue, trim);
let { classNames } = astUtil.extractClassnamesFromValue(originalClassNamesValue);

const detected = classNames.filter((cls) => {
const suffix = groupUtil.getSuffix(cls, mergedConfig.separator);
Expand Down
Loading