Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin StyleLint #910

Open
5 tasks
BioPhoton opened this issue Jan 6, 2025 · 0 comments
Open
5 tasks

Plugin StyleLint #910

BioPhoton opened this issue Jan 6, 2025 · 0 comments

Comments

@BioPhoton
Copy link
Collaborator

BioPhoton commented Jan 6, 2025

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


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

πŸ“ 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.

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.

Alternatives

  • in the past there was stylelint-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

// Syntax: rule-name: severity | [severity, options]

// Examples:
'indent': ['warn', 2]; // Severity + options
'no-unused-vars': 'error'; // Error
'no-console': 'off'; // Disabled

StyleLint

// 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. Below are rule equivalents:

ESLint CSS Plugin Rule Description Stylelint Rule Comments
no-duplicate-properties Disallows duplicate properties. βœ… declaration-block-no-duplicate-properties Direct match; both handle duplicate declarations.
no-invalid-hex Disallows invalid hex colors. βœ… color-no-invalid-hex Direct match; prevents malformed hex codes.
property-no-unknown Disallows unknown CSS properties. βœ… property-no-unknown Direct match; flags unknown properties.
selector-type-no-unknown Disallows unknown type selectors. βœ… selector-type-no-unknown Direct match; ensures valid selectors.

CSS Formats

Stylelint focuses on pure CSS, preprocessors (SCSS, LESS), and modern CSS standards, making it suitable for standalone workflows. The ESLint CSS plugin targets embedded CSS within JavaScript/TypeScript, particularly for CSS-in-JS frameworks like styled-components and Emotion.

Feature Stylelint ESLint CSS Plugin
CSS Formats βœ… Fully supported βœ… Embedded CSS in JS/TS
Preprocessors βœ… SCSS, LESS ❌
PostCSS βœ… Fully compatible ⚠️ Partial
CSS-in-JS ⚠️ Template literals only βœ… Full support for styled-components and Emotion
CSS Modules βœ… Fully supported βœ… Fully supported
CSS Versions βœ… CSS3 / CSS4 ❌ CSS3 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.

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"
Loading

Configured rules and considerations

stylelint-config.js
/**
 * 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;

Setup

To use the default configuration:

  1. Install all required peer dependencies:
   npm install -D @code-pushup/stylelint-plugin stylelint @code-pushup/stylelint-config stylelint-config-standard stylelint-config-recommended
  1. Extend the @code-pushup/stylelint-config in your .stylelintrc.next.js file:
module.exports = {
  extends: '@code-pushup/stylelint-config',
};

The plugin needs the following options:

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.

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

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:

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 - return value of `stylelint.lint(...)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant