mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2025-12-31 08:53:33 +00:00
feat 🥁: implement special ordering for fontFace APIs
- Ensure 'src' property always appears first - Sort remaining properties alphabetically - Handle both APIs correctly despite different argument structures - Handles font faces ordering the same in all 3 available CSS rules - Update documentation with fontFace ordering details
This commit is contained in:
parent
8916be7d16
commit
3e9bad1b02
12 changed files with 175 additions and 47 deletions
36
README.md
36
README.md
|
|
@ -180,6 +180,34 @@ export const myStyle = style({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Font Face Declarations
|
||||||
|
|
||||||
|
For `fontFace` and `globalFontFace` API calls, all three ordering rules (alphabetical, concentric, and custom) enforce the same special ordering:
|
||||||
|
|
||||||
|
1. The `src` property always appears first
|
||||||
|
2. All remaining properties are sorted alphabetically
|
||||||
|
|
||||||
|
This special handling is applied because:
|
||||||
|
|
||||||
|
- The `src` property is the most critical property in font face declarations
|
||||||
|
- Consistent ordering improves readability for these specific APIs
|
||||||
|
- Font-related properties are specialized and benefit from standardized ordering
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct ordering for font faces
|
||||||
|
export const theFont = fontFace({
|
||||||
|
src: ['url("/fonts/MyFont.woff2") format("woff2")', 'url("/fonts/MyFont.woff") format("woff")'],
|
||||||
|
ascentOverride: '90%',
|
||||||
|
descentOverride: '10%',
|
||||||
|
fontDisplay: 'swap',
|
||||||
|
fontFeatureSettings: '"liga" 1',
|
||||||
|
fontStretch: 'normal',
|
||||||
|
// ...other properties in alphabetical order
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Opinionated, but it is what it is. If someone has a suggestion for a better ordering, let me know!
|
||||||
|
|
||||||
## Concentric CSS Model
|
## Concentric CSS Model
|
||||||
|
|
||||||
Here's a list of all available groups from the provided [concentricGroups](src/css-rules/concentric-order/concentric-groups.ts) array:
|
Here's a list of all available groups from the provided [concentricGroups](src/css-rules/concentric-order/concentric-groups.ts) array:
|
||||||
|
|
@ -224,22 +252,22 @@ The roadmap outlines the project's current status and future plans:
|
||||||
|
|
||||||
- Initial release with support for alphabetical, concentric, and custom group CSS ordering.
|
- Initial release with support for alphabetical, concentric, and custom group CSS ordering.
|
||||||
- Auto-fix capability integrated into ESLint.
|
- Auto-fix capability integrated into ESLint.
|
||||||
- Support for multiple vanilla-extract APIs (e.g., `style`, `styleVariants`, `recipe`, `globalStyle` etc.).
|
- Support for multiple vanilla-extract APIs (e.g., `style`, `styleVariants`, `recipe`, `globalStyle`, `fontFace`, etc.).
|
||||||
|
|
||||||
### Current Work
|
### Current Work
|
||||||
|
|
||||||
- `fontFace` and `globalFontFace` linting support.
|
- Test coverage.
|
||||||
|
|
||||||
### Upcoming Features
|
### Upcoming Features
|
||||||
|
|
||||||
- Test coverage.
|
|
||||||
- `no-empty-blocks` rule to disallow empty blocks.
|
- `no-empty-blocks` rule to disallow empty blocks.
|
||||||
- `no-unknown-units` rule to disallow unknown units.
|
- `no-unknown-units` rule to disallow unknown units.
|
||||||
- `no-number-trailing-zeros` rule to disallow trailing zeros in numbers.
|
- `no-number-trailing-zeros` rule to disallow trailing zeros in numbers.
|
||||||
- `no-zero-unit` rule to disallow units when the value is zero.
|
- `no-zero-unit` rule to disallow units when the value is zero.
|
||||||
- `np-px-unit` rule to disallow use of `px` units with configurable whitelist.
|
- `no-px-unit` rule to disallow use of `px` units with configurable whitelist.
|
||||||
- `prefer-logical-properties` rule to enforce use of logical properties.
|
- `prefer-logical-properties` rule to enforce use of logical properties.
|
||||||
- `prefer-theme-tokens` rule to enforce use of theme tokens instead of hard-coded values when available.
|
- `prefer-theme-tokens` rule to enforce use of theme tokens instead of hard-coded values when available.
|
||||||
|
- `no-global-style` rule to disallow use of `globalStyle` function.
|
||||||
- Option to sort properties within user-defined concentric groups alphabetically instead of following the concentric order. **Note**: This feature will only be implemented if there's sufficient interest from the community.
|
- Option to sort properties within user-defined concentric groups alphabetically instead of following the concentric order. **Note**: This feature will only be implemented if there's sufficient interest from the community.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@antebudimir/eslint-plugin-vanilla-extract",
|
"name": "@antebudimir/eslint-plugin-vanilla-extract",
|
||||||
"version": "1.3.1",
|
"version": "1.4.0",
|
||||||
"description": "ESLint plugin for enforcing CSS ordering in vanilla-extract styles",
|
"description": "ESLint plugin for enforcing CSS ordering in vanilla-extract styles",
|
||||||
"author": "Ante Budimir",
|
"author": "Ante Budimir",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,9 @@
|
||||||
import type { Rule } from 'eslint';
|
import type { Rule } from 'eslint';
|
||||||
import { TSESTree } from '@typescript-eslint/utils';
|
import { TSESTree } from '@typescript-eslint/utils';
|
||||||
|
import { comparePropertiesAlphabetically } from '../shared-utils/alphabetical-property-comparator.js';
|
||||||
import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js';
|
import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js';
|
||||||
import { getPropertyName } from '../shared-utils/property-separator.js';
|
import { getPropertyName } from '../shared-utils/property-separator.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two CSS properties alphabetically.
|
|
||||||
* @param firstProperty The first property to compare.
|
|
||||||
* @param secondProperty The second property to compare.
|
|
||||||
* @returns A number indicating the relative order of the properties (-1, 0, or 1).
|
|
||||||
*/
|
|
||||||
const comparePropertiesAlphabetically = (
|
|
||||||
firstProperty: TSESTree.Property,
|
|
||||||
secondProperty: TSESTree.Property,
|
|
||||||
): number => {
|
|
||||||
const firstName = getPropertyName(firstProperty);
|
|
||||||
const secondName = getPropertyName(secondProperty);
|
|
||||||
return firstName.localeCompare(secondName);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reports an ordering issue to ESLint and generates fixes.
|
* Reports an ordering issue to ESLint and generates fixes.
|
||||||
* @param ruleContext The ESLint rule context.
|
* @param ruleContext The ESLint rule context.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ const alphabeticalOrderRule: Rule.RuleModule = {
|
||||||
fixable: 'code',
|
fixable: 'code',
|
||||||
schema: [],
|
schema: [],
|
||||||
messages: {
|
messages: {
|
||||||
alphabeticalOrder: "Property '{{next}}' should come before '{{current}}' in alphabetical order.",
|
alphabeticalOrder: "Property '{{nextProperty}}' should come before '{{currentProperty}}' in alphabetical order.",
|
||||||
|
fontFaceOrder:
|
||||||
|
"Properties in fontFace should be ordered with 'src' first, followed by other properties in alphabetical order. Property '{{nextProperty}}' should come before '{{currentProperty}}'.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
create(context) {
|
create(context) {
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ const reportOrderingIssue = (
|
||||||
node: nextProperty.node as Rule.Node,
|
node: nextProperty.node as Rule.Node,
|
||||||
messageId: 'incorrectOrder',
|
messageId: 'incorrectOrder',
|
||||||
data: {
|
data: {
|
||||||
next: nextProperty.name,
|
nextProperty: nextProperty.name,
|
||||||
current: currentProperty.name,
|
currentProperty: currentProperty.name,
|
||||||
},
|
},
|
||||||
fix: (fixer) =>
|
fix: (fixer) =>
|
||||||
generateFixesForCSSOrder(
|
generateFixesForCSSOrder(
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,15 @@ const concentricOrderRule: Rule.RuleModule = {
|
||||||
description: 'enforce concentric CSS property ordering in vanilla-extract styles',
|
description: 'enforce concentric CSS property ordering in vanilla-extract styles',
|
||||||
category: 'Stylistic Issues',
|
category: 'Stylistic Issues',
|
||||||
recommended: true,
|
recommended: true,
|
||||||
|
url: 'https://rhodesmill.org/brandon/2011/concentric-css/',
|
||||||
},
|
},
|
||||||
fixable: 'code',
|
fixable: 'code',
|
||||||
schema: [],
|
schema: [],
|
||||||
messages: {
|
messages: {
|
||||||
incorrectOrder: "Property '{{next}}' should come before '{{current}}' according to concentric CSS ordering.",
|
incorrectOrder:
|
||||||
|
"Property '{{nextProperty}}' should come before '{{currentProperty}}' according to concentric CSS ordering.",
|
||||||
|
fontFaceOrder:
|
||||||
|
"Properties in fontFace should be ordered with 'src' first, followed by other properties in alphabetical order. Property '{{nextProperty}}' should come before '{{currentProperty}}'.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
create(context) {
|
create(context) {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ const customGroupOrderRule: Rule.RuleModule = {
|
||||||
messages: {
|
messages: {
|
||||||
incorrectOrder:
|
incorrectOrder:
|
||||||
"Property '{{nextProperty}}' should come before '{{currentProperty}}' according to custom CSS group ordering.",
|
"Property '{{nextProperty}}' should come before '{{currentProperty}}' according to custom CSS group ordering.",
|
||||||
|
fontFaceOrder:
|
||||||
|
"Properties in fontFace should be ordered with 'src' first, followed by other properties in alphabetical order. Property '{{nextProperty}}' should come before '{{currentProperty}}'.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
create(ruleContext: Rule.RuleContext) {
|
create(ruleContext: Rule.RuleContext) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { getPropertyName } from './property-separator.js';
|
||||||
|
import type { TSESTree } from '@typescript-eslint/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two CSS properties alphabetically.
|
||||||
|
* @param firstProperty The first property to compare.
|
||||||
|
* @param secondProperty The second property to compare.
|
||||||
|
* @returns A number indicating the relative order of the properties (-1, 0, or 1).
|
||||||
|
*/
|
||||||
|
export const comparePropertiesAlphabetically = (
|
||||||
|
firstProperty: TSESTree.Property,
|
||||||
|
secondProperty: TSESTree.Property,
|
||||||
|
): number => {
|
||||||
|
const firstName = getPropertyName(firstProperty);
|
||||||
|
const secondName = getPropertyName(secondProperty);
|
||||||
|
|
||||||
|
// Special handling for 'src' property - it should always come first (relates to font face APIs only)
|
||||||
|
if (firstName === 'src') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (secondName === 'src') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstName.localeCompare(secondName);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import type { Rule } from 'eslint';
|
||||||
|
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
||||||
|
import { generateFixesForCSSOrder } from '../shared-utils/css-order-fixer.js';
|
||||||
|
import { getPropertyName, separateProperties } from '../shared-utils/property-separator.js';
|
||||||
|
import { comparePropertiesAlphabetically } from './alphabetical-property-comparator.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a font face declaration to enforce property ordering:
|
||||||
|
* 'src' first, then other properties alphabetically.
|
||||||
|
*
|
||||||
|
* @param ruleContext The ESLint rule context for reporting and fixing issues.
|
||||||
|
* @param fontFaceObject The object expression representing the font face declaration.
|
||||||
|
*/
|
||||||
|
export const enforceFontFaceOrder = (
|
||||||
|
ruleContext: Rule.RuleContext,
|
||||||
|
fontFaceObject: TSESTree.ObjectExpression,
|
||||||
|
): void => {
|
||||||
|
if (!fontFaceObject || fontFaceObject.type !== AST_NODE_TYPES.ObjectExpression) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { regularProperties } = separateProperties(fontFaceObject.properties);
|
||||||
|
|
||||||
|
if (regularProperties.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pairs of consecutive properties
|
||||||
|
const propertyPairs = regularProperties.slice(0, -1).map((currentProperty, index) => ({
|
||||||
|
currentProperty,
|
||||||
|
nextProperty: regularProperties[index + 1] as TSESTree.Property,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const violatingPair = propertyPairs.find(
|
||||||
|
({ currentProperty, nextProperty }) => comparePropertiesAlphabetically(currentProperty, nextProperty) > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (violatingPair) {
|
||||||
|
const nextPropertyName = getPropertyName(violatingPair.nextProperty);
|
||||||
|
const currentPropertyName = getPropertyName(violatingPair.currentProperty);
|
||||||
|
|
||||||
|
ruleContext.report({
|
||||||
|
node: violatingPair.nextProperty as Rule.Node,
|
||||||
|
messageId: 'fontFaceOrder',
|
||||||
|
data: {
|
||||||
|
nextProperty: nextPropertyName,
|
||||||
|
currentProperty: currentPropertyName,
|
||||||
|
},
|
||||||
|
fix: (fixer) =>
|
||||||
|
generateFixesForCSSOrder(
|
||||||
|
fixer,
|
||||||
|
ruleContext,
|
||||||
|
regularProperties,
|
||||||
|
comparePropertiesAlphabetically,
|
||||||
|
(property) => property as Rule.Node,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -6,6 +6,7 @@ import { enforceConcentricCSSOrderInRecipe } from '../concentric-order/recipe-or
|
||||||
import { enforceConcentricCSSOrderInStyleObject } from '../concentric-order/style-object-processor.js';
|
import { enforceConcentricCSSOrderInStyleObject } from '../concentric-order/style-object-processor.js';
|
||||||
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 { processStyleNode } from './style-node-processor.js';
|
import { processStyleNode } from './style-node-processor.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -58,6 +59,26 @@ export const createNodeVisitors = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontFaceFunctionArgumentIndexMap = {
|
||||||
|
fontFace: 0, // First argument (index 0)
|
||||||
|
globalFontFace: 1, // Second argument (index 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle font face functions with special ordering
|
||||||
|
if (
|
||||||
|
node.callee.name in fontFaceFunctionArgumentIndexMap &&
|
||||||
|
node.arguments.length >
|
||||||
|
fontFaceFunctionArgumentIndexMap[node.callee.name as keyof typeof fontFaceFunctionArgumentIndexMap]
|
||||||
|
) {
|
||||||
|
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
|
// Handle style-related functions
|
||||||
if (
|
if (
|
||||||
['createThemeContract', 'createVar', 'createTheme', 'keyframes', 'style', 'styleVariants'].includes(
|
['createThemeContract', 'createVar', 'createTheme', 'keyframes', 'style', 'styleVariants'].includes(
|
||||||
|
|
@ -65,8 +86,8 @@ export const createNodeVisitors = (
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (node.arguments.length > 0) {
|
if (node.arguments.length > 0) {
|
||||||
const styleArg = node.arguments[0];
|
const styleArguments = node.arguments[0];
|
||||||
processStyleNode(ruleContext, styleArg as TSESTree.Node, processProperty);
|
processStyleNode(ruleContext, styleArguments as TSESTree.Node, processProperty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,8 +96,8 @@ export const createNodeVisitors = (
|
||||||
(node.callee.name === 'globalKeyframes' || node.callee.name === 'globalStyle') &&
|
(node.callee.name === 'globalKeyframes' || node.callee.name === 'globalStyle') &&
|
||||||
node.arguments.length >= 2
|
node.arguments.length >= 2
|
||||||
) {
|
) {
|
||||||
const styleArg = node.arguments[1];
|
const styleArguments = node.arguments[1];
|
||||||
processStyleNode(ruleContext, styleArg as TSESTree.Node, processProperty);
|
processStyleNode(ruleContext, styleArguments as TSESTree.Node, processProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle recipe function
|
// Handle recipe function
|
||||||
|
|
|
||||||
|
|
@ -13,37 +13,37 @@ import { recipe } from '@vanilla-extract/recipes';
|
||||||
export const theFont = fontFace({
|
export const theFont = fontFace({
|
||||||
// Comment to test that the linter doesn't remove it
|
// Comment to test that the linter doesn't remove it
|
||||||
src: ['url("/fonts/MyFont.woff2") format("woff2")', 'url("/fonts/MyFont.woff") format("woff")'],
|
src: ['url("/fonts/MyFont.woff2") format("woff2")', 'url("/fonts/MyFont.woff") format("woff")'],
|
||||||
fontWeight: '400 700',
|
|
||||||
fontStyle: 'normal',
|
|
||||||
fontStretch: 'normal',
|
|
||||||
fontDisplay: 'swap',
|
|
||||||
unicodeRange:
|
|
||||||
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
|
|
||||||
ascentOverride: '90%',
|
ascentOverride: '90%',
|
||||||
descentOverride: '10%',
|
descentOverride: '10%',
|
||||||
|
fontDisplay: 'swap',
|
||||||
|
fontFeatureSettings: '"liga" 1',
|
||||||
|
fontStretch: 'normal',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
fontVariant: 'normal',
|
||||||
|
fontVariationSettings: '"wght" 400',
|
||||||
|
fontWeight: '400 700',
|
||||||
lineGapOverride: '10%',
|
lineGapOverride: '10%',
|
||||||
sizeAdjust: '90%',
|
sizeAdjust: '90%',
|
||||||
fontVariant: 'normal',
|
unicodeRange:
|
||||||
fontFeatureSettings: '"liga" 1',
|
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
|
||||||
fontVariationSettings: '"wght" 400',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
globalFontFace('GlobalFont', {
|
globalFontFace('GlobalFont', {
|
||||||
// Comment to test that the linter doesn't remove it
|
// Comment to test that the linter doesn't remove it
|
||||||
src: ['url("/fonts/MyFont.woff2") format("woff2")', 'url("/fonts/MyFont.woff") format("woff")'],
|
src: ['url("/fonts/MyFont.woff2") format("woff2")', 'url("/fonts/MyFont.woff") format("woff")'],
|
||||||
fontWeight: '400 700',
|
|
||||||
fontStyle: 'normal',
|
|
||||||
fontStretch: 'normal',
|
|
||||||
fontDisplay: 'swap',
|
|
||||||
unicodeRange:
|
|
||||||
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
|
|
||||||
ascentOverride: '90%',
|
ascentOverride: '90%',
|
||||||
descentOverride: '10%',
|
descentOverride: '10%',
|
||||||
|
fontDisplay: 'swap',
|
||||||
|
fontFeatureSettings: '"liga" 1',
|
||||||
|
fontStretch: 'normal',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
fontVariant: 'normal',
|
||||||
|
fontVariationSettings: '"wght" 400',
|
||||||
|
fontWeight: '400 700',
|
||||||
lineGapOverride: '10%',
|
lineGapOverride: '10%',
|
||||||
sizeAdjust: '90%',
|
sizeAdjust: '90%',
|
||||||
fontVariant: 'normal',
|
unicodeRange:
|
||||||
fontFeatureSettings: '"liga" 1',
|
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
|
||||||
fontVariationSettings: '"wght" 400',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// keyframes
|
// keyframes
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import customOrderRule from './css-rules/custom-order/rule-definition.js';
|
||||||
export const vanillaExtract = {
|
export const vanillaExtract = {
|
||||||
meta: {
|
meta: {
|
||||||
name: '@antebudimir/eslint-plugin-vanilla-extract',
|
name: '@antebudimir/eslint-plugin-vanilla-extract',
|
||||||
version: '1.3.1',
|
version: '1.4.0',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'alphabetical-order': alphabeticalOrderRule,
|
'alphabetical-order': alphabeticalOrderRule,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue