fix: move wrapper detection to global settings and support cross-module style/recipe wrappers

This commit is contained in:
seongminn 2026-03-21 17:54:16 +09:00
parent 62b1844b44
commit 3c223c2909
10 changed files with 482 additions and 7 deletions

View file

@ -88,5 +88,35 @@ run({
});
`,
},
// Imported local recipe wrapper with global settings recipe
{
code: `
import { componentRecipe } from './component-recipe.css.js';
const myRecipe = componentRecipe({
base: {
display: 'flex',
alignItems: 'center'
}
});
`,
settings: {
'vanilla-extract': {
recipe: ['componentRecipe'],
},
},
errors: [{ messageId: 'alphabeticalOrder' }],
output: `
import { componentRecipe } from './component-recipe.css.js';
const myRecipe = componentRecipe({
base: {
alignItems: 'center',
display: 'flex'
}
});
`,
},
],
});

View file

@ -195,5 +195,69 @@ run({
});
`,
},
// Imported local wrapper with global settings style
{
code: `
import { componentStyle } from './style.css.js';
export const myStyle = componentStyle({
padding: '18px',
backgroundColor: 'black',
});
`,
settings: {
'vanilla-extract': {
style: ['componentStyle'],
},
},
errors: [{ messageId: 'alphabeticalOrder' }],
output: `
import { componentStyle } from './style.css.js';
export const myStyle = componentStyle({
backgroundColor: 'black',
padding: '18px',
});
`,
},
// Both vanilla style and configured wrapper should be linted (wrapper augments, not replaces)
{
code: `
import { style } from '@vanilla-extract/css';
import { componentStyle } from './style.css.js';
export const a = style({
display: 'flex',
color: 'white',
});
export const b = componentStyle({
padding: '18px',
backgroundColor: 'black',
});
`,
settings: {
'vanilla-extract': {
style: ['componentStyle'],
},
},
errors: [{ messageId: 'alphabeticalOrder' }, { messageId: 'alphabeticalOrder' }],
output: `
import { style } from '@vanilla-extract/css';
import { componentStyle } from './style.css.js';
export const a = style({
color: 'white',
display: 'flex',
});
export const b = componentStyle({
backgroundColor: 'black',
padding: '18px',
});
`,
},
],
});

View file

@ -97,5 +97,39 @@ run({
});
`,
},
// Imported local recipe wrapper with global settings recipe
{
code: `
import { componentRecipe } from './recipe.css.js';
const myRecipe = componentRecipe({
base: {
backgroundColor: 'white',
width: '100%',
display: 'flex',
position: 'relative'
},
});
`,
settings: {
'vanilla-extract': {
recipe: ['componentRecipe'],
},
},
errors: [{ messageId: 'incorrectOrder' }],
output: `
import { componentRecipe } from './recipe.css.js';
const myRecipe = componentRecipe({
base: {
position: 'relative',
display: 'flex',
backgroundColor: 'white',
width: '100%'
},
});
`,
},
],
});

View file

@ -213,5 +213,69 @@ run({
});
`,
},
// Imported local wrapper with global settings style
{
code: `
import { componentStyle } from './style.css.js';
export const myStyle = componentStyle({
padding: '18px',
backgroundColor: 'black',
});
`,
settings: {
'vanilla-extract': {
style: ['componentStyle'],
},
},
errors: [{ messageId: 'incorrectOrder' }],
output: `
import { componentStyle } from './style.css.js';
export const myStyle = componentStyle({
backgroundColor: 'black',
padding: '18px',
});
`,
},
// Both vanilla style and configured wrapper should be linted (wrapper augments, not replaces)
{
code: `
import { style } from '@vanilla-extract/css';
import { componentStyle } from './layer-style.css.js';
export const a = style({
color: 'white',
display: 'flex',
});
export const b = componentStyle({
padding: '18px',
backgroundColor: 'black',
});
`,
settings: {
'vanilla-extract': {
style: ['componentStyle'],
},
},
errors: [{ messageId: 'incorrectOrder' }, { messageId: 'incorrectOrder' }],
output: `
import { style } from '@vanilla-extract/css';
import { componentStyle } from './layer-style.css.js';
export const a = style({
display: 'flex',
color: 'white',
});
export const b = componentStyle({
backgroundColor: 'black',
padding: '18px',
});
`,
},
],
});

View file

@ -207,5 +207,46 @@ run({
});
`,
},
// Imported local recipe wrapper with global settings recipe
{
code: `
import { componentRecipe } from './component-recipe.css.js';
const myRecipe = componentRecipe({
base: {
backgroundColor: 'white',
width: '100%',
display: 'flex',
alignItems: 'center',
margin: 0
}
});
`,
options: [
{
groupOrder: ['dimensions', 'margin', 'font', 'border', 'boxShadow'],
sortRemainingProperties: 'concentric',
},
],
settings: {
'vanilla-extract': {
recipe: ['componentRecipe'],
},
},
errors: [{ messageId: 'incorrectOrder' }],
output: `
import { componentRecipe } from './component-recipe.css.js';
const myRecipe = componentRecipe({
base: {
width: '100%',
margin: 0,
display: 'flex',
alignItems: 'center',
backgroundColor: 'white'
}
});
`,
},
],
});

View file

@ -375,5 +375,81 @@ run({
});
`,
},
// Imported local wrapper with global settings style
{
code: `
import { componentStyle } from './style.css.js';
export const myStyle = componentStyle({
padding: '18px',
backgroundColor: 'black',
});
`,
options: [
{
groupOrder: ['dimensions', 'margin', 'font', 'border', 'boxShadow'],
sortRemainingProperties: 'concentric',
},
],
settings: {
'vanilla-extract': {
style: ['componentStyle'],
},
},
errors: [{ messageId: 'incorrectOrder' }],
output: `
import { componentStyle } from './style.css.js';
export const myStyle = componentStyle({
backgroundColor: 'black',
padding: '18px',
});
`,
},
// Both vanilla style and configured wrapper should be linted (wrapper augments, not replaces)
{
code: `
import { style } from '@vanilla-extract/css';
import { componentStyle } from './style.css.js';
export const a = style({
color: 'white',
display: 'flex',
});
export const b = componentStyle({
padding: '18px',
backgroundColor: 'black',
});
`,
options: [
{
groupOrder: ['dimensions', 'margin', 'font', 'border', 'boxShadow'],
sortRemainingProperties: 'concentric',
},
],
settings: {
'vanilla-extract': {
style: ['componentStyle'],
},
},
errors: [{ messageId: 'incorrectOrder' }, { messageId: 'incorrectOrder' }],
output: `
import { style } from '@vanilla-extract/css';
import { componentStyle } from './style.css.js';
export const a = style({
display: 'flex',
color: 'white',
});
export const b = componentStyle({
backgroundColor: 'black',
padding: '18px',
});
`,
},
],
});

View file

@ -10,7 +10,7 @@ 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';
import type { OrderingStrategy, VanillaExtractPluginSettings } from '../types.js';
/**
* Creates an ESLint rule listener with visitors for style-related function calls using reference tracking.
@ -28,7 +28,8 @@ export const createNodeVisitors = (
userDefinedGroupOrder?: string[],
sortRemainingProperties?: SortRemainingProperties,
): Rule.RuleListener => {
const tracker = new ReferenceTracker();
const wrapperSettings = resolveWrapperSettings(ruleContext);
const tracker = new ReferenceTracker(wrapperSettings);
const trackingVisitor = createReferenceTrackingVisitor(tracker);
return {
@ -105,6 +106,21 @@ export const createNodeVisitors = (
};
};
const resolveWrapperSettings = (
ruleContext: Rule.RuleContext,
): {
style: string[];
recipe: string[];
} => {
const pluginSettings = (ruleContext.settings?.['vanilla-extract'] ??
{}) as VanillaExtractPluginSettings;
return {
style: pluginSettings.style ?? [],
recipe: pluginSettings.recipe ?? [],
};
};
/**
* Helper function to process style ordering for style-related functions
*/

View file

@ -27,8 +27,10 @@ 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
private style: Set<string>;
private recipe: Set<string>;
constructor() {
constructor(options?: { style?: string[]; recipe?: string[] }) {
this.trackedFunctions = {
styleFunctions: new Set(),
recipeFunctions: new Set(),
@ -36,6 +38,8 @@ export class ReferenceTracker {
globalFunctions: new Set(),
keyframeFunctions: new Set(),
};
this.style = new Set(options?.style ?? []);
this.recipe = new Set(options?.recipe ?? []);
}
/**
@ -44,25 +48,38 @@ export class ReferenceTracker {
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)) {
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: string;
if (isVanillaExtractImport) {
trackedImportName = importedName;
} else {
if (!customWrapper) {
return;
}
trackedImportName = customWrapper;
}
const reference: ImportReference = {
source,
importedName,
importedName: trackedImportName,
localName,
};
this.imports.set(localName, reference);
this.categorizeFunction(localName, importedName);
this.categorizeFunction(localName, trackedImportName);
}
});
}
@ -276,6 +293,18 @@ export class ReferenceTracker {
);
}
private getCustomWrapper(importedName: string, localName: string): 'style' | 'recipe' | null {
if (this.style.has(importedName) || this.style.has(localName)) {
return 'style';
}
if (this.recipe.has(importedName) || this.recipe.has(localName)) {
return 'recipe';
}
return null;
}
private categorizeFunction(localName: string, importedName: string): void {
switch (importedName) {
case 'style':

View file

@ -1 +1,6 @@
export type OrderingStrategy = 'alphabetical' | 'concentric' | 'userDefinedGroupOrder';
export interface VanillaExtractPluginSettings {
style?: string[];
recipe?: string[];
}