From c1b4e70bd9d73dfad1c89e3561f2bd1783d78dc1 Mon Sep 17 00:00:00 2001 From: Ante Budimir Date: Sat, 12 Apr 2025 20:43:11 +0300 Subject: [PATCH] =?UTF-8?q?feat=20=F0=9F=A5=81:=20add=20no-zero-unit=20rul?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 + README.md | 37 +- package.json | 2 +- .../no-empty-blocks/conditional-processor.ts | 6 +- .../empty-nested-style-processor.ts | 6 +- .../empty-style-visitor-creator.ts | 240 ++++++------ src/css-rules/no-empty-blocks/node-remover.ts | 4 +- .../no-empty-blocks/property-utils.ts | 4 +- .../no-empty-blocks/recipe-processor.ts | 6 +- .../style-variants-processor.ts | 6 +- .../no-zero-unit/_tests_/no-zero-unit.test.ts | 341 ++++++++++++++++++ src/css-rules/no-zero-unit/index.ts | 3 + src/css-rules/no-zero-unit/rule-definition.ts | 23 ++ .../no-zero-unit/zero-unit-processor.ts | 36 ++ .../no-zero-unit/zero-unit-visitor-creator.ts | 56 +++ .../order-strategy-visitor-creator.ts | 9 +- src/css-sample/sample.css.ts | 274 +++++++------- src/index.ts | 5 +- tsconfig.json | 2 + 19 files changed, 784 insertions(+), 285 deletions(-) create mode 100644 src/css-rules/no-zero-unit/_tests_/no-zero-unit.test.ts create mode 100644 src/css-rules/no-zero-unit/index.ts create mode 100644 src/css-rules/no-zero-unit/rule-definition.ts create mode 100644 src/css-rules/no-zero-unit/zero-unit-processor.ts create mode 100644 src/css-rules/no-zero-unit/zero-unit-visitor-creator.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac51c7..d53d151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.8.0] - 2025-04-12 + +- add new rule `no-zero-unit` that enforces unitless zero values in vanilla-extract style objects + - Automatically removes unnecessary units from zero values (e.g., '0px' → '0') + - Handles both positive and negative zero values + - Preserves units where required (time properties, CSS functions) + - Works with all vanilla-extract APIs including style, recipe, fontFace, and keyframes + - Supports nested objects, media queries, and pseudo-selectors + ## [1.7.0] - 2025-04-07 - add a recommended configuration preset that enables concentric-order and no-empty-style-blocks rules with error severity. diff --git a/README.md b/README.md index 9680823..3086400 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ export default [ // Optionally override specific rules // 'vanilla-extract/concentric-order': 'warn', // Change severity from error to warn // 'vanilla-extract/no-empty-style-blocks': 'off', // Disable a recommended rule + // 'vanilla-extract/no-zero-unit': 'warn', // Change severity from error to warn // Add additional rules not in recommended config // 'vanilla-extract/alphabetical-order': 'error', // Override concentric-order rule @@ -80,6 +81,7 @@ The recommended configuration enables the following rules with error severity: - `vanilla-extract/concentric-order`: Enforces concentric CSS property ordering - `vanilla-extract/no-empty-style-blocks`: Prevents empty style blocks +- `vanilla-extract/no-zero-unit`: removes unnecessary units for zero values You can use the recommended configuration as a starting point and override rules as needed for your project. @@ -108,6 +110,7 @@ export default [ sortRemainingProperties: 'concentric', // 'alphabetical' is default }, ], + 'vanilla-extract/no-zero-unit': 'warn', }, }, ]; @@ -266,6 +269,34 @@ export const recipeWithEmptyVariants = recipe({ }); ``` +## vanilla-extract/no-zero-unit + +This rule enforces the removal of unnecessary units for zero values in vanilla-extract style objects. It helps maintain cleaner and more consistent CSS by eliminating redundant units when the value is zero. + +```typescript +// ❌ Incorrect +import { style } from '@vanilla-extract/css'; + +export const myStyle = style({ + margin: '0px', + padding: '0rem', + width: '0%', + height: '0vh', + top: '-0em', +}); + +// ✅ Correct +import { style } from '@vanilla-extract/css'; + +export const myStyle = style({ + margin: '0', + padding: '0', + width: '0', + height: '0', + top: '0', +}); +``` + ## Font Face Declarations For `fontFace` and `globalFontFace` API calls, all three ordering rules (alphabetical, concentric, and custom) enforce the same special ordering: @@ -341,16 +372,16 @@ The roadmap outlines the project's current status and future plans: - Support for multiple vanilla-extract APIs (e.g., `style`, `styleVariants`, `recipe`, `globalStyle`, `fontFace`, etc.). - `no-empty-style-blocks` rule to disallow empty blocks. - Recommended ESLint configuration for the plugin. +- `no-zero-unit` rule to disallow units when the value is zero. - Comprehensive rule testing. ### Current Work -- `no-zero-unit` rule to disallow units when the value is zero. +- `no-unknown-unit` rule to disallow unknown units. ### Upcoming Features -- `no-unknown-units` rule to disallow unknown units. -- `no-number-trailing-zeros` rule to disallow trailing zeros in numbers. +- `no-number-trailing-zero` rule to disallow trailing zeros in numbers. - `no-px-unit` rule to disallow use of `px` units with configurable whitelist. - `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. diff --git a/package.json b/package.json index d35b1a9..14de43e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antebudimir/eslint-plugin-vanilla-extract", - "version": "1.7.0", + "version": "1.8.0", "description": "ESLint plugin for enforcing best practices in vanilla-extract CSS styles, including CSS property ordering and additional linting rules.", "author": "Ante Budimir", "license": "MIT", diff --git a/src/css-rules/no-empty-blocks/conditional-processor.ts b/src/css-rules/no-empty-blocks/conditional-processor.ts index 3996813..c7f1220 100644 --- a/src/css-rules/no-empty-blocks/conditional-processor.ts +++ b/src/css-rules/no-empty-blocks/conditional-processor.ts @@ -6,12 +6,12 @@ import { reportEmptyDeclaration } from './fix-utils.js'; /** * Handles conditional expressions with empty objects. */ -export function processConditionalExpression( +export const processConditionalExpression = ( context: Rule.RuleContext, node: TSESTree.ConditionalExpression, reportedNodes: Set, callNode: TSESTree.CallExpression, -): void { +): void => { const isConsequentEmpty = node.consequent.type === 'ObjectExpression' && isEmptyObject(node.consequent); const isAlternateEmpty = node.alternate.type === 'ObjectExpression' && isEmptyObject(node.alternate); @@ -33,4 +33,4 @@ export function processConditionalExpression( messageId: 'emptyConditionalStyle', }); } -} +}; diff --git a/src/css-rules/no-empty-blocks/empty-nested-style-processor.ts b/src/css-rules/no-empty-blocks/empty-nested-style-processor.ts index 9536e7f..ea65089 100644 --- a/src/css-rules/no-empty-blocks/empty-nested-style-processor.ts +++ b/src/css-rules/no-empty-blocks/empty-nested-style-processor.ts @@ -7,11 +7,11 @@ import { areAllChildrenEmpty, getStyleKeyName } from './property-utils.js'; /** * Processes nested style objects like selectors and media queries. */ -export function processEmptyNestedStyles( +export const processEmptyNestedStyles = ( ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression, reportedNodes: Set, -): void { +): void => { node.properties.forEach((property) => { if (property.type !== 'Property') { return; @@ -72,4 +72,4 @@ export function processEmptyNestedStyles( }); } }); -} +}; diff --git a/src/css-rules/no-empty-blocks/empty-style-visitor-creator.ts b/src/css-rules/no-empty-blocks/empty-style-visitor-creator.ts index ff9820f..643f36f 100644 --- a/src/css-rules/no-empty-blocks/empty-style-visitor-creator.ts +++ b/src/css-rules/no-empty-blocks/empty-style-visitor-creator.ts @@ -9,128 +9,10 @@ import { getStyleKeyName } from './property-utils.js'; import { processRecipeProperties } from './recipe-processor.js'; import { processStyleVariants } from './style-variants-processor.js'; -/** - * Creates ESLint rule visitors for detecting empty style blocks in vanilla-extract. - * @param ruleContext The ESLint rule rule context. - * @returns An object with visitor functions for the ESLint rule. - */ -export const createEmptyStyleVisitors = (ruleContext: Rule.RuleContext): Rule.RuleListener => { - // Track reported nodes to prevent duplicate reports - const reportedNodes = new Set(); - - return { - CallExpression(node) { - if (node.callee.type !== 'Identifier') { - return; - } - - // Target vanilla-extract style functions - const styleApiFunctions = [ - 'style', - 'styleVariants', - 'recipe', - 'globalStyle', - 'fontFace', - 'globalFontFace', - 'keyframes', - 'globalKeyframes', - ]; - - if (!styleApiFunctions.includes(node.callee.name) || node.arguments.length === 0) { - return; - } - - // Handle styleVariants specifically - if (node.callee.name === 'styleVariants' && node.arguments[0]?.type === 'ObjectExpression') { - processStyleVariants(ruleContext, node.arguments[0] as TSESTree.ObjectExpression, reportedNodes); - - // If the entire styleVariants object is empty after processing, remove the declaration - if (isEmptyObject(node.arguments[0] as TSESTree.ObjectExpression)) { - reportEmptyDeclaration(ruleContext, node.arguments[0] as TSESTree.Node, node as TSESTree.CallExpression); - } - return; - } - - const defaultStyleArgumentIndex = 0; - const globalFunctionNames = ['globalStyle', 'globalFontFace', 'globalKeyframes']; - // Determine the style argument index based on the function name - const styleArgumentIndex = globalFunctionNames.includes(node.callee.name) ? 1 : defaultStyleArgumentIndex; - - // For global functions, check if we have enough arguments - if (styleArgumentIndex === 1 && node.arguments.length <= styleArgumentIndex) { - return; - } - - const styleArgument = node.arguments[styleArgumentIndex]; - - // This defensive check prevents duplicate processing of nodes. - // This code path's difficult to test because the ESLint visitor pattern - // typically ensures each node is only visited once per rule execution. - if (reportedNodes.has(styleArgument as TSESTree.Node)) { - return; - } - - // Handle conditional expressions - if (styleArgument?.type === 'ConditionalExpression') { - processConditionalExpression( - ruleContext, - styleArgument as TSESTree.ConditionalExpression, - reportedNodes, - node as TSESTree.CallExpression, - ); - return; - } - - // Direct empty object case - remove the entire declaration - if (styleArgument?.type === 'ObjectExpression' && isEmptyObject(styleArgument as TSESTree.ObjectExpression)) { - reportedNodes.add(styleArgument as TSESTree.ObjectExpression); - reportEmptyDeclaration(ruleContext, styleArgument as TSESTree.Node, node as TSESTree.CallExpression); - return; - } - - // For recipe - check if entire recipe is effectively empty - if (node.callee.name === 'recipe' && styleArgument?.type === 'ObjectExpression') { - if (isEffectivelyEmptyStylesObject(styleArgument as TSESTree.ObjectExpression)) { - reportedNodes.add(styleArgument as TSESTree.ObjectExpression); - reportEmptyDeclaration(ruleContext, styleArgument as TSESTree.Node, node as TSESTree.CallExpression); - return; - } - - // Process individual properties in recipe - processRecipeProperties(ruleContext, styleArgument as TSESTree.ObjectExpression, reportedNodes); - } - - // For style objects with nested empty objects - if (styleArgument?.type === 'ObjectExpression') { - // Check for spread elements - styleArgument.properties.forEach((property) => { - if ( - property.type === 'SpreadElement' && - property.argument.type === 'ObjectExpression' && - isEmptyObject(property.argument as TSESTree.ObjectExpression) - ) { - reportedNodes.add(property.argument as TSESTree.Node); - ruleContext.report({ - node: property.argument as Rule.Node, - messageId: 'emptySpreadObject', - fix(fixer) { - return removeNodeWithComma(ruleContext, property as TSESTree.Node, fixer); - }, - }); - } - }); - - // Process nested selectors and media queries - processEmptyNestedStyles(ruleContext, styleArgument as TSESTree.ObjectExpression, reportedNodes); - } - }, - }; -}; - /** * Checks if a style object is effectively empty (contains only empty objects). */ -export function isEffectivelyEmptyStylesObject(stylesObject: TSESTree.ObjectExpression): boolean { +export const isEffectivelyEmptyStylesObject = (stylesObject: TSESTree.ObjectExpression): boolean => { // Empty object itself if (stylesObject.properties.length === 0) { return true; @@ -225,4 +107,122 @@ export function isEffectivelyEmptyStylesObject(stylesObject: TSESTree.ObjectExpr // If we have special properties and they're all empty, the style is effectively empty return specialProperties.length > 0 && allSpecialPropertiesEmpty; -} +}; + +/** + * Creates ESLint rule visitors for detecting empty style blocks in vanilla-extract. + * @param ruleContext The ESLint rule rule context. + * @returns An object with visitor functions for the ESLint rule. + */ +export const createEmptyStyleVisitors = (ruleContext: Rule.RuleContext): Rule.RuleListener => { + // Track reported nodes to prevent duplicate reports + const reportedNodes = new Set(); + + return { + CallExpression(node) { + if (node.callee.type !== 'Identifier') { + return; + } + + // Target vanilla-extract style functions + const styleApiFunctions = [ + 'style', + 'styleVariants', + 'recipe', + 'globalStyle', + 'fontFace', + 'globalFontFace', + 'keyframes', + 'globalKeyframes', + ]; + + if (!styleApiFunctions.includes(node.callee.name) || node.arguments.length === 0) { + return; + } + + // Handle styleVariants specifically + if (node.callee.name === 'styleVariants' && node.arguments[0]?.type === 'ObjectExpression') { + processStyleVariants(ruleContext, node.arguments[0] as TSESTree.ObjectExpression, reportedNodes); + + // If the entire styleVariants object is empty after processing, remove the declaration + if (isEmptyObject(node.arguments[0] as TSESTree.ObjectExpression)) { + reportEmptyDeclaration(ruleContext, node.arguments[0] as TSESTree.Node, node as TSESTree.CallExpression); + } + return; + } + + const defaultStyleArgumentIndex = 0; + const globalFunctionNames = ['globalStyle', 'globalFontFace', 'globalKeyframes']; + // Determine the style argument index based on the function name + const styleArgumentIndex = globalFunctionNames.includes(node.callee.name) ? 1 : defaultStyleArgumentIndex; + + // For global functions, check if we have enough arguments + if (styleArgumentIndex === 1 && node.arguments.length <= styleArgumentIndex) { + return; + } + + const styleArgument = node.arguments[styleArgumentIndex]; + + // This defensive check prevents duplicate processing of nodes. + // This code path's difficult to test because the ESLint visitor pattern + // typically ensures each node is only visited once per rule execution. + if (reportedNodes.has(styleArgument as TSESTree.ObjectExpression)) { + return; + } + + // Handle conditional expressions + if (styleArgument?.type === 'ConditionalExpression') { + processConditionalExpression( + ruleContext, + styleArgument as TSESTree.ConditionalExpression, + reportedNodes, + node as TSESTree.CallExpression, + ); + return; + } + + // Direct empty object case - remove the entire declaration + if (styleArgument?.type === 'ObjectExpression' && isEmptyObject(styleArgument as TSESTree.ObjectExpression)) { + reportedNodes.add(styleArgument as TSESTree.ObjectExpression); + reportEmptyDeclaration(ruleContext, styleArgument as TSESTree.Node, node as TSESTree.CallExpression); + return; + } + + // For recipe - check if entire recipe is effectively empty + if (node.callee.name === 'recipe' && styleArgument?.type === 'ObjectExpression') { + if (isEffectivelyEmptyStylesObject(styleArgument as TSESTree.ObjectExpression)) { + reportedNodes.add(styleArgument as TSESTree.ObjectExpression); + reportEmptyDeclaration(ruleContext, styleArgument as TSESTree.Node, node as TSESTree.CallExpression); + return; + } + + // Process individual properties in recipe + processRecipeProperties(ruleContext, styleArgument as TSESTree.ObjectExpression, reportedNodes); + } + + // For style objects with nested empty objects + if (styleArgument?.type === 'ObjectExpression') { + // Check for spread elements + styleArgument.properties.forEach((property) => { + if ( + property.type === 'SpreadElement' && + property.argument.type === 'ObjectExpression' && + isEmptyObject(property.argument as TSESTree.ObjectExpression) + ) { + reportedNodes.add(property.argument as TSESTree.ObjectExpression); + ruleContext.report({ + node: property.argument as Rule.Node, + messageId: 'emptySpreadObject', + fix(fixer) { + return removeNodeWithComma(ruleContext, property as TSESTree.Node, fixer); + }, + }); + } + }); + + // Process nested selectors and media queries + processEmptyNestedStyles(ruleContext, styleArgument as TSESTree.ObjectExpression, reportedNodes); + } + }, + }; +}; diff --git a/src/css-rules/no-empty-blocks/node-remover.ts b/src/css-rules/no-empty-blocks/node-remover.ts index 28971e2..88c5437 100644 --- a/src/css-rules/no-empty-blocks/node-remover.ts +++ b/src/css-rules/no-empty-blocks/node-remover.ts @@ -8,11 +8,11 @@ import type { TSESTree } from '@typescript-eslint/utils'; * @param fixer The ESLint fixer. * @returns The fix object. */ -export function removeNodeWithComma(ruleContext: Rule.RuleContext, node: TSESTree.Node, fixer: Rule.RuleFixer) { +export const removeNodeWithComma = (ruleContext: Rule.RuleContext, node: TSESTree.Node, fixer: Rule.RuleFixer) => { const sourceCode = ruleContext.sourceCode; const tokenAfter = sourceCode.getTokenAfter(node as Rule.Node); if (tokenAfter && tokenAfter.value === ',' && node.range && tokenAfter.range) { return fixer.removeRange([node.range[0], tokenAfter.range[1]]); } return fixer.remove(node as Rule.Node); -} +}; diff --git a/src/css-rules/no-empty-blocks/property-utils.ts b/src/css-rules/no-empty-blocks/property-utils.ts index 720a1bf..0839074 100644 --- a/src/css-rules/no-empty-blocks/property-utils.ts +++ b/src/css-rules/no-empty-blocks/property-utils.ts @@ -4,7 +4,7 @@ import { isEmptyObject } from '../shared-utils/empty-object-processor.js'; /** * Gets the property name regardless of whether it's an identifier or a literal. */ -export function getStyleKeyName(key: TSESTree.Expression | TSESTree.PrivateIdentifier): string | null { +export const getStyleKeyName = (key: TSESTree.Expression | TSESTree.PrivateIdentifier): string | null => { if (key.type === 'Identifier') { return key.name; } @@ -12,7 +12,7 @@ export function getStyleKeyName(key: TSESTree.Expression | TSESTree.PrivateIdent return key.value; } return null; -} +}; /** * Checks if all properties in a style object are empty objects. diff --git a/src/css-rules/no-empty-blocks/recipe-processor.ts b/src/css-rules/no-empty-blocks/recipe-processor.ts index e593da6..15c8470 100644 --- a/src/css-rules/no-empty-blocks/recipe-processor.ts +++ b/src/css-rules/no-empty-blocks/recipe-processor.ts @@ -13,11 +13,11 @@ import { areAllChildrenEmpty, getStyleKeyName } from './property-utils.js'; * @param recipeNode The recipe object node to process. * @param reportedNodes A set of nodes that have already been reported by other processors. */ -export function processRecipeProperties( +export const processRecipeProperties = ( ruleContext: Rule.RuleContext, recipeNode: TSESTree.ObjectExpression, reportedNodes: Set, -): void { +): void => { recipeNode.properties.forEach((property) => { if (property.type !== 'Property') { return; @@ -139,4 +139,4 @@ export function processRecipeProperties( } } }); -} +}; diff --git a/src/css-rules/no-empty-blocks/style-variants-processor.ts b/src/css-rules/no-empty-blocks/style-variants-processor.ts index 8992a2b..cbc7552 100644 --- a/src/css-rules/no-empty-blocks/style-variants-processor.ts +++ b/src/css-rules/no-empty-blocks/style-variants-processor.ts @@ -10,11 +10,11 @@ import { removeNodeWithComma } from './node-remover.js'; * @param node The styleVariants call argument (object expression). * @param reportedNodes A set of nodes that have already been reported. */ -export function processStyleVariants( +export const processStyleVariants = ( ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression, reportedNodes: Set, -): void { +): void => { node.properties.forEach((property) => { if (property.type !== 'Property') { return; @@ -50,4 +50,4 @@ export function processStyleVariants( return; } }); -} +}; diff --git a/src/css-rules/no-zero-unit/_tests_/no-zero-unit.test.ts b/src/css-rules/no-zero-unit/_tests_/no-zero-unit.test.ts new file mode 100644 index 0000000..0bf9452 --- /dev/null +++ b/src/css-rules/no-zero-unit/_tests_/no-zero-unit.test.ts @@ -0,0 +1,341 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import noZeroUnitRule from '../rule-definition.js'; + +run({ + name: 'vanilla-extract/no-zero-unit', + rule: noZeroUnitRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [ + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0', + padding: 0, + width: '100%', + }); + `, + }, + { + code: ` + import { recipe } from '@vanilla-extract/css'; + const myRecipe = recipe({ + base: { + margin: '0', + padding: 0, + }, + variants: { + size: { + small: { + height: '0', + width: '10px', + }, + }, + }, + }); + `, + }, + + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + ...spreadProps, + margin: 0, + '@media': { + '0rem': '0' // Key shouldn't be checked + } + }); + `, + name: 'should ignore spread elements and object keys', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: \`0\${someUnit}\`, // Template literal + padding: someVariable + }); + `, + name: 'should ignore non-literal values', + }, + + { + code: ` + import { globalStyle } from '@vanilla-extract/css'; + const callExpression = someObject.fontFace({ src: '...' }); // Non-Identifier callee + `, + name: 'should ignore member expression callees', + }, + { + code: ` + import { fontFace } from '@vanilla-extract/css'; + fontFace(); // Missing arguments + `, + name: 'should handle missing fontFace arguments', + }, + { + code: ` + import { globalFontFace } from '@vanilla-extract/css'; + globalFontFace('my-font'); // Missing style argument + `, + name: 'should handle missing globalFontFace style argument', + }, + ], + invalid: [ + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0px', + padding: '0rem', + }); + `, + errors: [{ messageId: 'noZeroUnit' }, { messageId: 'noZeroUnit' }], + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0', + padding: '0', + }); + `, + }, + { + code: ` + import { recipe } from '@vanilla-extract/css'; + const myRecipe = recipe({ + base: { + margin: '0px', + }, + variants: { + size: { + small: { + height: '0vh', + }, + }, + }, + }); + `, + errors: [{ messageId: 'noZeroUnit' }, { messageId: 'noZeroUnit' }], + output: ` + import { recipe } from '@vanilla-extract/css'; + const myRecipe = recipe({ + base: { + margin: '0', + }, + variants: { + size: { + small: { + height: '0', + }, + }, + }, + }); + `, + }, + + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0px', + '@media': { + '(min-width: 768px)': { + padding: '0rem' + } + } + }); + `, + errors: 2, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0', + '@media': { + '(min-width: 768px)': { + padding: '0' + } + } + }); + `, + name: 'should handle nested media queries', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + '::before': { + content: '""', + margin: '0px' + } + }); + `, + errors: 1, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + '::before': { + content: '""', + margin: '0' + } + }); + `, + name: 'should handle pseudo-elements', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0px', + nested: { + object: { + padding: '0rem', + deeper: { + width: '0%' + } + } + } + }); + `, + errors: 3, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0', + nested: { + object: { + padding: '0', + deeper: { + width: '0' + } + } + } + }); + `, + name: 'should handle multiple levels of nesting', + }, + + { + code: ` + import { fontFace, globalFontFace } from '@vanilla-extract/css'; + + fontFace({ + src: '...', + lineGap: '0rem' + }); + + globalFontFace('my-font', { + src: '...', + sizeAdjust: '0%' + }); + `, + errors: 2, + output: ` + import { fontFace, globalFontFace } from '@vanilla-extract/css'; + + fontFace({ + src: '...', + lineGap: '0' + }); + + globalFontFace('my-font', { + src: '...', + sizeAdjust: '0' + }); + `, + name: 'should handle fontFace and globalFontFace arguments', + }, + + // 0deg is valid (deg isn't in our unit check) + { + code: ` + import { globalKeyframes, globalStyle } from '@vanilla-extract/css'; + + globalKeyframes('spin', { + '0%': { transform: 'rotate(0deg)' }, + '100%': { transform: 'rotate(0deg)' } + }); + + globalStyle('html', { + margin: '0px', + padding: '0rem' + }); + `, + errors: 2, + output: ` + import { globalKeyframes, globalStyle } from '@vanilla-extract/css'; + + globalKeyframes('spin', { + '0%': { transform: 'rotate(0deg)' }, + '100%': { transform: 'rotate(0deg)' } + }); + + globalStyle('html', { + margin: '0', + padding: '0' + }); + `, + name: 'should handle globalKeyframes and globalStyle arguments', + }, + + { + code: ` + import { globalStyle } from '@vanilla-extract/css'; + globalStyle('html', { + '@media': { + '(min-width: 768px)': { + margin: '0px' + } + } + }); + `, + errors: 1, + output: ` + import { globalStyle } from '@vanilla-extract/css'; + globalStyle('html', { + '@media': { + '(min-width: 768px)': { + margin: '0' + } + } + }); + `, + name: 'should handle nested globalStyle arguments', + }, + + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '-0px', + padding: '-0rem', + top: '-0vh', + left: '-0%', + }); + `, + errors: [ + { messageId: 'noZeroUnit' }, + { messageId: 'noZeroUnit' }, + { messageId: 'noZeroUnit' }, + { messageId: 'noZeroUnit' }, + ], + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0', + padding: '0', + top: '0', + left: '0', + }); + `, + name: 'should convert negative zero with units to simple zero', + }, + ], +}); diff --git a/src/css-rules/no-zero-unit/index.ts b/src/css-rules/no-zero-unit/index.ts new file mode 100644 index 0000000..606424c --- /dev/null +++ b/src/css-rules/no-zero-unit/index.ts @@ -0,0 +1,3 @@ +import noZeroUnitRule from './rule-definition.js'; + +export default noZeroUnitRule; diff --git a/src/css-rules/no-zero-unit/rule-definition.ts b/src/css-rules/no-zero-unit/rule-definition.ts new file mode 100644 index 0000000..698674a --- /dev/null +++ b/src/css-rules/no-zero-unit/rule-definition.ts @@ -0,0 +1,23 @@ +import type { Rule } from 'eslint'; +import { createZeroUnitVisitors } from './zero-unit-visitor-creator.js'; + +const noZeroUnitRule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'enforce unitless zero in numeric values', + category: 'Stylistic Issues', + recommended: true, + }, + fixable: 'code', + schema: [], + messages: { + noZeroUnit: 'Unit with zero value is unnecessary. Use 0 instead.', + }, + }, + create(context) { + return createZeroUnitVisitors(context); + }, +}; + +export default noZeroUnitRule; diff --git a/src/css-rules/no-zero-unit/zero-unit-processor.ts b/src/css-rules/no-zero-unit/zero-unit-processor.ts new file mode 100644 index 0000000..e06dfd2 --- /dev/null +++ b/src/css-rules/no-zero-unit/zero-unit-processor.ts @@ -0,0 +1,36 @@ +import type { Rule } from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; + +const ZERO_VALUE_WITH_UNIT_REGEX = /^-?0(px|em|rem|%|vh|vw|vmin|vmax|ex|ch|cm|mm|in|pt|pc|Q|fr)$/; + +/** + * Recursively processes a style object, reporting and fixing instances of zero values with units. + * + * @param ruleContext The ESLint rule context. + * @param node The ObjectExpression node representing the style object to be processed. + */ +export const processZeroUnitInStyleObject = (ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression): void => { + node.properties.forEach((property) => { + if (property.type !== 'Property') { + return; + } + + // Process direct string literal values + if ( + property.value.type === 'Literal' && + typeof property.value.value === 'string' && + ZERO_VALUE_WITH_UNIT_REGEX.test(property.value.value) + ) { + ruleContext.report({ + node: property.value, + messageId: 'noZeroUnit', + fix: (fixer) => fixer.replaceText(property.value, "'0'"), + }); + } + + // Process nested objects (selectors, media queries, etc.) + if (property.value.type === 'ObjectExpression') { + processZeroUnitInStyleObject(ruleContext, property.value); + } + }); +}; diff --git a/src/css-rules/no-zero-unit/zero-unit-visitor-creator.ts b/src/css-rules/no-zero-unit/zero-unit-visitor-creator.ts new file mode 100644 index 0000000..f1c5004 --- /dev/null +++ b/src/css-rules/no-zero-unit/zero-unit-visitor-creator.ts @@ -0,0 +1,56 @@ +import type { Rule } from 'eslint'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import { processRecipeProperties } from '../shared-utils/recipe-property-processor.js'; +import { processStyleNode } from '../shared-utils/style-node-processor.js'; +import { processZeroUnitInStyleObject } from './zero-unit-processor.js'; + +/** + * Creates ESLint rule visitors for detecting and processing zero values with units in style-related function calls. + * + * @param context The ESLint rule context. + * @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, processing their object arguments. + * 2. Style-related functions: `keyframes`, `style`, `styleVariants`, processing their style objects. + * 3. The `globalKeyframes` and `globalStyle` functions, processing the second argument as style objects. + * 4. The `recipe` function, processing the first argument as the recipe object. + */ +export const createZeroUnitVisitors = (context: Rule.RuleContext): Rule.RuleListener => { + return { + CallExpression(node) { + if (node.callee.type !== AST_NODE_TYPES.Identifier) { + return; + } + + if (['fontFace', 'globalFontFace'].includes(node.callee.name)) { + const argumentIndex = node.callee.name === 'fontFace' ? 0 : 1; + if ( + node.arguments.length > argumentIndex && + node.arguments[argumentIndex]?.type === AST_NODE_TYPES.ObjectExpression + ) { + processZeroUnitInStyleObject(context, node.arguments[argumentIndex] as TSESTree.ObjectExpression); + } + return; + } + + if (['keyframes', 'style', 'styleVariants'].includes(node.callee.name)) { + if (node.arguments.length > 0) { + processStyleNode(context, node.arguments[0] as TSESTree.ObjectExpression, processZeroUnitInStyleObject); + } + } + + if (['globalKeyframes', 'globalStyle'].includes(node.callee.name) && node.arguments.length >= 2) { + processStyleNode(context, node.arguments[1] as TSESTree.ObjectExpression, processZeroUnitInStyleObject); + } + + if ( + node.callee.name === 'recipe' && + node.arguments.length > 0 && + node.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression + ) { + processRecipeProperties(context, node.arguments[0] as TSESTree.ObjectExpression, processZeroUnitInStyleObject); + } + }, + }; +}; diff --git a/src/css-rules/shared-utils/order-strategy-visitor-creator.ts b/src/css-rules/shared-utils/order-strategy-visitor-creator.ts index a754aca..84c178c 100644 --- a/src/css-rules/shared-utils/order-strategy-visitor-creator.ts +++ b/src/css-rules/shared-utils/order-strategy-visitor-creator.ts @@ -44,13 +44,8 @@ export const createNodeVisitors = ( if (!userDefinedGroupOrder || userDefinedGroupOrder.length === 0) { return enforceAlphabeticalCSSOrderInStyleObject; } - return (ruleContext: Rule.RuleContext, node: TSESTree.Node) => - enforceUserDefinedGroupOrderInStyleObject( - ruleContext, - node as TSESTree.ObjectExpression, - userDefinedGroupOrder, - sortRemainingProperties, - ); + return (ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression) => + enforceUserDefinedGroupOrderInStyleObject(ruleContext, node, userDefinedGroupOrder, sortRemainingProperties); default: return enforceAlphabeticalCSSOrderInStyleObject; } diff --git a/src/css-sample/sample.css.ts b/src/css-sample/sample.css.ts index f752015..491ee77 100644 --- a/src/css-sample/sample.css.ts +++ b/src/css-sample/sample.css.ts @@ -14,7 +14,7 @@ export const theFont = fontFace({ // Comment to test that the linter doesn't remove it src: ['url("/fonts/MyFont.woff2") format("woff2")', 'url("/fonts/MyFont.woff") format("woff")'], ascentOverride: '90%', - descentOverride: '10%', + descentOverride: '0', fontDisplay: 'swap', fontFeatureSettings: '"liga" 1', fontStretch: 'normal', @@ -32,7 +32,7 @@ globalFontFace('GlobalFont', { // Comment to test that the linter doesn't remove it src: ['url("/fonts/MyFont.woff2") format("woff2")', 'url("/fonts/MyFont.woff") format("woff")'], ascentOverride: '90%', - descentOverride: '10%', + descentOverride: '0', fontDisplay: 'swap', fontFeatureSettings: '"liga" 1', fontStretch: 'normal', @@ -50,9 +50,14 @@ globalFontFace('GlobalFont', { export const spinster = globalKeyframes('spin', { // Comment to test that the linter doesn't remove it from: { - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -60,24 +65,24 @@ export const spinster = globalKeyframes('spin', { borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, // Comment to test that the linter doesn't remove it to: { // Comment to test that the linter doesn't remove it - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -85,25 +90,25 @@ export const spinster = globalKeyframes('spin', { borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, }); export const starter = keyframes({ // Comment to test that the linter doesn't remove it '0%': { - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -111,24 +116,24 @@ export const starter = keyframes({ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, // Comment to test that the linter doesn't remove it '100%': { // Comment to test that the linter doesn't remove it - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -136,40 +141,35 @@ export const starter = keyframes({ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, }); globalStyle('*, ::before, ::after', { // Comment to test that the linter doesn't remove it - width: '100%', - margin: '0', - fontSize: 'large', - border: 'Background', - borderRight: 'ActiveBorder', - borderLeft: 'ActiveBorder', - borderRadius: 'initial', - borderBottomLeftRadius: 'initial', - borderBottomRightRadius: 'initial', boxSizing: 'inherit', position: 'relative', right: 'inherit', display: 'flex', gap: 'revert', transform: 'none', + margin: '0', outline: 'none', + border: 'Background', + borderRight: 'ActiveBorder', + borderLeft: 'ActiveBorder', + borderRadius: 'initial', + borderBottomLeftRadius: 'initial', + borderBottomRightRadius: 'initial', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }); // style with an array @@ -177,9 +177,14 @@ const accordionContentBase = style([ // Comment to test that the linter doesn't remove it { // Comment to test that the linter doesn't remove it - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -187,24 +192,24 @@ const accordionContentBase = style([ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', // special selector to test that the linter doesn't remove it '@supports': { '(hanging-punctuation: first) and (font: -apple-system-body) and (-webkit-appearance: none)': { // Comment to test that the linter doesn't remove it - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -212,16 +217,11 @@ const accordionContentBase = style([ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, }, }, @@ -237,9 +237,14 @@ export const accordionContent = recipe({ // Comment to test that the linter doesn't remove it false: { // Comment to test that the linter doesn't remove it - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -247,22 +252,22 @@ export const accordionContent = recipe({ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, true: { - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -270,23 +275,23 @@ export const accordionContent = recipe({ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', // pseudo selector inside a variant ':hover': { // Comment to test that the linter doesn't remove it - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -294,16 +299,11 @@ export const accordionContent = recipe({ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, }, }, @@ -311,32 +311,37 @@ export const accordionContent = recipe({ }); export const item = style({ - width: '100%', - margin: '0', - fontSize: 'large', - border: 'Background', - borderRight: 'ActiveBorder', - borderLeft: 'ActiveBorder', - borderRadius: 'initial', - borderBottomLeftRadius: 'initial', - borderBottomRightRadius: 'initial', boxSizing: 'inherit', position: 'relative', right: 'inherit', display: 'flex', gap: 'revert', transform: 'none', + margin: '0', outline: 'none', + border: 'Background', + borderRight: 'ActiveBorder', + borderLeft: 'ActiveBorder', + borderRadius: 'initial', + borderBottomLeftRadius: 'initial', + borderBottomRightRadius: 'initial', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', // pseudo selector inside a style ':focus-visible': { // Comment to test that the linter doesn't remove it - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -344,25 +349,25 @@ export const item = style({ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, selectors: { // Comment to test that the linter doesn't remove it '&[data-pressed]': { // Comment to test that the linter doesn't remove it - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -370,16 +375,11 @@ export const item = style({ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, }, }); @@ -388,31 +388,36 @@ export const selectButtonVariants = styleVariants({ // Comment to test that the linter doesn't remove it bordered: { // Comment to test that the linter doesn't remove it - width: '100%', - margin: '0', - fontSize: 'large', - border: 'Background', - borderRight: 'ActiveBorder', - borderLeft: 'ActiveBorder', - borderRadius: 'initial', - borderBottomLeftRadius: 'initial', - borderBottomRightRadius: 'initial', boxSizing: 'inherit', position: 'relative', right: 'inherit', display: 'flex', gap: 'revert', transform: 'none', + margin: '0', outline: 'none', + border: 'Background', + borderRight: 'ActiveBorder', + borderLeft: 'ActiveBorder', + borderRadius: 'initial', + borderBottomLeftRadius: 'initial', + borderBottomRightRadius: 'initial', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, borderless: { - width: '100%', + boxSizing: 'inherit', + position: 'relative', + right: 'inherit', + display: 'flex', + gap: 'revert', + transform: 'none', margin: '0', - fontSize: 'large', + outline: 'none', border: 'Background', borderRight: 'ActiveBorder', borderLeft: 'ActiveBorder', @@ -420,16 +425,11 @@ export const selectButtonVariants = styleVariants({ borderBottomLeftRadius: 'initial', borderBottomRightRadius: 'initial', boxShadow: 'none', - boxSizing: 'inherit', - position: 'relative', - right: 'inherit', - display: 'flex', - gap: 'revert', - transform: 'none', - outline: 'none', backgroundColor: 'initial', cursor: 'pointer', + width: '100%', color: 'red', + fontSize: 'large', }, }); diff --git a/src/index.ts b/src/index.ts index 46750d6..942507e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,17 +2,19 @@ import alphabeticalOrderRule from './css-rules/alphabetical-order/index.js'; import concentricOrderRule from './css-rules/concentric-order/index.js'; import customOrderRule from './css-rules/custom-order/rule-definition.js'; import noEmptyStyleBlocksRule from './css-rules/no-empty-blocks/rule-definition.js'; +import noZeroUnitRule from './css-rules/no-zero-unit/rule-definition.js'; export const vanillaExtract = { meta: { name: '@antebudimir/eslint-plugin-vanilla-extract', - version: '1.7.0', + version: '1.8.0', }, rules: { 'alphabetical-order': alphabeticalOrderRule, 'concentric-order': concentricOrderRule, 'custom-order': customOrderRule, 'no-empty-style-blocks': noEmptyStyleBlocksRule, + 'no-zero-unit': noZeroUnitRule, }, configs: { recommended: { @@ -20,6 +22,7 @@ export const vanillaExtract = { rules: { 'vanilla-extract/concentric-order': 'error', 'vanilla-extract/no-empty-style-blocks': 'error', + 'vanilla-extract/no-zero-unit': 'error', }, }, }, diff --git a/tsconfig.json b/tsconfig.json index 5c8577a..9f6109f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,8 @@ "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "useUnknownInCatchVariables": true, // Interop Options