# StyleLint Plugin **Quality standards & Incremental Migration for CSS Styles** Seamlessly improve your codebase, standardise code style, avoid missconfiguration or errors. --- ### π§ͺ **Reference PR** π [**#??? β StyleLint Plugin PoC Implementation**](https://github.com/code-pushup/cli/pull/???) --- ## Metric **CSS code quality based on StyleLint.** | **Property** | **Value** | **Description** | |-----------------|-----------:|-------------------------------------------| | **value** | `48` | Total number of detected issues. | | **displayValue** | `48 errors` | Human-readable representation of the value. | | **score** | `0` | Indicates whether the audit passed (`1`) or failed (`0`). | ## User story As a developer I want to be able to incrementally migrate to a better CSS code quality and track it over time. ## Setup and Requirements ### π¦ Package Dependencies - **Dependencies:** - [stylelint](https://www.npmjs.com/package/stylelint) β Required for linting CSS - **Dev Dependencies:** N/A - **Optional Dependencies:** - [stylelint-config-recommended](https://www.npmjs.com/package/stylelint-config-recommended) β Base recommended configuration ### π Configuration Files - `.stylelintrc.json` β Standard configuration file. - `.stylelintrc.next.json` β Migration-specific configuration. ### Audit, groups and category maintenance - π **Audit:** The audit slugs are directly derived from the configured rules in the `.stylelintrc.next.json` - π **Group:** The groups are automatically detected by the configured severity (`warning` or `error`). A recommended set of rules provided by code-pushup statically pre-configures the core rules related to `suggestions` with `warning` and the ones for `problems` with `error`. - π **Category:** The categories are same as for eslint `code-style` and `bug-prevention`. ### Details maintenance - π **Issues:** The details contain only issues. Every rule has lint issues that are directly mapped to the audits. - π **Table:** N/A ### Runner maintenance To get full access to configuration parsing we have to path the `stylelint` package in a `postinstall` hook. This keeps the runner logic easy to maintain. Issue opened: https://github.com/stylelint/stylelint/issues/8293 ## Acceptance criteria - [ ] supports all config formats (`js`, `json`) - [ ] rule name maps directly to audit name - [ ] audits are grouped into `errors` and `warnings` - [ ] the `.stylelint.json` controls the listed audits - [ ] a recommended set of `stylelint` rules is provided by `code-pushup` ## Stylelint vs Prettier Pretties and StyleLint don't have conflicting rules since version [v15](https://stylelint.io/migration-guide/to-15/#deprecated-stylistic-rules). ### Alternatives - in the past there was [`stylelint-prettier`](https://github.com/prettier/stylelint-config-prettier?tab=readme-ov-file#stylelint-config-prettier) that you could use to report Prettier issues as Stylelint warnings or errors. This is deprecated now as the conflicting rules are now disable by default. ## Stylelint vs ESLint CSS Plugin Stylelint and the ESLint CSS plugin are powerful tools for linting CSS, each tailored to specific workflows. Stylelint offers extensive rules for standalone styles and preprocessors, while ESLint excels in CSS-in-JS scenarios. Both tools provide autofix support. --- ### Rule Definition and Severity Levels | **Aspect** | **ESLint** | **Stylelint** | |----------------------|---------------------------|---------------------------| | **Default Severity** | `off` | `error` (if `true`) | | **Explicit Levels** | `off`, `warn`, `error` | `off`, `warning`, `error` | | **Default State** | Off unless set to `on`. | Off unless set to `true`. | #### ESLint ```javascript // Syntax: rule-name: severity | [severity, options] // Examples: 'indent': ['warn', 2]; // Severity + options 'no-unused-vars': 'error'; // Error 'no-console': 'off'; // Disabled ```` #### StyleLint ```javascript // Syntax: rule-name: true | [option, { severity }] // Examples: 'indentation': [2, { severity: 'warning' }]; // Warning 'block-no-empty': true; // Error 'block-no-empty': null; // Disabled ```` ### Rules Stylelint provides a significantly broader rule set, with **134 core rules**, compared to the **4 rules** offered by the [ESLint CSS plugin](https://github.com/eslint/css). Below are rule equivalents: | **ESLint CSS Plugin Rule** | **Description** | **Stylelint Rule** | **Comments** | |--------------------------------------|-----------------------------------------------------|---------------------------------------|---------------------------------------------------------| | [`no-duplicate-properties`](https://github.com/eslint/css/blob/main/docs/rules/no-duplicate-properties.md) | Disallows duplicate properties. | β [`declaration-block-no-duplicate-properties`](https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-properties) | Direct match; both handle duplicate declarations. | | [`no-invalid-hex`](https://github.com/eslint/css/blob/main/docs/rules/no-invalid-hex.md) | Disallows invalid hex colors. | β [`color-no-invalid-hex`](https://stylelint.io/user-guide/rules/color-no-invalid-hex) | Direct match; prevents malformed hex codes. | | [`property-no-unknown`](https://github.com/eslint/css/blob/main/docs/rules/property-no-unknown.md) | Disallows unknown CSS properties. | β [`property-no-unknown`](https://stylelint.io/user-guide/rules/property-no-unknown) | Direct match; flags unknown properties. | | [`selector-type-no-unknown`](https://github.com/eslint/css/blob/main/docs/rules/selector-type-no-unknown.md) | Disallows unknown type selectors. | β [`selector-type-no-unknown`](https://stylelint.io/user-guide/rules/selector-type-no-unknown) | Direct match; ensures valid selectors. | --- ### CSS Formats Stylelint focuses on pure CSS, preprocessors ([SCSS](https://sass-lang.com/), [LESS](http://lesscss.org/)), and modern CSS standards, making it suitable for standalone workflows. The [ESLint CSS plugin](https://github.com/eslint/css) targets embedded CSS within JavaScript/TypeScript, particularly for CSS-in-JS frameworks like [styled-components](https://styled-components.com/) and [Emotion](https://emotion.sh/docs/introduction). | **Feature** | **Stylelint** | **ESLint CSS Plugin** | |----------------------------|--------------------------------------------|-----------------------------------------| | **CSS Formats** | β Fully supported | β Embedded CSS in JS/TS | | **Preprocessors** | β [SCSS](https://sass-lang.com/), [LESS](http://lesscss.org/) | β | | **PostCSS** | β Fully compatible | β οΈ Partial | | **CSS-in-JS** | β οΈ Template literals only | β Full support for [styled-components](https://styled-components.com/) and [Emotion](https://emotion.sh/docs/introduction) | | **CSS Modules** | β Fully supported | β Fully supported | | **CSS Versions** | β [CSS3](https://www.w3.org/TR/css-2018/) / [CSS4](https://www.w3.org/Style/CSS/current-work) | β [CSS3](https://www.w3.org/TR/css-2018/) only | | **Dynamic Styling** | β Not supported | β Fully supported | | **Customization** | β Highly customizable | β οΈ Limited | --- ### Comparison conclusion Stylelint has more comprehensive CSS linting for standalone styles and preprocessors, with robust autofix capabilities for common styling issues. In contrast, ESLint with the CSS plugin is optimized for JavaScript-focused workflows, particularly CSS-in-JS, but offers limited autofix functionality. ## Implementation details > **π Key Note:** The biggest flaw of the current PoC is the `postinstall` hook - there is a overlap in formatting rules with prettier that needs to be considered in the styllintpreset A draft implementation of the plugin can be found here: TODO. ### `stylelint-config` provided under `@code-pushup/stylelint-config` Some configurations extend others, as shown in the diagram below. For example, extending the `stylelint-config` implicitly includes the `stylelint-config-standard` and `stylelint-config-recommended` configurations. ```mermaid graph BT; A[stylelint-config-standard] --> B[stylelint-config-recommended]; C[stylelint-config Custom] --> A; %% Add links as notes click A href "https://github.com/stylelint/stylelint-config-standard/blob/main/index.js" "stylelint-config-standard on GitHub" click B href "https://github.com/stylelint/stylelint-config-recommended/blob/main/index.js" "stylelint-config-recommended on GitHub" ``` #### Configured rules and considerations <details> <summary><code>stylelint-config.js</code></summary> ```javascript /** * Standard Stylelint configuration that extends the stylelint-config-standard. * "Avoid errors" rules are set to "error" severity. * "Enforce conventions" rules are set to "warning" severity. */ const stylelintConfig = { extends: ['stylelint-config-standard'], rules: { // = Avoid errors - set as errors // == Descending 'no-descending-specificity': [true, { severity: 'error' }], // == Duplicate 'declaration-block-no-duplicate-custom-properties': [true, { severity: 'error' }], 'declaration-block-no-duplicate-properties': [ true, { severity: 'error', ignore: ['consecutive-duplicates-with-different-syntaxes'] }, ], 'font-family-no-duplicate-names': [true, { severity: 'error' }], 'keyframe-block-no-duplicate-selectors': [true, { severity: 'error' }], 'no-duplicate-at-import-rules': [true, { severity: 'error' }], 'no-duplicate-selectors': [true, { severity: 'error' }], // == Empty 'block-no-empty': [true, { severity: 'error' }], 'comment-no-empty': [true, { severity: 'error' }], 'no-empty-source': [true, { severity: 'error' }], // == Invalid 'color-no-invalid-hex': [true, { severity: 'error' }], 'function-calc-no-unspaced-operator': [true, { severity: 'error' }], 'keyframe-declaration-no-important': [true, { severity: 'error' }], 'media-query-no-invalid': [true, { severity: 'error' }], 'named-grid-areas-no-invalid': [true, { severity: 'error' }], 'no-invalid-double-slash-comments': [true, { severity: 'error' }], 'no-invalid-position-at-import-rule': [true, { severity: 'error' }], 'string-no-newline': [true, { severity: 'error' }], // == Irregular 'no-irregular-whitespace': [true, { severity: 'error' }], // == Missing 'custom-property-no-missing-var-function': [true, { severity: 'error' }], 'font-family-no-missing-generic-family-keyword': [true, { severity: 'error' }], // == Non-standard 'function-linear-gradient-no-nonstandard-direction': [true, { severity: 'error' }], // == Overrides 'declaration-block-no-shorthand-property-overrides': [true, { severity: 'error' }], // == Unmatchable 'selector-anb-no-unmatchable': [true, { severity: 'error' }], // == Unknown 'annotation-no-unknown': [true, { severity: 'error' }], 'at-rule-no-unknown': [true, { severity: 'error' }], 'function-no-unknown': [true, { severity: 'error' }], 'media-feature-name-no-unknown': [true, { severity: 'error' }], 'property-no-unknown': [true, { severity: 'error' }], 'selector-pseudo-class-no-unknown': [true, { severity: 'error' }], 'selector-type-no-unknown': [true, { severity: 'error' }], 'unit-no-unknown': [true, { severity: 'error' }], // == Maintainability Rules // Prevent overly specific selectors // Example: Good: `.class1 .class2`, Bad: `#id.class1 .class2` "selector-max-specificity": ["0,2,0", { severity: "warning" }], // Enforces a maximum specificity of 2 classes, no IDs, and no inline styles. // Encourages maintainable selectors. // Disallow the use of ID selectors // Example: Good: `.button`, Bad: `#button` "selector-max-id": [0, { severity: "warning" }], // Prevents the use of IDs in selectors, as they are too specific and hard to override. // Limit the number of class selectors in a rule // Example: Good: `.btn.primary`, Bad: `.btn.primary.large.rounded` "selector-max-class": [3, { severity: "off" }], // Can help avoid overly complex class chains, but may be unnecessary if specificity is already managed. // Limit the number of pseudo-classes in a selector // Example: Good: `.list-item:hover`, Bad: `.list-item:nth-child(2):hover:active` "selector-max-pseudo-class": [3, { severity: "warning" }], // Allows up to 3 pseudo-classes in a single selector to balance flexibility and simplicity. // Restrict the number of type selectors (e.g., `div`, `span`) // Example: Good: `.header`, Bad: `div.header` "selector-max-type": [1, { severity: "warning" }], // Promotes the use of semantic classes over type selectors for better reusability and maintainability. // Optional: Additional rules for project-specific preferences // Uncomment the following if relevant to your project: /* // Example: Limit the depth of combinators // Good: `.parent > .child`, Bad: `.parent > .child > .grandchild` "selector-max-combinators": [2, { severity: "warning" }], // Example: Restrict the number of universal selectors in a rule // Good: `* { margin: 0; }`, Bad: `.wrapper * .content { padding: 0; }` "selector-max-universal": [1, { severity: "warning" }], */ // = Enforce conventions - set as warnings // == Allowed, disallowed & required 'at-rule-no-vendor-prefix': [true, { severity: 'warning' }], 'length-zero-no-unit': [true, { severity: 'warning' }], 'media-feature-name-no-vendor-prefix': [true, { severity: 'warning' }], 'property-no-vendor-prefix': [true, { severity: 'warning' }], 'value-no-vendor-prefix': [true, { severity: 'warning' }], // == Case 'function-name-case': ['lower', { severity: 'warning' }], 'selector-type-case': ['lower', { severity: 'warning' }], 'value-keyword-case': ['lower', { severity: 'warning' }], // == Empty lines 'at-rule-empty-line-before': ['always', { severity: 'warning' }], 'comment-empty-line-before': ['always', { severity: 'warning' }], 'custom-property-empty-line-before': ['always', { severity: 'warning' }], 'declaration-empty-line-before': ['always', { severity: 'warning' }], 'rule-empty-line-before': ['always', { severity: 'warning' }], // == Max & min 'declaration-block-single-line-max-declarations': [1, { severity: 'warning' }], 'number-max-precision': [4, { severity: 'warning' }], // == Notation 'alpha-value-notation': ['percentage', { severity: 'warning' }], 'color-function-notation': ['modern', { severity: 'warning' }], 'color-hex-length': ['short', { severity: 'warning' }], 'hue-degree-notation': ['angle', { severity: 'warning' }], 'import-notation': ['string', { severity: 'warning' }], 'keyframe-selector-notation': ['percentage', { severity: 'warning' }], 'lightness-notation': ['percentage', { severity: 'warning' }], 'media-feature-range-notation': ['context', { severity: 'warning' }], 'selector-not-notation': ['complex', { severity: 'warning' }], 'selector-pseudo-element-colon-notation': ['double', { severity: 'warning' }], // == Pattern 'custom-media-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], 'custom-property-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], 'keyframes-name-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], 'selector-class-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], 'selector-id-pattern': ['^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { severity: 'warning' }], // == Quotes 'font-family-name-quotes': ['always-where-recommended', { severity: 'warning' }], 'function-url-quotes': ['always', { severity: 'warning' }], 'selector-attribute-quotes': ['always', { severity: 'warning' }], // == Redundant 'declaration-block-no-redundant-longhand-properties': [true, { severity: 'warning' }], 'shorthand-property-no-redundant-values': [true, { severity: 'warning' }], // == Whitespace inside 'comment-whitespace-inside': ['always', { severity: 'warning' }], }, }; export default stylelintConfig; ``` </details> ### Setup To use the default configuration: 1. Install all required peer dependencies: ```sh npm install -D @code-pushup/stylelint-plugin stylelint @code-pushup/stylelint-config stylelint-config-standard stylelint-config-recommended ``` 2. Extend the `@code-pushup/stylelint-config` in your `.stylelintrc.next.js` file: ```javascript module.exports = { extends: '@code-pushup/stylelint-config', }; ``` The plugin needs the following options: ```ts type PluginOptions = { stylelintrc?: string, onlyAudits?: AuditSlug[]} | undefined; const pluginOptions: PluginOptions = { stylelintrc: `stylelintrc.next.js`, // default is `.stylelintrc.json` onlyAudits: [ 'no-empty-blocks' ] }; ``` ### Gather Confiig **Problem 1:** The current state on `stylelint` does not export a way to load the configiratoin from a `.stylelintrc.(js|json)` file and consider extends properties. **Solution:** Setup a `postinstall` hook that exports the code. ```typescript const stylelintEntryFromPackageRoot = resolve( '..', '..', 'stylelint/lib/index.mjs', ); export async function patchStylelint( stylelintPath = stylelintEntryFromPackageRoot, ) { try { let content = await readFile(stylelintPath, 'utf-8'); if (!content.includes('default as getConfigForFile')) { content += ` export { default as getConfigForFile } from './getConfigForFile.mjs'; `; await writeFile(stylelintPath, content, 'utf-8'); console.log('Patched Stylelint successfully.'); } else { console.log('Stylelint already patched.'); } } catch (error) { console.error('Error patching Stylelint:', (error as Error).message); } } ``` ### Generating StyleLint Warnings ```typescript export type LinterOptions = { files?: OneOrMany<string>; configFile?: string; }; export async function lintStyles({ config, ...options }: LinterOptions) { try { // polyfill console assert globalThis.console.assert = globalThis.console.assert || (() => {}); const { results } = await stylelint.lint({ ...options, formatter: 'json', }); return results; } catch (error) { throw new Error(`Error while linting: ${error}`); } } const { source, warnings, invalidOptionWarnings, deprecations, parseErrors } = result; const auditOutput = warningsToAuditOutputs(warnings); ``` The `stylelint.lint` function produces a couple of interesting information: ```typescript const { source, warnings, invalidOptionWarnings, deprecations, parseErrors } = stylelint.lint(...); ``` #### Relevant result output | **Property Name** | **Property Description** | **Group** | |--------------------------|-------------------------------------------------------------------------------------------------------------|--------------------| | **`invalidOptionWarnings`** | Flags invalid configurations in the Stylelint configuration. | `Configuration` | | **~`deprecations`~** | ~Warns about deprecated rules to ensure updated configurations.~ | ~`Configuration`~ | | **`warnings`** | Lists all rule violations, including `error`-level issues (indicated by `errored`) and `warning` severities. | `Code Style`, `Bug Prevention` | | **~`errored`~** | ~Boolean indicating whether any `error`-level issues exist in the `warnings` array.~ | ~N/A~ | | `parseErrors` | Contains critical CSS parsing errors (e.g., invalid syntax), distinct from `warnings` and not associated with any rules. | `Bug Prevention` | | **~`ignoredFiles`~** | ~Represents files skipped during linting due to `.stylelintignore` or `ignoreFiles` configuration.~ | ~`Ignored Issues`~ | | **[1] ~`_postcssResult`~** | ~Internal Stylelint processing data (`_postcssResult`), not directly tied to user-facing results.~ | ~`N/A`~ | - **[1]** - PostCSS Result (Internal): All post processor results are included in a condensed form inside `warnings`. ## Resources - [LintResult](https://github.com/stylelint/stylelint/blob/2e790d91202300d182801c70547fd2d9fc58d003/types/stylelint/index.d.ts#L774) - return value of `stylelint.lint(...)