├── test ├── fixtures │ ├── empty │ │ └── index.js │ ├── lint-one │ │ ├── test.scss │ │ └── index.js │ ├── lint-two │ │ ├── test1.scss │ │ ├── test2.scss │ │ └── index.js │ ├── error │ │ ├── index.js │ │ └── test.scss │ ├── good │ │ ├── index.js │ │ └── test.scss │ ├── [symbols] │ │ ├── index.js │ │ └── test.scss │ ├── exclude │ │ ├── index.js │ │ └── test.scss │ ├── multiple │ │ ├── good.scss │ │ ├── error.scss │ │ └── index.js │ ├── warning │ │ ├── index.js │ │ └── test.scss │ ├── stylelint-path │ │ ├── test.scss │ │ ├── ignore.scss │ │ └── index.js │ ├── full-of-problems │ │ ├── index.js │ │ └── test.scss │ ├── stylelint-ignore │ │ ├── test.scss │ │ ├── ignore.scss │ │ ├── .stylelintignore │ │ └── index.js │ ├── exclude-folder │ │ ├── index.js │ │ └── folder │ │ │ └── test.scss │ ├── lint-dirty-modules-only │ │ └── index.js │ └── watch │ │ └── index.js ├── .badstylelintrc ├── .stylelintrc ├── symbols.test.js ├── warning.test.js ├── ok.test.js ├── stylelint-ignore.test.js ├── empty.test.js ├── fail-on-warning.test.js ├── fail-on-config.test.js ├── mock │ └── stylelint │ │ └── index.js ├── stylelint-path.test.js ├── utils │ ├── conf.js │ └── pack.js ├── quiet.test.js ├── context.test.js ├── error.test.js ├── fail-on-error.test.js ├── exclude.test.js ├── stylelint-options.test.js ├── output-report.test.js ├── formatter.test.js ├── emit-error.test.js ├── emit-warning.test.js ├── lint-dirty-modules-only.test.js ├── stylelint-lint.test.js ├── multiple-instances.test.js ├── utils.test.js ├── threads.test.js └── watch.test.js ├── .husky ├── pre-commit └── commit-msg ├── .gitattributes ├── .prettierignore ├── jest.config.js ├── lint-staged.config.js ├── commitlint.config.js ├── eslint.config.mjs ├── types ├── StylelintError.d.ts ├── worker.d.ts ├── index.d.ts ├── getStylelint.d.ts ├── utils.d.ts ├── linter.d.ts └── options.d.ts ├── .editorconfig ├── .gitignore ├── src ├── StylelintError.js ├── worker.js ├── options.js ├── options.json ├── utils.js ├── getStylelint.js ├── index.js └── linter.js ├── tsconfig.json ├── babel.config.js ├── .github └── workflows │ ├── dependency-review.yml │ └── nodejs.yml ├── .cspell.json ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /test/fixtures/empty/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /test/fixtures/lint-one/test.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/lint-two/test1.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/lint-two/test2.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit $1 2 | -------------------------------------------------------------------------------- /test/fixtures/error/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/good/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/good/test.scss: -------------------------------------------------------------------------------- 1 | body { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/[symbols]/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/exclude/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/lint-one/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/multiple/good.scss: -------------------------------------------------------------------------------- 1 | body { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/warning/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/stylelint-path/test.scss: -------------------------------------------------------------------------------- 1 | body { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/warning/test.scss: -------------------------------------------------------------------------------- 1 | body { 2 | color: #FFF; // warning 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/error/test.scss: -------------------------------------------------------------------------------- 1 | #stuff { 2 | background: black; // error 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/full-of-problems/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/stylelint-ignore/test.scss: -------------------------------------------------------------------------------- 1 | body { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/stylelint-path/ignore.scss: -------------------------------------------------------------------------------- 1 | body { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/[symbols]/test.scss: -------------------------------------------------------------------------------- 1 | #stuff { 2 | background: black; // error 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/exclude-folder/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./folder/test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/exclude/test.scss: -------------------------------------------------------------------------------- 1 | #stuff { 2 | background: black; // error 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/lint-dirty-modules-only/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | -------------------------------------------------------------------------------- /test/fixtures/multiple/error.scss: -------------------------------------------------------------------------------- 1 | #stuff { 2 | background: black; // error 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | bin/* eol=lf 3 | package-lock.json -diff 4 | yarn.lock -diff 5 | -------------------------------------------------------------------------------- /test/fixtures/exclude-folder/folder/test.scss: -------------------------------------------------------------------------------- 1 | #stuff { 2 | background: black; // error 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/stylelint-ignore/ignore.scss: -------------------------------------------------------------------------------- 1 | #stuff { 2 | display: "block"; // error 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | /test/output 6 | CHANGELOG.md 7 | -------------------------------------------------------------------------------- /test/fixtures/lint-two/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test1.scss'); 2 | require('file-loader!./test2.scss'); 3 | -------------------------------------------------------------------------------- /test/fixtures/multiple/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./good.scss'); 2 | require('file-loader!./error.scss'); 3 | -------------------------------------------------------------------------------- /test/fixtures/watch/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./entry.scss'); 2 | require('file-loader!./leaf.scss'); 3 | -------------------------------------------------------------------------------- /test/fixtures/full-of-problems/test.scss: -------------------------------------------------------------------------------- 1 | #stuff { 2 | background: black; // error 3 | color: #FFF; // warning 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/stylelint-ignore/.stylelintignore: -------------------------------------------------------------------------------- 1 | ignore.scss 2 | 3 | # comment 4 | 5 | noop.scss 6 | 7 | ignore.scss 8 | -------------------------------------------------------------------------------- /test/fixtures/stylelint-ignore/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | require('file-loader!./ignore.scss'); 3 | -------------------------------------------------------------------------------- /test/fixtures/stylelint-path/index.js: -------------------------------------------------------------------------------- 1 | require('file-loader!./test.scss'); 2 | require('file-loader!./ignore.scss'); 3 | -------------------------------------------------------------------------------- /test/.badstylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "color-hex-length": "short", 4 | "color-hex-length": "short" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ["src/**/*.js"], 3 | collectCoverage: true, 4 | testEnvironment: "node", 5 | testTimeout: 60000, 6 | }; 7 | -------------------------------------------------------------------------------- /test/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "customSyntax": "postcss-scss", 3 | "rules": { 4 | "color-hex-length": ["long", { "severity": "warning" }], 5 | "color-named": "never" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*": [ 3 | "prettier --cache --write --ignore-unknown", 4 | "cspell --cache --no-must-find-files", 5 | ], 6 | "*.js": ["eslint --cache --fix"], 7 | }; 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "header-max-length": [0], 5 | "body-max-line-length": [0], 6 | "footer-max-line-length": [0], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import configs from "eslint-config-webpack/configs.js"; 3 | 4 | export default defineConfig([ 5 | { 6 | extends: [configs["recommended-dirty"]], 7 | }, 8 | ]); 9 | -------------------------------------------------------------------------------- /types/StylelintError.d.ts: -------------------------------------------------------------------------------- 1 | export = StylelintError; 2 | declare class StylelintError extends Error { 3 | /** 4 | * @param {string=} messages messages 5 | */ 6 | constructor(messages?: string | undefined); 7 | stack: string; 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | .cspellcache 5 | .eslintcache 6 | 7 | /coverage 8 | /dist 9 | /local 10 | /reports 11 | /node_modules 12 | /test/output 13 | 14 | .DS_Store 15 | Thumbs.db 16 | .idea 17 | .vscode 18 | *.sublime-project 19 | *.sublime-workspace 20 | *.iml 21 | -------------------------------------------------------------------------------- /src/StylelintError.js: -------------------------------------------------------------------------------- 1 | class StylelintError extends Error { 2 | /** 3 | * @param {string=} messages messages 4 | */ 5 | constructor(messages) { 6 | super(`[stylelint] ${messages}`); 7 | this.name = "StylelintError"; 8 | this.stack = ""; 9 | } 10 | } 11 | 12 | module.exports = StylelintError; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "strict": true, 8 | "useUnknownInCatchVariables": false, 9 | "types": ["node"], 10 | "resolveJsonModule": true 11 | }, 12 | "include": ["./src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /test/symbols.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("symbols", () => { 4 | it("should return error", async () => { 5 | const compiler = pack("[symbols]"); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasWarnings()).toBe(false); 8 | expect(stats.hasErrors()).toBe(true); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/warning.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("warning", () => { 4 | it("should emit warnings", async () => { 5 | const compiler = pack("warning"); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasWarnings()).toBe(true); 8 | expect(stats.hasErrors()).toBe(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/ok.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("ok", () => { 4 | it("should don't throw error if file is ok", async () => { 5 | const compiler = pack("good"); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasWarnings()).toBe(false); 8 | expect(stats.hasErrors()).toBe(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/stylelint-ignore.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("stylelint ignore", () => { 4 | it("should ignore file", async () => { 5 | const compiler = pack("stylelint-ignore"); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasWarnings()).toBe(false); 8 | expect(stats.hasErrors()).toBe(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const MIN_BABEL_VERSION = 7; 2 | 3 | module.exports = (api) => { 4 | api.assertVersion(MIN_BABEL_VERSION); 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | "@babel/preset-env", 11 | { 12 | targets: { 13 | node: "18.12.0", 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: "Dependency Review" 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Checkout Repository" 12 | uses: actions/checkout@v5 13 | - name: "Dependency Review" 14 | uses: actions/dependency-review-action@v4 15 | -------------------------------------------------------------------------------- /test/empty.test.js: -------------------------------------------------------------------------------- 1 | import StylelintWebpackPlugin from "../src"; 2 | 3 | import pack from "./utils/pack"; 4 | 5 | describe("empty", () => { 6 | it("no error when no files matching", async () => { 7 | const compiler = pack( 8 | "empty", 9 | {}, 10 | { 11 | plugins: [new StylelintWebpackPlugin()], 12 | }, 13 | ); 14 | const stats = await compiler.runAsync(); 15 | expect(stats.hasWarnings()).toBe(false); 16 | expect(stats.hasErrors()).toBe(false); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/fail-on-warning.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("fail on warning", () => { 4 | it("should emits errors", async () => { 5 | const compiler = pack("warning", { failOnWarning: true }); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasErrors()).toBe(true); 8 | }); 9 | 10 | it("should correctly indentifies a success", async () => { 11 | const compiler = pack("good", { failOnWarning: true }); 12 | const stats = await compiler.runAsync(); 13 | expect(stats.hasErrors()).toBe(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/fail-on-config.test.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | import pack from "./utils/pack"; 4 | 5 | describe("fail on config", () => { 6 | it("fails when .stylelintrc is not a proper format", async () => { 7 | const configFile = join(__dirname, ".badstylelintrc"); 8 | const compiler = pack("error", { configFile }); 9 | const stats = await compiler.runAsync(); 10 | const { errors } = stats.compilation; 11 | expect(stats.hasWarnings()).toBe(false); 12 | expect(stats.hasErrors()).toBe(true); 13 | expect(errors).toHaveLength(1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/mock/stylelint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | formatters: { 3 | string(results) { 4 | return JSON.stringify(results); 5 | }, 6 | }, 7 | 8 | lint() { 9 | return { 10 | results: [ 11 | { 12 | source: "", 13 | errored: true, 14 | warnings: [ 15 | { 16 | line: 1, 17 | column: 11, 18 | rule: "fake-error", 19 | severity: "error", 20 | text: "Fake error", 21 | }, 22 | ], 23 | }, 24 | ], 25 | }; 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /test/stylelint-path.test.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | import pack from "./utils/pack"; 4 | 5 | describe("stylelint path", () => { 6 | it("should use another instance of stylelint via stylelintPath config", async () => { 7 | const stylelintPath = join(__dirname, "mock/stylelint"); 8 | const compiler = pack("stylelint-path", { stylelintPath }); 9 | const stats = await compiler.runAsync(); 10 | expect(stats.hasWarnings()).toBe(false); 11 | expect(stats.hasErrors()).toBe(true); 12 | expect(stats.compilation.errors[0].message).toContain("Fake error"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en,en-gb", 4 | "words": [ 5 | "commitlint", 6 | "stylelint", 7 | "Stylelint", 8 | "stylelint's", 9 | "Autofixing", 10 | "globby", 11 | "arrify", 12 | "stylelintrc", 13 | "badstylelintrc", 14 | "indentifies", 15 | "stylelintignore", 16 | "notcss", 17 | "stylelintcache", 18 | "eslintcache", 19 | "autocrlf" 20 | ], 21 | 22 | "ignorePaths": [ 23 | "CHANGELOG.md", 24 | "package.json", 25 | "coverage/**", 26 | "dist/**", 27 | "**/__snapshots__/**", 28 | "package-lock.json", 29 | "/test/output" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /test/utils/conf.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | import StylelintPlugin from "../../src/index"; 4 | 5 | export default (context, pluginConf = {}, webpackConf = {}) => { 6 | const testDir = join(__dirname, ".."); 7 | 8 | return { 9 | context: join(testDir, "fixtures", context), 10 | mode: "development", 11 | entry: "./index", 12 | output: { 13 | path: join(testDir, "output"), 14 | }, 15 | plugins: [ 16 | new StylelintPlugin({ 17 | cache: false, 18 | configFile: join(testDir, ".stylelintrc"), 19 | ...pluginConf, 20 | }), 21 | ], 22 | ...webpackConf, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /test/quiet.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("quiet", () => { 4 | it("should not emit warnings if quiet is set", async () => { 5 | const compiler = pack("warning", { quiet: true }); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasWarnings()).toBe(false); 8 | expect(stats.hasErrors()).toBe(false); 9 | }); 10 | 11 | it("should emit errors, but not emit warnings if quiet is set", async () => { 12 | const compiler = pack("full-of-problems", { quiet: true }); 13 | const stats = await compiler.runAsync(); 14 | expect(stats.hasWarnings()).toBe(false); 15 | expect(stats.hasErrors()).toBe(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/context.test.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | import pack from "./utils/pack"; 4 | 5 | describe("context", () => { 6 | it("absolute", async () => { 7 | const compiler = pack("good", { 8 | context: join(__dirname, "fixtures/good"), 9 | }); 10 | const stats = await compiler.runAsync(); 11 | expect(stats.hasWarnings()).toBe(false); 12 | expect(stats.hasErrors()).toBe(false); 13 | }); 14 | 15 | it("relative", async () => { 16 | const compiler = pack("good", { context: "../good/" }); 17 | const stats = await compiler.runAsync(); 18 | expect(stats.hasWarnings()).toBe(false); 19 | expect(stats.hasErrors()).toBe(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/utils/pack.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | 3 | import conf from "./conf"; 4 | 5 | export default (context, pluginConf = {}, webpackConf = {}) => { 6 | const compiler = webpack(conf(context, pluginConf, webpackConf)); 7 | 8 | return { 9 | get outputPath() { 10 | return compiler.outputPath; 11 | }, 12 | 13 | runAsync() { 14 | return new Promise((resolve, reject) => { 15 | compiler.run((err, stats) => { 16 | if (err) { 17 | reject(err); 18 | } else { 19 | resolve(stats); 20 | } 21 | }); 22 | }); 23 | }, 24 | watch(options, fn) { 25 | return compiler.watch(options, fn); 26 | }, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /types/worker.d.ts: -------------------------------------------------------------------------------- 1 | export type Stylelint = import("./getStylelint").Stylelint; 2 | export type StylelintOptions = import("./getStylelint").LinterOptions; 3 | export type LintResult = import("./getStylelint").LintResult; 4 | export type Options = import("./options").Options; 5 | /** 6 | * @param {string | string[]} files files 7 | * @returns {Promise} results 8 | */ 9 | export function lintFiles(files: string | string[]): Promise; 10 | /** 11 | * @param {Options} options the worker options 12 | * @param {Partial} stylelintOptions the stylelint options 13 | * @returns {Stylelint} stylelint instance 14 | */ 15 | export function setup( 16 | options: Options, 17 | stylelintOptions: Partial, 18 | ): Stylelint; 19 | -------------------------------------------------------------------------------- /test/error.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("error", () => { 4 | afterEach(() => { 5 | jest.restoreAllMocks(); 6 | }); 7 | 8 | it("should return error if file is bad", async () => { 9 | const compiler = pack("error"); 10 | const stats = await compiler.runAsync(); 11 | expect(stats.hasWarnings()).toBe(false); 12 | expect(stats.hasErrors()).toBe(true); 13 | }); 14 | 15 | it("should propagate stylelint exceptions as errors", async () => { 16 | jest.mock("stylelint", () => { 17 | throw new Error("Oh no!"); 18 | }); 19 | 20 | const compiler = pack("good"); 21 | const stats = await compiler.runAsync(); 22 | expect(stats.hasWarnings()).toBe(false); 23 | expect(stats.hasErrors()).toBe(true); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/fail-on-error.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("fail on error", () => { 4 | it("should emits errors", async () => { 5 | const compiler = pack("error", { failOnError: true }); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasErrors()).toBe(true); 8 | }); 9 | 10 | it("should emit warnings when disabled", async () => { 11 | const compiler = pack("error", { failOnError: false }); 12 | const stats = await compiler.runAsync(); 13 | expect(stats.hasErrors()).toBe(false); 14 | expect(stats.hasWarnings()).toBe(true); 15 | }); 16 | 17 | it("should correctly indentifies a success", async () => { 18 | const compiler = pack("good", { failOnError: true }); 19 | const stats = await compiler.runAsync(); 20 | expect(stats.hasErrors()).toBe(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/exclude.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("exclude", () => { 4 | it("should exclude with globs", async () => { 5 | const compiler = pack("exclude", { exclude: ["*test*"] }); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasWarnings()).toBe(false); 8 | expect(stats.hasErrors()).toBe(false); 9 | }); 10 | 11 | it("should exclude files", async () => { 12 | const compiler = pack("exclude", { exclude: ["test.scss"] }); 13 | const stats = await compiler.runAsync(); 14 | expect(stats.hasWarnings()).toBe(false); 15 | expect(stats.hasErrors()).toBe(false); 16 | }); 17 | 18 | it("should exclude folders", async () => { 19 | const compiler = pack("exclude-folder", { exclude: ["folder"] }); 20 | const stats = await compiler.runAsync(); 21 | expect(stats.hasWarnings()).toBe(false); 22 | expect(stats.hasErrors()).toBe(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/stylelint-options.test.js: -------------------------------------------------------------------------------- 1 | import { getStylelintOptions } from "../src/options"; 2 | 3 | describe("eslint options", () => { 4 | it("should filter plugin options", () => { 5 | const options = { 6 | formatter: "json", 7 | emitError: false, 8 | }; 9 | expect(getStylelintOptions(options)).toStrictEqual({ 10 | formatter: "json", 11 | }); 12 | }); 13 | 14 | it("should keep the stylelint options", () => { 15 | const options = { 16 | stylelintPath: "some/place/where/stylelint/lives", 17 | formatter: "json", 18 | files: ["file.scss"], 19 | emitError: false, 20 | emitWarning: false, 21 | failOnError: true, 22 | failOnWarning: true, 23 | quiet: false, 24 | outputReport: true, 25 | }; 26 | expect(getStylelintOptions(options)).toStrictEqual({ 27 | formatter: "json", 28 | files: ["file.scss"], 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/output-report.test.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | import { existsSync, readFileSync } from "fs-extra"; 4 | 5 | import pack from "./utils/pack"; 6 | 7 | describe("output report", () => { 8 | it("should output report a default formatter", async () => { 9 | const filePath = "report.txt"; 10 | const compiler = pack("error", { 11 | outputReport: { filePath }, 12 | }); 13 | const stats = await compiler.runAsync(); 14 | expect(stats.hasWarnings()).toBe(false); 15 | expect(stats.hasErrors()).toBe(true); 16 | expect(existsSync(join(compiler.outputPath, filePath))).toBe(true); 17 | }); 18 | 19 | it("should output report with a custom formatter", async () => { 20 | const filePath = join(__dirname, "output", "report.json"); 21 | const compiler = pack("error", { 22 | outputReport: { 23 | filePath, 24 | formatter: "json", 25 | }, 26 | }); 27 | const stats = await compiler.runAsync(); 28 | expect(stats.hasWarnings()).toBe(false); 29 | expect(stats.hasErrors()).toBe(true); 30 | expect(existsSync(filePath)).toBe(true); 31 | expect(JSON.parse(readFileSync(filePath, "utf8"))).toMatchObject([ 32 | { source: expect.stringContaining("test.scss") }, 33 | ]); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export = StylelintWebpackPlugin; 2 | declare class StylelintWebpackPlugin { 3 | /** 4 | * @param {Options=} options options 5 | */ 6 | constructor(options?: Options | undefined); 7 | key: string; 8 | options: Partial; 9 | /** 10 | * @param {Compiler} compiler compiler 11 | * @param {Options} options options 12 | * @param {string[]} wanted wanted files 13 | * @param {string[]} exclude exclude files 14 | */ 15 | run( 16 | compiler: Compiler, 17 | options: Options, 18 | wanted: string[], 19 | exclude: string[], 20 | ): Promise; 21 | startTime: number; 22 | prevTimestamps: Map; 23 | /** 24 | * @param {Compiler} compiler compiler 25 | * @returns {void} 26 | */ 27 | apply(compiler: Compiler): void; 28 | /** 29 | * @param {Compiler} compiler compiler 30 | * @returns {string} context 31 | */ 32 | getContext(compiler: Compiler): string; 33 | } 34 | declare namespace StylelintWebpackPlugin { 35 | export { Compiler, Module, Options, FileSystemInfoEntry }; 36 | } 37 | type Compiler = import("webpack").Compiler; 38 | type Module = import("webpack").Module; 39 | type Options = import("./options").Options; 40 | type FileSystemInfoEntry = Partial< 41 | | { 42 | timestamp: number; 43 | } 44 | | number 45 | >; 46 | -------------------------------------------------------------------------------- /types/getStylelint.d.ts: -------------------------------------------------------------------------------- 1 | export = getStylelint; 2 | /** 3 | * @param {string|undefined} key a cache key 4 | * @param {Options} options options 5 | * @returns {Linter} linter 6 | */ 7 | declare function getStylelint( 8 | key: string | undefined, 9 | { threads, ...options }: Options, 10 | ): Linter; 11 | declare namespace getStylelint { 12 | export { 13 | Stylelint, 14 | LintResult, 15 | LinterOptions, 16 | LinterResult, 17 | Formatter, 18 | FormatterType, 19 | RuleMeta, 20 | Options, 21 | AsyncTask, 22 | LintTask, 23 | Linter, 24 | Worker, 25 | }; 26 | } 27 | type Stylelint = { 28 | lint: (options: LinterOptions) => Promise; 29 | formatters: { 30 | [k: string]: Formatter; 31 | }; 32 | }; 33 | type LintResult = import("stylelint").LintResult; 34 | type LinterOptions = import("stylelint").LinterOptions; 35 | type LinterResult = import("stylelint").LinterResult; 36 | type Formatter = import("stylelint").Formatter; 37 | type FormatterType = import("stylelint").FormatterType; 38 | type RuleMeta = import("stylelint").RuleMeta; 39 | type Options = import("./options").Options; 40 | type AsyncTask = () => Promise; 41 | type LintTask = (files: string | string[]) => Promise; 42 | type Linter = { 43 | stylelint: Stylelint; 44 | lintFiles: LintTask; 45 | cleanup: AsyncTask; 46 | threads: number; 47 | }; 48 | type Worker = JestWorker & { 49 | lintFiles: LintTask; 50 | }; 51 | import { Worker as JestWorker } from "jest-worker"; 52 | -------------------------------------------------------------------------------- /test/formatter.test.js: -------------------------------------------------------------------------------- 1 | import { formatters } from "stylelint"; 2 | 3 | import pack from "./utils/pack"; 4 | 5 | describe("formatter", () => { 6 | it("should use default formatter", async () => { 7 | const compiler = pack("error"); 8 | const stats = await compiler.runAsync(); 9 | expect(stats.hasWarnings()).toBe(false); 10 | expect(stats.hasErrors()).toBe(true); 11 | expect(stats.compilation.errors[0].message).toBeTruthy(); 12 | }); 13 | 14 | it("should use default formatter when invalid", async () => { 15 | const compiler = pack("error", { formatter: "invalid" }); 16 | const stats = await compiler.runAsync(); 17 | expect(stats.hasWarnings()).toBe(false); 18 | expect(stats.hasErrors()).toBe(true); 19 | expect(stats.compilation.errors[0].message).toBeTruthy(); 20 | }); 21 | 22 | it("should use string formatter", async () => { 23 | const compiler = pack("error", { formatter: "json" }); 24 | const stats = await compiler.runAsync(); 25 | expect(stats.hasWarnings()).toBe(false); 26 | expect(stats.hasErrors()).toBe(true); 27 | expect(stats.compilation.errors[0].message).toBeTruthy(); 28 | }); 29 | 30 | it("should use function formatter", async () => { 31 | const compiler = pack("error", { formatter: await formatters.verbose }); 32 | const stats = await compiler.runAsync(); 33 | expect(stats.hasWarnings()).toBe(false); 34 | expect(stats.hasErrors()).toBe(true); 35 | expect(stats.compilation.errors[0].message).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/emit-error.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("emit error", () => { 4 | it("should not emit errors if emitError is false", async () => { 5 | const compiler = pack("error", { emitError: false }); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasErrors()).toBe(false); 8 | }); 9 | 10 | it("should emit errors if emitError is undefined", async () => { 11 | const compiler = pack("error"); 12 | const stats = await compiler.runAsync(); 13 | expect(stats.hasErrors()).toBe(true); 14 | }); 15 | 16 | it("should emit errors if emitError is true", async () => { 17 | const compiler = pack("error", { emitError: true }); 18 | const stats = await compiler.runAsync(); 19 | expect(stats.hasErrors()).toBe(true); 20 | }); 21 | 22 | it("should emit errors, but not warnings if emitError is true and emitWarning is false", async () => { 23 | const compiler = pack("full-of-problems", { 24 | emitError: true, 25 | emitWarning: false, 26 | }); 27 | 28 | const stats = await compiler.runAsync(); 29 | expect(stats.hasWarnings()).toBe(false); 30 | expect(stats.hasErrors()).toBe(true); 31 | }); 32 | 33 | it("should emit errors and warnings if emitError is true and emitWarning is undefined", async () => { 34 | const compiler = pack("full-of-problems", { emitError: true }); 35 | const stats = await compiler.runAsync(); 36 | expect(stats.hasWarnings()).toBe(true); 37 | expect(stats.hasErrors()).toBe(true); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('./getStylelint').Stylelint} Stylelint */ 2 | /** @typedef {import('./getStylelint').LinterOptions} StylelintOptions */ 3 | /** @typedef {import('./getStylelint').LintResult} LintResult */ 4 | /** @typedef {import('./options').Options} Options */ 5 | 6 | /** @type {Stylelint} */ 7 | let stylelint; 8 | 9 | /** @type {Partial} */ 10 | let linterOptions; 11 | 12 | /** 13 | * @param {Options} options the worker options 14 | * @param {Partial} stylelintOptions the stylelint options 15 | * @returns {Stylelint} stylelint instance 16 | */ 17 | function setup(options, stylelintOptions) { 18 | stylelint = require(options.stylelintPath || "stylelint"); 19 | linterOptions = stylelintOptions; 20 | 21 | return stylelint; 22 | } 23 | 24 | /** 25 | * @param {string | string[]} files files 26 | * @returns {Promise} results 27 | */ 28 | async function lintFiles(files) { 29 | const { results } = await stylelint.lint({ 30 | ...linterOptions, 31 | files, 32 | quietDeprecationWarnings: true, 33 | }); 34 | 35 | // Reset result to work with worker 36 | return results.map((result) => ({ 37 | source: result.source, 38 | errored: result.errored, 39 | ignored: result.ignored, 40 | warnings: result.warnings, 41 | deprecations: result.deprecations, 42 | invalidOptionWarnings: result.invalidOptionWarnings, 43 | parseErrors: result.parseErrors, 44 | })); 45 | } 46 | 47 | module.exports.lintFiles = lintFiles; 48 | module.exports.setup = setup; 49 | -------------------------------------------------------------------------------- /test/emit-warning.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("emit warning", () => { 4 | it("should not emit warnings if emitWarning is false", async () => { 5 | const compiler = pack("warning", { emitWarning: false }); 6 | const stats = await compiler.runAsync(); 7 | expect(stats.hasWarnings()).toBe(false); 8 | }); 9 | 10 | it("should emit warnings if emitWarning is undefined", async () => { 11 | const compiler = pack("warning"); 12 | const stats = await compiler.runAsync(); 13 | expect(stats.hasWarnings()).toBe(true); 14 | }); 15 | 16 | it("should emit warnings if emitWarning is true", async () => { 17 | const compiler = pack("warning", { emitWarning: true }); 18 | const stats = await compiler.runAsync(); 19 | expect(stats.hasWarnings()).toBe(true); 20 | }); 21 | 22 | it("should emit warnings, but not warnings if emitWarning is true and emitError is false", async () => { 23 | const compiler = pack("full-of-problems", { 24 | emitWarning: true, 25 | emitError: false, 26 | }); 27 | const stats = await compiler.runAsync(); 28 | expect(stats.hasWarnings()).toBe(true); 29 | expect(stats.hasErrors()).toBe(false); 30 | }); 31 | 32 | it("should emit warnings and errors if emitWarning is true and emitError is undefined", async () => { 33 | const compiler = pack("full-of-problems", { emitWarning: true }); 34 | const stats = await compiler.runAsync(); 35 | expect(stats.hasWarnings()).toBe(true); 36 | expect(stats.hasErrors()).toBe(true); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type ArrifyResult = T extends null | undefined 2 | ? [] 3 | : T extends string 4 | ? [string] 5 | : T extends readonly unknown[] 6 | ? T 7 | : T extends Iterable 8 | ? T_1[] 9 | : [T]; 10 | export type EXPECTED_ANY = any; 11 | /** 12 | * @template T 13 | * @typedef {T extends (null | undefined) 14 | * ? [] 15 | * : T extends string 16 | * ? [string] 17 | * : T extends readonly unknown[] 18 | * ? T 19 | * : T extends Iterable 20 | * ? T[] 21 | * : [T]} ArrifyResult 22 | */ 23 | /** 24 | * @template T 25 | * @param {T} value value 26 | * @returns {ArrifyResult} array of values 27 | */ 28 | export function arrify(value: T): ArrifyResult; 29 | /** 30 | * @param {string} _ key, but unused 31 | * @param {EXPECTED_ANY} value value 32 | * @returns {{ [x: string]: EXPECTED_ANY }} result 33 | */ 34 | export function jsonStringifyReplacerSortKeys( 35 | _: string, 36 | value: EXPECTED_ANY, 37 | ): { 38 | [x: string]: EXPECTED_ANY; 39 | }; 40 | /** 41 | * @param {string | string[]} files files 42 | * @param {string} context context 43 | * @returns {string[]} normalized paths 44 | */ 45 | export function parseFiles(files: string | string[], context: string): string[]; 46 | /** 47 | * @param {string | string[]} patterns patterns 48 | * @param {string | string[]} extensions extensions 49 | * @returns {string[]} globs 50 | */ 51 | export function parseFoldersToGlobs( 52 | patterns: string | string[], 53 | extensions?: string | string[], 54 | ): string[]; 55 | -------------------------------------------------------------------------------- /test/lint-dirty-modules-only.test.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | import { removeSync, writeFileSync } from "fs-extra"; 4 | 5 | import pack from "./utils/pack"; 6 | 7 | const target = join(__dirname, "fixtures/lint-dirty-modules-only/test.scss"); 8 | 9 | describe("lint dirty modules only", () => { 10 | let watch; 11 | 12 | afterEach(() => { 13 | if (watch) { 14 | watch.close(); 15 | } 16 | removeSync(target); 17 | }); 18 | 19 | it("skips linting on initial run", (done) => { 20 | writeFileSync(target, "body { }\n"); 21 | 22 | // eslint-disable-next-line no-use-before-define 23 | let next = firstPass; 24 | const compiler = pack("lint-dirty-modules-only", { 25 | lintDirtyModulesOnly: true, 26 | }); 27 | watch = compiler.watch({}, (err, stats) => next(err, stats)); 28 | 29 | function secondPass(err, stats) { 30 | expect(err).toBeNull(); 31 | expect(stats.hasWarnings()).toBe(false); 32 | expect(stats.hasErrors()).toBe(true); 33 | const { errors } = stats.compilation; 34 | expect(errors).toHaveLength(1); 35 | const [{ message }] = errors; 36 | expect(message).toEqual(expect.stringMatching("color-named")); 37 | done(); 38 | } 39 | 40 | function firstPass(err, stats) { 41 | expect(err).toBeNull(); 42 | expect(stats.hasWarnings()).toBe(false); 43 | expect(stats.hasErrors()).toBe(false); 44 | 45 | next = secondPass; 46 | 47 | writeFileSync(target, "#stuff { background: black; }\n"); 48 | } 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/stylelint-lint.test.js: -------------------------------------------------------------------------------- 1 | import pack from "./utils/pack"; 2 | 3 | describe("stylelint lint", () => { 4 | const mockLintFiles = jest.fn().mockReturnValue({ 5 | results: [], 6 | }); 7 | 8 | beforeAll(() => { 9 | jest.mock("stylelint", () => ({ 10 | lint: mockLintFiles, 11 | })); 12 | }); 13 | 14 | beforeEach(() => { 15 | mockLintFiles.mockClear(); 16 | }); 17 | 18 | it("should lint one file", async () => { 19 | const compiler = pack("lint-one", { configFile: null }); 20 | const stats = await compiler.runAsync(); 21 | expect(stats.hasErrors()).toBe(false); 22 | const files = [expect.stringMatching("test.scss")]; 23 | expect(mockLintFiles).toHaveBeenCalledWith({ 24 | cache: false, 25 | cacheLocation: 26 | "node_modules/.cache/stylelint-webpack-plugin/.stylelintcache", 27 | configFile: null, 28 | files, 29 | quietDeprecationWarnings: true, 30 | }); 31 | }); 32 | 33 | it("should lint two files", async () => { 34 | const compiler = pack("lint-two", { configFile: null }); 35 | const stats = await compiler.runAsync(); 36 | expect(stats.hasErrors()).toBe(false); 37 | const files = [ 38 | expect.stringMatching(/test[12]\.scss$/), 39 | expect.stringMatching(/test[12]\.scss$/), 40 | ]; 41 | expect(mockLintFiles).toHaveBeenCalledWith({ 42 | cache: false, 43 | cacheLocation: 44 | "node_modules/.cache/stylelint-webpack-plugin/.stylelintcache", 45 | configFile: null, 46 | files, 47 | quietDeprecationWarnings: true, 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/multiple-instances.test.js: -------------------------------------------------------------------------------- 1 | import StylelintPlugin from "../src"; 2 | 3 | import pack from "./utils/pack"; 4 | 5 | describe("multiple instances", () => { 6 | it("should don't fail", async () => { 7 | const compiler = pack( 8 | "multiple", 9 | {}, 10 | { 11 | plugins: [ 12 | new StylelintPlugin({ exclude: "error.scss" }), 13 | new StylelintPlugin({ exclude: "error.scss" }), 14 | ], 15 | }, 16 | ); 17 | 18 | const stats = await compiler.runAsync(); 19 | expect(stats.hasWarnings()).toBe(false); 20 | expect(stats.hasErrors()).toBe(false); 21 | }); 22 | 23 | it("should fail on first instance", async () => { 24 | const compiler = pack( 25 | "multiple", 26 | {}, 27 | { 28 | plugins: [ 29 | new StylelintPlugin({ exclude: "good.scss" }), 30 | new StylelintPlugin({ exclude: "error.scss" }), 31 | ], 32 | }, 33 | ); 34 | 35 | const stats = await compiler.runAsync(); 36 | expect(stats.hasWarnings()).toBe(false); 37 | expect(stats.hasErrors()).toBe(true); 38 | }); 39 | 40 | it("should fail on second instance", async () => { 41 | const compiler = pack( 42 | "multiple", 43 | {}, 44 | { 45 | plugins: [ 46 | new StylelintPlugin({ exclude: "error.scss" }), 47 | new StylelintPlugin({ exclude: "good.scss" }), 48 | ], 49 | }, 50 | ); 51 | 52 | const stats = await compiler.runAsync(); 53 | expect(stats.hasWarnings()).toBe(false); 54 | expect(stats.hasErrors()).toBe(true); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /types/linter.d.ts: -------------------------------------------------------------------------------- 1 | export = linter; 2 | /** 3 | * @param {string | undefined} key a cache key 4 | * @param {Options} options options 5 | * @param {Compilation} compilation compilation 6 | * @returns {{ lint: Linter, report: Reporter, threads: number }} the linter with additional functions 7 | */ 8 | declare function linter( 9 | key: string | undefined, 10 | options: Options, 11 | compilation: Compilation, 12 | ): { 13 | lint: Linter; 14 | report: Reporter; 15 | threads: number; 16 | }; 17 | declare namespace linter { 18 | export { 19 | Compiler, 20 | Compilation, 21 | Stylelint, 22 | LintResult, 23 | LinterResult, 24 | Formatter, 25 | FormatterType, 26 | RuleMeta, 27 | Options, 28 | GenerateReport, 29 | Report, 30 | Reporter, 31 | Linter, 32 | LintResultMap, 33 | }; 34 | } 35 | type Compiler = import("webpack").Compiler; 36 | type Compilation = import("webpack").Compilation; 37 | type Stylelint = import("./getStylelint").Stylelint; 38 | type LintResult = import("./getStylelint").LintResult; 39 | type LinterResult = import("./getStylelint").LinterResult; 40 | type Formatter = import("./getStylelint").Formatter; 41 | type FormatterType = import("./getStylelint").FormatterType; 42 | type RuleMeta = import("./getStylelint").RuleMeta; 43 | type Options = import("./options").Options; 44 | type GenerateReport = (compilation: Compilation) => Promise; 45 | type Report = { 46 | errors?: StylelintError; 47 | warnings?: StylelintError; 48 | generateReportAsset?: GenerateReport; 49 | }; 50 | type Reporter = () => Promise; 51 | type Linter = (files: string | string[]) => void; 52 | type LintResultMap = { 53 | [files: string]: LintResult; 54 | }; 55 | import StylelintError = require("./StylelintError"); 56 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | import { parseFiles, parseFoldersToGlobs } from "../src/utils"; 2 | 3 | jest.mock("fs", () => ({ 4 | statSync(pattern) { 5 | return { 6 | isDirectory() { 7 | return pattern.indexOf("/path/") === 0; 8 | }, 9 | }; 10 | }, 11 | })); 12 | 13 | describe("utils test", () => { 14 | it("parseFiles should return relative files from context", () => { 15 | expect( 16 | parseFiles( 17 | ["**/*", "../package-a/src/**/", "../package-b/src/**/"], 18 | "main/src", 19 | ), 20 | ).toEqual( 21 | expect.arrayContaining([ 22 | expect.stringContaining("main/src/**/*"), 23 | expect.stringContaining("main/package-a/src/**"), 24 | expect.stringContaining("main/package-b/src/**"), 25 | ]), 26 | ); 27 | }); 28 | 29 | it("parseFoldersToGlobs should return globs for folders", () => { 30 | const withoutSlash = "/path/to/code"; 31 | const withSlash = `${withoutSlash}/`; 32 | 33 | expect(parseFoldersToGlobs(withoutSlash, "css")).toMatchInlineSnapshot(` 34 | [ 35 | "/path/to/code/**/*.css", 36 | ] 37 | `); 38 | expect(parseFoldersToGlobs(withSlash, "scss")).toMatchInlineSnapshot(` 39 | [ 40 | "/path/to/code/**/*.scss", 41 | ] 42 | `); 43 | 44 | expect( 45 | parseFoldersToGlobs( 46 | [withoutSlash, withSlash, "/some/file.scss"], 47 | ["scss", "css", "sass"], 48 | ), 49 | ).toMatchInlineSnapshot(` 50 | [ 51 | "/path/to/code/**/*.{scss,css,sass}", 52 | "/path/to/code/**/*.{scss,css,sass}", 53 | "/some/file.scss", 54 | ] 55 | `); 56 | 57 | expect(parseFoldersToGlobs(withoutSlash)).toMatchInlineSnapshot(` 58 | [ 59 | "/path/to/code/**", 60 | ] 61 | `); 62 | 63 | expect(parseFoldersToGlobs(withSlash)).toMatchInlineSnapshot(` 64 | [ 65 | "/path/to/code/**", 66 | ] 67 | `); 68 | }); 69 | 70 | it("parseFoldersToGlobs should return unmodified globs for globs (ignoring extensions)", () => { 71 | expect(parseFoldersToGlobs("**.notcss", "css")).toMatchInlineSnapshot(` 72 | [ 73 | "**.notcss", 74 | ] 75 | `); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/threads.test.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | // @ts-expect-error no types 4 | import normalizePath from "normalize-path"; 5 | 6 | import getStylelint from "../src/getStylelint"; 7 | 8 | import pack from "./utils/pack"; 9 | 10 | describe("Threading", () => { 11 | it("should don't throw error if file is ok with threads", async () => { 12 | const compiler = pack("good", { threads: 2 }); 13 | const stats = await compiler.runAsync(); 14 | expect(stats.hasWarnings()).toBe(false); 15 | expect(stats.hasErrors()).toBe(false); 16 | }); 17 | 18 | it("threaded interface should look like non-threaded interface", async () => { 19 | const single = getStylelint("single", {}); 20 | const threaded = getStylelint("threaded", { threads: 2 }); 21 | for (const key of Object.keys(single)) { 22 | expect(typeof single[key]).toEqual(typeof threaded[key]); 23 | } 24 | 25 | expect(single.lintFiles).not.toBe(threaded.lintFiles); 26 | expect(single.cleanup).not.toBe(threaded.cleanup); 27 | 28 | single.cleanup(); 29 | threaded.cleanup(); 30 | }); 31 | 32 | it("threaded should lint files", async () => { 33 | const threaded = getStylelint("bar", { threads: true }); 34 | try { 35 | const [good, bad] = await Promise.all([ 36 | threaded.lintFiles( 37 | normalizePath(join(__dirname, "fixtures/good/test.scss")), 38 | ), 39 | threaded.lintFiles( 40 | normalizePath(join(__dirname, "fixtures/error/test.scss")), 41 | ), 42 | ]); 43 | expect(good[0].errored).toBe(false); 44 | expect(bad[0].errored).toBe(true); 45 | } finally { 46 | threaded.cleanup(); 47 | } 48 | }); 49 | 50 | describe("worker coverage", () => { 51 | beforeEach(() => { 52 | jest.resetModules(); 53 | }); 54 | 55 | it("worker can start", async () => { 56 | const { lintFiles, setup } = require("../src/worker"); 57 | 58 | const mockThread = { parentPort: { on: jest.fn() }, workerData: {} }; 59 | const mockLintFiles = jest.fn().mockReturnValue({ 60 | results: [], 61 | }); 62 | 63 | jest.mock("worker_threads", () => mockThread); 64 | jest.mock("stylelint", () => ({ lint: mockLintFiles })); 65 | 66 | setup({}); 67 | 68 | await lintFiles("foo"); 69 | 70 | expect(mockLintFiles).toHaveBeenCalledWith({ 71 | files: "foo", 72 | quietDeprecationWarnings: true, 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/watch.test.js: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | 3 | import { removeSync, writeFileSync } from "fs-extra"; 4 | 5 | import pack from "./utils/pack"; 6 | 7 | const target = join(__dirname, "fixtures", "watch", "entry.scss"); 8 | const target2 = join(__dirname, "fixtures", "watch", "leaf.scss"); 9 | 10 | describe("watch", () => { 11 | let watch; 12 | 13 | afterEach(() => { 14 | if (watch) { 15 | watch.close(); 16 | } 17 | removeSync(target); 18 | removeSync(target2); 19 | }); 20 | 21 | it("should watch", (done) => { 22 | const compiler = pack("good"); 23 | 24 | watch = compiler.watch({}, (err, stats) => { 25 | expect(err).toBeNull(); 26 | expect(stats.hasWarnings()).toBe(false); 27 | expect(stats.hasErrors()).toBe(false); 28 | done(); 29 | }); 30 | }); 31 | 32 | it("should watch with unique messages", (done) => { 33 | writeFileSync(target, "#foo { background: black; }\n"); 34 | writeFileSync(target2, ""); 35 | 36 | // eslint-disable-next-line no-use-before-define 37 | let next = firstPass; 38 | const compiler = pack("watch"); 39 | watch = compiler.watch({}, (err, stats) => next(err, stats)); 40 | 41 | function finish(err, stats) { 42 | expect(err).toBeNull(); 43 | expect(stats.hasWarnings()).toBe(false); 44 | expect(stats.hasErrors()).toBe(false); 45 | done(); 46 | } 47 | 48 | function thirdPass(err, stats) { 49 | expect(err).toBeNull(); 50 | expect(stats.hasWarnings()).toBe(false); 51 | expect(stats.hasErrors()).toBe(true); 52 | const { errors } = stats.compilation; 53 | expect(errors).toHaveLength(1); 54 | const [{ message }] = errors; 55 | expect(message).toEqual(expect.stringMatching("entry.scss")); 56 | expect(message).not.toEqual(expect.stringMatching("leaf.scss")); 57 | 58 | next = finish; 59 | writeFileSync(target, "#bar { background: #000000; }\n"); 60 | } 61 | 62 | function secondPass(err, stats) { 63 | expect(err).toBeNull(); 64 | expect(stats.hasWarnings()).toBe(false); 65 | expect(stats.hasErrors()).toBe(true); 66 | const { errors } = stats.compilation; 67 | expect(errors).toHaveLength(1); 68 | const [{ message }] = errors; 69 | expect(message).toEqual(expect.stringMatching("entry.scss")); 70 | expect(message).toEqual(expect.stringMatching("leaf.scss")); 71 | 72 | next = thirdPass; 73 | writeFileSync(target2, "#bar { background: #000000; }\n"); 74 | } 75 | 76 | function firstPass(err, stats) { 77 | expect(err).toBeNull(); 78 | expect(stats.hasWarnings()).toBe(false); 79 | expect(stats.hasErrors()).toBe(true); 80 | const { errors } = stats.compilation; 81 | expect(errors).toHaveLength(1); 82 | const [{ message }] = errors; 83 | expect(message).toEqual(expect.stringMatching("entry.scss")); 84 | expect(message).not.toEqual(expect.stringMatching("leaf.scss")); 85 | 86 | next = secondPass; 87 | writeFileSync(target2, "#bar { background: black; }\n"); 88 | writeFileSync(target, "#foo { background: black; }\n"); 89 | } 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: stylelint-webpack-plugin 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 19 | 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | node-version: [lts/*] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | concurrency: 31 | group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 32 | cancel-in-progress: true 33 | 34 | steps: 35 | - uses: actions/checkout@v5 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | cache: "npm" 44 | 45 | - name: Install dependencies 46 | run: npm ci 47 | 48 | - name: Lint 49 | run: npm run lint 50 | 51 | - name: Security audit 52 | run: npm run security 53 | 54 | - name: Validate PR commits with commitlint 55 | if: github.event_name == 'pull_request' 56 | run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose 57 | 58 | test: 59 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Stylelint ${{ matrix.stylelint-version }}, Webpack ${{ matrix.webpack-version }} 60 | 61 | strategy: 62 | matrix: 63 | os: [ubuntu-latest, windows-latest, macos-latest] 64 | node-version: [18.x, 20.x, 22.x, 24.x] 65 | stylelint-version: [13.x, 14.x, 15.x, 16.x] 66 | webpack-version: [latest] 67 | 68 | runs-on: ${{ matrix.os }} 69 | 70 | concurrency: 71 | group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ matrix.stylelint-version }}-${{ matrix.webpack-version }}-${{ github.ref }} 72 | cancel-in-progress: true 73 | 74 | steps: 75 | - name: Setup Git 76 | if: matrix.os == 'windows-latest' 77 | run: git config --global core.autocrlf input 78 | 79 | - uses: actions/checkout@v5 80 | 81 | - name: Use Node.js ${{ matrix.node-version }} 82 | uses: actions/setup-node@v4 83 | with: 84 | node-version: ${{ matrix.node-version }} 85 | cache: "npm" 86 | 87 | - name: Install dependencies 88 | run: npm ci 89 | 90 | - name: Install webpack ${{ matrix.webpack-version }} 91 | run: npm i webpack@${{ matrix.webpack-version }} 92 | 93 | - name: Install stylelint ${{ matrix.stylelint-version }} 94 | run: npm i stylelint@${{ matrix.stylelint-version }} 95 | 96 | - name: Run tests for webpack version ${{ matrix.webpack-version }} 97 | run: npm run test:coverage -- --ci 98 | 99 | - name: Submit coverage data to codecov 100 | uses: codecov/codecov-action@v5 101 | with: 102 | token: ${{ secrets.CODECOV_TOKEN }} 103 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | const { validate } = require("schema-utils"); 2 | 3 | const schema = require("./options.json"); 4 | 5 | /** @typedef {import('./getStylelint').LinterOptions} StylelintOptions */ 6 | /** @typedef {import('./getStylelint').FormatterType} FormatterType */ 7 | 8 | /** 9 | * @typedef {object} OutputReport 10 | * @property {string=} filePath file path 11 | * @property {FormatterType=} formatter formatter 12 | */ 13 | 14 | /** 15 | * @typedef {object} PluginOptions 16 | * @property {string} context a string indicating the root of your files 17 | * @property {boolean} emitError the errors found will always be emitted 18 | * @property {boolean} emitWarning the warnings found will always be emitted 19 | * @property {string | string[]=} exclude specify the files and/or directories to exclude 20 | * @property {string | string[]} extensions specify the extensions that should be checked 21 | * @property {boolean} failOnError will cause the module build to fail if there are any errors 22 | * @property {boolean} failOnWarning will cause the module build to fail if there are any warning 23 | * @property {string | string[]} files specify directories, files, or globs 24 | * @property {FormatterType} formatter specify the formatter you would like to use to format your results 25 | * @property {boolean} lintDirtyModulesOnly lint only changed files, skip linting on start 26 | * @property {boolean} quiet will process and report errors only and ignore warnings 27 | * @property {string} stylelintPath path to `stylelint` instance that will be used for linting 28 | * @property {OutputReport} outputReport writes the output of the errors to a file - for example, a `json` file for use for reporting 29 | * @property {number | boolean=} threads number of worker threads 30 | */ 31 | 32 | /** @typedef {Partial} Options */ 33 | 34 | /** 35 | * @param {Options} pluginOptions plugin options 36 | * @returns {Partial} partial plugin options 37 | */ 38 | function getOptions(pluginOptions) { 39 | const options = { 40 | cache: true, 41 | cacheLocation: 42 | "node_modules/.cache/stylelint-webpack-plugin/.stylelintcache", 43 | extensions: ["css", "scss", "sass"], 44 | emitError: true, 45 | emitWarning: true, 46 | failOnError: true, 47 | ...pluginOptions, 48 | ...(pluginOptions.quiet ? { emitError: true, emitWarning: false } : {}), 49 | }; 50 | 51 | // @ts-expect-error need better types 52 | validate(schema, options, { 53 | name: "Stylelint Webpack Plugin", 54 | baseDataPath: "options", 55 | }); 56 | 57 | return options; 58 | } 59 | 60 | /** 61 | * @param {Options} pluginOptions plugin options 62 | * @returns {Partial} stylelint options 63 | */ 64 | function getStylelintOptions(pluginOptions) { 65 | const stylelintOptions = { ...pluginOptions }; 66 | 67 | // Keep the files and formatter option because it is common to both the plugin and Stylelint. 68 | const { files, formatter, ...stylelintOnlyOptions } = schema.properties; 69 | 70 | // No need to guard the for-in because schema.properties has hardcoded keys. 71 | for (const option in stylelintOnlyOptions) { 72 | // @ts-expect-error need better types 73 | delete stylelintOptions[option]; 74 | } 75 | 76 | return stylelintOptions; 77 | } 78 | 79 | module.exports = { 80 | getOptions, 81 | getStylelintOptions, 82 | }; 83 | -------------------------------------------------------------------------------- /src/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": true, 4 | "properties": { 5 | "context": { 6 | "description": "A string indicating the root of your files.", 7 | "type": "string" 8 | }, 9 | "emitError": { 10 | "description": "The errors found will always be emitted, to disable set to `false`.", 11 | "type": "boolean" 12 | }, 13 | "emitWarning": { 14 | "description": "The warnings found will always be emitted, to disable set to `false`.", 15 | "type": "boolean" 16 | }, 17 | "exclude": { 18 | "description": "Specify the files and/or directories to exclude. Must be relative to `options.context`.", 19 | "anyOf": [{ "type": "string" }, { "type": "array" }] 20 | }, 21 | "extensions": { 22 | "description": "Specify extensions that should be checked.", 23 | "anyOf": [{ "type": "string" }, { "type": "array" }] 24 | }, 25 | "failOnError": { 26 | "description": "Will cause the module build to fail if there are any errors, to disable set to `false`.", 27 | "type": "boolean" 28 | }, 29 | "failOnWarning": { 30 | "description": "Will cause the module build to fail if there are any warnings, if set to `true`.", 31 | "type": "boolean" 32 | }, 33 | "files": { 34 | "description": "Specify directories, files, or globs. Must be relative to `options.context`. Directories are traversed recursively looking for files matching `options.extensions`. File and glob patterns ignore `options.extensions`.", 35 | "anyOf": [{ "type": "string" }, { "type": "array" }] 36 | }, 37 | "formatter": { 38 | "description": "Specify the formatter that you would like to use to format your results.", 39 | "anyOf": [ 40 | { "type": "string" }, 41 | { "instanceof": "Function" }, 42 | { "instanceof": "Promise" } 43 | ] 44 | }, 45 | "lintDirtyModulesOnly": { 46 | "description": "Lint only changed files, skip lint on start.", 47 | "type": "boolean" 48 | }, 49 | "quiet": { 50 | "description": "Will process and report errors only and ignore warnings, if set to `true`.", 51 | "type": "boolean" 52 | }, 53 | "stylelintPath": { 54 | "description": "Path to `stylelint` instance that will be used for linting.", 55 | "type": "string" 56 | }, 57 | "outputReport": { 58 | "description": "Write the output of the errors to a file, for example a `json` file for use for reporting.", 59 | "anyOf": [ 60 | { 61 | "type": "boolean" 62 | }, 63 | { 64 | "type": "object", 65 | "additionalProperties": false, 66 | "properties": { 67 | "filePath": { 68 | "description": "The `filePath` is relative to the webpack config: `output.path`.", 69 | "anyOf": [{ "type": "string" }] 70 | }, 71 | "formatter": { 72 | "description": "You can pass in a different formatter for the output file, if none is passed in the default/configured formatter will be used.", 73 | "anyOf": [{ "type": "string" }, { "instanceof": "Function" }] 74 | } 75 | } 76 | } 77 | ] 78 | }, 79 | "threads": { 80 | "description": "Set to true for an auto-selected pool size based on number of cpus. Set to a number greater than 1 to set an explicit pool size. Set to false, 1, or less to disable and only run in main process.", 81 | "anyOf": [{ "type": "number" }, { "type": "boolean" }] 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line jsdoc/no-restricted-syntax 2 | /** @typedef {any} EXPECTED_ANY */ 3 | 4 | const { statSync } = require("node:fs"); 5 | const { resolve } = require("node:path"); 6 | 7 | const normalizePath = require("normalize-path"); 8 | 9 | /** 10 | * @template T 11 | * @typedef {T extends (null | undefined) 12 | * ? [] 13 | * : T extends string 14 | * ? [string] 15 | * : T extends readonly unknown[] 16 | * ? T 17 | * : T extends Iterable 18 | * ? T[] 19 | * : [T]} ArrifyResult 20 | */ 21 | 22 | /* istanbul ignore next */ 23 | /** 24 | * @template T 25 | * @param {T} value value 26 | * @returns {ArrifyResult} array of values 27 | */ 28 | function arrify(value) { 29 | if (value === null || value === undefined) { 30 | return /** @type {ArrifyResult} */ ([]); 31 | } 32 | 33 | if (Array.isArray(value)) { 34 | return /** @type {ArrifyResult} */ (value); 35 | } 36 | 37 | if (typeof value === "string") { 38 | return /** @type {ArrifyResult} */ ([value]); 39 | } 40 | 41 | // @ts-expect-error need better types 42 | if (typeof value[Symbol.iterator] === "function") { 43 | // @ts-expect-error need better types 44 | return [...value]; 45 | } 46 | 47 | return /** @type {ArrifyResult} */ ([value]); 48 | } 49 | 50 | /** 51 | * @param {string | string[]} files files 52 | * @param {string} context context 53 | * @returns {string[]} normalized paths 54 | */ 55 | function parseFiles(files, context) { 56 | return arrify(files).map((/** @type {string} */ file) => 57 | normalizePath(resolve(context, file)), 58 | ); 59 | } 60 | 61 | /** 62 | * @param {string | string[]} patterns patterns 63 | * @param {string | string[]} extensions extensions 64 | * @returns {string[]} globs 65 | */ 66 | function parseFoldersToGlobs(patterns, extensions = []) { 67 | const extensionsList = arrify(extensions); 68 | const [prefix, postfix] = extensionsList.length > 1 ? ["{", "}"] : ["", ""]; 69 | const extensionsGlob = extensionsList 70 | .map((/** @type {string} */ extension) => extension.replace(/^\./u, "")) 71 | .join(","); 72 | 73 | return arrify(patterns).map((/** @type {string} */ pattern) => { 74 | try { 75 | // The patterns are absolute because they are prepended with the context. 76 | const stats = statSync(pattern); 77 | /* istanbul ignore else */ 78 | if (stats.isDirectory()) { 79 | return pattern.replace( 80 | /[/\\]*?$/u, 81 | `/**${ 82 | extensionsGlob ? `/*.${prefix + extensionsGlob + postfix}` : "" 83 | }`, 84 | ); 85 | } 86 | } catch { 87 | // Return the pattern as is on error. 88 | } 89 | return pattern; 90 | }); 91 | } 92 | 93 | /** 94 | * @param {string} _ key, but unused 95 | * @param {EXPECTED_ANY} value value 96 | * @returns {{ [x: string]: EXPECTED_ANY }} result 97 | */ 98 | const jsonStringifyReplacerSortKeys = (_, value) => { 99 | /** 100 | * @param {{ [x: string]: EXPECTED_ANY }} sorted sorted 101 | * @param {string | number} key key 102 | * @returns {{ [x: string]: EXPECTED_ANY }} result 103 | */ 104 | const insert = (sorted, key) => { 105 | sorted[key] = value[key]; 106 | return sorted; 107 | }; 108 | 109 | return value instanceof Object && !Array.isArray(value) 110 | ? Object.keys(value).sort().reduce(insert, {}) 111 | : value; 112 | }; 113 | 114 | module.exports = { 115 | arrify, 116 | jsonStringifyReplacerSortKeys, 117 | parseFiles, 118 | parseFoldersToGlobs, 119 | }; 120 | -------------------------------------------------------------------------------- /src/getStylelint.js: -------------------------------------------------------------------------------- 1 | const { cpus } = require("node:os"); 2 | 3 | const { Worker: JestWorker } = require("jest-worker"); 4 | 5 | const { getStylelintOptions } = require("./options"); 6 | const { jsonStringifyReplacerSortKeys } = require("./utils"); 7 | const { lintFiles, setup } = require("./worker"); 8 | 9 | /** @type {{ [key: string]: Linter }} */ 10 | const cache = {}; 11 | 12 | /** @typedef {{lint: (options: LinterOptions) => Promise, formatters: { [k: string]: Formatter }}} Stylelint */ 13 | /** @typedef {import('stylelint').LintResult} LintResult */ 14 | /** @typedef {import('stylelint').LinterOptions} LinterOptions */ 15 | /** @typedef {import('stylelint').LinterResult} LinterResult */ 16 | /** @typedef {import('stylelint').Formatter} Formatter */ 17 | /** @typedef {import('stylelint').FormatterType} FormatterType */ 18 | /** @typedef {import('stylelint').RuleMeta} RuleMeta */ 19 | /** @typedef {import('./options').Options} Options */ 20 | /** @typedef {() => Promise} AsyncTask */ 21 | /** @typedef {(files: string|string[]) => Promise} LintTask */ 22 | /** @typedef {{stylelint: Stylelint, lintFiles: LintTask, cleanup: AsyncTask, threads: number }} Linter */ 23 | /** @typedef {JestWorker & {lintFiles: LintTask}} Worker */ 24 | 25 | /** 26 | * @param {Options} options linter options 27 | * @returns {Linter} linter 28 | */ 29 | function loadStylelint(options) { 30 | const stylelintOptions = getStylelintOptions(options); 31 | const stylelint = setup(options, stylelintOptions); 32 | 33 | return { 34 | stylelint, 35 | lintFiles, 36 | cleanup: async () => {}, 37 | threads: 1, 38 | }; 39 | } 40 | 41 | /** 42 | * @param {string | undefined} key a cache key 43 | * @param {Options} options options 44 | * @returns {string} a stringified cache key 45 | */ 46 | function getCacheKey(key, options) { 47 | return JSON.stringify({ key, options }, jsonStringifyReplacerSortKeys); 48 | } 49 | 50 | /** 51 | * @param {string|undefined} key a cache key 52 | * @param {number} poolSize number of workers 53 | * @param {Options} options options 54 | * @returns {Linter} linter 55 | */ 56 | function loadStylelintThreaded(key, poolSize, options) { 57 | const cacheKey = getCacheKey(key, options); 58 | const source = require.resolve("./worker"); 59 | const workerOptions = { 60 | enableWorkerThreads: true, 61 | numWorkers: poolSize, 62 | setupArgs: [options, getStylelintOptions(options)], 63 | }; 64 | 65 | const local = loadStylelint(options); 66 | 67 | let worker = 68 | /** @type {Worker | null} */ 69 | (new JestWorker(source, workerOptions)); 70 | 71 | /** @type {Linter} */ 72 | const context = { 73 | ...local, 74 | threads: poolSize, 75 | lintFiles: async (files) => 76 | /* istanbul ignore next */ 77 | worker ? worker.lintFiles(files) : local.lintFiles(files), 78 | cleanup: async () => { 79 | cache[cacheKey] = local; 80 | context.lintFiles = (files) => local.lintFiles(files); 81 | /* istanbul ignore next */ 82 | if (worker) { 83 | worker.end(); 84 | worker = null; 85 | } 86 | }, 87 | }; 88 | 89 | return context; 90 | } 91 | 92 | /** 93 | * @param {string|undefined} key a cache key 94 | * @param {Options} options options 95 | * @returns {Linter} linter 96 | */ 97 | function getStylelint(key, { threads, ...options }) { 98 | const max = 99 | typeof threads !== "number" ? (threads ? cpus().length - 1 : 1) : threads; 100 | 101 | const cacheKey = getCacheKey(key, { threads, ...options }); 102 | if (!cache[cacheKey]) { 103 | cache[cacheKey] = 104 | max > 1 105 | ? loadStylelintThreaded(key, max, options) 106 | : loadStylelint(options); 107 | } 108 | return cache[cacheKey]; 109 | } 110 | 111 | module.exports = getStylelint; 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylelint-webpack-plugin", 3 | "version": "5.0.1", 4 | "description": "A Stylelint plugin for webpack", 5 | "keywords": [ 6 | "stylelint", 7 | "lint", 8 | "linter", 9 | "plugin", 10 | "webpack" 11 | ], 12 | "homepage": "https://github.com/webpack/stylelint-webpack-plugin", 13 | "bugs": "https://github.com/webpack/stylelint-webpack-plugin/issues", 14 | "repository": "webpack/stylelint-webpack-plugin", 15 | "funding": { 16 | "type": "opencollective", 17 | "url": "https://opencollective.com/webpack" 18 | }, 19 | "license": "MIT", 20 | "author": "Ricardo Gobbo de Souza ", 21 | "main": "dist/index.js", 22 | "types": "types/index.d.ts", 23 | "files": [ 24 | "dist", 25 | "types" 26 | ], 27 | "scripts": { 28 | "start": "npm run build -- -w", 29 | "clean": "del-cli dist types", 30 | "prebuild": "npm run clean", 31 | "build:types": "tsc --declaration --emitDeclarationOnly --outDir types && prettier \"types/**/*.ts\" --write", 32 | "build:code": "cross-env NODE_ENV=production babel src -d dist --copy-files", 33 | "build": "npm-run-all -p \"build:**\"", 34 | "commitlint": "commitlint --from=main", 35 | "security": "npm audit", 36 | "lint:prettier": "prettier -w --list-different .", 37 | "lint:code": "eslint --cache .", 38 | "lint:spelling": "cspell \"**/*.*\"", 39 | "lint:types": "tsc --pretty --noEmit", 40 | "lint": "npm-run-all -l -p \"lint:**\"", 41 | "fix:code": "npm run lint:code -- --fix", 42 | "fix:prettier": "npm run lint:prettier -- --write", 43 | "fix": "npm-run-all -l fix:code fix:prettier", 44 | "test:only": "cross-env NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --testTimeout=60000", 45 | "test:watch": "npm run test:only -- --watch", 46 | "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", 47 | "pretest": "npm run lint", 48 | "test": "npm run test:coverage", 49 | "prepare": "husky && npm run build", 50 | "release": "standard-version" 51 | }, 52 | "dependencies": { 53 | "globby": "^11.1.0", 54 | "jest-worker": "^29.7.0", 55 | "micromatch": "^4.0.5", 56 | "normalize-path": "^3.0.0", 57 | "schema-utils": "^4.2.0" 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "^7.24.7", 61 | "@babel/core": "^7.24.7", 62 | "@babel/preset-env": "^7.24.7", 63 | "@commitlint/cli": "^19.3.0", 64 | "@commitlint/config-conventional": "^19.2.2", 65 | "@eslint/js": "^9.32.0", 66 | "@eslint/markdown": "^7.0.0", 67 | "@stylistic/eslint-plugin": "^5.2.2", 68 | "@types/file-entry-cache": "^5.0.4", 69 | "@types/fs-extra": "^11.0.4", 70 | "@types/micromatch": "^4.0.9", 71 | "@types/node": "^20.14.9", 72 | "@types/normalize-path": "^3.0.2", 73 | "babel-jest": "^30.0.0", 74 | "chokidar": "^3.6.0", 75 | "cross-env": "^7.0.3", 76 | "cspell": "^8.10.0", 77 | "del": "^7.1.0", 78 | "del-cli": "^5.1.0", 79 | "eslint": "^9.32.0", 80 | "eslint-config-prettier": "^10.1.8", 81 | "eslint-config-webpack": "^4.4.2", 82 | "eslint-plugin-import": "^2.32.0", 83 | "eslint-plugin-jest": "^29.0.1", 84 | "eslint-plugin-jsdoc": "^52.0.0", 85 | "eslint-plugin-n": "^17.21.0", 86 | "eslint-plugin-prettier": "^5.5.3", 87 | "eslint-plugin-unicorn": "^60.0.0", 88 | "file-loader": "^6.2.0", 89 | "fs-extra": "^11.2.0", 90 | "husky": "^9.1.3", 91 | "jest": "^30.0.0", 92 | "lint-staged": "^15.2.7", 93 | "npm-run-all": "^4.1.5", 94 | "postcss-scss": "^4.0.9", 95 | "prettier": "^3.3.2", 96 | "standard-version": "^9.5.0", 97 | "stylelint": "^16.6.1", 98 | "typescript": "^5.5.3", 99 | "typescript-eslint": "^8.38.0", 100 | "webpack": "^5.92.1" 101 | }, 102 | "peerDependencies": { 103 | "stylelint": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", 104 | "webpack": "^5.0.0" 105 | }, 106 | "engines": { 107 | "node": ">= 18.12.0" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /types/options.d.ts: -------------------------------------------------------------------------------- 1 | export type StylelintOptions = import("./getStylelint").LinterOptions; 2 | export type FormatterType = import("./getStylelint").FormatterType; 3 | export type OutputReport = { 4 | /** 5 | * file path 6 | */ 7 | filePath?: string | undefined; 8 | /** 9 | * formatter 10 | */ 11 | formatter?: FormatterType | undefined; 12 | }; 13 | export type PluginOptions = { 14 | /** 15 | * a string indicating the root of your files 16 | */ 17 | context: string; 18 | /** 19 | * the errors found will always be emitted 20 | */ 21 | emitError: boolean; 22 | /** 23 | * the warnings found will always be emitted 24 | */ 25 | emitWarning: boolean; 26 | /** 27 | * specify the files and/or directories to exclude 28 | */ 29 | exclude?: (string | string[]) | undefined; 30 | /** 31 | * specify the extensions that should be checked 32 | */ 33 | extensions: string | string[]; 34 | /** 35 | * will cause the module build to fail if there are any errors 36 | */ 37 | failOnError: boolean; 38 | /** 39 | * will cause the module build to fail if there are any warning 40 | */ 41 | failOnWarning: boolean; 42 | /** 43 | * specify directories, files, or globs 44 | */ 45 | files: string | string[]; 46 | /** 47 | * specify the formatter you would like to use to format your results 48 | */ 49 | formatter: FormatterType; 50 | /** 51 | * lint only changed files, skip linting on start 52 | */ 53 | lintDirtyModulesOnly: boolean; 54 | /** 55 | * will process and report errors only and ignore warnings 56 | */ 57 | quiet: boolean; 58 | /** 59 | * path to `stylelint` instance that will be used for linting 60 | */ 61 | stylelintPath: string; 62 | /** 63 | * writes the output of the errors to a file - for example, a `json` file for use for reporting 64 | */ 65 | outputReport: OutputReport; 66 | /** 67 | * number of worker threads 68 | */ 69 | threads?: (number | boolean) | undefined; 70 | }; 71 | export type Options = Partial; 72 | /** @typedef {import('./getStylelint').LinterOptions} StylelintOptions */ 73 | /** @typedef {import('./getStylelint').FormatterType} FormatterType */ 74 | /** 75 | * @typedef {object} OutputReport 76 | * @property {string=} filePath file path 77 | * @property {FormatterType=} formatter formatter 78 | */ 79 | /** 80 | * @typedef {object} PluginOptions 81 | * @property {string} context a string indicating the root of your files 82 | * @property {boolean} emitError the errors found will always be emitted 83 | * @property {boolean} emitWarning the warnings found will always be emitted 84 | * @property {string | string[]=} exclude specify the files and/or directories to exclude 85 | * @property {string | string[]} extensions specify the extensions that should be checked 86 | * @property {boolean} failOnError will cause the module build to fail if there are any errors 87 | * @property {boolean} failOnWarning will cause the module build to fail if there are any warning 88 | * @property {string | string[]} files specify directories, files, or globs 89 | * @property {FormatterType} formatter specify the formatter you would like to use to format your results 90 | * @property {boolean} lintDirtyModulesOnly lint only changed files, skip linting on start 91 | * @property {boolean} quiet will process and report errors only and ignore warnings 92 | * @property {string} stylelintPath path to `stylelint` instance that will be used for linting 93 | * @property {OutputReport} outputReport writes the output of the errors to a file - for example, a `json` file for use for reporting 94 | * @property {number | boolean=} threads number of worker threads 95 | */ 96 | /** @typedef {Partial} Options */ 97 | /** 98 | * @param {Options} pluginOptions plugin options 99 | * @returns {Partial} partial plugin options 100 | */ 101 | export function getOptions(pluginOptions: Options): Partial; 102 | /** 103 | * @param {Options} pluginOptions plugin options 104 | * @returns {Partial} stylelint options 105 | */ 106 | export function getStylelintOptions( 107 | pluginOptions: Options, 108 | ): Partial; 109 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { isAbsolute, join } = require("node:path"); 2 | 3 | const globby = require("globby"); 4 | const { isMatch } = require("micromatch"); 5 | 6 | const linter = require("./linter"); 7 | const { getOptions } = require("./options"); 8 | const { arrify, parseFiles, parseFoldersToGlobs } = require("./utils"); 9 | 10 | /** @typedef {import('webpack').Compiler} Compiler */ 11 | /** @typedef {import('webpack').Module} Module */ 12 | /** @typedef {import('./options').Options} Options */ 13 | /** @typedef {Partial<{ timestamp:number } | number>} FileSystemInfoEntry */ 14 | 15 | const STYLELINT_PLUGIN = "StylelintWebpackPlugin"; 16 | let counter = 0; 17 | 18 | class StylelintWebpackPlugin { 19 | /** 20 | * @param {Options=} options options 21 | */ 22 | constructor(options = {}) { 23 | this.key = STYLELINT_PLUGIN; 24 | this.options = getOptions(options); 25 | this.run = this.run.bind(this); 26 | this.startTime = Date.now(); 27 | this.prevTimestamps = new Map(); 28 | } 29 | 30 | /** 31 | * @param {Compiler} compiler compiler 32 | * @returns {void} 33 | */ 34 | apply(compiler) { 35 | // Generate key for each compilation, 36 | // this differentiates one from the other when being cached. 37 | this.key = compiler.name || `${this.key}_${(counter += 1)}`; 38 | 39 | const context = this.getContext(compiler); 40 | const excludeDefault = [ 41 | "**/node_modules/**", 42 | String(compiler.options.output.path), 43 | ]; 44 | 45 | const options = { 46 | ...this.options, 47 | context, 48 | exclude: parseFiles(this.options.exclude || excludeDefault, context), 49 | extensions: arrify(this.options.extensions), 50 | files: parseFiles(this.options.files || "", context), 51 | }; 52 | 53 | const wanted = parseFoldersToGlobs(options.files, options.extensions); 54 | const exclude = parseFoldersToGlobs(options.exclude); 55 | 56 | // If `lintDirtyModulesOnly` is disabled, 57 | // execute the linter on the build 58 | if (!this.options.lintDirtyModulesOnly) { 59 | compiler.hooks.run.tapPromise(this.key, (compiler) => 60 | this.run(compiler, options, wanted, exclude), 61 | ); 62 | } 63 | 64 | let hasCompilerRunByDirtyModule = this.options.lintDirtyModulesOnly; 65 | 66 | compiler.hooks.watchRun.tapPromise(this.key, (compiler) => { 67 | if (hasCompilerRunByDirtyModule) { 68 | hasCompilerRunByDirtyModule = false; 69 | 70 | return Promise.resolve(); 71 | } 72 | 73 | return this.run(compiler, options, wanted, exclude); 74 | }); 75 | } 76 | 77 | /** 78 | * @param {Compiler} compiler compiler 79 | * @param {Options} options options 80 | * @param {string[]} wanted wanted files 81 | * @param {string[]} exclude exclude files 82 | */ 83 | async run(compiler, options, wanted, exclude) { 84 | // Do not re-hook 85 | const isCompilerHooked = compiler.hooks.thisCompilation.taps.find( 86 | ({ name }) => name === this.key, 87 | ); 88 | 89 | if (isCompilerHooked) return; 90 | 91 | compiler.hooks.thisCompilation.tap(this.key, (compilation) => { 92 | /** @type {import('./linter').Linter} */ 93 | let lint; 94 | 95 | /** @type {import('./linter').Reporter} */ 96 | let report; 97 | 98 | /** @type number */ 99 | let threads; 100 | 101 | try { 102 | ({ lint, report, threads } = linter(this.key, options, compilation)); 103 | } catch (err) { 104 | compilation.errors.push(err); 105 | return; 106 | } 107 | 108 | compilation.hooks.finishModules.tapPromise(this.key, async () => { 109 | /** @type {string[]} */ 110 | const files = compiler.modifiedFiles 111 | ? [...compiler.modifiedFiles].filter( 112 | (file) => 113 | isMatch(file, wanted, { dot: true }) && 114 | !isMatch(file, exclude, { dot: true }), 115 | ) 116 | : globby.sync(wanted, { dot: true, ignore: exclude }); 117 | 118 | if (threads > 1) { 119 | for (const file of files) { 120 | lint(parseFiles(file, String(options.context))); 121 | } 122 | } else if (files.length > 0) { 123 | lint(parseFiles(files, String(options.context))); 124 | } 125 | }); 126 | 127 | /** 128 | * @returns {Promise} 129 | */ 130 | async function processResults() { 131 | const { errors, warnings, generateReportAsset } = await report(); 132 | 133 | if (warnings && !options.failOnWarning) { 134 | compilation.warnings.push(warnings); 135 | } else if (warnings && options.failOnWarning) { 136 | compilation.errors.push(warnings); 137 | } 138 | 139 | if (errors && options.failOnError) { 140 | compilation.errors.push(errors); 141 | } else if (errors && !options.failOnError) { 142 | compilation.warnings.push(errors); 143 | } 144 | 145 | if (generateReportAsset) { 146 | await generateReportAsset(compilation); 147 | } 148 | } 149 | 150 | // await and interpret results 151 | compilation.hooks.additionalAssets.tapPromise(this.key, processResults); 152 | }); 153 | } 154 | 155 | /** 156 | * @param {Compiler} compiler compiler 157 | * @returns {string} context 158 | */ 159 | getContext(compiler) { 160 | if (!this.options.context) { 161 | return String(compiler.options.context); 162 | } 163 | 164 | if (!isAbsolute(this.options.context)) { 165 | return join(String(compiler.options.context), this.options.context); 166 | } 167 | 168 | return this.options.context; 169 | } 170 | } 171 | 172 | module.exports = StylelintWebpackPlugin; 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | [![npm][npm]][npm-url] 7 | [![node][node]][node-url] 8 | [![tests][tests]][tests-url] 9 | [![coverage][cover]][cover-url] 10 | [![discussion][discussion]][discussion-url] 11 | [![size][size]][size-url] 12 | 13 | # stylelint-webpack-plugin 14 | 15 | > This version of `stylelint-webpack-plugin` only works with webpack 5. For webpack 4, see the [2.x branch](https://github.com/webpack/stylelint-webpack-plugin/tree/2.x). 16 | 17 | This plugin uses [`stylelint`](https://stylelint.io/), which helps you avoid errors and enforce conventions in your styles. 18 | 19 | ## Getting Started 20 | 21 | To begin, you'll need to install `stylelint-webpack-plugin`: 22 | 23 | ```console 24 | npm install stylelint-webpack-plugin --save-dev 25 | ``` 26 | 27 | or 28 | 29 | ```console 30 | yarn add -D stylelint-webpack-plugin 31 | ``` 32 | 33 | or 34 | 35 | ```console 36 | pnpm add -D stylelint-webpack-plugin 37 | ``` 38 | 39 | > [!NOTE] 40 | > 41 | > You also need to install `stylelint >= 13` from npm, if you haven't already: 42 | 43 | ```console 44 | npm install stylelint --save-dev 45 | ``` 46 | 47 | or 48 | 49 | ```console 50 | yarn add -D stylelint 51 | ``` 52 | 53 | or 54 | 55 | ```console 56 | pnpm add -D stylelint 57 | ``` 58 | 59 | > [!NOTE] 60 | > 61 | > If you are using Stylelint 13 rather than 14+, you might also need to install `@types/stylelint` as a dev dependency if you encounter Stylelint-related type errors. 62 | 63 | Then add the plugin to your webpack configuration. For example: 64 | 65 | ```js 66 | const StylelintPlugin = require("stylelint-webpack-plugin"); 67 | 68 | module.exports = { 69 | // ... 70 | plugins: [new StylelintPlugin(options)], 71 | // ... 72 | }; 73 | ``` 74 | 75 | ## Options 76 | 77 | See [stylelint's options](https://stylelint.io/user-guide/usage/node-api#options) for the complete list of available options . These options are passed directly to `stylelint`. 78 | 79 | ### `cache` 80 | 81 | - Type: 82 | 83 | ```ts 84 | type cache = boolean; 85 | ``` 86 | 87 | - Default: `true` 88 | 89 | The cache is enabled by default to decrease execution time. 90 | 91 | ### `cacheLocation` 92 | 93 | - Type: 94 | 95 | ```ts 96 | type cacheLocation = string; 97 | ``` 98 | 99 | - Default: `node_modules/.cache/stylelint-webpack-plugin/.stylelintcache` 100 | 101 | Specify the path to the cache location. This can be a file or a directory. 102 | 103 | ### `configFile` 104 | 105 | - Type: 106 | 107 | ```ts 108 | type context = string; 109 | ``` 110 | 111 | - Default: `undefined` 112 | 113 | Specify the config file location to be used by `stylelint`. 114 | 115 | > **Note:** 116 | > 117 | > By default this is [handled by `stylelint`](https://stylelint.io/user-guide/configure). 118 | 119 | ### `context` 120 | 121 | - Type: 122 | 123 | ```ts 124 | type context = string; 125 | ``` 126 | 127 | - Default: `compiler.context` 128 | 129 | A string indicating the root of your files. 130 | 131 | ### `exclude` 132 | 133 | - Type: 134 | 135 | ```ts 136 | type exclude = string | string[]; 137 | ``` 138 | 139 | - Default: `['node_modules', compiler.options.output.path]` 140 | 141 | Specify the files and/or directories to exclude. Must be relative to `options.context`. 142 | 143 | ### `extensions` 144 | 145 | - Type: 146 | 147 | ```ts 148 | type extensions = string | string[]; 149 | ``` 150 | 151 | - Default: `['css', 'scss', 'sass']` 152 | 153 | Specify the extensions that should be checked. 154 | 155 | ### `files` 156 | 157 | - Type: 158 | 159 | ```ts 160 | type files = string | string[]; 161 | ``` 162 | 163 | - Default: `null` 164 | 165 | Specify directories, files, or globs. Must be relative to `options.context`. Directories are traversed recursively, looking for files matching `options.extensions`. File and glob patterns ignore `options.extensions`. 166 | 167 | ### `fix` 168 | 169 | - Type: 170 | 171 | ```ts 172 | type fix = boolean; 173 | ``` 174 | 175 | - Default: `false` 176 | 177 | If `true`, `stylelint` will fix as many errors as possible. The fixes are made to the actual source files. All unfixed errors will be reported. See [Autofixing errors](https://stylelint.io/user-guide/usage/options#fix) docs. 178 | 179 | ### `formatter` 180 | 181 | - Type: 182 | 183 | ```ts 184 | type formatter = 185 | | string 186 | | ((results: import("stylelint").LintResult[]) => string); 187 | ``` 188 | 189 | - Default: `'string'` 190 | 191 | Specify the formatter you would like to use to format your results. See the [formatter option](https://stylelint.io/user-guide/usage/options#formatter). 192 | 193 | ### `lintDirtyModulesOnly` 194 | 195 | - Type: 196 | 197 | ```ts 198 | type lintDirtyModulesOnly = boolean; 199 | ``` 200 | 201 | - Default: `false` 202 | 203 | Lint only changed files; skip linting on start. 204 | 205 | ### `stylelintPath` 206 | 207 | - Type: 208 | 209 | ```ts 210 | type stylelintPath = string; 211 | ``` 212 | 213 | - Default: `stylelint` 214 | 215 | Path to `stylelint` instance that will be used for linting. 216 | 217 | ### `threads` 218 | 219 | - Type: 220 | 221 | ```ts 222 | type threads = boolean | number; 223 | ``` 224 | 225 | - Default: `false` 226 | 227 | Set to `true` for an auto-selected pool size based on number of CPUs. Set to a number greater than 1 to set an explicit pool size. 228 | 229 | Set to `false`, 1, or less to disable and run only in main process. 230 | 231 | ### Errors and Warning 232 | 233 | **By default, the plugin will automatically adjust error reporting depending on the number of Stylelint errors/warnings.** 234 | 235 | You can still force this behavior by using the `emitError` **or** `emitWarning` options: 236 | 237 | #### `emitError` 238 | 239 | - Type: 240 | 241 | ```ts 242 | type emitError = boolean; 243 | ``` 244 | 245 | - Default: `true` 246 | 247 | The errors found will always be emitted. To disable, set to `false`. 248 | 249 | #### `emitWarning` 250 | 251 | - Type: 252 | 253 | ```ts 254 | type emitWarning = boolean; 255 | ``` 256 | 257 | - Default: `true` 258 | 259 | The warnings found will always be emitted. To disable, set to `false`. 260 | 261 | #### `failOnError` 262 | 263 | - Type: 264 | 265 | ```ts 266 | type failOnError = boolean; 267 | ``` 268 | 269 | - Default: `true` 270 | 271 | Will cause the module build to fail if there are any errors. To disable, set to `false`. 272 | 273 | #### `failOnWarning` 274 | 275 | - Type: 276 | 277 | ```ts 278 | type failOnWarning = boolean; 279 | ``` 280 | 281 | - Default: `false` 282 | 283 | Will cause the module build to fail if there are any warnings, when set to `true`. 284 | 285 | #### `quiet` 286 | 287 | - Type: 288 | 289 | ```ts 290 | type quiet = boolean; 291 | ``` 292 | 293 | - Default: `false` 294 | 295 | Will process and report errors only, and ignore warnings, when set to `true`. 296 | 297 | #### `outputReport` 298 | 299 | - Type: 300 | 301 | ```ts 302 | type outputReport = 303 | | boolean 304 | | { 305 | filePath?: string | undefined; 306 | formatter?: 307 | | (string | ((results: import("stylelint").LintResult[]) => string)) 308 | | undefined; 309 | }; 310 | ``` 311 | 312 | - Default: `false` 313 | 314 | Writes the output of the errors to a file - for example, a `json` file for use for reporting. 315 | 316 | The `filePath` is relative to the webpack config: `output.path`. 317 | 318 | You can pass in a different formatter for the output file. If none is passed in the default/configured formatter will be used. 319 | 320 | ```js 321 | const outputReport = { 322 | filePath: "path/to/file", 323 | formatter: "json", 324 | }; 325 | ``` 326 | 327 | ## Changelog 328 | 329 | [Changelog](CHANGELOG.md) 330 | 331 | ## Contributing 332 | 333 | We welcome all contributions! 334 | If you're new here, please take a moment to review our contributing guidelines before submitting issues or pull requests. 335 | 336 | [CONTRIBUTING](https://github.com/webpack/stylelint-webpack-plugin?tab=contributing-ov-file#contributing) 337 | 338 | ## License 339 | 340 | [MIT](./LICENSE) 341 | 342 | [npm]: https://img.shields.io/npm/v/stylelint-webpack-plugin.svg 343 | [npm-url]: https://npmjs.com/package/stylelint-webpack-plugin 344 | [node]: https://img.shields.io/node/v/stylelint-webpack-plugin.svg 345 | [node-url]: https://nodejs.org 346 | [tests]: https://github.com/webpack/stylelint-webpack-plugin/workflows/stylelint-webpack-plugin/badge.svg 347 | [tests-url]: https://github.com/webpack/stylelint-webpack-plugin/actions 348 | [cover]: https://codecov.io/gh/webpack/stylelint-webpack-plugin/branch/main/graph/badge.svg 349 | [cover-url]: https://codecov.io/gh/webpack/stylelint-webpack-plugin 350 | [discussion]: https://img.shields.io/github/discussions/webpack/webpack 351 | [discussion-url]: https://github.com/webpack/webpack/discussions 352 | [size]: https://packagephobia.now.sh/badge?p=stylelint-webpack-plugin 353 | [size-url]: https://packagephobia.now.sh/result?p=stylelint-webpack-plugin 354 | -------------------------------------------------------------------------------- /src/linter.js: -------------------------------------------------------------------------------- 1 | const { dirname, isAbsolute, join } = require("node:path"); 2 | 3 | const StylelintError = require("./StylelintError"); 4 | const getStylelint = require("./getStylelint"); 5 | const { arrify } = require("./utils"); 6 | 7 | /** @typedef {import('webpack').Compiler} Compiler */ 8 | /** @typedef {import('webpack').Compilation} Compilation */ 9 | /** @typedef {import('./getStylelint').Stylelint} Stylelint */ 10 | /** @typedef {import('./getStylelint').LintResult} LintResult */ 11 | /** @typedef {import('./getStylelint').LinterResult} LinterResult */ 12 | /** @typedef {import('./getStylelint').Formatter} Formatter */ 13 | /** @typedef {import('./getStylelint').FormatterType} FormatterType */ 14 | /** @typedef {import('./getStylelint').RuleMeta} RuleMeta */ 15 | /** @typedef {import('./options').Options} Options */ 16 | /** @typedef {(compilation: Compilation) => Promise} GenerateReport */ 17 | /** @typedef {{errors?: StylelintError, warnings?: StylelintError, generateReportAsset?: GenerateReport}} Report */ 18 | /** @typedef {() => Promise} Reporter */ 19 | /** @typedef {(files: string|string[]) => void} Linter */ 20 | /** @typedef {{[files: string]: LintResult}} LintResultMap */ 21 | 22 | /** @type {WeakMap} */ 23 | const resultStorage = new WeakMap(); 24 | 25 | /** 26 | * @param {Compilation} compilation compilation 27 | * @returns {LintResultMap} lint result map 28 | */ 29 | function getResultStorage({ compiler }) { 30 | let storage = resultStorage.get(compiler); 31 | if (!storage) { 32 | resultStorage.set(compiler, (storage = {})); 33 | } 34 | return storage; 35 | } 36 | 37 | /** 38 | * @param {LintResult[]} results results 39 | * @returns {LintResult[]} filtered results without ignored 40 | */ 41 | function removeIgnoredWarnings(results) { 42 | return results.filter((result) => !result.ignored); 43 | } 44 | 45 | /** 46 | * @param {Promise[]} results results 47 | * @returns {Promise} flatten results 48 | */ 49 | async function flatten(results) { 50 | /** 51 | * @param {LintResult[]} acc acc 52 | * @param {LintResult[]} list list 53 | * @returns {LintResult[]} result 54 | */ 55 | const flat = (acc, list) => [...acc, ...list]; 56 | return (await Promise.all(results)).reduce(flat, []); 57 | } 58 | 59 | /** 60 | * @param {Stylelint} stylelint stylelint 61 | * @param {FormatterType=} formatter formatter 62 | * @returns {Promise | Formatter} resolved formatter 63 | */ 64 | function loadFormatter(stylelint, formatter) { 65 | if (typeof formatter === "function") { 66 | return formatter; 67 | } 68 | 69 | if (typeof formatter === "string") { 70 | try { 71 | return stylelint.formatters[formatter]; 72 | } catch { 73 | // Load the default formatter. 74 | } 75 | } 76 | 77 | return stylelint.formatters.string; 78 | } 79 | 80 | /* istanbul ignore next */ 81 | /** 82 | * @param {LintResult[]} lintResults lint results 83 | * @returns {{ [ruleName: string]: Partial }} a rule meta 84 | */ 85 | function getRuleMetadata(lintResults) { 86 | const [lintResult] = lintResults; 87 | 88 | if (lintResult === undefined) return {}; 89 | 90 | if (lintResult._postcssResult === undefined) return {}; 91 | 92 | return lintResult._postcssResult.stylelint.ruleMetadata; 93 | } 94 | 95 | /** 96 | * @param {Formatter} formatter formatter 97 | * @param {{ errors: LintResult[]; warnings: LintResult[]; }} results results 98 | * @param {LinterResult} returnValue return value 99 | * @returns {{ errors?: StylelintError, warnings?: StylelintError }} formatted result 100 | */ 101 | function formatResults(formatter, results, returnValue) { 102 | let errors; 103 | let warnings; 104 | if (results.warnings.length > 0) { 105 | warnings = new StylelintError(formatter(results.warnings, returnValue)); 106 | } 107 | 108 | if (results.errors.length > 0) { 109 | errors = new StylelintError(formatter(results.errors, returnValue)); 110 | } 111 | 112 | return { 113 | errors, 114 | warnings, 115 | }; 116 | } 117 | 118 | /** 119 | * @param {Options} options options 120 | * @param {LintResult[]} results results 121 | * @returns {{ errors: LintResult[], warnings: LintResult[] }} parsed results 122 | */ 123 | function parseResults(options, results) { 124 | /** @type {LintResult[]} */ 125 | const errors = []; 126 | 127 | /** @type {LintResult[]} */ 128 | const warnings = []; 129 | 130 | for (const file of results) { 131 | const fileErrors = file.warnings.filter( 132 | (message) => options.emitError && message.severity === "error", 133 | ); 134 | 135 | if (fileErrors.length > 0) { 136 | errors.push({ 137 | ...file, 138 | warnings: fileErrors, 139 | }); 140 | } 141 | 142 | const fileWarnings = file.warnings.filter( 143 | (message) => options.emitWarning && message.severity === "warning", 144 | ); 145 | 146 | if (fileWarnings.length > 0) { 147 | warnings.push({ 148 | ...file, 149 | warnings: fileWarnings, 150 | }); 151 | } 152 | } 153 | 154 | return { 155 | errors, 156 | warnings, 157 | }; 158 | } 159 | 160 | /** 161 | * @param {string | undefined} key a cache key 162 | * @param {Options} options options 163 | * @param {Compilation} compilation compilation 164 | * @returns {{ lint: Linter, report: Reporter, threads: number }} the linter with additional functions 165 | */ 166 | function linter(key, options, compilation) { 167 | /** @type {Stylelint} */ 168 | let stylelint; 169 | 170 | /** @type {(files: string | string[]) => Promise} */ 171 | let lintFiles; 172 | 173 | /** @type {() => Promise} */ 174 | let cleanup; 175 | 176 | /** @type number */ 177 | let threads; 178 | 179 | /** @type {Promise[]} */ 180 | const rawResults = []; 181 | 182 | const crossRunResultStorage = getResultStorage(compilation); 183 | 184 | try { 185 | ({ stylelint, lintFiles, cleanup, threads } = getStylelint(key, options)); 186 | } catch (err) { 187 | throw new StylelintError(err.message); 188 | } 189 | 190 | /** 191 | * @param {string | string[]} files files 192 | */ 193 | function lint(files) { 194 | for (const file of arrify(files)) { 195 | delete crossRunResultStorage[file]; 196 | } 197 | rawResults.push( 198 | lintFiles(files).catch((err) => { 199 | compilation.errors.push(new StylelintError(err.message)); 200 | return []; 201 | }), 202 | ); 203 | } 204 | 205 | /** 206 | * @returns {Promise} report 207 | */ 208 | async function report() { 209 | // Filter out ignored files. 210 | let results = removeIgnoredWarnings( 211 | // Get the current results, resetting the rawResults to empty 212 | await flatten(rawResults.splice(0)), 213 | ); 214 | 215 | await cleanup(); 216 | 217 | for (const result of results) { 218 | crossRunResultStorage[String(result.source)] = result; 219 | } 220 | 221 | results = Object.values(crossRunResultStorage); 222 | 223 | // do not analyze if there are no results or stylelint config 224 | if (!results || results.length < 1) { 225 | return {}; 226 | } 227 | 228 | const formatter = await loadFormatter(stylelint, options.formatter); 229 | 230 | /** @type {LinterResult} */ 231 | // @ts-expect-error need better types 232 | const returnValue = { 233 | cwd: /** @type {string} */ (options.cwd), 234 | errored: false, 235 | results: [], 236 | output: "", 237 | reportedDisables: [], 238 | ruleMetadata: getRuleMetadata(results), 239 | }; 240 | 241 | const { errors, warnings } = formatResults( 242 | formatter, 243 | parseResults(options, results), 244 | returnValue, 245 | ); 246 | 247 | /** 248 | * @param {Compilation} compilation compilation 249 | * @returns {Promise} 250 | */ 251 | async function generateReportAsset({ compiler }) { 252 | const { outputReport } = options; 253 | /** 254 | * @param {string} name name 255 | * @param {string | Buffer} content content 256 | * @returns {Promise} 257 | */ 258 | const save = (name, content) => 259 | /** @type {Promise} */ 260 | ( 261 | new Promise((finish, bail) => { 262 | if (!compiler.outputFileSystem) return; 263 | const { mkdir, writeFile } = compiler.outputFileSystem; 264 | // ensure directory exists 265 | mkdir(dirname(name), { recursive: true }, (err) => { 266 | /* istanbul ignore if */ 267 | if (err) { 268 | bail(err); 269 | } else { 270 | writeFile(name, content, (err2) => { 271 | /* istanbul ignore if */ 272 | if (err2) bail(err2); 273 | else finish(); 274 | }); 275 | } 276 | }); 277 | }) 278 | ); 279 | 280 | if (!outputReport || !outputReport.filePath) { 281 | return; 282 | } 283 | 284 | const content = outputReport.formatter 285 | ? (await loadFormatter(stylelint, outputReport.formatter))( 286 | results, 287 | returnValue, 288 | ) 289 | : formatter(results, returnValue); 290 | 291 | let { filePath } = outputReport; 292 | if (!isAbsolute(filePath)) { 293 | filePath = join(compiler.outputPath, filePath); 294 | } 295 | 296 | await save(filePath, String(content)); 297 | } 298 | 299 | return { 300 | errors, 301 | warnings, 302 | generateReportAsset, 303 | }; 304 | } 305 | 306 | return { 307 | lint, 308 | report, 309 | threads, 310 | }; 311 | } 312 | 313 | module.exports = linter; 314 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [5.0.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v5.0.0...v5.0.1) (2024-05-24) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * output report generation ([#355](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/355)) ([d5d4ac1](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/d5d4ac1841d7de030d449ba267056d44da16f30e)) 11 | 12 | ## [5.0.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v4.1.0...v5.0.0) (2024-02-02) 13 | 14 | 15 | ### ⚠ BREAKING CHANGES 16 | 17 | * minimum supported Node.js version is 18 (#345) 18 | 19 | ### Features 20 | 21 | * support stylelint v16 ([#346](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/346)) ([a40857c](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/a40857c7004a2637482ef43fc4a9540f92e67d59)) 22 | 23 | 24 | ### build 25 | 26 | * minimum supported Node.js version is 18 ([#345](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/345)) ([1ee4588](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/1ee4588efaef953ab287edf359937b1eda325ae2)) 27 | 28 | ### [4.1.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v4.1.0...v4.1.1) (2023-04-10) 29 | 30 | 31 | ### Performance 32 | 33 | * enable `cache` by default ([#327](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/327)) 34 | 35 | ## [4.1.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v4.0.0...v4.1.0) (2023-02-18) 36 | 37 | 38 | ### Features 39 | 40 | * add stylelint v15 ([#323](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/323)) ([8dc5881](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/8dc58810c10cdc099eec89b2761950c68975a3bd)) 41 | 42 | ## [4.0.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v3.3.0...v4.0.0) (2023-02-02) 43 | 44 | 45 | ### ⚠ BREAKING CHANGES 46 | 47 | * drop node v12 ([#284](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/284)) ([1d0a5a8](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/1d0a5a8be39ceabcc170a3b7709d2587c0dd3a62)) 48 | 49 | ### Bug Fixes 50 | 51 | * path ignored ([#312](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/312)) ([805b54c](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/805b54c1b9a48f603f018ad7cfde9daee71944e4)) 52 | 53 | 54 | ## [3.3.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v3.2.0...v3.3.0) (2022-05-20) 55 | 56 | 57 | ### Features 58 | 59 | * add stylelint prefix to CLI output for better debugging ([#273](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/273)) ([4ce7da3](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/4ce7da3f543a25f18d34b8529ee554773d9b810f)) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * types ([#274](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/274)) ([da40303](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/da403032d903b85a50cecb86e3dd1aba062f6ef6)) 65 | 66 | ## [3.2.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v3.1.1...v3.2.0) (2022-03-21) 67 | 68 | 69 | ### Features 70 | 71 | * removed cjs wrapper ([#265](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/265)) ([dc9a2e3](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/dc9a2e3522c26310cf38126571edd3cdf4966792)) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * ignored file ([#266](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/266)) ([ac50bf5](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/ac50bf51454260b1e52518c0e20e6d55f4496b58)) 77 | 78 | ### [3.1.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v3.1.0...v3.1.1) (2022-01-14) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * remove outdated stylelint types ([#260](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/260)) ([f5e5e4c](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/f5e5e4c5c481a5fc35c63b21d908dc7d63f17c73)) 84 | 85 | ## [3.1.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v3.0.1...v3.1.0) (2021-11-01) 86 | 87 | 88 | ### Features 89 | 90 | * support stylelint 14 ([#250](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/250)) ([8becae6](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/8becae6f73a0f78c7ade7402d7c6b709a8728399)) 91 | 92 | ### [3.0.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v3.0.0...v3.0.1) (2021-07-20) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * crash with `ERR_REQUIRE_ESM` error ([#240](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/240)) ([643cede](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/643cedeb8ad56ad44df4bf24306fe1b9f84e417e)) 98 | 99 | ## [3.0.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v2.2.2...v3.0.0) (2021-07-19) 100 | 101 | 102 | ### ⚠ BREAKING CHANGES 103 | 104 | * webpack v4 and nodejs v10 dropped (#238) 105 | 106 | * webpack v4 and nodejs v10 dropped ([#238](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/238)) ([de6de67](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/de6de67fb532c7d2dcd62218f3657e1409333864)) 107 | 108 | ### [2.2.2](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v2.2.1...v2.2.2) (2021-06-24) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * display errors ([#237](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/237)) ([e343427](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/e343427ccf877bfaa997d9ca988a0de64466dd74)) 114 | 115 | ### [2.2.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v2.2.0...v2.2.1) (2021-06-21) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * performance ([#236](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/236)) ([ea7eadb](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/ea7eadb9c194b0ee4b8c3c733ad8ef93c2b2b78d)) 121 | 122 | ## [2.2.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v2.1.1...v2.2.0) (2021-06-15) 123 | 124 | ### [2.1.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.2.3...v2.1.1) (2020-10-14) 125 | 126 | ### Bug Fixes 127 | 128 | * use better micromatch extglobs ([#216](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/216)) ([a70ed3d](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/a70ed3d6b6d8da90bf4dc371057cbe1433b4558d)) 129 | 130 | ## [2.1.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.2.3...v2.1.0) (2020-06-17) 131 | 132 | 133 | ### Features 134 | 135 | * support typescript ([#213](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/213)) ([b7dfa19](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/b7dfa195b7836bad7ac94a64a0c0a6163021a3e7)) 136 | 137 | ## [2.0.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.2.3...v2.0.0) (2020-05-04) 138 | 139 | ### ⚠ BREAKING CHANGES 140 | 141 | * minimum supported Node.js version is `10.13` 142 | * minimum supported stylelint version is `13.0.0` 143 | 144 | ### Bug Fixes 145 | 146 | * avoiding https://github.com/mrmlnc/fast-glob/issues/158 ([#209](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/209)) ([14ae30d](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/14ae30df8a6d6b629c4e1fa647b4c6989377aec8)) 147 | 148 | ### [1.2.3](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.2.2...v1.2.3) (2020-02-08) 149 | 150 | 151 | ### Performance 152 | 153 | * require lint of stylelint only one time ([#207](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/207)) ([7e2495e](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/7e2495e6ba4d8cebb7f07cc9418020ea494670f8)) 154 | 155 | ### [1.2.2](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.2.1...v1.2.2) (2020-02-08) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * replace back slashes on changed files ([#206](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/206)) ([7508028](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/7508028398d366c37d1a14e254baec9dc39b816c)) 161 | 162 | ### [1.2.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.2.0...v1.2.1) (2020-01-16) 163 | 164 | 165 | ### Bug Fixes 166 | 167 | * compatibility stylelint v13 ([#204](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/204)) ([483be31](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/483be318450ec9a4f9eeb4bf1b1db203ba0c863d)) 168 | 169 | ## [1.2.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.1.2...v1.2.0) (2020-01-13) 170 | 171 | 172 | ### Features 173 | 174 | * make possible to define official formatter as string ([#202](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/202)) ([8d6599c](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/8d6599c3f2f0e26d1515b01f6ecbafabeaa68fac)) 175 | * support stylelint v13 ([#203](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/203)) ([6fb31a3](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/6fb31a3931cb9d7cb0ce8cc99c9db28f928c82f4)) 176 | 177 | ### [1.1.2](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.1.1...v1.1.2) (2019-12-04) 178 | 179 | 180 | ### Bug Fixes 181 | 182 | * support webpack 5 ([#199](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/199)) ([3d9e544](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/3d9e544f31172b7c01f4bd7c7254cfc7e38466c9)) 183 | 184 | ### [1.1.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.1.0...v1.1.1) (2019-12-01) 185 | 186 | 187 | ### Bug Fixes 188 | 189 | * use hook `afterEmit` and emit error on catch ([17f7421](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/17f7421030e6a5b589b2cab015d9af80b868ca95)) 190 | 191 | ## [1.1.0](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.0.4...v1.1.0) (2019-11-18) 192 | 193 | 194 | ### Features 195 | 196 | * support stylelint v12 ([#196](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/196)) ([aacf7ad](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/aacf7ad)) 197 | 198 | ### [1.0.4](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.0.3...v1.0.4) (2019-11-13) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * hooks ([#195](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/195)) ([792fe19](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/792fe19)) 204 | 205 | ### [1.0.3](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.0.2...v1.0.3) (2019-10-25) 206 | 207 | 208 | ### Bug Fixes 209 | 210 | * options variable ([#193](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/193)) ([3389aec](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/3389aec)) 211 | 212 | ### [1.0.2](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.0.1...v1.0.2) (2019-10-07) 213 | 214 | 215 | ### Bug Fixes 216 | 217 | * convert back-slashes ([#186](https://github.com/webpack-contrib/stylelint-webpack-plugin/issues/186)) ([41b0f53](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/41b0f53)) 218 | 219 | ### [1.0.1](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v1.0.0...v1.0.1) (2019-09-30) 220 | 221 | 222 | ### Bug Fixes 223 | 224 | * compiler hooks ([aca2c1d](https://github.com/webpack-contrib/stylelint-webpack-plugin/commit/aca2c1d)) 225 | 226 | ## 1.0.0 (2019-09-30) 227 | 228 | ### Bug Fixes 229 | 230 | * Handle compilation.fileTimestamps for webpack 4 231 | * DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead 232 | * Update option `emitError` 233 | * Update option `failOnError` 234 | 235 | ### Features 236 | 237 | * Modernize project to latest defaults 238 | * Validate options 239 | * Support absolute paths in files array 240 | * New option `stylelintPath` 241 | * New option `emitWarning` 242 | * New option `failOnWarning` 243 | * New option `quiet` 244 | 245 | ### ⚠ BREAKING CHANGES 246 | 247 | * Drop support for Node < 8.9.0 248 | * Minimum supported `webpack` version is 4 249 | * Minimum supported `stylelint` version is 9 250 | --------------------------------------------------------------------------------