mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2026-01-01 01:13:32 +00:00
feat 🥁: initialize project with complete codebase
This commit is contained in:
commit
d569dea1fb
35 changed files with 4413 additions and 0 deletions
3
src/css-rules/alphabetical-order/index.ts
Normal file
3
src/css-rules/alphabetical-order/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import alphabeticalOrderRule from './rule-definition.js';
|
||||
|
||||
export default alphabeticalOrderRule;
|
||||
84
src/css-rules/alphabetical-order/property-order-enforcer.ts
Normal file
84
src/css-rules/alphabetical-order/property-order-enforcer.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
28
src/css-rules/alphabetical-order/recipe-order-enforcer.ts
Normal file
28
src/css-rules/alphabetical-order/recipe-order-enforcer.ts
Normal 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),
|
||||
);
|
||||
};
|
||||
23
src/css-rules/alphabetical-order/rule-definition.ts
Normal file
23
src/css-rules/alphabetical-order/rule-definition.ts
Normal 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;
|
||||
37
src/css-rules/alphabetical-order/style-object-processor.ts
Normal file
37
src/css-rules/alphabetical-order/style-object-processor.ts
Normal 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);
|
||||
};
|
||||
336
src/css-rules/concentric-order/concentric-groups.ts
Normal file
336
src/css-rules/concentric-order/concentric-groups.ts
Normal 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);
|
||||
3
src/css-rules/concentric-order/index.ts
Normal file
3
src/css-rules/concentric-order/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import concentricOrderRule from './rule-definition.js';
|
||||
|
||||
export default concentricOrderRule;
|
||||
85
src/css-rules/concentric-order/property-order-enforcer.ts
Normal file
85
src/css-rules/concentric-order/property-order-enforcer.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
31
src/css-rules/concentric-order/recipe-order-enforcer.ts
Normal file
31
src/css-rules/concentric-order/recipe-order-enforcer.ts
Normal 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),
|
||||
);
|
||||
};
|
||||
23
src/css-rules/concentric-order/rule-definition.ts
Normal file
23
src/css-rules/concentric-order/rule-definition.ts
Normal 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;
|
||||
72
src/css-rules/concentric-order/style-object-processor.ts
Normal file
72
src/css-rules/concentric-order/style-object-processor.ts
Normal 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);
|
||||
};
|
||||
9
src/css-rules/concentric-order/types.ts
Normal file
9
src/css-rules/concentric-order/types.ts
Normal 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;
|
||||
}
|
||||
91
src/css-rules/custom-order/property-order-enforcer.ts
Normal file
91
src/css-rules/custom-order/property-order-enforcer.ts
Normal 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,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
42
src/css-rules/custom-order/recipe-order-enforcer.ts
Normal file
42
src/css-rules/custom-order/recipe-order-enforcer.ts
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
55
src/css-rules/custom-order/rule-definition.ts
Normal file
55
src/css-rules/custom-order/rule-definition.ts
Normal 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;
|
||||
79
src/css-rules/custom-order/style-object-processor.ts
Normal file
79
src/css-rules/custom-order/style-object-processor.ts
Normal 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,
|
||||
),
|
||||
);
|
||||
};
|
||||
55
src/css-rules/shared-utils/css-order-fixer.ts
Normal file
55
src/css-rules/shared-utils/css-order-fixer.ts
Normal 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);
|
||||
};
|
||||
60
src/css-rules/shared-utils/css-property-priority-map.ts
Normal file
60
src/css-rules/shared-utils/css-property-priority-map.ts
Normal 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;
|
||||
};
|
||||
44
src/css-rules/shared-utils/nested-selectors-processor.ts
Normal file
44
src/css-rules/shared-utils/nested-selectors-processor.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
98
src/css-rules/shared-utils/order-strategy-visitor-creator.ts
Normal file
98
src/css-rules/shared-utils/order-strategy-visitor-creator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
57
src/css-rules/shared-utils/property-separator.ts
Normal file
57
src/css-rules/shared-utils/property-separator.ts
Normal 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 };
|
||||
};
|
||||
45
src/css-rules/shared-utils/recipe-property-processor.ts
Normal file
45
src/css-rules/shared-utils/recipe-property-processor.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
30
src/css-rules/shared-utils/style-node-processor.ts
Normal file
30
src/css-rules/shared-utils/style-node-processor.ts
Normal 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
39
src/index.ts
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue