@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.
283 lines (282 loc) • 11.1 kB
JavaScript
import { TSESTree } from '@typescript-eslint/utils';
/**
* Tracks vanilla-extract function imports and their local bindings
*/
export class ReferenceTracker {
constructor(options) {
this.imports = new Map();
this.wrapperFunctions = new Map(); // wrapper function name -> detailed info
this.trackedFunctions = {
styleFunctions: new Set(),
recipeFunctions: new Set(),
fontFaceFunctions: new Set(),
globalFunctions: new Set(),
keyframeFunctions: new Set(),
};
this.style = new Set(options?.style ?? []);
this.recipe = new Set(options?.recipe ?? []);
}
/**
* Processes import declarations to track vanilla-extract functions
*/
processImportDeclaration(node) {
const source = node.source.value;
if (typeof source !== 'string') {
return;
}
const isVanillaExtractImport = this.isVanillaExtractSource(source);
node.specifiers.forEach((specifier) => {
if (specifier.type === 'ImportSpecifier') {
const importedName = specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value;
const localName = specifier.local.name;
const customWrapper = this.getCustomWrapper(importedName, localName);
let trackedImportName;
if (isVanillaExtractImport) {
trackedImportName = importedName;
}
else {
if (!customWrapper) {
return;
}
trackedImportName = customWrapper;
}
const reference = {
source,
importedName: trackedImportName,
localName,
};
this.imports.set(localName, reference);
this.categorizeFunction(localName, trackedImportName);
}
});
}
/**
* Processes variable declarations to track re-assignments and destructuring
*/
processVariableDeclarator(node) {
// Handle destructuring assignments like: const { style, recipe } = vanillaExtract;
if (node.id.type === 'ObjectPattern' && node.init?.type === 'Identifier') {
const sourceIdentifier = node.init.name;
const sourceReference = this.imports.get(sourceIdentifier);
if (sourceReference && this.isVanillaExtractSource(sourceReference.source)) {
node.id.properties.forEach((property) => {
if (property.type === 'Property' &&
property.key.type === 'Identifier' &&
property.value.type === 'Identifier') {
const importedName = property.key.name;
const localName = property.value.name;
const reference = {
source: sourceReference.source,
importedName,
localName,
};
this.imports.set(localName, reference);
this.categorizeFunction(localName, importedName);
}
});
}
}
// Handle simple assignments like: const myStyle = style;
if (node.id.type === 'Identifier' && node.init?.type === 'Identifier') {
const sourceReference = this.imports.get(node.init.name);
if (sourceReference) {
this.imports.set(node.id.name, sourceReference);
this.categorizeFunction(node.id.name, sourceReference.importedName);
}
}
// Handle arrow function assignments that wrap vanilla-extract functions
if (node.id.type === 'Identifier' && node.init?.type === 'ArrowFunctionExpression') {
this.analyzeWrapperFunction(node.id.name, node.init);
}
}
/**
* Processes function declarations to detect wrapper functions
*/
processFunctionDeclaration(node) {
if (node.id?.name) {
this.analyzeWrapperFunction(node.id.name, node);
}
}
/**
* Analyzes a function to see if it wraps a vanilla-extract function
*/
analyzeWrapperFunction(functionName, functionNode) {
const body = functionNode.body;
// Handle arrow functions with expression body
if (functionNode.type === 'ArrowFunctionExpression' && body.type !== 'BlockStatement') {
this.analyzeWrapperExpression(functionName, body);
return;
}
// Handle functions with block statement body
if (body.type === 'BlockStatement') {
this.traverseBlockForVanillaExtractCalls(functionName, body);
}
}
/**
* Analyzes a wrapper function expression to detect vanilla-extract calls and parameter mapping
*/
analyzeWrapperExpression(wrapperName, expression) {
if (expression.type === 'CallExpression' && expression.callee.type === 'Identifier') {
const calledFunction = expression.callee.name;
if (this.isTrackedFunction(calledFunction)) {
const originalName = this.getOriginalName(calledFunction);
if (originalName) {
// For now, create a simple wrapper info
const wrapperInfo = {
originalFunction: originalName,
parameterMapping: 1, // layerStyle uses second parameter as the style object
};
this.wrapperFunctions.set(wrapperName, wrapperInfo);
this.categorizeFunction(wrapperName, originalName);
}
}
}
}
/**
* Checks if a node is a vanilla-extract function call
*/
checkForVanillaExtractCall(wrapperName, node) {
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
const calledFunction = node.callee.name;
if (this.isTrackedFunction(calledFunction)) {
const originalName = this.getOriginalName(calledFunction);
if (originalName) {
const wrapperInfo = {
originalFunction: originalName,
parameterMapping: 0, // Default to first parameter
};
this.wrapperFunctions.set(wrapperName, wrapperInfo);
this.categorizeFunction(wrapperName, originalName);
}
}
}
}
/**
* Traverses a block statement to find vanilla-extract calls
*/
traverseBlockForVanillaExtractCalls(wrapperName, block) {
for (const statement of block.body) {
if (statement.type === 'ReturnStatement' && statement.argument) {
this.checkForVanillaExtractCall(wrapperName, statement.argument);
}
else if (statement.type === 'ExpressionStatement') {
this.checkForVanillaExtractCall(wrapperName, statement.expression);
}
}
}
/**
* Checks if a function name corresponds to a tracked vanilla-extract function
*/
isTrackedFunction(functionName) {
return this.imports.has(functionName) || this.wrapperFunctions.has(functionName);
}
/**
* Gets the category of a tracked function
*/
getFunctionCategory(functionName) {
if (this.trackedFunctions.styleFunctions.has(functionName)) {
return 'styleFunctions';
}
if (this.trackedFunctions.recipeFunctions.has(functionName)) {
return 'recipeFunctions';
}
if (this.trackedFunctions.fontFaceFunctions.has(functionName)) {
return 'fontFaceFunctions';
}
if (this.trackedFunctions.globalFunctions.has(functionName)) {
return 'globalFunctions';
}
if (this.trackedFunctions.keyframeFunctions.has(functionName)) {
return 'keyframeFunctions';
}
return null;
}
/**
* Gets the original imported name for a local function name
*/
getOriginalName(localName) {
const reference = this.imports.get(localName);
if (reference) {
return reference.importedName;
}
// Check if it's a wrapper function
const wrapperInfo = this.wrapperFunctions.get(localName);
return wrapperInfo?.originalFunction ?? null;
}
/**
* Gets wrapper function information
*/
getWrapperInfo(functionName) {
return this.wrapperFunctions.get(functionName) ?? null;
}
/**
* Gets all tracked functions by category
*/
getTrackedFunctions() {
return this.trackedFunctions;
}
/**
* Resets the tracker state (useful for processing multiple files)
*/
reset() {
this.imports.clear();
this.wrapperFunctions.clear();
this.trackedFunctions.styleFunctions.clear();
this.trackedFunctions.recipeFunctions.clear();
this.trackedFunctions.fontFaceFunctions.clear();
this.trackedFunctions.globalFunctions.clear();
this.trackedFunctions.keyframeFunctions.clear();
}
isVanillaExtractSource(source) {
return (source === '@vanilla-extract/css' ||
source === '@vanilla-extract/recipes' ||
source.startsWith('@vanilla-extract/'));
}
getCustomWrapper(importedName, localName) {
if (this.style.has(importedName) || this.style.has(localName)) {
return 'style';
}
if (this.recipe.has(importedName) || this.recipe.has(localName)) {
return 'recipe';
}
return null;
}
categorizeFunction(localName, importedName) {
switch (importedName) {
case 'style':
case 'styleVariants':
this.trackedFunctions.styleFunctions.add(localName);
break;
case 'recipe':
this.trackedFunctions.recipeFunctions.add(localName);
break;
case 'fontFace':
case 'globalFontFace':
this.trackedFunctions.fontFaceFunctions.add(localName);
break;
case 'globalStyle':
case 'globalKeyframes':
this.trackedFunctions.globalFunctions.add(localName);
break;
case 'keyframes':
this.trackedFunctions.keyframeFunctions.add(localName);
break;
}
}
}
/**
* Creates a visitor that tracks vanilla-extract imports and bindings
*/
export function createReferenceTrackingVisitor(tracker) {
return {
ImportDeclaration(node) {
tracker.processImportDeclaration(node);
},
VariableDeclarator(node) {
tracker.processVariableDeclarator(node);
},
FunctionDeclaration(node) {
tracker.processFunctionDeclaration(node);
},
};
}