mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2025-12-31 17:03:32 +00:00
feat: Implement reference tracking system for vanilla-extract functions
This commit is contained in:
parent
35875fbb31
commit
6be1a0659e
3 changed files with 596 additions and 76 deletions
|
|
@ -7,25 +7,20 @@ import { enforceConcentricCSSOrderInStyleObject } from '../concentric-order/styl
|
||||||
import { enforceUserDefinedGroupOrderInRecipe } from '../custom-order/recipe-order-enforcer.js';
|
import { enforceUserDefinedGroupOrderInRecipe } from '../custom-order/recipe-order-enforcer.js';
|
||||||
import { enforceUserDefinedGroupOrderInStyleObject } from '../custom-order/style-object-processor.js';
|
import { enforceUserDefinedGroupOrderInStyleObject } from '../custom-order/style-object-processor.js';
|
||||||
import { enforceFontFaceOrder } from './font-face-property-order-enforcer.js';
|
import { enforceFontFaceOrder } from './font-face-property-order-enforcer.js';
|
||||||
|
import { ReferenceTracker, createReferenceTrackingVisitor } from './reference-tracker.js';
|
||||||
import { processStyleNode } from './style-node-processor.js';
|
import { processStyleNode } from './style-node-processor.js';
|
||||||
import type { SortRemainingProperties } from '../concentric-order/types.js';
|
import type { SortRemainingProperties } from '../concentric-order/types.js';
|
||||||
import type { OrderingStrategy } from '../types.js';
|
import type { OrderingStrategy } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an ESLint rule listener with visitors for style-related function calls.
|
* Creates an ESLint rule listener with visitors for style-related function calls using reference tracking.
|
||||||
|
* This automatically detects vanilla-extract functions based on their import statements.
|
||||||
|
*
|
||||||
* @param ruleContext The ESLint rule context.
|
* @param ruleContext The ESLint rule context.
|
||||||
* @param orderingStrategy The strategy to use for ordering CSS properties ('alphabetical', 'concentric', or 'userDefinedGroupOrder').
|
* @param orderingStrategy The strategy to use for ordering CSS properties ('alphabetical', 'concentric', or 'userDefinedGroupOrder').
|
||||||
* @param userDefinedGroupOrder An optional array of property groups for the 'userDefinedGroupOrder' strategy.
|
* @param userDefinedGroupOrder An optional array of property groups for the 'userDefinedGroupOrder' strategy.
|
||||||
* @param sortRemainingProperties An optional strategy for sorting properties not in user-defined groups.
|
* @param sortRemainingProperties An optional strategy for sorting properties not in user-defined groups.
|
||||||
* @returns An object with visitor functions for the ESLint rule.
|
* @returns An object with visitor functions for the ESLint rule.
|
||||||
*
|
|
||||||
* This function sets up visitors for the following cases:
|
|
||||||
* 1. The fontFace and globalFontFace functions.
|
|
||||||
* 2. Style-related functions: 'keyframes', 'style', 'styleVariants'.
|
|
||||||
* 3. The 'globalStyle' and 'globalKeyframes' function
|
|
||||||
* 4. The 'recipe' function
|
|
||||||
*
|
|
||||||
* Each visitor applies the appropriate ordering strategy to the style objects in these function calls.
|
|
||||||
*/
|
*/
|
||||||
export const createNodeVisitors = (
|
export const createNodeVisitors = (
|
||||||
ruleContext: Rule.RuleContext,
|
ruleContext: Rule.RuleContext,
|
||||||
|
|
@ -33,7 +28,95 @@ export const createNodeVisitors = (
|
||||||
userDefinedGroupOrder?: string[],
|
userDefinedGroupOrder?: string[],
|
||||||
sortRemainingProperties?: SortRemainingProperties,
|
sortRemainingProperties?: SortRemainingProperties,
|
||||||
): Rule.RuleListener => {
|
): Rule.RuleListener => {
|
||||||
// Select the appropriate property processing function based on the ordering strategy
|
const tracker = new ReferenceTracker();
|
||||||
|
const trackingVisitor = createReferenceTrackingVisitor(tracker);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Include the import/variable tracking visitors
|
||||||
|
...trackingVisitor,
|
||||||
|
|
||||||
|
CallExpression(node) {
|
||||||
|
if (node.callee.type !== 'Identifier') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionName = node.callee.name;
|
||||||
|
|
||||||
|
// Check if this function is tracked as a vanilla-extract function
|
||||||
|
if (!tracker.isTrackedFunction(functionName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalName = tracker.getOriginalName(functionName);
|
||||||
|
if (!originalName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different function types based on their original imported name
|
||||||
|
switch (originalName) {
|
||||||
|
case 'fontFace':
|
||||||
|
processFontFaceOrdering(ruleContext, node as TSESTree.CallExpression, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'globalFontFace':
|
||||||
|
processFontFaceOrdering(ruleContext, node as TSESTree.CallExpression, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'style':
|
||||||
|
case 'styleVariants':
|
||||||
|
case 'keyframes':
|
||||||
|
// Check if this is a wrapper function
|
||||||
|
const wrapperInfo = tracker.getWrapperInfo(functionName);
|
||||||
|
const argumentIndex = wrapperInfo?.parameterMapping ?? 0;
|
||||||
|
|
||||||
|
processStyleOrdering(
|
||||||
|
ruleContext,
|
||||||
|
node as TSESTree.CallExpression,
|
||||||
|
orderingStrategy,
|
||||||
|
userDefinedGroupOrder,
|
||||||
|
sortRemainingProperties,
|
||||||
|
argumentIndex,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'globalStyle':
|
||||||
|
case 'globalKeyframes':
|
||||||
|
processStyleOrdering(
|
||||||
|
ruleContext,
|
||||||
|
node as TSESTree.CallExpression,
|
||||||
|
orderingStrategy,
|
||||||
|
userDefinedGroupOrder,
|
||||||
|
sortRemainingProperties,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'recipe':
|
||||||
|
processRecipeOrdering(
|
||||||
|
ruleContext,
|
||||||
|
node as TSESTree.CallExpression,
|
||||||
|
orderingStrategy,
|
||||||
|
userDefinedGroupOrder,
|
||||||
|
sortRemainingProperties,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to process style ordering for style-related functions
|
||||||
|
*/
|
||||||
|
const processStyleOrdering = (
|
||||||
|
ruleContext: Rule.RuleContext,
|
||||||
|
node: TSESTree.CallExpression,
|
||||||
|
orderingStrategy: OrderingStrategy,
|
||||||
|
userDefinedGroupOrder?: string[],
|
||||||
|
sortRemainingProperties?: SortRemainingProperties,
|
||||||
|
argumentIndex: number = 0,
|
||||||
|
) => {
|
||||||
|
if (node.arguments.length > argumentIndex) {
|
||||||
const processProperty = (() => {
|
const processProperty = (() => {
|
||||||
switch (orderingStrategy) {
|
switch (orderingStrategy) {
|
||||||
case 'alphabetical':
|
case 'alphabetical':
|
||||||
|
|
@ -45,76 +128,57 @@ export const createNodeVisitors = (
|
||||||
return enforceAlphabeticalCSSOrderInStyleObject;
|
return enforceAlphabeticalCSSOrderInStyleObject;
|
||||||
}
|
}
|
||||||
return (ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression) =>
|
return (ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression) =>
|
||||||
enforceUserDefinedGroupOrderInStyleObject(ruleContext, node, userDefinedGroupOrder, sortRemainingProperties);
|
enforceUserDefinedGroupOrderInStyleObject(
|
||||||
|
ruleContext,
|
||||||
|
node,
|
||||||
|
userDefinedGroupOrder,
|
||||||
|
sortRemainingProperties,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return enforceAlphabeticalCSSOrderInStyleObject;
|
return enforceAlphabeticalCSSOrderInStyleObject;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return {
|
processStyleNode(ruleContext, node.arguments[argumentIndex] as TSESTree.ObjectExpression, processProperty);
|
||||||
CallExpression(node) {
|
|
||||||
if (node.callee.type !== 'Identifier') {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fontFaceFunctionArgumentIndexMap = {
|
/**
|
||||||
fontFace: 0, // First argument (index 0)
|
* Helper function to process font face ordering
|
||||||
globalFontFace: 1, // Second argument (index 1)
|
*/
|
||||||
};
|
const processFontFaceOrdering = (
|
||||||
|
ruleContext: Rule.RuleContext,
|
||||||
// Handle font face functions with special ordering
|
node: TSESTree.CallExpression,
|
||||||
if (
|
argumentIndex: number,
|
||||||
node.callee.name in fontFaceFunctionArgumentIndexMap &&
|
) => {
|
||||||
node.arguments.length >
|
if (node.arguments.length > argumentIndex) {
|
||||||
fontFaceFunctionArgumentIndexMap[node.callee.name as keyof typeof fontFaceFunctionArgumentIndexMap]
|
enforceFontFaceOrder(ruleContext, node.arguments[argumentIndex] as TSESTree.ObjectExpression);
|
||||||
) {
|
|
||||||
const argumentIndex =
|
|
||||||
fontFaceFunctionArgumentIndexMap[node.callee.name as keyof typeof fontFaceFunctionArgumentIndexMap];
|
|
||||||
const styleArguments = node.arguments[argumentIndex];
|
|
||||||
|
|
||||||
enforceFontFaceOrder(ruleContext, styleArguments as TSESTree.ObjectExpression);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle style-related functions
|
/**
|
||||||
if (['keyframes', 'style', 'styleVariants'].includes(node.callee.name)) {
|
* Helper function to process recipe ordering
|
||||||
|
*/
|
||||||
|
const processRecipeOrdering = (
|
||||||
|
ruleContext: Rule.RuleContext,
|
||||||
|
node: TSESTree.CallExpression,
|
||||||
|
orderingStrategy: OrderingStrategy,
|
||||||
|
userDefinedGroupOrder?: string[],
|
||||||
|
sortRemainingProperties?: SortRemainingProperties,
|
||||||
|
) => {
|
||||||
if (node.arguments.length > 0) {
|
if (node.arguments.length > 0) {
|
||||||
const styleArguments = node.arguments[0];
|
|
||||||
processStyleNode(ruleContext, styleArguments as TSESTree.Node, processProperty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle global functions
|
|
||||||
if (
|
|
||||||
(node.callee.name === 'globalKeyframes' || node.callee.name === 'globalStyle') &&
|
|
||||||
node.arguments.length >= 2
|
|
||||||
) {
|
|
||||||
const styleArguments = node.arguments[1];
|
|
||||||
processStyleNode(ruleContext, styleArguments as TSESTree.Node, processProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle recipe function
|
|
||||||
if (node.callee.name === 'recipe') {
|
|
||||||
switch (orderingStrategy) {
|
switch (orderingStrategy) {
|
||||||
case 'alphabetical':
|
case 'alphabetical':
|
||||||
enforceAlphabeticalCSSOrderInRecipe(node as TSESTree.CallExpression, ruleContext);
|
enforceAlphabeticalCSSOrderInRecipe(node, ruleContext);
|
||||||
break;
|
break;
|
||||||
case 'concentric':
|
case 'concentric':
|
||||||
enforceConcentricCSSOrderInRecipe(ruleContext, node as TSESTree.CallExpression);
|
enforceConcentricCSSOrderInRecipe(ruleContext, node);
|
||||||
break;
|
break;
|
||||||
case 'userDefinedGroupOrder':
|
case 'userDefinedGroupOrder':
|
||||||
if (userDefinedGroupOrder) {
|
if (userDefinedGroupOrder) {
|
||||||
enforceUserDefinedGroupOrderInRecipe(
|
enforceUserDefinedGroupOrderInRecipe(ruleContext, node, userDefinedGroupOrder, sortRemainingProperties);
|
||||||
ruleContext,
|
|
||||||
node as TSESTree.CallExpression,
|
|
||||||
userDefinedGroupOrder,
|
|
||||||
sortRemainingProperties,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
135
src/css-rules/shared-utils/reference-based-visitor-creator.ts
Normal file
135
src/css-rules/shared-utils/reference-based-visitor-creator.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
import type { Rule } from 'eslint';
|
||||||
|
import { TSESTree } from '@typescript-eslint/utils';
|
||||||
|
import { enforceAlphabeticalCSSOrderInRecipe } from '../alphabetical-order/recipe-order-enforcer.js';
|
||||||
|
import { enforceAlphabeticalCSSOrderInStyleObject } from '../alphabetical-order/style-object-processor.js';
|
||||||
|
import { enforceConcentricCSSOrderInRecipe } from '../concentric-order/recipe-order-enforcer.js';
|
||||||
|
import { enforceConcentricCSSOrderInStyleObject } from '../concentric-order/style-object-processor.js';
|
||||||
|
import { enforceUserDefinedGroupOrderInRecipe } from '../custom-order/recipe-order-enforcer.js';
|
||||||
|
import { enforceUserDefinedGroupOrderInStyleObject } from '../custom-order/style-object-processor.js';
|
||||||
|
import { enforceFontFaceOrder } from './font-face-property-order-enforcer.js';
|
||||||
|
import { ReferenceTracker, createReferenceTrackingVisitor } from './reference-tracker.js';
|
||||||
|
import { processStyleNode } from './style-node-processor.js';
|
||||||
|
import type { SortRemainingProperties } from '../concentric-order/types.js';
|
||||||
|
import type { OrderingStrategy } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an ESLint rule listener with visitors for style-related function calls using reference tracking.
|
||||||
|
* This automatically detects vanilla-extract functions based on their import statements.
|
||||||
|
*
|
||||||
|
* @param ruleContext The ESLint rule context.
|
||||||
|
* @param orderingStrategy The strategy to use for ordering CSS properties.
|
||||||
|
* @param userDefinedGroupOrder An optional array of property groups for the 'userDefinedGroupOrder' strategy.
|
||||||
|
* @param sortRemainingProperties An optional strategy for sorting properties not in user-defined groups.
|
||||||
|
* @returns An object with visitor functions for the ESLint rule.
|
||||||
|
*/
|
||||||
|
export const createReferenceBasedNodeVisitors = (
|
||||||
|
ruleContext: Rule.RuleContext,
|
||||||
|
orderingStrategy: OrderingStrategy,
|
||||||
|
userDefinedGroupOrder?: string[],
|
||||||
|
sortRemainingProperties?: SortRemainingProperties,
|
||||||
|
): Rule.RuleListener => {
|
||||||
|
const tracker = new ReferenceTracker();
|
||||||
|
const trackingVisitor = createReferenceTrackingVisitor(tracker);
|
||||||
|
|
||||||
|
// Select the appropriate property processing function based on the ordering strategy
|
||||||
|
const processProperty = (() => {
|
||||||
|
switch (orderingStrategy) {
|
||||||
|
case 'alphabetical':
|
||||||
|
return enforceAlphabeticalCSSOrderInStyleObject;
|
||||||
|
case 'concentric':
|
||||||
|
return enforceConcentricCSSOrderInStyleObject;
|
||||||
|
case 'userDefinedGroupOrder':
|
||||||
|
if (!userDefinedGroupOrder || userDefinedGroupOrder.length === 0) {
|
||||||
|
return enforceAlphabeticalCSSOrderInStyleObject;
|
||||||
|
}
|
||||||
|
return (ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression) =>
|
||||||
|
enforceUserDefinedGroupOrderInStyleObject(ruleContext, node, userDefinedGroupOrder, sortRemainingProperties);
|
||||||
|
default:
|
||||||
|
return enforceAlphabeticalCSSOrderInStyleObject;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Include the reference tracking visitors
|
||||||
|
...trackingVisitor,
|
||||||
|
|
||||||
|
CallExpression(callExpression) {
|
||||||
|
if (callExpression.callee.type !== 'Identifier') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionName = callExpression.callee.name;
|
||||||
|
|
||||||
|
// Check if this function is tracked as a vanilla-extract function
|
||||||
|
if (!tracker.isTrackedFunction(functionName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalName = tracker.getOriginalName(functionName);
|
||||||
|
if (!originalName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different function types based on their original imported name
|
||||||
|
switch (originalName) {
|
||||||
|
case 'fontFace':
|
||||||
|
if (callExpression.arguments.length > 0) {
|
||||||
|
const styleArguments = callExpression.arguments[0];
|
||||||
|
enforceFontFaceOrder(ruleContext, styleArguments as TSESTree.ObjectExpression);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'globalFontFace':
|
||||||
|
if (callExpression.arguments.length > 1) {
|
||||||
|
const styleArguments = callExpression.arguments[1];
|
||||||
|
enforceFontFaceOrder(ruleContext, styleArguments as TSESTree.ObjectExpression);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'style':
|
||||||
|
case 'styleVariants':
|
||||||
|
case 'keyframes':
|
||||||
|
if (callExpression.arguments.length > 0) {
|
||||||
|
const styleArguments = callExpression.arguments[0];
|
||||||
|
processStyleNode(ruleContext, styleArguments as TSESTree.Node, processProperty);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'globalStyle':
|
||||||
|
case 'globalKeyframes':
|
||||||
|
if (callExpression.arguments.length > 1) {
|
||||||
|
const styleArguments = callExpression.arguments[1];
|
||||||
|
processStyleNode(ruleContext, styleArguments as TSESTree.Node, processProperty);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'recipe':
|
||||||
|
switch (orderingStrategy) {
|
||||||
|
case 'alphabetical':
|
||||||
|
enforceAlphabeticalCSSOrderInRecipe(callExpression as TSESTree.CallExpression, ruleContext);
|
||||||
|
break;
|
||||||
|
case 'concentric':
|
||||||
|
enforceConcentricCSSOrderInRecipe(ruleContext, callExpression as TSESTree.CallExpression);
|
||||||
|
break;
|
||||||
|
case 'userDefinedGroupOrder':
|
||||||
|
if (userDefinedGroupOrder) {
|
||||||
|
enforceUserDefinedGroupOrderInRecipe(
|
||||||
|
ruleContext,
|
||||||
|
callExpression as TSESTree.CallExpression,
|
||||||
|
userDefinedGroupOrder,
|
||||||
|
sortRemainingProperties,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backwards-compatible alias that maintains the original API.
|
||||||
|
* Uses reference tracking internally for automatic detection of vanilla-extract functions.
|
||||||
|
*/
|
||||||
|
export const createNodeVisitors = createReferenceBasedNodeVisitors;
|
||||||
321
src/css-rules/shared-utils/reference-tracker.ts
Normal file
321
src/css-rules/shared-utils/reference-tracker.ts
Normal file
|
|
@ -0,0 +1,321 @@
|
||||||
|
import type { Rule } from 'eslint';
|
||||||
|
import { TSESTree } from '@typescript-eslint/utils';
|
||||||
|
|
||||||
|
export interface ImportReference {
|
||||||
|
source: string;
|
||||||
|
importedName: string;
|
||||||
|
localName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WrapperFunctionInfo {
|
||||||
|
originalFunction: string; // 'style', 'recipe', etc.
|
||||||
|
parameterMapping: number; // which parameter index contains the style object
|
||||||
|
objectPath?: string[]; // path to the style object within the parameter (e.g., ['@layer', 'componentLayer'])
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrackedFunctions {
|
||||||
|
styleFunctions: Set<string>;
|
||||||
|
recipeFunctions: Set<string>;
|
||||||
|
fontFaceFunctions: Set<string>;
|
||||||
|
globalFunctions: Set<string>;
|
||||||
|
keyframeFunctions: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks vanilla-extract function imports and their local bindings
|
||||||
|
*/
|
||||||
|
export class ReferenceTracker {
|
||||||
|
private imports: Map<string, ImportReference> = new Map();
|
||||||
|
private trackedFunctions: TrackedFunctions;
|
||||||
|
private wrapperFunctions: Map<string, WrapperFunctionInfo> = new Map(); // wrapper function name -> detailed info
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.trackedFunctions = {
|
||||||
|
styleFunctions: new Set(),
|
||||||
|
recipeFunctions: new Set(),
|
||||||
|
fontFaceFunctions: new Set(),
|
||||||
|
globalFunctions: new Set(),
|
||||||
|
keyframeFunctions: new Set(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes import declarations to track vanilla-extract functions
|
||||||
|
*/
|
||||||
|
processImportDeclaration(node: TSESTree.ImportDeclaration): void {
|
||||||
|
const source = node.source.value;
|
||||||
|
|
||||||
|
// Check if this is a vanilla-extract import
|
||||||
|
if (typeof source !== 'string' || !this.isVanillaExtractSource(source)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 reference: ImportReference = {
|
||||||
|
source,
|
||||||
|
importedName,
|
||||||
|
localName,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.imports.set(localName, reference);
|
||||||
|
this.categorizeFunction(localName, importedName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes variable declarations to track re-assignments and destructuring
|
||||||
|
*/
|
||||||
|
processVariableDeclarator(node: TSESTree.VariableDeclarator): void {
|
||||||
|
// 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: ImportReference = {
|
||||||
|
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: TSESTree.FunctionDeclaration): void {
|
||||||
|
if (node.id?.name) {
|
||||||
|
this.analyzeWrapperFunction(node.id.name, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyzes a function to see if it wraps a vanilla-extract function
|
||||||
|
*/
|
||||||
|
private analyzeWrapperFunction(
|
||||||
|
functionName: string,
|
||||||
|
functionNode: TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration,
|
||||||
|
): void {
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private analyzeWrapperExpression(wrapperName: string, expression: TSESTree.Node): void {
|
||||||
|
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: WrapperFunctionInfo = {
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private checkForVanillaExtractCall(wrapperName: string, node: TSESTree.Node): void {
|
||||||
|
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: WrapperFunctionInfo = {
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private traverseBlockForVanillaExtractCalls(wrapperName: string, block: TSESTree.BlockStatement): void {
|
||||||
|
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: string): boolean {
|
||||||
|
return this.imports.has(functionName) || this.wrapperFunctions.has(functionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the category of a tracked function
|
||||||
|
*/
|
||||||
|
getFunctionCategory(functionName: string): keyof TrackedFunctions | null {
|
||||||
|
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: string): string | null {
|
||||||
|
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: string): WrapperFunctionInfo | null {
|
||||||
|
return this.wrapperFunctions.get(functionName) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all tracked functions by category
|
||||||
|
*/
|
||||||
|
getTrackedFunctions(): TrackedFunctions {
|
||||||
|
return this.trackedFunctions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the tracker state (useful for processing multiple files)
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isVanillaExtractSource(source: string): boolean {
|
||||||
|
return (
|
||||||
|
source === '@vanilla-extract/css' ||
|
||||||
|
source === '@vanilla-extract/recipes' ||
|
||||||
|
source.startsWith('@vanilla-extract/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private categorizeFunction(localName: string, importedName: string): void {
|
||||||
|
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: ReferenceTracker): Rule.RuleListener {
|
||||||
|
return {
|
||||||
|
ImportDeclaration(node: Rule.Node) {
|
||||||
|
tracker.processImportDeclaration(node as TSESTree.ImportDeclaration);
|
||||||
|
},
|
||||||
|
|
||||||
|
VariableDeclarator(node: Rule.Node) {
|
||||||
|
tracker.processVariableDeclarator(node as TSESTree.VariableDeclarator);
|
||||||
|
},
|
||||||
|
|
||||||
|
FunctionDeclaration(node: Rule.Node) {
|
||||||
|
tracker.processFunctionDeclaration(node as TSESTree.FunctionDeclaration);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue