eslint-plugin-better-tailwindcss
Version:
auto-wraps tailwind classes after a certain print width or class count into multiple lines to improve readability.
262 lines • 10.9 kB
JavaScript
import { description, literal, optional, pipe, strictObject, union } from "valibot";
import { createGetClassOrder, getClassOrder } from "../tailwindcss/class-order.js";
import { createGetCustomComponentClasses, getCustomComponentClasses } from "../tailwindcss/custom-component-classes.js";
import { createGetDissectedClasses, getDissectedClasses } from "../tailwindcss/dissect-classes.js";
import { async } from "../utils/context.js";
import { escapeNestedQuotes } from "../utils/quotes.js";
import { createRule } from "../utils/rule.js";
import { display, splitClasses, splitWhitespaces } from "../utils/utils.js";
export const enforceConsistentClassOrder = createRule({
autofix: true,
category: "stylistic",
description: "Enforce a consistent order for tailwind classes.",
docs: "https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-consistent-class-order.md",
name: "enforce-consistent-class-order",
recommended: true,
messages: {
order: "Incorrect class order. Expected\n\n{{ notSorted }}\n\nto be\n\n{{ sorted }}"
},
schema: strictObject({
componentClassOrder: optional(pipe(union([
literal("asc"),
literal("desc"),
literal("preserve")
]), description("Defines how component classes should be ordered among themselves.")), "preserve"),
componentClassPosition: optional(pipe(union([
literal("start"),
literal("end")
]), description("Defines where component classes should be placed in relation to the whole string literal.")), "start"),
order: optional(pipe(union([
literal("asc"),
literal("desc"),
literal("official"),
literal("strict")
]), description("The algorithm to use when sorting classes.")), "official"),
unknownClassOrder: optional(pipe(union([
literal("asc"),
literal("desc"),
literal("preserve")
]), description("Defines how component classes should be ordered among themselves.")), "preserve"),
unknownClassPosition: optional(pipe(union([
literal("start"),
literal("end")
]), description("Defines where component classes should be placed in relation to the whole string literal.")), "start")
}),
initialize: ctx => {
const { detectComponentClasses } = ctx.options;
createGetClassOrder(ctx);
createGetDissectedClasses(ctx);
if (detectComponentClasses) {
createGetCustomComponentClasses(ctx);
}
},
lintLiterals: (ctx, literals) => {
const { messageStyle } = ctx.options;
for (const literal of literals) {
const classChunks = splitClasses(literal.content);
if (classChunks.length <= 1) {
continue;
}
const whitespaceChunks = splitWhitespaces(literal.content);
const unsortableClasses = ["", ""];
// remove sticky classes
if (literal.closingBraces && whitespaceChunks[0] === "") {
whitespaceChunks.shift();
unsortableClasses[0] = classChunks.shift() ?? "";
}
if (literal.openingBraces && whitespaceChunks[whitespaceChunks.length - 1] === "") {
whitespaceChunks.pop();
unsortableClasses[1] = classChunks.pop() ?? "";
}
const [sortedClassChunks, warnings] = sortClassNames(ctx, classChunks);
const classes = [];
for (let i = 0; i < Math.max(sortedClassChunks.length, whitespaceChunks.length); i++) {
whitespaceChunks[i] && classes.push(whitespaceChunks[i]);
sortedClassChunks[i] && classes.push(sortedClassChunks[i]);
}
const escapedClasses = escapeNestedQuotes([
unsortableClasses[0],
...classes,
unsortableClasses[1]
].join(""), literal.openingQuote ?? literal.closingQuote ?? "`");
const fixedClasses = [
literal.openingQuote ?? "",
literal.isInterpolated && literal.closingBraces ? literal.closingBraces : "",
escapedClasses,
literal.isInterpolated && literal.openingBraces ? literal.openingBraces : "",
literal.closingQuote ?? ""
].join("");
if (literal.raw === fixedClasses) {
continue;
}
ctx.report({
data: {
notSorted: display(messageStyle, literal.raw),
sorted: display(messageStyle, fixedClasses)
},
fix: fixedClasses,
id: "order",
range: literal.range,
warnings
});
}
}
});
function sortClassNames(ctx, classes) {
var _a;
const { componentClassOrder, componentClassPosition, order, unknownClassOrder, unknownClassPosition } = ctx.options;
if (order === "asc") {
return [classes.toSorted((a, b) => compareClasses(a, b))];
}
if (order === "desc") {
return [classes.toSorted((a, b) => compareClasses(b, a))];
}
if (classes.length <= 1) {
return [classes];
}
const { classOrder, warnings } = getClassOrder(async(ctx), classes);
const { detectComponentClasses } = ctx.options;
const customComponentClasses = detectComponentClasses
? getCustomComponentClasses(async(ctx)).customComponentClasses
: [];
const officiallySortedClasses = classOrder
.toSorted((a, b) => {
const [classA, aIndex] = a;
const [classB, bIndex] = b;
const componentClassSorting = getCustomOrder(componentClassPosition, componentClassOrder, classA, classB, className => customComponentClasses.includes(className));
if (componentClassSorting !== undefined) {
return componentClassSorting;
}
const unknownClassSorting = getCustomOrder(unknownClassPosition, unknownClassOrder, classA, classB, className => {
return ((classA === className && aIndex === null || classB === className && bIndex === null) &&
!customComponentClasses.includes(className));
});
if (unknownClassSorting !== undefined) {
return unknownClassSorting;
}
if (aIndex === bIndex) {
return 0;
}
if (aIndex === null) {
return -1;
}
if (bIndex === null) {
return +1;
}
return +(aIndex - bIndex > 0n) - +(aIndex - bIndex < 0n);
})
.map(([className]) => className);
if (order === "official") {
return [officiallySortedClasses, warnings];
}
const { dissectedClasses } = getDissectedClasses(async(ctx), classes);
const variantMap = {};
for (const originalClass in dissectedClasses) {
const dissectedClass = dissectedClasses[originalClass];
// parse variants manually for custom component classes
const variants = dissectedClass.variants ?? originalClass.match(/^(.*):/)?.[1]?.split(":") ?? [];
variants.unshift("");
for (let v = 0, variantMapLevel = variantMap; v < variants.length; v++) {
const isLastVariant = v === variants.length - 1;
variantMapLevel[_a = variants[v]] ?? (variantMapLevel[_a] = {
dissectedClasses: [],
nested: {}
});
if (isLastVariant) {
variantMapLevel[variants[v]].dissectedClasses.push(dissectedClass);
continue;
}
variantMapLevel = variantMapLevel[variants[v]].nested;
}
}
const strictOrder = getStrictOrder(variantMap);
return [strictOrder, warnings];
}
function getStrictOrder(variantMap) {
const orderedClasses = [];
const orderedVariants = Object.keys(variantMap).sort((a, b) => {
const aIsArbitrary = isArbitrary(a);
const bIsArbitrary = isArbitrary(b);
// sort arbitrary variants last
if (aIsArbitrary && !bIsArbitrary) {
return +1;
}
if (!aIsArbitrary && bIsArbitrary) {
return -1;
}
return 0;
});
for (let v = 0; v < orderedVariants.length; v++) {
const variant = orderedVariants[v];
const nextVariant = orderedVariants[v + 1];
const variantIsArbitrary = isArbitrary(variant);
const nextVariantIsArbitrary = isArbitrary(nextVariant);
const { dissectedClasses, nested } = variantMap[variant];
orderedClasses.push(...dissectedClasses.map(dissectedClass => dissectedClass.className));
if (dissectedClasses.length > 0 || !variantIsArbitrary && nextVariantIsArbitrary) {
orderedClasses.push(...getStrictOrder(nested));
}
}
for (let v = 0; v < orderedVariants.length; v++) {
const variant = orderedVariants[v];
const nextVariant = orderedVariants[v + 1];
const variantIsArbitrary = isArbitrary(variant);
const nextVariantIsArbitrary = isArbitrary(nextVariant);
const { dissectedClasses, nested } = variantMap[variant];
if (!(dissectedClasses.length > 0 || !variantIsArbitrary && nextVariantIsArbitrary)) {
orderedClasses.push(...getStrictOrder(nested));
}
}
return orderedClasses;
}
function getCustomOrder(position, order, classA, classB, isCustomClass) {
const aIsCustomClass = isCustomClass(classA);
const bIsCustomClass = isCustomClass(classB);
if (position === "start") {
if (aIsCustomClass && !bIsCustomClass) {
return -1;
}
if (!aIsCustomClass && bIsCustomClass) {
return +1;
}
if (aIsCustomClass && bIsCustomClass) {
if (order === "asc") {
return compareClasses(classA, classB);
}
if (order === "desc") {
return compareClasses(classB, classA);
}
return 0;
}
}
if (position === "end") {
if (aIsCustomClass && !bIsCustomClass) {
return +1;
}
if (!aIsCustomClass && bIsCustomClass) {
return -1;
}
if (aIsCustomClass && bIsCustomClass) {
if (order === "asc") {
return compareClasses(classA, classB);
}
if (order === "desc") {
return compareClasses(classB, classA);
}
return 0;
}
}
}
function compareClasses(classA, classB) {
if (classA === classB) {
return 0;
}
return classA < classB ? -1 : +1;
}
function isArbitrary(variant) {
if (!variant) {
return false;
}
return variant.includes("[") && variant.includes("]");
}
//# sourceMappingURL=enforce-consistent-class-order.js.map