UNPKG

@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.

176 lines (175 loc) 5.17 kB
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; /** * List of valid CSS units according to CSS specifications. */ const VALID_CSS_UNITS = [ // Absolute length units 'px', 'cm', 'mm', 'Q', 'in', 'pc', 'pt', // Relative length units 'em', 'ex', 'ch', 'rem', 'lh', 'rlh', 'vw', 'vh', 'vmin', 'vmax', 'vb', 'vi', 'svw', 'svh', 'lvw', 'lvh', 'dvw', 'dvh', // Percentage '%', // Angle units 'deg', 'grad', 'rad', 'turn', // Time units 'ms', 's', // Frequency units 'Hz', 'kHz', // Resolution units 'dpi', 'dpcm', 'dppx', 'x', // Flexible length units 'fr', // Other valid units 'cap', 'ic', 'rex', 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', ]; /** * Regular expression to extract units from CSS values. * Matches numeric values followed by a unit. */ const CSS_VALUE_WITH_UNIT_REGEX = /^(-?\d*\.?\d+)([a-zA-Z%]+)$/i; /** * Splits a CSS value string into individual parts, handling spaces not inside functions. */ const splitCssValues = (value) => { return value .split(/(?<!\([^)]*)\s+/) // Split on spaces not inside functions .map((part) => part.trim()) .filter((part) => part.length > 0); }; /** * Check if a CSS value contains a valid CSS unit. */ const checkCssUnit = (value) => { const values = splitCssValues(value); for (const value of values) { // Skip values containing CSS functions if (value.includes('(')) { continue; } const match = value.match(CSS_VALUE_WITH_UNIT_REGEX); if (!match) { continue; } const unit = match[2].toLowerCase(); // match[2] is guaranteed by regex pattern if (!VALID_CSS_UNITS.includes(unit)) { return { hasUnit: true, unit: match[2], // Preserve original casing isValid: false, invalidValue: value, }; } } return { hasUnit: false, unit: null, isValid: true }; }; /** * Extracts string value from a node if it's a string literal or template literal. */ const getStringValue = (node) => { if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') { return node.value; } if (node.type === AST_NODE_TYPES.TemplateLiteral && node.quasis.length === 1) { const firstQuasi = node.quasis[0]; return firstQuasi?.value.raw ? firstQuasi.value.raw : null; } return null; }; /** * Recursively processes a style object, reporting instances of * unknown CSS units. * * @param context The ESLint rule context. * @param node The ObjectExpression node representing the style object to be * processed. */ export const processUnknownUnitInStyleObject = (context, node) => { // Defensive: This function is only called with ObjectExpression nodes by the rule visitor. // This check's for type safety and future-proofing. It's not covered by rule tests // because the rule architecture prevents non-ObjectExpression nodes from reaching here. if (!node || node.type !== AST_NODE_TYPES.ObjectExpression) { return; } for (const property of node.properties) { if (property.type !== AST_NODE_TYPES.Property) { continue; } // Get property key name if possible 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 === '@media' || propertyName === 'selectors') { if (property.value.type === AST_NODE_TYPES.ObjectExpression) { for (const nestedProperty of property.value.properties) { if (nestedProperty.type === AST_NODE_TYPES.Property && nestedProperty.value.type === AST_NODE_TYPES.ObjectExpression) { processUnknownUnitInStyleObject(context, nestedProperty.value); } } } continue; } // Process direct string values const value = getStringValue(property.value); if (value) { const result = checkCssUnit(value); if (result.hasUnit && !result.isValid && result.invalidValue) { context.report({ node: property.value, messageId: 'unknownUnit', data: { unit: result.unit || '', value: result.invalidValue, }, }); } } // Process nested objects (including those not handled by special cases) if (property.value.type === AST_NODE_TYPES.ObjectExpression) { processUnknownUnitInStyleObject(context, property.value); } } };