@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.
191 lines (190 loc) • 6.24 kB
JavaScript
import { parseExpression } from '@babel/parser';
/**
* Safe evaluator for static theme values with support for:
* - rem() from polished
* - clsx() for combining values
* - Template literals with expressions
* - Basic arithmetic
*/
export class ValueEvaluator {
constructor(remBase = 16) {
this.remBase = remBase;
}
/**
* Evaluate a string expression to a concrete value
*/
evaluate(expression) {
try {
const ast = parseExpression(expression, {
sourceType: 'module',
plugins: ['typescript'],
});
return this.evaluateNode(ast);
}
catch {
// If parsing fails, return the original expression if it's a simple string
const trimmed = expression.trim();
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith('`') && trimmed.endsWith('`'))) {
return trimmed.slice(1, -1);
}
return null;
}
}
/**
* Evaluate an AST node
*/
evaluateNode(node) {
if (!node)
return null;
switch (node.type) {
case 'StringLiteral':
return node.value;
case 'NumericLiteral':
return String(node.value);
case 'TemplateLiteral':
return this.evaluateTemplateLiteral(node);
case 'CallExpression':
return this.evaluateCallExpression(node);
case 'BinaryExpression':
return this.evaluateBinaryExpression(node);
case 'Identifier':
// Only allow known safe identifiers
return null;
case 'MemberExpression':
// Don't evaluate member expressions for security
return null;
default:
return null;
}
}
/**
* Evaluate a template literal
*/
evaluateTemplateLiteral(node) {
const parts = [];
for (let i = 0; i < node.quasis.length; i++) {
// Add the string part
const quasi = node.quasis[i];
if (!quasi)
continue;
parts.push(quasi.value.cooked || quasi.value.raw);
// Add the expression part if it exists
if (i < node.expressions.length) {
const exprValue = this.evaluateNode(node.expressions[i]);
if (exprValue === null) {
return null; // Can't evaluate this expression
}
parts.push(exprValue);
}
}
return parts.join('');
}
/**
* Evaluate a call expression (rem, clsx, etc.)
*/
evaluateCallExpression(node) {
// Get function name
let functionName = null;
if (node.callee.type === 'Identifier') {
functionName = node.callee.name;
}
else if (node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier') {
functionName = node.callee.property.name;
}
if (!functionName)
return null;
switch (functionName) {
case 'rem':
return this.evaluateRem(node);
case 'clsx':
return this.evaluateClsx(node);
default:
return null;
}
}
/**
* Evaluate rem() function from polished
*/
evaluateRem(node) {
if (node.arguments.length === 0)
return null;
const arg = node.arguments[0];
if (!arg || arg.type === 'SpreadElement')
return null;
const value = this.evaluateNode(arg);
if (value === null)
return null;
const numValue = parseFloat(value);
if (isNaN(numValue))
return null;
// Convert pixels to rem
const remValue = numValue / this.remBase;
return `${remValue}rem`;
}
/**
* Evaluate clsx() function
*/
evaluateClsx(node) {
const parts = [];
for (const arg of node.arguments) {
if (arg.type === 'SpreadElement')
return null;
const value = this.evaluateNode(arg);
if (value === null)
return null;
parts.push(value);
}
return parts.join(' ');
}
/**
* Evaluate binary expression (mainly for string concatenation)
*/
evaluateBinaryExpression(node) {
const left = this.evaluateNode(node.left);
const right = this.evaluateNode(node.right);
if (left === null || right === null)
return null;
switch (node.operator) {
case '+':
// String concatenation or addition
const leftNum = parseFloat(left);
const rightNum = parseFloat(right);
if (!isNaN(leftNum) && !isNaN(rightNum)) {
return String(leftNum + rightNum);
}
return left + right;
case '-':
const leftNum2 = parseFloat(left);
const rightNum2 = parseFloat(right);
if (!isNaN(leftNum2) && !isNaN(rightNum2)) {
return String(leftNum2 - rightNum2);
}
return null;
case '*':
const leftNum3 = parseFloat(left);
const rightNum3 = parseFloat(right);
if (!isNaN(leftNum3) && !isNaN(rightNum3)) {
return String(leftNum3 * rightNum3);
}
return null;
case '/':
const leftNum4 = parseFloat(left);
const rightNum4 = parseFloat(right);
if (!isNaN(leftNum4) && !isNaN(rightNum4) && rightNum4 !== 0) {
return String(leftNum4 / rightNum4);
}
return null;
default:
return null;
}
}
/**
* Set rem base for evaluation
*/
setRemBase(base) {
this.remBase = base;
}
}