Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Stylelint - linter dla CSS

Stylelint to potężne narzędzie do analizy statycznej kodu CSS, które pomaga w utrzymaniu spójności, wykrywaniu błędów i egzekwowaniu dobrych praktyk w arkuszach stylów. Jest to niezbędne narzędzie w profesjonalnym developmencie front-end.

Podstawy Stylelint

Instalacja i konfiguracja

1# Instalacja podstawowa
2npm install --save-dev stylelint stylelint-config-standard
3
4# Z dodatkowymi pluginami
5npm install --save-dev stylelint-scss stylelint-order stylelint-config-prettier

Podstawowy plik konfiguracyjny .stylelintrc.json

1{
2  "extends": [
3    "stylelint-config-standard"
4  ],
5  "plugins": [
6    "stylelint-order",
7    "stylelint-scss"
8  ],
9  "rules": {
10    "indentation": 2,
11    "string-quotes": "single",
12    "no-duplicate-selectors": true,
13    "color-hex-case": "lower",
14    "color-hex-length": "short",
15    "color-named": "never",
16    "selector-combinator-space-after": "always",
17    "selector-combinator-space-before": "always",
18    "selector-pseudo-element-colon-notation": "double",
19    "selector-pseudo-class-parentheses-space-inside": "never",
20    "selector-no-qualifying-type": true,
21    "property-no-unknown": true,
22    "keyframe-declaration-no-important": true,
23    "declaration-block-trailing-semicolon": "always",
24    "declaration-no-important": true,
25    "declaration-block-single-line-max-declarations": 1,
26    "selector-class-pattern": "^[a-z0-9\-]+$",
27    "selector-id-pattern": "^[a-z0-9\-]+$",
28    "custom-property-pattern": "^[a-z0-9\-]+$",
29    "max-nesting-depth": 3,
30    "order/order": [
31      "custom-properties",
32      "declarations"
33    ],
34    "order/properties-order": [
35      "position",
36      "top",
37      "right",
38      "bottom",
39      "left",
40      "z-index",
41      "display",
42      "flex",
43      "flex-direction",
44      "flex-wrap",
45      "justify-content",
46      "align-items",
47      "width",
48      "height",
49      "margin",
50      "padding",
51      "background",
52      "color",
53      "font-family",
54      "font-size",
55      "line-height",
56      "border",
57      "border-radius",
58      "transition",
59      "transform"
60    ]
61  }
62}

Rozszerzona konfiguracja dla większych projektów

1{
2  "extends": [
3    "stylelint-config-standard",
4    "stylelint-config-prettier"
5  ],
6  "plugins": [
7    "stylelint-order",
8    "stylelint-scss",
9    "stylelint-selector-bem-pattern",
10    "stylelint-declaration-use-variable"
11  ],
12  "rules": {
13    "plugin/selector-bem-pattern": {
14      "preset": "bem",
15      "presetOptions": {
16        "namespace": "c-"
17      }
18    },
19    "sh-waqar/declaration-use-variable": [
20      [
21        "color",
22        "background-color",
23        "border-color",
24        "font-family",
25        "font-size",
26        "z-index"
27      ]
28    ],
29    "color-function-notation": "modern",
30    "alpha-value-notation": "percentage",
31    "color-hex-alpha": "never",
32    "font-family-name-quotes": "always-where-recommended",
33    "function-url-quotes": "always",
34    "import-notation": "string",
35    "keyframe-selector-notation": "percentage-unless-within-keyword-only-block",
36    "media-feature-range-notation": "context",
37    "selector-not-notation": "complex",
38    "at-rule-no-vendor-prefix": true,
39    "media-feature-name-no-vendor-prefix": true,
40    "property-no-vendor-prefix": true,
41    "selector-no-vendor-prefix": true,
42    "value-no-vendor-prefix": true,
43    "max-line-length": 120,
44    "no-empty-source": null,
45    "declaration-block-no-redundant-longhand-properties": true,
46    "shorthand-property-no-redundant-values": true,
47    "comment-empty-line-before": [
48      "always",
49      {
50        "except": ["first-nested"],
51        "ignore": ["stylelint-commands"]
52      }
53    ],
54    "custom-property-empty-line-before": [
55      "always",
56      {
57        "except": ["after-custom-property", "first-nested"],
58        "ignore": ["after-comment", "inside-single-line-block"]
59      }
60    ],
61    "declaration-empty-line-before": [
62      "always",
63      {
64        "except": ["after-declaration", "first-nested"],
65        "ignore": ["after-comment", "inside-single-line-block"]
66      }
67    ]
68  },
69  "overrides": [
70    {
71      "files": ["*.scss", "**/*.scss"],
72      "customSyntax": "postcss-scss",
73      "rules": {
74        "scss/at-rule-no-unknown": true,
75        "scss/selector-no-redundant-nesting-selector": true,
76        "scss/no-duplicate-dollar-variables": true,
77        "scss/dollar-variable-pattern": "^[a-z0-9\-]+$",
78        "scss/percent-placeholder-pattern": "^[a-z0-9\-]+$"
79      }
80    },
81    {
82      "files": ["*.vue", "**/*.vue"],
83      "customSyntax": "postcss-html"
84    }
85  ]
86}

Integracja z narzędziami deweloperskimi

Package.json scripts

1{
2  "scripts": {
3    "lint:css": "stylelint "src/**/*.{css,scss}"",
4    "lint:css:fix": "stylelint "src/**/*.{css,scss}" --fix",
5    "lint:css:report": "stylelint "src/**/*.{css,scss}" --formatter json > stylelint-report.json",
6    "prebuild": "npm run lint:css",
7    "test:css": "stylelint "src/**/*.{css,scss}" --formatter tap"
8  }
9}

Integracja z VS Code (.vscode/settings.json)

1{
2  "editor.formatOnSave": true,
3  "editor.codeActionsOnSave": {
4    "source.fixAll.stylelint": true
5  },
6  "css.validate": false,
7  "less.validate": false,
8  "scss.validate": false,
9  "stylelint.validate": ["css", "postcss", "scss", "sass"],
10  "stylelint.snippet": ["css", "postcss", "scss", "sass"],
11  "stylelint.enable": true
12}

Pre-commit hook z Husky

1# Instalacja husky i lint-staged
2npm install --save-dev husky lint-staged
3
4# package.json
5{
6  "lint-staged": {
7    "*.{css,scss}": [
8      "stylelint --fix",
9      "git add"
10    ]
11  },
12  "husky": {
13    "hooks": {
14      "pre-commit": "lint-staged"
15    }
16  }
17}

Przykłady reguł i ich zastosowania

1. Formatowanie i spójność

1/* ❌ Błędy wykrywane przez Stylelint */
2
3/* Nieprawidłowe wcięcia */
4.component {
5margin: 0;
6  padding: 1rem;
7}
8
9/* Brak spacji */
10.component{margin:0;padding:1rem;}
11
12/* Nieprawidłowe quotes */
13.component {
14  font-family: "Arial";
15  background: url(image.jpg);
16}
17
18/* Nieprawidłowe kolory */
19.component {
20  color: RED;
21  background: #ffffff;
22  border: 1px solid #fff;
23}
24
25/* ✅ Poprawiony kod zgodny z regułami */
26
27/* Poprawne wcięcia */
28.component {
29  margin: 0;
30  padding: 1rem;
31}
32
33/* Poprawne spacje */
34.component {
35  margin: 0;
36  padding: 1rem;
37}
38
39/* Poprawne quotes */
40.component {
41  font-family: 'Arial', sans-serif;
42  background: url('image.jpg');
43}
44
45/* Poprawne kolory */
46.component {
47  color: #f00;
48  background: #fff;
49  border: 1px solid #fff;
50}

2. Kolejność właściwości

1/* ❌ Nieprawidłowa kolejność */
2.component {
3  color: #333;
4  position: absolute;
5  background: #fff;
6  top: 0;
7  margin: 1rem;
8  display: flex;
9  left: 0;
10  padding: 1rem;
11}
12
13/* ✅ Poprawiona kolejność według konfiguracji */
14.component {
15  position: absolute;
16  top: 0;
17  left: 0;
18  display: flex;
19  margin: 1rem;
20  padding: 1rem;
21  background: #fff;
22  color: #333;
23}
24
25/* ✅ Jeszcze lepsza organizacja z grupowaniem */
26.component {
27  /* Positioning */
28  position: absolute;
29  top: 0;
30  left: 0;
31  z-index: 1;
32  
33  /* Layout */
34  display: flex;
35  flex-direction: column;
36  justify-content: center;
37  align-items: center;
38  
39  /* Box model */
40  width: 100%;
41  height: 200px;
42  margin: 1rem;
43  padding: 1rem;
44  
45  /* Visual */
46  background: #fff;
47  color: #333;
48  border: 1px solid #ddd;
49  border-radius: 4px;
50  
51  /* Typography */
52  font-family: 'Inter', sans-serif;
53  font-size: 1rem;
54  line-height: 1.5;
55  
56  /* Animation */
57  transition: all 0.3s ease;
58  transform: translateY(0);
59}

3. BEM pattern validation

1/* ❌ Nieprawidłowe nazwy klas BEM */
2.Component { } /* PascalCase */
3.component_element { } /* single underscore */
4.component-element-modifier { } /* dash instead of -- */
5.componentElement { } /* camelCase */
6
7/* ✅ Poprawne nazwy klas BEM */
8.component { }
9.component__element { }
10.component__element--modifier { }
11.component--variant { }
12.multi-word-component { }
13.multi-word-component__element-name { }
14.multi-word-component__element-name--modifier-name { }

4. SCSS specific rules

1/* ❌ Nieprawidłowy SCSS */
2$primaryColor: #007bff; /* camelCase variable */
3$primary_color: #007bff; /* underscore variable */
4
5%button-style { /* nieużywany placeholder */
6  padding: 1rem;
7}
8
9.component {
10  .nested {
11    .deep {
12      .too {
13        .deep { /* zbyt głębokie zagnieżdżenie */
14          color: red;
15        }
16      }
17    }
18  }
19}
20
21@mixin buttonMixin() { /* camelCase mixin */
22  padding: 1rem;
23}
24
25/* ✅ Poprawny SCSS */
26$primary-color: #007bff;
27$secondary-color: #6c757d;
28
29%button-base {
30  padding: 1rem;
31  border: none;
32  border-radius: 4px;
33  cursor: pointer;
34}
35
36.component {
37  @include button-mixin;
38  
39  &__element {
40    color: $primary-color;
41    
42    &--modifier {
43      background: $secondary-color;
44    }
45  }
46}
47
48@mixin button-mixin($size: medium) {
49  @extend %button-base;
50  
51  @if $size == small {
52    padding: 0.5rem;
53  } @else if $size == large {
54    padding: 1.5rem;
55  }
56}

Zaawansowana konfiguracja

Custom rules dla specific projektów

1{
2  "rules": {
3    "comment-pattern": "^[A-Z][\s\S]*\.$",
4    "custom-property-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
5    "keyframes-name-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
6    "selector-class-pattern": [
7      "^[a-z]([a-z0-9-]+)?(__([a-z0-9]+-?)+)?(--([a-z0-9]+-?)+){0,2}$",
8      {
9        "message": "Expected class selector to be kebab-case BEM format"
10      }
11    ],
12    "declaration-property-value-allowed-list": {
13      "border": ["/^0$/"],
14      "border-top": ["/^0$/"],
15      "border-right": ["/^0$/"],
16      "border-bottom": ["/^0$/"],
17      "border-left": ["/^0$/"]
18    },
19    "property-allowed-list": [
20      "position",
21      "top",
22      "right", 
23      "bottom",
24      "left",
25      "z-index",
26      "display",
27      "flex",
28      "flex-direction",
29      "flex-wrap",
30      "justify-content",
31      "align-items",
32      "align-content",
33      "gap",
34      "width",
35      "height",
36      "min-width",
37      "min-height",
38      "max-width", 
39      "max-height",
40      "margin",
41      "margin-top",
42      "margin-right",
43      "margin-bottom",
44      "margin-left",
45      "padding",
46      "padding-top",
47      "padding-right",
48      "padding-bottom",
49      "padding-left",
50      "background",
51      "background-color",
52      "background-image",
53      "background-size",
54      "background-position",
55      "background-repeat",
56      "color",
57      "font-family",
58      "font-size",
59      "font-weight",
60      "line-height",
61      "text-align",
62      "text-decoration",
63      "text-transform",
64      "border",
65      "border-top",
66      "border-right",
67      "border-bottom", 
68      "border-left",
69      "border-radius",
70      "box-shadow",
71      "opacity",
72      "visibility",
73      "overflow",
74      "overflow-x",
75      "overflow-y",
76      "cursor",
77      "transition",
78      "transform",
79      "animation"
80    ]
81  }
82}

Performance-focused rules

1{
2  "rules": {
3    "selector-max-id": 0,
4    "selector-max-universal": 1,
5    "selector-max-type": 2,
6    "selector-max-class": 4,
7    "selector-max-attribute": 2,
8    "selector-max-pseudo-class": 3,
9    "selector-max-compound-selectors": 4,
10    "selector-max-specificity": "0,4,0",
11    "max-nesting-depth": 3,
12    "declaration-block-no-redundant-longhand-properties": true,
13    "shorthand-property-no-redundant-values": true,
14    "no-unknown-animations": true
15  }
16}

Integracja z build tools

Webpack integration

1// webpack.config.js
2const StylelintPlugin = require('stylelint-webpack-plugin');
3
4module.exports = {
5  plugins: [
6    new StylelintPlugin({
7      configFile: '.stylelintrc.json',
8      context: 'src',
9      files: '**/*.{css,scss}',
10      failOnError: false,
11      quiet: false,
12      emitErrors: true
13    })
14  ]
15};

Gulp integration

1// gulpfile.js
2const gulp = require('gulp');
3const stylelint = require('gulp-stylelint');
4
5gulp.task('lint-css', function() {
6  return gulp
7    .src('src/**/*.{css,scss}')
8    .pipe(stylelint({
9      reporters: [
10        { formatter: 'string', console: true }
11      ]
12    }));
13});
14
15gulp.task('fix-css', function() {
16  return gulp
17    .src('src/**/*.{css,scss}')
18    .pipe(stylelint({
19      fix: true
20    }))
21    .pipe(gulp.dest('src'));
22});

PostCSS integration

1// postcss.config.js
2module.exports = {
3  plugins: [
4    require('stylelint')({
5      config: require('./.stylelintrc.json')
6    }),
7    require('autoprefixer'),
8    require('cssnano')({
9      preset: 'default'
10    })
11  ]
12};

CI/CD Integration

GitHub Actions

1# .github/workflows/css-lint.yml
2name: CSS Lint
3
4on: [push, pull_request]
5
6jobs:
7  lint:
8    runs-on: ubuntu-latest
9    
10    steps:
11    - uses: actions/checkout@v2
12    
13    - name: Setup Node.js
14      uses: actions/setup-node@v2
15      with:
16        node-version: '18'
17        
18    - name: Install dependencies
19      run: npm ci
20      
21    - name: Run Stylelint
22      run: npm run lint:css
23      
24    - name: Upload lint results
25      uses: actions/upload-artifact@v2
26      if: failure()
27      with:
28        name: stylelint-results
29        path: stylelint-report.json

Custom reporting

1// scripts/css-report.js
2const stylelint = require('stylelint');
3const fs = require('fs');
4
5stylelint.lint({
6  files: 'src/**/*.{css,scss}',
7  formatter: 'json'
8}).then(function(resultObject) {
9  const results = JSON.parse(resultObject.output);
10  
11  const summary = {
12    totalFiles: results.length,
13    totalErrors: results.reduce((sum, file) => sum + file.errors.length, 0),
14    errorsByRule: {}
15  };
16  
17  results.forEach(file => {
18    file.warnings.forEach(warning => {
19      const rule = warning.rule;
20      summary.errorsByRule[rule] = (summary.errorsByRule[rule] || 0) + 1;
21    });
22  });
23  
24  console.log('CSS Lint Summary:');
25  console.log(`Total files analyzed: ${summary.totalFiles}`);
26  console.log(`Total errors found: ${summary.totalErrors}`);
27  console.log('Errors by rule:');
28  
29  Object.entries(summary.errorsByRule)
30    .sort(([,a], [,b]) => b - a)
31    .forEach(([rule, count]) => {
32      console.log(`  ${rule}: ${count}`);
33    });
34  
35  fs.writeFileSync('css-lint-summary.json', JSON.stringify(summary, null, 2));
36});

Stylelint jest niezbędnym narzędziem w profesjonalnym developmencie CSS, zapewniającym spójność kodu, wykrywanie błędów i egzekwowanie dobrych praktyk. Proper konfiguracja i integracja znacząco poprawia jakość i utrzymywalność stylów w projekcie.

Przejdź do CodeWorlds