mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2026-01-02 09:33:33 +00:00
feat 🥁: initialize project with complete codebase
This commit is contained in:
commit
d569dea1fb
35 changed files with 4413 additions and 0 deletions
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue