feat 🥁: initialize project with complete codebase

This commit is contained in:
Ante Budimir 2025-03-04 20:16:50 +02:00
commit d569dea1fb
35 changed files with 4413 additions and 0 deletions

View file

@ -0,0 +1,3 @@
import alphabeticalOrderRule from './rule-definition.js';
export default alphabeticalOrderRule;

View file

@ -0,0 +1,84 @@
import type { Rule } from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js';
import { getPropertyName } from '../shared-utils/property-separator.js';
/**
* Compares two CSS properties alphabetically.
* @param firstProperty The first property to compare.
* @param secondProperty The second property to compare.
* @returns A number indicating the relative order of the properties (-1, 0, or 1).
*/
const comparePropertiesAlphabetically = (
firstProperty: TSESTree.Property,
secondProperty: TSESTree.Property,
): number => {
const firstName = getPropertyName(firstProperty);
const secondName = getPropertyName(secondProperty);
return firstName.localeCompare(secondName);
};
/**
* Reports an ordering issue to ESLint and generates fixes.
* @param ruleContext The ESLint rule context.
* @param currentProperty The current property in the order.
* @param nextProperty The next property that is out of order.
* @param regularProperties The full list of regular properties to be reordered.
*/
const reportOrderingIssue = (
ruleContext: Rule.RuleContext,
currentProperty: TSESTree.Property,
nextProperty: TSESTree.Property,
regularProperties: TSESTree.Property[],
): void => {
ruleContext.report({
node: nextProperty as Rule.Node,
messageId: 'alphabeticalOrder',
data: {
nextProperty: getPropertyName(nextProperty),
currentProperty: getPropertyName(currentProperty),
},
fix: (fixer) =>
generateFixesForCSSOrder(
fixer,
ruleContext,
regularProperties,
comparePropertiesAlphabetically,
(property) => property as Rule.Node,
),
});
};
/**
* Enforces alphabetical ordering of CSS properties.
* @param ruleContext The ESLint rule context.
* @param regularProperties An array of regular CSS properties to be checked.
*
* This function does the following:
* 1. Checks if there are enough properties to compare (more than 1).
* 2. Creates pairs of consecutive properties for comparison.
* 3. Finds the first pair that violates alphabetical order.
* 4. If a violation is found, reports the issue and generates fixes.
*/
export const enforceAlphabeticalCSSOrder = (
ruleContext: Rule.RuleContext,
regularProperties: TSESTree.Property[],
): void => {
if (regularProperties.length <= 1) {
return;
}
// Create pairs of consecutive properties
const propertyPairs = regularProperties.slice(0, -1).map((currentProperty, index) => ({
currentProperty,
nextProperty: regularProperties[index + 1] as TSESTree.Property,
}));
const violatingPair = propertyPairs.find(
({ currentProperty, nextProperty }) => comparePropertiesAlphabetically(currentProperty, nextProperty) > 0,
);
if (violatingPair) {
reportOrderingIssue(ruleContext, violatingPair.currentProperty, violatingPair.nextProperty, regularProperties);
}
};

View file

@ -0,0 +1,28 @@
import type { Rule } from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
import { processRecipeProperties } from '../shared-utils/recipe-property-processor.js';
import { processStyleNode } from '../shared-utils/style-node-processor.js';
import { enforceAlphabeticalCSSOrderInStyleObject } from './style-object-processor.js';
/**
* Enforces alphabetical ordering of CSS properties within a recipe function call.
*
* @param node The CallExpression node representing the recipe function call.
* @param context The ESLint rule context.
*
* This function does the following:
* 1. Checks if the first argument of the recipe function is an ObjectExpression.
* 2. If valid, processes the recipe object's properties.
* 3. For each relevant property (e.g., 'base', 'variants'), it applies alphabetical ordering to the CSS properties.
*/
export const enforceAlphabeticalCSSOrderInRecipe = (node: TSESTree.CallExpression, context: Rule.RuleContext): void => {
if (!node.arguments[0] || node.arguments[0].type !== 'ObjectExpression') {
return;
}
const recipeObject = node.arguments[0];
processRecipeProperties(context, recipeObject, (context, object) =>
processStyleNode(context, object, enforceAlphabeticalCSSOrderInStyleObject),
);
};

View file

@ -0,0 +1,23 @@
import type { Rule } from 'eslint';
import { createNodeVisitors } from '../shared-utils/order-strategy-visitor-creator.js';
const alphabeticalOrderRule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce alphabetical CSS property ordering in vanilla-extract styles',
category: 'Stylistic Issues',
recommended: true,
},
fixable: 'code',
schema: [],
messages: {
alphabeticalOrder: "Property '{{next}}' should come before '{{current}}' in alphabetical order.",
},
},
create(context) {
return createNodeVisitors(context, 'alphabetical');
},
};
export default alphabeticalOrderRule;

View file

@ -0,0 +1,37 @@
import type { Rule } from 'eslint';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import { isSelectorsObject, processNestedSelectors } from '../shared-utils/nested-selectors-processor.js';
import { separateProperties } from '../shared-utils/property-separator.js';
import { enforceAlphabeticalCSSOrder } from './property-order-enforcer.js';
/**
* Processes a style object to enforce alphabetical ordering of CSS properties.
*
* This function handles different types of style objects:
* 1. If the object is invalid or not an ObjectExpression, it returns immediately.
* 2. For 'selectors' objects, it processes nested selectors recursively.
* 3. For regular style objects, it separates and enforces alphabetical order on properties.
* 4. It always processes nested objects recursively, regardless of type.
*
* @param ruleContext - The ESLint rule context for reporting and fixing issues.
* @param styleObject - The object expression representing the style object to be processed.
*/
export const enforceAlphabeticalCSSOrderInStyleObject = (
ruleContext: Rule.RuleContext,
styleObject: TSESTree.ObjectExpression,
): void => {
if (!styleObject || styleObject.type !== AST_NODE_TYPES.ObjectExpression) {
return;
}
if (isSelectorsObject(styleObject)) {
processNestedSelectors(ruleContext, styleObject, enforceAlphabeticalCSSOrderInStyleObject);
return;
}
const { regularProperties } = separateProperties(styleObject.properties);
enforceAlphabeticalCSSOrder(ruleContext, regularProperties);
processNestedSelectors(ruleContext, styleObject, enforceAlphabeticalCSSOrderInStyleObject);
};

View file

@ -0,0 +1,336 @@
// CSS property groups in order of importance (outside -> inside)
export const concentricGroups: { [key: string]: string[] } = {
boxSizing: ['all', 'box-sizing', 'aspect-ratio', 'contain'],
position: [
'position',
'z-index',
'top',
'right',
'bottom',
'left',
'offset',
'offset-path',
'offset-distance',
'offset-rotate',
'inset',
'inset-block',
'inset-block-start',
'inset-block-end',
'inset-inline',
'inset-inline-start',
'inset-inline-end',
],
display: ['display', 'float', 'clear', 'isolation', 'appearance'],
flex: ['flex', 'flex-basis', 'flex-direction', 'flex-flow', 'flex-grow', 'flex-shrink', 'flex-wrap'],
grid: [
'grid',
'grid-area',
'grid-template',
'grid-template-areas',
'grid-template-rows',
'grid-template-columns',
'grid-row',
'grid-row-start',
'grid-row-end',
'grid-column',
'grid-column-start',
'grid-column-end',
'grid-auto-rows',
'grid-auto-columns',
'grid-auto-flow',
'grid-gap',
'grid-row-gap',
'grid-column-gap',
],
alignment: [
'align-content',
'align-items',
'align-self',
'justify-content',
'justify-items',
'justify-self',
'place-content',
'place-items',
'place-self',
'order',
'gap',
'row-gap',
'column-gap',
],
columns: [
'columns',
'column-fill',
'column-rule',
'column-rule-width',
'column-rule-style',
'column-rule-color',
'column-span',
'column-count',
'column-width',
],
transform: [
'backface-visibility',
'perspective',
'perspective-origin',
'transform',
'transform-origin',
'transform-style',
'transform-box',
],
transitions: [
'transition',
'transition-delay',
'transition-duration',
'transition-property',
'transition-timing-function',
'transition-behavior',
],
visibility: [
'visibility',
'opacity',
'backdrop-filter',
'content-visibility',
'filter',
'mix-blend-mode',
'will-change',
],
shape: ['clip-path', 'shape-outside', 'shape-image-threshold', 'shape-margin'],
margin: [
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'margin-block',
'margin-block-start',
'margin-block-end',
'margin-inline',
'margin-inline-start',
'margin-inline-end',
'margin-trim',
],
outline: ['outline', 'outline-offset', 'outline-width', 'outline-style', 'outline-color'],
border: [
'border',
'border-top',
'border-right',
'border-bottom',
'border-left',
'border-width',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-style',
'border-top-style',
'border-right-style',
'border-bottom-style',
'border-left-style',
'border-radius',
'border-top-left-radius',
'border-top-right-radius',
'border-bottom-left-radius',
'border-bottom-right-radius',
'border-start-start-radius',
'border-start-end-radius',
'border-end-start-radius',
'border-end-end-radius',
'border-color',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
'border-image',
'border-image-source',
'border-image-width',
'border-image-outset',
'border-image-repeat',
'border-image-slice',
'border-block',
'border-block-start',
'border-block-end',
'border-block-width',
'border-block-style',
'border-block-color',
'border-inline',
'border-inline-start',
'border-inline-end',
'border-inline-width',
'border-inline-style',
'border-inline-color',
],
boxShadow: ['box-shadow'],
background: [
'background',
'background-attachment',
'background-clip',
'background-color',
'background-image',
'background-origin',
'background-position',
'background-repeat',
'background-size',
'background-blend-mode',
'object-fit',
'object-position',
'image-orientation',
'image-rendering',
],
cursor: ['cursor', 'pointer-events', 'touch-action'],
padding: [
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'padding-block',
'padding-block-start',
'padding-block-end',
'padding-inline',
'padding-inline-start',
'padding-inline-end',
],
dimensions: [
'width',
'min-width',
'max-width',
'height',
'min-height',
'max-height',
'block-size',
'min-block-size',
'max-block-size',
'inline-size',
'min-inline-size',
'max-inline-size',
],
overflow: [
'overflow',
'overflow-x',
'overflow-y',
'overflow-block',
'overflow-inline',
'overflow-clip-margin',
'overflow-anchor',
'overflow-wrap',
'overscroll-behavior',
'overscroll-behavior-x',
'overscroll-behavior-y',
'resize',
'scrollbar-width',
'scrollbar-color',
'scrollbar-gutter',
'scroll-behavior',
'scroll-margin',
'scroll-padding',
'scroll-snap-align',
'scroll-snap-stop',
'scroll-snap-type',
],
listStyle: ['list-style', 'list-style-type', 'list-style-position', 'list-style-image', 'caption-side'],
tables: ['table-layout', 'border-collapse', 'border-spacing', 'empty-cells'],
animation: [
'animation',
'animation-name',
'animation-duration',
'animation-timing-function',
'animation-delay',
'animation-iteration-count',
'animation-direction',
'animation-fill-mode',
'animation-play-state',
],
text: [
'vertical-align',
'direction',
'writing-mode',
'text-orientation',
'unicode-bidi',
'tab-size',
'text-align',
'text-align-last',
'text-justify',
'text-indent',
'text-transform',
'text-decoration',
'text-decoration-color',
'text-decoration-line',
'text-decoration-style',
'text-decoration-thickness',
'text-decoration-skip-ink',
'text-underline-position',
'text-rendering',
'text-shadow',
'text-overflow',
'text-wrap',
'text-size-adjust',
'text-combine-upright',
'hyphens',
'line-break',
'ruby-position',
'caret-color',
'user-select',
],
textSpacing: [
'line-height',
'word-spacing',
'letter-spacing',
'white-space',
'word-break',
'word-wrap',
'orphans',
'widows',
'color',
],
font: [
'font',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-weight',
'font-smoothing',
'osx-font-smoothing',
'font-variant',
'font-style',
'font-feature-settings',
'font-kerning',
'font-optical-sizing',
],
content: ['content', 'quotes'],
counters: ['counter-reset', 'counter-increment', 'counter-set'],
breaks: ['page-break-before', 'page-break-after', 'page-break-inside', 'break-before', 'break-after', 'break-inside'],
};
/**
* Export all available groups for use in custom groups rule
*/
export const availableGroups = Object.keys(concentricGroups);

View file

@ -0,0 +1,3 @@
import concentricOrderRule from './rule-definition.js';
export default concentricOrderRule;

View file

@ -0,0 +1,85 @@
import type { Rule } from 'eslint';
import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js';
import type { CSSPropertyInfo } from './types.js';
/**
* Reports a violation of the concentric CSS ordering rule and generates fixes.
*
* @param ruleContext The ESLint rule context used for reporting and fixing.
* @param currentProperty The current property in the order.
* @param nextProperty The next property that is out of order.
* @param cssPropertyInfoList The full list of CSS properties to be reordered.
*/
const reportOrderingIssue = (
ruleContext: Rule.RuleContext,
currentProperty: CSSPropertyInfo,
nextProperty: CSSPropertyInfo,
cssPropertyInfoList: CSSPropertyInfo[],
): void => {
ruleContext.report({
node: nextProperty.node as Rule.Node,
messageId: 'incorrectOrder',
data: {
next: nextProperty.name,
current: currentProperty.name,
},
fix: (fixer) =>
generateFixesForCSSOrder(
fixer,
ruleContext,
cssPropertyInfoList,
compareProperties,
(propertyInfo) => propertyInfo.node as Rule.Node,
),
});
};
/**
* Compares two CSS properties based on their priority and position within their group.
*
* @param firstProperty The first property to compare.
* @param secondProperty The second property to compare.
* @returns A number indicating the relative order of the properties (-1, 0, or 1).
*/
const compareProperties = (firstProperty: CSSPropertyInfo, secondProperty: CSSPropertyInfo): number => {
if (firstProperty.priority !== secondProperty.priority) {
return firstProperty.priority - secondProperty.priority;
}
return firstProperty.positionInGroup - secondProperty.positionInGroup;
};
/**
* Enforces concentric ordering of CSS properties.
*
* This function checks the order of CSS properties to ensure they follow a concentric order
* based on their priority and position within their group. It performs the following steps:
* 1. Validates if there are enough properties to compare.
* 2. Creates pairs of consecutive properties for comparison.
* 3. Identifies the first pair that violates the concentric order.
* 4. If a violation is detected, reports the issue and suggests fixes using ESLint.
*
* @param ruleContext - The ESLint rule context used for reporting and fixing.
* @param cssPropertyInfoList - An array of CSS property information objects to be checked.
*/
export const enforceConcentricCSSOrder = (
ruleContext: Rule.RuleContext,
cssPropertyInfoList: CSSPropertyInfo[],
): void => {
if (cssPropertyInfoList.length <= 1) {
return;
}
// Create pairs of consecutive properties
const propertyPairs = cssPropertyInfoList.slice(0, -1).map((currentProperty, index) => ({
currentProperty,
nextProperty: cssPropertyInfoList[index + 1] as CSSPropertyInfo,
}));
const violatingPair = propertyPairs.find(
({ currentProperty, nextProperty }) => compareProperties(currentProperty, nextProperty) > 0,
);
if (violatingPair) {
reportOrderingIssue(ruleContext, violatingPair.currentProperty, violatingPair.nextProperty, cssPropertyInfoList);
}
};

View file

@ -0,0 +1,31 @@
import type { Rule } from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
import { processRecipeProperties } from '../shared-utils/recipe-property-processor.js';
import { processStyleNode } from '../shared-utils/style-node-processor.js';
import { enforceConcentricCSSOrderInStyleObject } from './style-object-processor.js';
/**
* Enforces concentric ordering of CSS properties within a recipe function call.
*
* @param ruleContext The ESLint rule context for reporting and fixing issues.
* @param callExpression The CallExpression node representing the recipe function call.
*
* This function does the following:
* 1. Checks if the first argument of the recipe function is an ObjectExpression.
* 2. If valid, processes the recipe object's properties.
* 3. For each relevant property (e.g., 'base', 'variants'), it applies concentric ordering to the CSS properties.
*/
export const enforceConcentricCSSOrderInRecipe = (
ruleContext: Rule.RuleContext,
callExpression: TSESTree.CallExpression,
): void => {
if (!callExpression.arguments[0] || callExpression.arguments[0].type !== 'ObjectExpression') {
return;
}
const recipeObjectExpression = callExpression.arguments[0];
processRecipeProperties(ruleContext, recipeObjectExpression, (currentContext, styleObject) =>
processStyleNode(currentContext, styleObject, enforceConcentricCSSOrderInStyleObject),
);
};

View file

@ -0,0 +1,23 @@
import type { Rule } from 'eslint';
import { createNodeVisitors } from '../shared-utils/order-strategy-visitor-creator.js';
const concentricOrderRule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce concentric CSS property ordering in vanilla-extract styles',
category: 'Stylistic Issues',
recommended: true,
},
fixable: 'code',
schema: [],
messages: {
incorrectOrder: "Property '{{next}}' should come before '{{current}}' according to concentric CSS ordering.",
},
},
create(context) {
return createNodeVisitors(context, 'concentric');
},
};
export default concentricOrderRule;

View file

@ -0,0 +1,72 @@
import type { Rule } from 'eslint';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import { createCSSPropertyPriorityMap } from '../shared-utils/css-property-priority-map.js';
import { isSelectorsObject, processNestedSelectors } from '../shared-utils/nested-selectors-processor.js';
import { getPropertyName, separateProperties } from '../shared-utils/property-separator.js';
import { enforceConcentricCSSOrder } from './property-order-enforcer.js';
import type { CSSPropertyInfo } from './types.js';
const cssPropertyPriorityMap = createCSSPropertyPriorityMap();
/**
* Builds a list of CSS properties with their priority information in the concentric order of their groups.
*
* @param regularStyleProperties An array of regular style properties.
* @returns An array of CSSPropertyInfo objects containing name, node, priority, and position information.
*/
const buildCSSPropertyInfoList = (regularStyleProperties: TSESTree.Property[]): CSSPropertyInfo[] => {
return regularStyleProperties.map((styleProperty) => {
const propertyName = getPropertyName(styleProperty);
const propertyInfo = cssPropertyPriorityMap.get(propertyName);
return {
name: propertyName,
node: styleProperty,
priority: propertyInfo?.groupIndex ?? Number.MAX_SAFE_INTEGER,
positionInGroup: propertyInfo?.positionInGroup ?? Number.MAX_SAFE_INTEGER,
};
});
};
/**
* Enforces concentric ordering of CSS properties within a style object.
*
* This function processes the given style object to ensure that CSS properties
* follow a concentric order based on predefined priority groups. It handles
* different types of style objects by:
* 1. Validating that the input is an ObjectExpression.
* 2. Processing 'selectors' objects separately and recursively applying the
* concentric order enforcement.
* 3. Separating regular properties and building a list of CSSPropertyInfo
* objects with priority details.
* 4. Enforcing concentric order on the properties using their priority
* information.
* 5. Recursively processing nested selectors and style objects.
*
* @param ruleContext - The ESLint rule context for reporting and fixing issues.
* @param styleObject - The object expression representing the style object to be processed.
*/
export const enforceConcentricCSSOrderInStyleObject = (
ruleContext: Rule.RuleContext,
styleObject: TSESTree.ObjectExpression,
): void => {
if (!styleObject || styleObject.type !== AST_NODE_TYPES.ObjectExpression) {
return;
}
if (isSelectorsObject(styleObject)) {
styleObject.properties.forEach((property) => {
if (property.type === AST_NODE_TYPES.Property && property.value.type === AST_NODE_TYPES.ObjectExpression) {
enforceConcentricCSSOrderInStyleObject(ruleContext, property.value);
}
});
return;
}
const { regularProperties } = separateProperties(styleObject.properties);
const cssPropertyInfoList = buildCSSPropertyInfoList(regularProperties);
enforceConcentricCSSOrder(ruleContext, cssPropertyInfoList);
processNestedSelectors(ruleContext, styleObject, enforceConcentricCSSOrderInStyleObject);
};

View file

@ -0,0 +1,9 @@
import { TSESTree } from '@typescript-eslint/utils';
export interface CSSPropertyInfo {
name: string;
node: TSESTree.Property;
priority: number;
positionInGroup: number;
group?: string;
}

View file

@ -0,0 +1,91 @@
import type { Rule } from 'eslint';
import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js';
import { createCSSPropertyPriorityMap } from '../shared-utils/css-property-priority-map.js';
import type { CSSPropertyInfo } from '../concentric-order/types.js';
/**
* Enforces a custom ordering of CSS properties based on user-defined groups.
*
* @param ruleContext The ESLint rule context used for reporting and fixing.
* @param cssPropertyInfoList An array of CSS property information objects to be ordered.
* @param userDefinedGroups Array of user-defined property groups for custom ordering.
* @param sortRemainingProperties Strategy for sorting properties not in user-defined groups
* ('alphabetical' or 'concentric'). Defaults to 'concentric'.
*
* This function compares CSS properties based on their group priority and position within
* those groups. Properties not part of user-defined groups are sorted according to the
* specified strategy. If an ordering violation is detected, an ESLint report is generated
* with a suggested fix.
*/
export const enforceCustomGroupOrder = (
ruleContext: Rule.RuleContext,
cssPropertyInfoList: CSSPropertyInfo[],
userDefinedGroups: string[] = [],
sortRemainingProperties?: 'alphabetical' | 'concentric',
): void => {
if (cssPropertyInfoList.length <= 1) {
return;
}
const cssPropertyPriorityMap = createCSSPropertyPriorityMap(userDefinedGroups);
const compareProperties = (firstProperty: CSSPropertyInfo, secondProperty: CSSPropertyInfo) => {
const firstPropertyInfo = cssPropertyPriorityMap.get(firstProperty.name) || {
groupIndex: Infinity,
positionInGroup: Infinity,
inUserGroup: false,
};
const secondPropertyInfo = cssPropertyPriorityMap.get(secondProperty.name) || {
groupIndex: Infinity,
positionInGroup: Infinity,
inUserGroup: false,
};
if (firstPropertyInfo.inUserGroup !== secondPropertyInfo.inUserGroup) {
return firstPropertyInfo.inUserGroup ? -1 : 1;
}
if (firstPropertyInfo.inUserGroup) {
if (firstPropertyInfo.groupIndex !== secondPropertyInfo.groupIndex) {
return firstPropertyInfo.groupIndex - secondPropertyInfo.groupIndex;
}
return firstPropertyInfo.positionInGroup - secondPropertyInfo.positionInGroup;
}
// For properties not in user-defined groups
if (sortRemainingProperties === 'alphabetical') {
return firstProperty.name.localeCompare(secondProperty.name);
} else {
return (
firstPropertyInfo.groupIndex - secondPropertyInfo.groupIndex ||
firstPropertyInfo.positionInGroup - secondPropertyInfo.positionInGroup
);
}
};
const sortedPropertyList = [...cssPropertyInfoList].sort(compareProperties);
// Find the first pair that violates the new ordering
const violatingProperty = cssPropertyInfoList
.slice(0, -1)
.find((currentProperty, index) => currentProperty.name !== sortedPropertyList[index]?.name);
if (violatingProperty) {
ruleContext.report({
node: violatingProperty.node as Rule.Node,
messageId: 'incorrectOrder',
data: {
currentProperty: violatingProperty.name,
nextProperty: sortedPropertyList[cssPropertyInfoList.indexOf(violatingProperty)]?.name || '',
},
fix: (fixer) =>
generateFixesForCSSOrder(
fixer,
ruleContext,
cssPropertyInfoList,
compareProperties,
(propertyInfo) => propertyInfo.node as Rule.Node,
),
});
}
};

View file

@ -0,0 +1,42 @@
import type { Rule } from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
import { processRecipeProperties } from '../shared-utils/recipe-property-processor.js';
import { processStyleNode } from '../shared-utils/style-node-processor.js';
import { enforceUserDefinedGroupOrderInStyleObject } from './style-object-processor.js';
/**
* Enforces custom group ordering of CSS properties within a recipe function call.
*
* @param ruleContext The ESLint rule context for reporting and fixing issues.
* @param callExpression The CallExpression node representing the recipe function call.
* @param userDefinedGroups An array of property groups in the desired order.
*
* This function does the following:
* 1. Validates that the first argument of the recipe function is an ObjectExpression.
* 2. Processes the recipe object's properties if valid.
* 3. Applies custom group ordering to CSS properties in relevant properties (e.g., 'base', 'variants').
* 4. Processes nested selectors and style objects recursively.
*/
export const enforceUserDefinedGroupOrderInRecipe = (
ruleContext: Rule.RuleContext,
callExpression: TSESTree.CallExpression,
userDefinedGroups: string[],
sortRemainingPropertiesMethod?: 'alphabetical' | 'concentric',
): void => {
if (!callExpression.arguments[0] || callExpression.arguments[0].type !== 'ObjectExpression') {
return;
}
const recipeObjectExpression = callExpression.arguments[0];
processRecipeProperties(ruleContext, recipeObjectExpression, (currentContext, styleObject) =>
processStyleNode(currentContext, styleObject, (styleContext, styleObjectNode) =>
enforceUserDefinedGroupOrderInStyleObject(
styleContext,
styleObjectNode,
userDefinedGroups,
sortRemainingPropertiesMethod,
),
),
);
};

View file

@ -0,0 +1,55 @@
import type { Rule } from 'eslint';
import { availableGroups } from '../concentric-order/concentric-groups.js';
import { createNodeVisitors } from '../shared-utils/order-strategy-visitor-creator.js';
interface CustomGroupRuleConfiguration {
groupOrder?: string[];
sortRemainingProperties: 'alphabetical' | 'concentric';
}
const customGroupOrderRule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce custom group CSS property ordering in vanilla-extract styles',
category: 'Stylistic Issues',
recommended: true,
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
groupOrder: {
type: 'array',
items: {
enum: availableGroups,
},
},
sortRemainingProperties: {
enum: ['alphabetical', 'concentric'],
},
},
additionalProperties: false,
},
],
messages: {
incorrectOrder:
"Property '{{nextProperty}}' should come before '{{currentProperty}}' according to custom CSS group ordering.",
},
},
create(ruleContext: Rule.RuleContext) {
const ruleConfiguration = ruleContext.options[0] as CustomGroupRuleConfiguration;
const userDefinedGroupOrder = ruleConfiguration?.groupOrder ?? [];
const sortRemainingPropertiesMethod = ruleConfiguration?.sortRemainingProperties ?? 'alphabetical';
return createNodeVisitors(
ruleContext,
'userDefinedGroupOrder',
userDefinedGroupOrder,
sortRemainingPropertiesMethod,
);
},
};
export default customGroupOrderRule;

View file

@ -0,0 +1,79 @@
import type { Rule } from 'eslint';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import { concentricGroups } from '../concentric-order/concentric-groups.js';
import { createCSSPropertyPriorityMap } from '../shared-utils/css-property-priority-map.js';
import { isSelectorsObject, processNestedSelectors } from '../shared-utils/nested-selectors-processor.js';
import { getPropertyName, separateProperties } from '../shared-utils/property-separator.js';
import { enforceCustomGroupOrder } from './property-order-enforcer.js';
import type { CSSPropertyInfo } from '../concentric-order/types.js';
/**
* Enforces a custom ordering of CSS properties based on user-defined groups in a given style object.
*
* @param context The ESLint rule context for reporting and fixing issues.
* @param styleObject The ObjectExpression node representing the style object to be processed.
* @param userDefinedGroups An array of property groups in the desired order.
* @param sortRemainingPropertiesMethod Strategy for sorting properties not in user-defined groups ('alphabetical' or 'concentric'). Defaults to 'concentric'.
*
* This function:
* 1. Validates the input styleObject.
* 2. Handles 'selectors' objects separately, processing their nested style objects.
* 3. Creates a priority map based on user-defined groups.
* 4. Processes regular properties, creating a list of CSSPropertyInfo objects.
* 5. Enforces custom group ordering on the properties.
* 6. Recursively processes nested selectors and style objects.
*/
export const enforceUserDefinedGroupOrderInStyleObject = (
ruleContext: Rule.RuleContext,
styleObject: TSESTree.ObjectExpression,
userDefinedGroups: string[],
sortRemainingPropertiesMethod: 'alphabetical' | 'concentric' = 'concentric',
): void => {
if (!styleObject || styleObject.type !== AST_NODE_TYPES.ObjectExpression) {
return;
}
if (isSelectorsObject(styleObject)) {
styleObject.properties.forEach((property) => {
if (property.type === AST_NODE_TYPES.Property && property.value.type === AST_NODE_TYPES.ObjectExpression) {
enforceUserDefinedGroupOrderInStyleObject(
ruleContext,
property.value,
userDefinedGroups,
sortRemainingPropertiesMethod,
);
}
});
return;
}
const cssPropertyPriorityMap = createCSSPropertyPriorityMap(userDefinedGroups);
const { regularProperties } = separateProperties(styleObject.properties);
const cssPropertyInfoList: CSSPropertyInfo[] = regularProperties.map((property) => {
const propertyName = getPropertyName(property);
const propertyInfo = cssPropertyPriorityMap.get(propertyName);
const group =
userDefinedGroups.find((groupName) => concentricGroups[groupName]?.includes(propertyName)) || 'remaining';
return {
name: propertyName,
node: property,
priority: propertyInfo?.groupIndex ?? Number.MAX_SAFE_INTEGER,
positionInGroup: propertyInfo?.positionInGroup ?? Number.MAX_SAFE_INTEGER,
group,
inUserGroup: propertyInfo?.inUserGroup ?? false,
};
});
enforceCustomGroupOrder(ruleContext, cssPropertyInfoList, userDefinedGroups, sortRemainingPropertiesMethod);
processNestedSelectors(ruleContext, styleObject, (nestedContext, nestedNode) =>
enforceUserDefinedGroupOrderInStyleObject(
nestedContext,
nestedNode,
userDefinedGroups,
sortRemainingPropertiesMethod,
),
);
};

View file

@ -0,0 +1,55 @@
import type { Rule, SourceCode } from 'eslint';
/**
* Generates ESLint fixes for CSS property ordering violations.
* @param eslintFixer The ESLint fixer instance used to create fix objects.
* @param ruleContext The ESLint rule context, providing access to the source code.
* @param cssProperties The list of CSS properties to sort (can be TSESTree.Property[] or CSSPropertyInfo[]).
* @param compareProperties A comparison function that defines the sorting logic for the properties.
* @param extractNode A function that extracts the AST node from each property (used for text replacement).
* @returns An array of ESLint Fix objects to correct the property order.
*
* This function performs the following steps:
* 1. Sorts the input properties using the provided comparison function.
* 2. Maps the original and sorted properties to their text ranges.
* 3. Creates fix objects for properties whose positions have changed after sorting.
* 4. Returns an array of fixes that, when applied, will reorder the properties correctly.
*/
export const generateFixesForCSSOrder = <T>(
eslintFixer: Rule.RuleFixer,
ruleContext: Rule.RuleContext,
cssProperties: T[],
compareProperties: (firstProperty: T, secondProperty: T) => number,
extractNode: (property: T) => Rule.Node,
): Rule.Fix[] => {
const sourceCode: SourceCode = ruleContext.sourceCode;
// Sort properties using the provided comparison function
const sortedProperties = [...cssProperties].sort(compareProperties);
// Map each original property to its text range
const originalPropertyRanges = cssProperties.map((property) => ({
property,
range: extractNode(property).range,
}));
// Map sorted properties back to their original range information
const sortedPropertyRanges = sortedProperties.map((property) =>
originalPropertyRanges.find((rangeInfo) => rangeInfo.property === property),
);
// Generate fixes for properties that have changed position
return originalPropertyRanges
.map((originalRangeInfo, index) => {
const sortedRangeInfo = sortedPropertyRanges[index];
// Create a fix only if the property's position has changed
if (originalRangeInfo && sortedRangeInfo && originalRangeInfo !== sortedRangeInfo) {
const sortedPropertyText = sourceCode.getText(extractNode(sortedRangeInfo.property));
return eslintFixer.replaceText(extractNode(originalRangeInfo.property), sortedPropertyText);
}
return null;
})
.filter((fix): fix is Rule.Fix => fix !== null);
};

View file

@ -0,0 +1,60 @@
import { concentricGroups } from '../concentric-order/concentric-groups.js';
/**
* Creates a map of CSS properties to their priority information.
*
* This function generates a Map where each key is a CSS property (in camelCase),
* and each value is an object containing:
* - groupIndex: The index of the property's group
* - positionInGroup: The position of the property within its group
* - inUserGroup: Whether the property is in a user-specified group
*
* The function prioritizes user-specified groups over default concentric groups.
* If user groups are provided, they are processed first. Any remaining concentric
* groups are then processed to ensure complete coverage of CSS properties.
*
* @param userGroups - An optional array of user-specified group names to prioritize
* @returns A Map of CSS properties to their priority information
*
* @example
* const priorityMap = createCSSPropertyPriorityMap(['layout', 'typography']);
* console.log(priorityMap.get('display')); // { groupIndex: 0, positionInGroup: 0, inUserGroup: true }
*/
export const createCSSPropertyPriorityMap = (userGroups: string[] = []) => {
const cssPropertyPriorityMap = new Map<
string,
{
groupIndex: number;
positionInGroup: number;
inUserGroup: boolean;
}
>();
const processGroup = (groupName: string, groupIndex: number, inUserGroup: boolean) => {
const properties = concentricGroups[groupName] || [];
properties.forEach((property, positionInGroup) => {
const camelCaseProperty = property.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
if (!cssPropertyPriorityMap.has(camelCaseProperty)) {
cssPropertyPriorityMap.set(camelCaseProperty, {
groupIndex,
positionInGroup,
inUserGroup,
});
}
});
};
// Process user-specified groups first
userGroups.forEach((groupName, index) => processGroup(groupName, index, true));
// Process remaining groups if needed (for concentric order or as fallback)
if (userGroups.length === 0 || userGroups.length < Object.keys(concentricGroups).length) {
Object.keys(concentricGroups).forEach((groupName, index) => {
if (!userGroups.includes(groupName)) {
processGroup(groupName, userGroups.length + index, false);
}
});
}
return cssPropertyPriorityMap;
};

View file

@ -0,0 +1,44 @@
import type { Rule } from 'eslint';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
/**
* Determines if the given object node is a 'selectors' object within a style definition.
* @param objectNode The object expression node to check.
* @returns True if the node is a 'selectors' object, false otherwise.
*
* This function checks if:
* 1. The node has a parent
* 2. The parent is a Property node
* 3. The parent's key is an Identifier
* 4. The parent's key name is 'selectors'
*/
export const isSelectorsObject = (objectNode: TSESTree.ObjectExpression): boolean => {
return (
objectNode.parent &&
objectNode.parent.type === AST_NODE_TYPES.Property &&
objectNode.parent.key.type === 'Identifier' &&
objectNode.parent.key.name === 'selectors'
);
};
/**
* Processes nested selectors within a 'selectors' object by recursively validating their value objects.
* @param context The ESLint rule context.
* @param objectNode The object expression node representing the 'selectors' object.
* @param validateFn A function to validate each nested selector's value object.
*
* This function iterates through each property of the 'selectors' object:
* - If a property's value is an ObjectExpression, it applies the validateFn to that object.
* - This allows for validation of nested style objects within selectors.
*/
export const processNestedSelectors = (
context: Rule.RuleContext,
objectNode: TSESTree.ObjectExpression,
validateFn: (context: Rule.RuleContext, objectNode: TSESTree.ObjectExpression) => void,
): void => {
objectNode.properties.forEach((property) => {
if (property.type === AST_NODE_TYPES.Property && property.value.type === AST_NODE_TYPES.ObjectExpression) {
validateFn(context, property.value);
}
});
};

View file

@ -0,0 +1,98 @@
import type { Rule } from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
import { enforceAlphabeticalCSSOrderInRecipe } from '../alphabetical-order/recipe-order-enforcer.js';
import { enforceAlphabeticalCSSOrderInStyleObject } from '../alphabetical-order/style-object-processor.js';
import { enforceConcentricCSSOrderInRecipe } from '../concentric-order/recipe-order-enforcer.js';
import { enforceConcentricCSSOrderInStyleObject } from '../concentric-order/style-object-processor.js';
import { enforceUserDefinedGroupOrderInRecipe } from '../custom-order/recipe-order-enforcer.js';
import { enforceUserDefinedGroupOrderInStyleObject } from '../custom-order/style-object-processor.js';
import { processStyleNode } from './style-node-processor.js';
/**
* Creates an ESLint rule listener with visitors for style-related function calls.
* @param ruleContext The ESLint rule context.
* @param orderingStrategy The strategy to use for ordering CSS properties ('alphabetical', 'concentric', or 'userDefinedGroupOrder').
* @param userDefinedGroupOrder An optional array of property groups for the 'userDefinedGroupOrder' strategy.
* @param sortRemainingProperties An optional strategy for sorting properties not in user-defined groups.
* @returns An object with visitor functions for the ESLint rule.
*
* This function sets up visitors for the following cases:
* 1. Style-related functions: 'style', 'styleVariants', 'createVar', 'createTheme', 'createThemeContract'
* 2. The 'globalStyle' function
* 3. The 'recipe' function
*
* Each visitor applies the appropriate ordering strategy to the style objects in these function calls.
*/
export const createNodeVisitors = (
ruleContext: Rule.RuleContext,
orderingStrategy: 'alphabetical' | 'concentric' | 'userDefinedGroupOrder',
userDefinedGroupOrder?: string[],
sortRemainingProperties?: 'alphabetical' | 'concentric',
): Rule.RuleListener => {
// Select the appropriate property processing function based on the ordering strategy
const processProperty = (() => {
switch (orderingStrategy) {
case 'alphabetical':
return enforceAlphabeticalCSSOrderInStyleObject;
case 'concentric':
return enforceConcentricCSSOrderInStyleObject;
case 'userDefinedGroupOrder':
if (!userDefinedGroupOrder || userDefinedGroupOrder.length === 0) {
throw new Error('💥 👿 User-defined group order must be provided for userDefinedGroupOrder strategy');
}
return (ruleContext: Rule.RuleContext, node: TSESTree.Node) =>
enforceUserDefinedGroupOrderInStyleObject(
ruleContext,
node as TSESTree.ObjectExpression,
userDefinedGroupOrder,
sortRemainingProperties,
);
default:
return enforceAlphabeticalCSSOrderInStyleObject;
}
})();
return {
CallExpression(node) {
if (node.callee.type !== 'Identifier') {
return;
}
// Handle style-related functions
if (['style', 'styleVariants', 'createVar', 'createTheme', 'createThemeContract'].includes(node.callee.name)) {
if (node.arguments.length > 0) {
const styleArg = node.arguments[0];
processStyleNode(ruleContext, styleArg as TSESTree.Node, processProperty);
}
}
// Handle globalStyle function
if (node.callee.name === 'globalStyle' && node.arguments.length >= 2) {
const styleArg = node.arguments[1];
processStyleNode(ruleContext, styleArg as TSESTree.Node, processProperty);
}
// Handle recipe function
if (node.callee.name === 'recipe') {
switch (orderingStrategy) {
case 'alphabetical':
enforceAlphabeticalCSSOrderInRecipe(node as TSESTree.CallExpression, ruleContext);
break;
case 'concentric':
enforceConcentricCSSOrderInRecipe(ruleContext, node as TSESTree.CallExpression);
break;
case 'userDefinedGroupOrder':
if (userDefinedGroupOrder) {
enforceUserDefinedGroupOrderInRecipe(
ruleContext,
node as TSESTree.CallExpression,
userDefinedGroupOrder,
sortRemainingProperties,
);
}
break;
}
}
},
};
};

View file

@ -0,0 +1,57 @@
import { TSESTree } from '@typescript-eslint/utils';
/**
* Extracts the name of a property from a TSESTree.Property node.
* @param property The property node to extract the name from.
* @returns The name of the property as a string, or an empty string if the name cannot be determined.
*
* This function handles two types of property keys:
* - Identifier: Returns the name directly.
* - Literal (string): Returns the string value.
* For any other type of key, it returns an empty string.
*/
export const getPropertyName = (property: TSESTree.Property): string => {
if (property.key.type === 'Identifier') {
return property.key.name;
} else if (property.key.type === 'Literal' && typeof property.key.value === 'string') {
return property.key.value;
}
return '';
};
/**
* Separates object properties into regular and special categories.
* @param properties An array of object literal elements to be categorized.
* @returns An object containing two arrays: regularProperties and specialProperties.
*
* This function categorizes properties as follows:
* - Regular properties: Standard CSS properties.
* - Special properties: Properties that start with ':' (pseudo-selectors),
* '@' (at-rules), or are named 'selectors'.
*
* Non-Property type elements in the input array are ignored.
*/
export const separateProperties = (
properties: TSESTree.ObjectLiteralElement[],
): {
regularProperties: TSESTree.Property[];
specialProperties: TSESTree.Property[];
} => {
const regularProperties: TSESTree.Property[] = [];
const specialProperties: TSESTree.Property[] = [];
// Separate regular CSS properties from special ones (pseudo selectors, etc.)
properties.forEach((property) => {
if (property.type === 'Property') {
const propName = getPropertyName(property);
if (propName.startsWith(':') || propName.startsWith('@') || propName === 'selectors') {
specialProperties.push(property);
} else {
regularProperties.push(property);
}
}
});
return { regularProperties, specialProperties };
};

View file

@ -0,0 +1,45 @@
import type { Rule } from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
/**
* Processes the `base` and `variants` properties of a recipe object.
* @param ruleContext The ESLint rule context.
* @param recipeNode The recipe object node to process.
* @param processProperty A callback function to process each property object (e.g., for alphabetical or concentric ordering).
*
* This function iterates through the properties of the recipe object:
* - For the `base` property, it directly processes the object.
* - For the `variants` property, it processes each variant's options individually.
*
* The function skips any non-Property nodes or nodes without an Identifier key.
* It only processes ObjectExpression values to ensure type safety.
*/
export const processRecipeProperties = (
ruleContext: Rule.RuleContext,
recipeNode: TSESTree.ObjectExpression,
processProperty: (ruleContext: Rule.RuleContext, value: TSESTree.ObjectExpression) => void,
): void => {
recipeNode.properties.forEach((property: TSESTree.Property | TSESTree.SpreadElement) => {
if (property.type !== 'Property' || property.key.type !== 'Identifier') {
return; // Skip non-property nodes or nodes without an identifier key
}
// Process the `base` property
if (property.key.name === 'base' && property.value.type === 'ObjectExpression') {
processProperty(ruleContext, property.value);
}
// Process the `variants` property
if (property.key.name === 'variants' && property.value.type === 'ObjectExpression') {
property.value.properties.forEach((variantProperty) => {
if (variantProperty.type === 'Property' && variantProperty.value.type === 'ObjectExpression') {
variantProperty.value.properties.forEach((optionProperty) => {
if (optionProperty.type === 'Property' && optionProperty.value.type === 'ObjectExpression') {
processProperty(ruleContext, optionProperty.value);
}
});
}
});
}
});
};

View file

@ -0,0 +1,30 @@
import type { Rule } from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
/**
* Recursively processes a style node, which can be an object or an array of objects.
* @param ruleContext The ESLint rule context.
* @param node The node to process.
* @param processProperty A function to process each object expression.
*/
export const processStyleNode = (
ruleContext: Rule.RuleContext,
node: TSESTree.Node | undefined,
processProperty: (ruleContext: Rule.RuleContext, value: TSESTree.ObjectExpression) => void,
): void => {
if (!node) {
return;
}
if (node.type === 'ObjectExpression') {
processProperty(ruleContext, node);
}
if (node.type === 'ArrayExpression') {
node.elements.forEach((element) => {
if (element && element.type === 'ObjectExpression') {
processProperty(ruleContext, element);
}
});
}
};

39
src/index.ts Normal file
View file

@ -0,0 +1,39 @@
import alphabeticalOrderRule from './css-rules/alphabetical-order/index.js';
import concentricOrderRule from './css-rules/concentric-order/index.js';
import customOrderRule from './css-rules/custom-order/rule-definition.js';
export const vanillaExtract = {
meta: {
name: '@antebudimir/eslint-plugin-vanilla-extract',
version: '1.0.0',
},
rules: {
'alphabetical-order': alphabeticalOrderRule,
'concentric-order': concentricOrderRule,
'custom-order': customOrderRule,
},
configs: {
recommended: [
{
plugins: {
'vanilla-extract': {
rules: { 'concentric-order': concentricOrderRule },
},
},
rules: { 'vanilla-extract/concentric-order': 'warn' },
},
],
alphabetical: [
{
plugins: {
'vanilla-extract': {
rules: { 'alphabetical-order': alphabeticalOrderRule },
},
},
rules: { 'vanilla-extract/alphabetical-order': 'warn' },
},
],
},
};
export default vanillaExtract;