@antebudimir/eslint-plugin-vanilla-extract
Version:
Comprehensive ESLint plugin for vanilla-extract with CSS property ordering, style validation, and best practices enforcement. Supports alphabetical, concentric and custom CSS ordering, auto-fixing, and zero-runtime safety.
203 lines (202 loc) • 7.93 kB
JavaScript
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import { isPhysicalProperty, getLogicalProperty, toKebabCase, toCamelCase, TEXT_ALIGN_PHYSICAL_VALUES, FLOAT_PHYSICAL_VALUES, CLEAR_PHYSICAL_VALUES, VALUE_BASED_PHYSICAL_PROPERTIES, } from './property-mappings.js';
/**
* Get the text value from a node (string literal or simple template literal)
*/
const getValueText = (node) => {
if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') {
return node.value;
}
if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
return node.quasis.map((quasi) => quasi.value.raw ?? '').join('');
}
return null;
};
/**
* Check if a node can be auto-fixed (literal or simple template literal)
*/
const canAutoFix = (node) => {
if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') {
return 'literal';
}
if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
return 'simple-template';
}
return null;
};
/**
* Check if a property value contains physical directional values
*/
const hasPhysicalValue = (propertyName, value) => {
const trimmedValue = value.trim().toLowerCase();
if (propertyName === 'text-align' || propertyName === 'textAlign') {
if (trimmedValue in TEXT_ALIGN_PHYSICAL_VALUES) {
return {
hasPhysical: true,
fixedValue: TEXT_ALIGN_PHYSICAL_VALUES[trimmedValue],
};
}
}
if (propertyName === 'float') {
if (trimmedValue in FLOAT_PHYSICAL_VALUES) {
return {
hasPhysical: true,
fixedValue: FLOAT_PHYSICAL_VALUES[trimmedValue],
};
}
}
if (propertyName === 'clear') {
if (trimmedValue in CLEAR_PHYSICAL_VALUES) {
return {
hasPhysical: true,
fixedValue: CLEAR_PHYSICAL_VALUES[trimmedValue],
};
}
}
if (propertyName === 'resize') {
if (trimmedValue === 'horizontal' || trimmedValue === 'vertical') {
const fixedValue = trimmedValue === 'horizontal' ? 'inline' : 'block';
return { hasPhysical: true, fixedValue };
}
}
return { hasPhysical: false };
};
/**
* Normalize property name to both camelCase and kebab-case for checking
*/
const normalizePropertyName = (name) => {
const kebab = toKebabCase(name);
const camel = toCamelCase(name);
return { camel, kebab };
};
/**
* Check if a property is in the allow list
*/
const isAllowed = (propertyName, allowSet) => {
const { camel, kebab } = normalizePropertyName(propertyName);
return allowSet.has(propertyName) || allowSet.has(camel) || allowSet.has(kebab);
};
/**
* Get the appropriate logical property name based on the original format
*/
const getLogicalPropertyInFormat = (originalName, logicalName) => {
// If original is kebab-case (contains hyphen), return kebab-case
if (originalName.includes('-')) {
return toKebabCase(logicalName);
}
// Otherwise return camelCase
return toCamelCase(logicalName);
};
/**
* Create a fix for replacing a property key
*/
const createPropertyKeyFix = (fixer, property, newPropertyName, context) => {
const key = property.key;
if (key.type === AST_NODE_TYPES.Identifier) {
return fixer.replaceText(key, newPropertyName);
}
if (key.type === AST_NODE_TYPES.Literal && typeof key.value === 'string') {
// Preserve quote style
const sourceCode = context.getSourceCode();
const originalText = sourceCode.getText(key);
const quote = originalText[0];
return fixer.replaceText(key, `${quote}${newPropertyName}${quote}`);
}
return null;
};
/**
* Create a fix for replacing a property value
*/
const createPropertyValueFix = (fixer, valueNode, newValue, fixType) => {
if (fixType === 'literal') {
return fixer.replaceText(valueNode, `'${newValue}'`);
}
// simple-template
return fixer.replaceText(valueNode, `\`${newValue}\``);
};
/**
* Recursively processes a vanilla-extract style object and reports physical CSS properties.
*
* - Detects physical property names and suggests logical equivalents
* - Detects physical directional values (e.g., text-align: left)
* - Skips properties in the allow list
* - Provides auto-fixes where unambiguous
* - Traverses nested objects, @media, and selectors
*
* @param context ESLint rule context
* @param node The ObjectExpression node representing the style object
* @param allowSet Set of property names to skip
*/
export const processLogicalPropertiesInStyleObject = (context, node, allowSet) => {
for (const property of node.properties) {
if (property.type !== AST_NODE_TYPES.Property)
continue;
// Determine property name
let propertyName = null;
if (property.key.type === AST_NODE_TYPES.Identifier) {
propertyName = property.key.name;
}
else if (property.key.type === AST_NODE_TYPES.Literal && typeof property.key.value === 'string') {
propertyName = property.key.value;
}
if (!propertyName)
continue;
// Handle nested containers (@media, selectors, etc.)
if (propertyName === '@media' || propertyName === 'selectors') {
if (property.value.type === AST_NODE_TYPES.ObjectExpression) {
for (const nested of property.value.properties) {
if (nested.type === AST_NODE_TYPES.Property && nested.value.type === AST_NODE_TYPES.ObjectExpression) {
processLogicalPropertiesInStyleObject(context, nested.value, allowSet);
}
}
}
continue;
}
// Recurse into nested objects
if (property.value.type === AST_NODE_TYPES.ObjectExpression) {
processLogicalPropertiesInStyleObject(context, property.value, allowSet);
continue;
}
// Skip if property is in allow list
if (isAllowed(propertyName, allowSet)) {
continue;
}
// Check for physical property names
if (isPhysicalProperty(propertyName)) {
const logicalProp = getLogicalProperty(propertyName);
if (logicalProp) {
const logicalInFormat = getLogicalPropertyInFormat(propertyName, logicalProp);
context.report({
node: property.key,
messageId: 'preferLogicalProperty',
data: {
physical: propertyName,
logical: logicalInFormat,
},
fix: (fixer) => createPropertyKeyFix(fixer, property, logicalInFormat, context),
});
}
continue;
}
// Check for value-based physical properties
if (VALUE_BASED_PHYSICAL_PROPERTIES.has(propertyName)) {
const valueText = getValueText(property.value);
if (valueText) {
const { hasPhysical, fixedValue } = hasPhysicalValue(propertyName, valueText);
if (hasPhysical && fixedValue) {
const fixType = canAutoFix(property.value);
context.report({
node: property.value,
messageId: 'preferLogicalValue',
data: {
property: propertyName,
physical: valueText.trim(),
logical: fixedValue,
},
fix: fixType ? (fixer) => createPropertyValueFix(fixer, property.value, fixedValue, fixType) : undefined,
});
}
}
}
}
};