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.
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-prettier1{
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}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}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}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}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}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}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}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 { }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}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}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}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};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});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};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.json1// 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.