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.

283 lines (282 loc) 11.1 kB
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); }, }; }