mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2026-04-15 16:27:25 +00:00
Merge 3c223c2909 into 62b1844b44
This commit is contained in:
commit
108d1ba5c8
10 changed files with 482 additions and 7 deletions
116
README.md
116
README.md
|
|
@ -304,6 +304,36 @@ export default [
|
|||
|
||||
> **Note:** Remember to enable only one ordering rule at a time. See the "Important" section above for details on switching between ordering rules.
|
||||
|
||||
### Global Settings (Recommended for shared wrappers)
|
||||
|
||||
If you use imported wrapper functions across multiple ordering rules, configure them once in ESLint `settings`.
|
||||
All ordering rules (`alphabetical-order`, `concentric-order`, `custom-order`) read this value.
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import vanillaExtract from '@antebudimir/eslint-plugin-vanilla-extract';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ['**/*.css.ts'],
|
||||
plugins: {
|
||||
'vanilla-extract': vanillaExtract,
|
||||
},
|
||||
settings: {
|
||||
'vanilla-extract': {
|
||||
style: ['componentStyle', 'layerStyle'],
|
||||
recipe: ['componentRecipe'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...vanillaExtract.configs.recommended.rules,
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
Configure wrapper names only via ESLint `settings` (`style`, `recipe`).
|
||||
|
||||
## Rules
|
||||
|
||||
### vanilla-extract/alphabetical-order
|
||||
|
|
@ -336,6 +366,32 @@ export const myStyle = style({
|
|||
});
|
||||
```
|
||||
|
||||
#### Global setting: `settings['vanilla-extract'].style`
|
||||
|
||||
`vanilla-extract/alphabetical-order` reads wrapper names from global ESLint settings.
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import vanillaExtract from '@antebudimir/eslint-plugin-vanilla-extract';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ['**/*.css.ts'],
|
||||
plugins: {
|
||||
'vanilla-extract': vanillaExtract,
|
||||
},
|
||||
settings: {
|
||||
'vanilla-extract': {
|
||||
style: ['componentStyle'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'vanilla-extract/alphabetical-order': 'error',
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### vanilla-extract/concentric-order
|
||||
|
||||
This rule enforces that CSS properties in vanilla-extract style objects follow the [concentric CSS](#concentric-css-model) ordering pattern, which organizes properties from outside to inside.
|
||||
|
|
@ -364,6 +420,33 @@ export const myStyle = style({
|
|||
});
|
||||
```
|
||||
|
||||
#### Global setting: `settings['vanilla-extract'].style`
|
||||
|
||||
Use this setting when style wrappers are imported from local modules (for example `componentStyle`, `layerStyle`).
|
||||
If a wrapper function name is listed, the rule treats calls to that function like `style(...)` calls.
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import vanillaExtract from '@antebudimir/eslint-plugin-vanilla-extract';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ['**/*.css.ts'],
|
||||
plugins: {
|
||||
'vanilla-extract': vanillaExtract,
|
||||
},
|
||||
settings: {
|
||||
'vanilla-extract': {
|
||||
style: ['componentStyle', 'layerStyle'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'vanilla-extract/concentric-order': 'error',
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### vanilla-extract/custom-order
|
||||
|
||||
The `vanilla-extract/custom-order` rule enables you to enforce a custom ordering of CSS properties in your
|
||||
|
|
@ -388,6 +471,39 @@ To configure the rule, add it to your ESLint configuration file with your desire
|
|||
`groups` array to include any number of available CSS property groups you want to enforce, with a minimum of one group
|
||||
required.
|
||||
|
||||
#### Global setting: `settings['vanilla-extract'].style`
|
||||
|
||||
`vanilla-extract/custom-order` also reads `style` from global settings together with
|
||||
`groupOrder` and `sortRemainingProperties`.
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import vanillaExtract from '@antebudimir/eslint-plugin-vanilla-extract';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ['**/*.css.ts'],
|
||||
plugins: {
|
||||
'vanilla-extract': vanillaExtract,
|
||||
},
|
||||
settings: {
|
||||
'vanilla-extract': {
|
||||
style: ['componentStyle'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'vanilla-extract/custom-order': [
|
||||
'error',
|
||||
{
|
||||
groupOrder: ['font', 'dimensions', 'margin', 'padding', 'position', 'border'],
|
||||
sortRemainingProperties: 'alphabetical',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Incorrect (Unordered)
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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%'
|
||||
},
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -1 +1,6 @@
|
|||
export type OrderingStrategy = 'alphabetical' | 'concentric' | 'userDefinedGroupOrder';
|
||||
|
||||
export interface VanillaExtractPluginSettings {
|
||||
style?: string[];
|
||||
recipe?: string[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue