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,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);
}
});
}
};