This plugin only supports webpack
>= 5.106.0and Node.js>= 22.12.0.
This plugin runs linters, type checkers and other diagnostic tools over your sources during the webpack build and reports what they find as webpack errors and warnings.
It replaces eslint-webpack-plugin and stylelint-webpack-plugin: one plugin, one place to configure how problems are reported, and one pass over your project. Today it runs ESLint, Stylelint, oxlint, Biome and TypeScript; more linters and diagnostic tools are meant to be added the same way.
To begin, you'll need to install diagnostics-webpack-plugin:
npm install diagnostics-webpack-plugin --save-devor
yarn add -D diagnostics-webpack-pluginor
pnpm add -D diagnostics-webpack-pluginNote
Install the tools you want to run as well — the plugin only requires the ones you enable. It supports eslint >= 9, stylelint >= 17, oxlint >= 1, @biomejs/biome >= 2 and typescript >= 5:
npm install eslint stylelint oxlint @biomejs/biome typescript --save-devThen add the plugin to your webpack configuration and enable a check for each language you want inspected:
import DiagnosticsPlugin from "diagnostics-webpack-plugin";
export default {
// ...
plugins: [
new DiagnosticsPlugin({
checks: [
{ use: "eslint", extensions: ["js", "mjs"] },
{ use: "stylelint", extensions: ["css", "scss"] },
],
}),
],
// ...
};The package ships an ECMAScript build next to a CommonJS one, so a CommonJS configuration works just as well:
const DiagnosticsPlugin = require("diagnostics-webpack-plugin");The plugin options have three layers:
| Layer | Where it goes | What it covers |
|---|---|---|
| Plugin | Top level only | How the plugin schedules its work, for every check at once. |
| Shared | Top level or in a checks entry |
Which files are linted and how problems are reported. An entry overrides what it sets. |
| Check | In a checks entry |
Options only that tool understands, plus everything its own Node.js API accepts. |
The options are checked against their schema from webpack's own validate hook, so a mistake is reported when webpack validates the rest of your configuration, and validate: false turns the check off along with webpack's.
Every check to run is an entry in checks, named by its use. The list may name the same tool more than once, so one instance can inspect two file sets under different configurations.
| Option | Layer | What it decides |
|---|---|---|
checks |
Plugin | Which checks run, and the options only each of them understands. |
context |
Plugin | The folder every relative files and exclude pattern is resolved against. |
lintOnStart |
Plugin | Whether the first compilation checks everything it covers. |
cache |
Shared | Whether the tool keeps a cache of its own between runs. |
cacheLocation |
Shared | Where that cache is written. |
exclude |
Shared | What is left out. |
extensions |
Shared | Which extensions a named folder is walked for. |
files |
Shared | What to check: naming it checks every file it matches, built or not. |
fix |
Shared | Whether the tool writes back what it can fix. |
formatter |
Shared | How results are turned into the message that is reported. |
outputReport |
Shared | A file the same results are written to. |
reportAs |
Shared | What the build carries: an error, a warning, a log line, or nothing. |
resourceQueryExclude |
Shared | Which module queries are left out. |
threads |
Shared | How wide the work is spread. |
configType, eslintPath |
Check | ESLint's own. |
stylelintPath |
Check | Stylelint's own. |
oxlintPath, configFile, args |
Check | oxlint's own. |
biomePath, command, configFile, args |
Check | Biome's own. |
typescriptPath, configFile, compilerOptions, build, diagnosticOptions, ignoreDiagnostics |
Check | TypeScript's own. |
Anything else written in a checks entry is handed to the tool itself, so its
own Node.js API options go next to these.
new DiagnosticsPlugin({
// Plugin options
context: "src",
// Shared options, every check uses them unless it says otherwise
reportAs: "warning", // report every check's results as warnings
exclude: ["node_modules", "vendor"],
// The checks to run, each with the options only it understands
checks: [
{ use: "eslint", extensions: ["js", "ts"], fix: true },
{ use: "stylelint", extensions: ["css", "scss"], threads: true },
],
});- Type:
type checks = (string | ({ use: string | CheckAdapter } & Options))[];- Default: none — this is the one option the plugin requires
The checks to run. An entry is the name of a built-in check, or an object naming
it under use next to the options it takes, so these two are the same thing:
new DiagnosticsPlugin({ checks: ["eslint", { use: "eslint" }] });Naming the same tool twice runs it twice, which is how one build checks two file sets under two configurations:
new DiagnosticsPlugin({
checks: [
{ use: "eslint", files: "src", fix: true },
{ use: "eslint", files: "scripts", reportAs: "warning" },
],
});use also takes an adapter of your own rather than a built-in name — see
Adding a check.
- Type:
type context = string;- Default:
compiler.context
Base directory for linting. Every relative files and exclude pattern is resolved against it.
- Type:
type lintOnStart = boolean;- Default:
true
Whether the first compilation lints every file it covers. Leave it alone and a build lints everything while a watch run lints everything once and then only what webpack rebuilds.
Set it to false to start a watch run quiet: nothing is linted until you touch
a file, and only the modules webpack rebuilds are reported. A build is nothing
but a first compilation, so it lints either way — the option cannot silence one.
There is no option for this and none is needed: plugins ignores a falsy entry,
so && decides whether the plugin is there at all. A configuration function is
handed the --env values, and webpack-cli states the mode in them:
| Value | Set by |
|---|---|
WEBPACK_SERVE |
webpack serve |
WEBPACK_WATCH |
webpack watch and webpack --watch |
WEBPACK_BUILD, WEBPACK_BUNDLE |
webpack, a build that runs once |
import DiagnosticsPlugin from "diagnostics-webpack-plugin";
import { defineConfig } from "webpack";
export default defineConfig((env) => ({
plugins: [
// Both values are read because each command sets only its own: `serve` sets
// `WEBPACK_SERVE` and `--watch` sets `WEBPACK_WATCH`, so asking for one of
// them misses the other way of watching. Read `WEBPACK_BUILD` instead to
// check a build that runs once and leave a watch run alone.
(env.WEBPACK_SERVE || env.WEBPACK_WATCH) &&
new DiagnosticsPlugin({ checks: ["eslint", "stylelint"] }),
],
}));This is worth doing when something else already lints the same files — a CI job
running eslint ., or an editor — and the release build need not pay for it
twice.
A client and a server configuration lint the same sources twice. Where two compilers run a check configured the same way over the same files — the whole of it for a check reading from the file system, whatever they both built for one reading the module graph — the second joins the run the first is making rather than starting its own, and both compilations report what it finds and watch what it read.
Over three hundred files with the typescript check, two compilers build one
program rather than two: 1.52 s rather than 1.79 s, and 248 MB rather than
298 MB. What a compiler checks alone it still checks alone, and a check
configured differently in the second configuration runs on its own.
These can be set at the top level, where they apply to every check, or inside one check, where they apply to that check alone.
- Type:
type cache = boolean;- Default:
true
The cache is enabled by default to decrease execution time.
It is turned off for an ESLint check doing typed linting: a cache keyed on one file's own contents cannot answer for a rule that read another.
- Type:
type cacheLocation = string;- Default:
node_modules/.cache/diagnostics-webpack-plugin/.<tool>cache
Specify the path to the cache location. Can be a file or a directory.
- Type:
type files = string | string[];- Default:
options.context
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.
Naming them says what to check, so every file they match is checked whether or not webpack built it — a module nothing imports yet, or one reached through a loader webpack resolves differently, is checked all the same. Leave it unset and a check reads whatever it reads by itself: ESLint the modules webpack built, Stylelint a walk of the context.
In a watch run the folder a check takes its files from is watched, so a file
you add there is checked without anything else having to change — and for the
typescript check, so is every folder the tsconfig.json include covers. A
folder is only watched when the whole of it can be: webpack rebuilds on a
change anywhere under one, so a folder holding what the build writes
(output.path) or what the check itself leaves out
(exclude, node_modules by default) is left alone rather than
turning every write into a rebuild. Pointing files at your sources rather
than leaving it at the project root is what makes it watchable.
- Type:
type extensions = string | string[];- Default:
'js'for ESLint,['css', 'scss', 'sass']for Stylelint
Specify file extensions that should be checked.
- Type:
type exclude = string | string[];- Default:
'node_modules', plusoutput.pathfor Stylelint
Specify the files/directories to exclude. Must be relative to options.context.
- Type:
type threads = boolean | number | "auto";- Default:
"auto"
How many threads a check spreads its work over. "auto" takes one fewer than the
machine has, leaving webpack the thread it builds on; a number asks for that
many; false, 0 or 1 keeps the work on webpack's own thread.
A check that threads its own work is asked to, and one that cannot is run in a pool of workers — so the option means the same thing whatever is being checked, and a check added later gets it for nothing. ESLint threads its own work from 9.34.0 under flat config; Stylelint threads none of its own, so it is pooled. TypeScript reads a program whole rather than a file at a time, so it is run on one worker: what a thread buys it is webpack's own thread back.
Prefer "auto" over a count. A check sizes "auto" against the machine, where a
fixed number can end up competing with webpack for the same cores: over three
hundred modules on four of them, "auto" took about a fifth off the build while
asking for three was no better than asking for none. A count above what the
machine has is held to it, since asking for sixteen threads on four cores took
twice as long as asking for none — the ceiling cannot save a number written
against the tool itself, such as ESLint's concurrency, which is passed through
as written.
Anything written against the tool itself wins — ESLint's own concurrency, say —
and a check configured with a function, such as a formatter written in the
configuration, is run on webpack's thread, because a function cannot be handed
to a worker.
- Type:
type resourceQueryExclude = RegExp | RegExp[];- Default:
[]
Specify the resource query to exclude. Only affects checks that read the module graph, such as ESLint.
- Type:
type fix = boolean;- Default:
false
Will enable the autofix feature of the tool.
Be careful: this option will modify source files.
- Type:
type formatter = string | ((results: LintResult[]) => string);- Default: the tool's own default formatter
Accepts the name of a formatter the tool ships, or a function that receives its results and returns the output as a string.
See the ESLint formatters and the Stylelint formatter option.
Every check reports its errors as webpack errors and its warnings as webpack warnings, which is what fails the build. reportAs overrides that.
- Type:
type reportAs = Severity | { errors?: Severity; warnings?: Severity };
type Severity = "error" | "warning" | "log" | false;- Default: unset — each result stays at the severity the check gave it
What a check reports its results as. One value covers its errors and its warnings alike; an object sets them apart, and a severity the object leaves out keeps its own:
| Value | Effect |
|---|---|
| unset | Errors fail the build, warnings do not. |
"error" |
Everything fails the build, warnings included. |
"warning" |
Nothing fails the build; errors are reported as warnings. |
"log" |
The terminal alone, through webpack's log. |
false |
Nothing is reported. An outputReport is still written. |
{ warnings: false } |
The errors alone, still failing the build. |
{ warnings: "error" } |
Warnings fail the build too, and errors keep failing it. |
{ errors: "warning" } |
Errors stop failing the build, and warnings stay warnings. |
new DiagnosticsPlugin({
reportAs: { warnings: false }, // the errors alone
checks: [{ use: "eslint" }],
});"log" is for a check nobody should be stopped by yet. Its results reach
webpack's log — the terminal, and stats.logging — rather than
compilation.errors or compilation.warnings, so the build carries neither,
stats.hasWarnings() stays false and a dev server overlays nothing. An
outputReport is written either way.
new DiagnosticsPlugin({
// the whole project is linted, and only the new check is advisory
checks: [{ use: "eslint" }, { use: "typescript", reportAs: "log" }],
});A check nothing can be reported from is one a watch rebuild does not wait
for: every reportAs that leaves both errors and warnings off the compilation
— "log", false, or an object spelling both out — with no
outputReport to write. The first build still waits, since it
is what tells the watcher which files the check reads. From the rebuild on, the
check is run once the build is over rather than beside it, and what it finds is
printed through webpack's infrastructure log rather than through
stats.logging.
The work is moved out of the rebuild rather than off the machine: an edit arriving while such a check is still running waits for the rest of it, since a check holds the thread the build runs on. A report a newer rebuild has already overtaken is dropped, and a check never has two of its runs going at once — the next starts when the last has reported.
- Type:
type ignoreDiagnostics =
IgnoreOne | IgnoreOne[] | ((message: Message) => boolean);
type IgnoreOne = number | string | IgnoreMatch;
interface IgnoreMatch {
file?: string | undefined;
code?: string | undefined;
severity?: "error" | "warning" | undefined;
}
interface Message {
file?: string | undefined;
code?: string | undefined;
severity: "error" | "warning";
text: string;
}- Default: unset
What not to report. A linter's rule can be turned off in its own configuration and a TypeScript diagnostic cannot, so this is where one thing a check found is left out whichever check found it — for adopting a check on a project that is not clean yet, without turning the check off altogether.
| Written as | Leaves out |
|---|---|
2307 |
The TypeScript code, as tsc prints it after TS. |
"no-unused-vars" |
The rule or code a check names. |
{ file, code, severity } |
What all three match; one left out matches everything. |
[…] |
Anything the list matches. |
(message) => boolean |
Whatever the function answers true for. |
new DiagnosticsPlugin({
checks: [
// TS2307 is `Cannot find module`, TS7016 an untyped dependency
{ use: "typescript", ignoreDiagnostics: [2307, 7016] },
// a rule that has not been dealt with in one folder yet
{
use: "eslint",
ignoreDiagnostics: { code: "no-unused-vars", file: "src/legacy/**" },
},
],
});A file is a glob matched against the path relative to
context, as exclude is. Something a check reported
of no file in particular — a configuration it could not read — is not a file's
to leave out, so a match naming one keeps it.
What is left out is left out of everything the check reports, an
outputReport included, and of the counts a formatter prints.
A check shipped outside this package answers for its own results, so it reads
this option once it says how (see Adding a check).
- Type:
type outputReport =
| boolean
| {
filePath?: string | undefined;
formatter?: (string | ((results: LintResult[]) => string)) | undefined;
};- Default:
false
Write the results to a file, for example a checkstyle xml file for use for reporting on Jenkins CI.
filePath: path to the output report file, relative tooutput.pathunless absolute.formatter: a differentformatterfor the output file; the default/configured formatter is used when none is passed in.
Set at the top level, every check appends its report to the same file. Set it inside a checks entry to give that check a file of its own.
new DiagnosticsPlugin({
checks: [
{
use: "eslint",
outputReport: { filePath: "eslint.json", formatter: "json" },
},
{
use: "stylelint",
outputReport: { filePath: "stylelint.json", formatter: "json" },
},
],
});Run with { use: "eslint" }. It lints the files webpack builds, so only the modules that end up in the bundle are checked.
Alongside the shared options you can pass any ESLint Node.js API option — they are handed to the ESLint class as they are.
threads reaches ESLint as its own concurrency from 9.34.0 under flat config, and an older ESLint or an eslintrc one is pooled instead. Write concurrency yourself and that is what ESLint is given, whatever threads says. Note that webpack spells this idea parallelism, and uses the word concurrency for something else again — bounded work on one thread.
A rebuild lints only the files webpack rebuilt and reports the rest from the previous run, so lintOnStart is only worth setting to start a watch run quiet.
Typed linting is the exception, and it is read from your configuration rather than asked of you. Where parserOptions.project or parserOptions.projectService is set, what a rule reports of one file can turn on another — a type it imported, a declaration file something regenerated — so a file that did not itself change is not one whose last result still holds. A rebuild then lints every file the check covers, and ESLint's own cache is turned off for it, since that cache is keyed on each file's own contents and would answer for a type it never read.
- Type:
type configType = "flat" | "eslintrc";- Default:
flat
Specify the type of configuration to use with ESLint.
flatis the current standard configuration format.eslintrcis the legacy configuration format and has been officially deprecated.
The new configuration format is explained in its own documentation.
eslintrc needs an ESLint that still reads one: ESLint 9 does, ESLint 10 removed
it, and the option stays for as long as this plugin supports 9. The check loads
ESLint the same way wherever it runs it, in the build's own thread and in a
worker of a pool alike, and the ESLint 9 job in CI checks both.
- Type:
type eslintPath = string;- Default:
eslint
Path to the eslint instance that will be used for linting.
If the eslintPath is a folder like the official ESLint, or you specify a formatter option, you don't have to install eslint.
Bulk suppressions are supported: enable ESLint's own applySuppressions, and point suppressionsLocation at the file if it is not the default eslint-suppressions.json.
new DiagnosticsPlugin({
checks: [{ use: "eslint", applySuppressions: true }],
});Important
ESLint resolves the suppressions file, and every path recorded inside it, against its own cwd — not against the plugin's context. Where the two differ, pass cwd to the check as well:
new DiagnosticsPlugin({
context: "src",
checks: [
{ use: "eslint", applySuppressions: true, cwd: import.meta.dirname },
],
});Suppressions need ESLint 9.24 or later. ESLint 10 takes both options itself; below that they reach its CLI alone, so the plugin applies the suppressions after linting instead — the same file, the same paths, the same result.
Run with { use: "stylelint" }, and requires stylelint >= 17. It lints every file matching files and extensions on disk, whether or not webpack imported it, so a stylesheet nothing imports yet is still checked.
Alongside the shared options you can pass any Stylelint option — they are handed to stylelint.lint() as they are.
- Type:
type stylelintPath = string;- Default:
stylelint
Path to the stylelint instance that will be used for linting.
Run with { use: "oxlint" }, and requires oxlint >= 1. It lints the files
webpack builds, as the ESLint check does, and reads whatever .oxlintrc.json
oxlint finds for itself.
new DiagnosticsPlugin({ checks: ["eslint", "oxlint"] });oxlint is a binary behind a Node entry rather than a library, so this check runs
it and reads the JSON it answers with. Two things follow. Its severities are
oxlint's own — a rule set to "error" in .oxlintrc.json is reported as a
webpack error and one set to "warn" as a warning, and reportAs
moves them from there as it does for every check. And the options this check does
not name are not guessed at: write them as args, the flags oxlint
itself documents.
- Type:
type oxlintPath = string;- Default:
oxlint
Path to the oxlint instance that will be used for linting.
- Type:
type configFile = string;- Default: unset, leaving oxlint to find its own
Path to the .oxlintrc.json to lint with.
- Type:
type args = string[];- Default:
[]
Arguments passed to oxlint as they are, for the flags this check does not name —
["--deny", "correctness"], say. Not --format: the check asks for JSON and
formats the results itself, and oxlint declines being asked twice.
Run with { use: "biome" }, and requires @biomejs/biome >= 2. It checks the
files webpack builds and reads whatever biome.json Biome finds for itself,
reporting at Biome's own severities.
new DiagnosticsPlugin({ checks: ["biome"] });Like the oxlint check this runs a binary and reads the JSON it answers with, which Biome calls an experimental reporter — a release of its own may move the shape, and it is the only reporter carrying the severities this check needs.
- Type:
type biomePath = string;- Default:
@biomejs/biome
Path to the @biomejs/biome instance that will be used for checking.
- Type:
type command = "lint" | "check";- Default:
"lint"
Which of Biome's commands to run. "lint" runs the linter; "check" adds its
formatting and assist diagnostics, so a badly formatted file is reported as well
as one breaking a rule.
- Type:
type configFile = string;- Default: unset, leaving Biome to find its own
Path to the biome.json to check with.
- Type:
type args = string[];- Default:
[]
Arguments passed to Biome as they are, for the flags this check does not name.
Not --reporter: the check asks for JSON and formats the results itself.
Run with { use: "typescript" }, and requires typescript >= 5. It type checks
the program a tsconfig.json describes, so every file that config includes is
checked whether or not webpack built it, and a type error in a module nothing
imports yet is still reported. Emit is forced off: webpack writes the output.
new DiagnosticsPlugin({
checks: [{ use: "typescript", configFile: "tsconfig.build.json" }],
});Project references are read rather than built unless build says
otherwise, which is what a tsconfig.json listing only references needs.
Two things differ from the linters. A diagnostic belongs to the program rather
than to one file, so there is nothing to report a single file from. And
extensions only decides which files make the check run at all; what is checked
is whatever the config file includes.
A program is read whole, so threads buys it webpack's own thread
back rather than a share of the work: the check runs on one worker while webpack
builds, and is read where it is reported. Over a three-hundred-file solution
beside three hundred modules that took the build from 2149 ms to 1920 ms and the
module graph from 2004 ms to 385 ms, and an edit-to-report in watch from 259 ms
to 203 ms. A small project pays the thread's start without much to hide behind
it; threads: false keeps the work where webpack is.
While webpack watches, the program is kept and handed to the build after it, so a rebuild type checks what the change reaches rather than the project over again — about 30 ms rather than 300 over three hundred files, for the memory the program holds on to (some 50 MB there). A change to the config file, or to the compiler options given here, starts a new one.
Alongside the shared options you can pass any
compiler option — they override what
the config file sets, as --strict would on the command line.
- Type:
type typescriptPath = string;- Default:
typescript
Path to the typescript instance that will be used for checking.
- Type:
type configFile = string;- Default: the nearest
tsconfig.jsonat or abovecontext
Path to the tsconfig.json that describes the program.
- Type:
type compilerOptions = object;- Default: unset
Compiler options overriding the ones the config file sets. The same as writing them at the top level of the check, and useful when a name collides with one of the plugin's own.
- Type:
type build = boolean;- Default:
false
Build the projects the config file references before reporting, the way
tsc -b does. A tsconfig.json that only lists references describes an
empty program of its own, so without this a solution reports nothing at all;
with it, every project in the graph is checked in the order its references
say.
new DiagnosticsPlugin({
checks: [{ use: "typescript", build: true }],
});A reference is read through the declarations the referenced project publishes,
so building one is how the next is checked at all — the build therefore writes
each project's .d.ts and its .tsbuildinfo, and nothing else. The JavaScript
stays webpack's to write: a later tsc -b of your own still emits it.
mode says so in full, and can say otherwise.
The cost is a tsc -b pass rather than the kept program the check uses
otherwise. Over five projects and 205 files: 573 ms with nothing built yet,
4–8 ms when the solution is up to date, 258 ms after one file changed, and
around 200 ms on every rebuild for as long as an error stands, since a project
that fails is never up to date. Pair it with
reportAs: "log" if you would rather a watch rebuild did not wait
for that.
- Type:
type mode = "readonly" | "write-tsbuildinfo" | "write-dts" | "write-references";- Default:
"write-dts"underbuild,"readonly"otherwise
What the check writes as well as reports. A check reports, so by default a program writes nothing at all and a solution writes only what the next project has to read.
| Mode | What reaches the disk |
|---|---|
readonly |
Nothing. |
write-tsbuildinfo |
Only .tsbuildinfo, which a later build resumes from. |
write-dts |
.tsbuildinfo, .d.ts and .d.ts.map. |
write-references |
Everything the compiler emits, the JavaScript included. |
new DiagnosticsPlugin({
checks: [{ use: "typescript", build: true, mode: "write-references" }],
});readonly under build is not the compiler's noEmit, which tsc -b refuses
with TS6310: the build runs and its output is kept where only that build can
read it, so a project still finds the declarations of the one it references
while your working tree is left alone.
- Type:
interface diagnosticOptions {
syntactic?: boolean | undefined;
semantic?: boolean | undefined;
declaration?: boolean | undefined;
global?: boolean | undefined;
}- Default: every kind reported
Which kinds of diagnostic to report. Turning one off asks TypeScript for less
rather than dropping what it answered, so it is also the one filter that saves
the work — semantic: false is what makes a check that only wants syntax errors
cheap.
new DiagnosticsPlugin({
// webpack's own parser reports a syntax error in a file it builds
checks: [{ use: "typescript", diagnosticOptions: { syntactic: false } }],
});What the tsconfig.json itself is wrong about is reported whatever is turned
off here: with the config file misread, nothing below it would be answering the
right question. build reports the whole of what it finds either way,
since a solution is built rather than asked kind by kind.
fork-ts-checker-webpack-plugin spells this option the same way, with
semantic alone on by default.
A use may also be an adapter of its own rather than a built-in name, so a check can ship as its own package without an entry in this one:
new DiagnosticsPlugin({
checks: [
{ use: require("diagnostics-webpack-plugin-typescript"), strict: true },
],
});Such an adapter is an object with a name, and a create returning the five functions the plugin drives it through — what to lint, what came back, which results are errors and which warnings, how to format them, and what to release afterwards. It splits its results by their own severity and nothing else; reportAs is applied to what it returns:
module.exports = {
name: "made-up",
// "modules" lints the files webpack built, "glob" every file matching `files`
filesSource: "glob",
// Merged under the options the user passes, and under the shared options
defaults: { extensions: ["ts"] },
async create({ key, options, compilation }) {
return {
lintFiles: async (files) => runTheTool(files),
getResults: async (results) => results,
splitResults: (results) => ({ errors: results, warnings: [] }),
getFormatter: async (formatter) => async (results) => format(results),
cleanup: async () => {},
};
},
};label, filesSource, defaults, defaultExclude and schema are optional; the plugin fills in the defaults of a module-scanning check that excludes node_modules.
Both plugins become one, and every option they had is still here. What changed is where an option is written and how the four that decided severity are spelled.
Where an option goes. context, lintOnStart and checks are the plugin's own and stay at the top level. Everything else is shared: write it at the top level to cover every check, or inside a checks entry to cover that one. configType, eslintPath and stylelintPath belong to a single check and go in its entry.
Severity is one option. emitError, emitWarning, failOnError, failOnWarning and quiet are reportAs, because reporting a result as a webpack error is what fails the build:
| Was | Is |
|---|---|
quiet: true, emitWarning: false |
reportAs: { warnings: false } |
emitError: false |
reportAs: { errors: false } |
emitError: false and emitWarning: false |
reportAs: false |
failOnError: true |
the default |
failOnError: false |
reportAs: "warning" |
failOnWarning: true |
reportAs: { warnings: "error" } |
The build is no longer aborted from inside the plugin. A result reported as a webpack error fails the build the way every other webpack error does — stats.hasErrors() is true and the CLI exits non-zero — and the assets are still written. Nothing about severity depends on mode any more.
Requirements. Node >= 22.12, webpack >= 5.106, and ESLint 9 or 10 / Stylelint 17 for whichever checks you run.
-const ESLintPlugin = require("eslint-webpack-plugin");
+const DiagnosticsPlugin = require("diagnostics-webpack-plugin");
module.exports = {
plugins: [
- new ESLintPlugin({ extensions: ["js"], fix: true }),
+ new DiagnosticsPlugin({
+ checks: [{ use: "eslint", extensions: ["js"], fix: true }],
+ }),
],
};Every option eslint-webpack-plugin accepted, and where it is now:
| Option | Now |
|---|---|
cache |
Unchanged, shared. |
cacheLocation |
Unchanged, shared. The default moved to node_modules/.cache/diagnostics-webpack-plugin/.eslintcache. |
configType |
Unchanged, in the eslint entry. |
context |
Unchanged, top level. |
emitError |
reportAs, see the table above. |
emitWarning |
reportAs, see the table above. |
eslintPath |
Unchanged, in the eslint entry. |
exclude |
Unchanged, shared. |
extensions |
Unchanged, shared. Still defaults to js. |
failOnError |
reportAs. It defaulted to on outside development mode; the default no longer depends on mode. |
failOnWarning |
reportAs, see the table above. |
files |
Unchanged, shared. |
fix |
Unchanged, shared. |
formatter |
Unchanged, shared. |
lintDirtyModulesOnly |
lintOnStart, inverted: lintDirtyModulesOnly: true is lintOnStart: false. Top level. |
outputReport |
Unchanged, shared. It is still written even when reportAs is false. |
quiet |
reportAs: { warnings: false }. |
resourceQueryExclude |
Unchanged, shared. |
Any other option is passed to ESLint itself, as before.
-const StylelintPlugin = require("stylelint-webpack-plugin");
+const DiagnosticsPlugin = require("diagnostics-webpack-plugin");
module.exports = {
plugins: [
- new StylelintPlugin({ extensions: ["css"], threads: true }),
+ new DiagnosticsPlugin({
+ checks: [{ use: "stylelint", extensions: ["css"], threads: true }],
+ }),
],
};Every option stylelint-webpack-plugin accepted, and where it is now:
| Option | Now |
|---|---|
cache |
Unchanged, shared. |
cacheLocation |
Unchanged, shared. The default moved to node_modules/.cache/diagnostics-webpack-plugin/.stylelintcache. |
context |
Unchanged, top level. |
emitError |
reportAs, see the table above. |
emitWarning |
reportAs, see the table above. |
exclude |
Unchanged, shared. |
extensions |
Unchanged, shared. Still defaults to css, scss and sass. |
failOnError |
reportAs. It defaulted to on in every mode, and the default is still to fail on an error. |
failOnWarning |
reportAs, see the table above. |
files |
Unchanged, shared. |
formatter |
Unchanged, shared. |
lintDirtyModulesOnly |
lintOnStart, inverted: lintDirtyModulesOnly: true is lintOnStart: false. Top level. |
outputReport |
Unchanged, shared. It is still written even when reportAs is false. |
quiet |
reportAs: { warnings: false }. |
stylelintPath |
Unchanged, in the stylelint entry. |
threads |
Shared now, and "auto" by default rather than off. Every check honours it. |
Any other option is passed to Stylelint itself, as before. Two more things changed for Stylelint alone:
- Stylelint 17 or later is required.
stylelint-webpack-pluginaccepted13through17; the merged plugin drops the older majors rather than carrying their compatibility branches forward. Stylelint 17 itself needs Node>= 20.19. - Errors and warnings are no longer swapped.
failOnError: falseused to report errors as webpack warnings, andfailOnWarning: trueto report warnings as webpack errors. Each result now keeps its own severity unlessreportAssays otherwise — which is what those two spellings in the table above do, explicitly.
fix is a documented option now rather than one passed through to Stylelint unnamed. resourceQueryExclude is shared but has no effect here: it reads the query of a module webpack built, and Stylelint is given the files matching files instead.
The two plugins become one instance, and options they had in common are written once:
module.exports = {
plugins: [
- new ESLintPlugin({ context: "src", failOnError: true, extensions: ["js"] }),
- new StylelintPlugin({ context: "src", failOnError: true, extensions: ["css"] }),
+ new DiagnosticsPlugin({
+ context: "src",
+ checks: [
+ { use: "eslint", extensions: ["js"] },
+ { use: "stylelint", extensions: ["css"] },
+ ],
+ }),
],
};The typescript check does what that plugin does — type check the program a
tsconfig.json describes, rather than the files webpack happens to build — and
is written as one entry in checks:
-const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
+const DiagnosticsPlugin = require("diagnostics-webpack-plugin");
module.exports = {
plugins: [
- new ForkTsCheckerWebpackPlugin({
- typescript: { configFile: "tsconfig.build.json", build: true },
- }),
+ new DiagnosticsPlugin({
+ checks: [
+ { use: "typescript", configFile: "tsconfig.build.json", build: true },
+ ],
+ }),
],
};The check runs in the build's own process. That plugin forks one, which is
why it has a memoryLimit and a profile of its own; here the program is kept
between rebuilds instead, so a rebuild type checks what the change reaches
rather than the project again. What that costs and saves is under
the TypeScript check.
fork-ts-checker-webpack-plugin |
Here |
|---|---|
async |
Not an option: the check runs on a worker thread while webpack builds (threads), and a check the compilation carries nothing of — reportAs "log" or false, with no outputReport — is run after a watch rebuild rather than during it. |
typescript.configFile |
configFile, in the entry — see the TypeScript check. |
typescript.context |
context, the plugin's own. |
typescript.build |
build. |
typescript.configOverwrite |
compilerOptions, or any compiler option written at the top of the entry. |
typescript.typescriptPath |
typescriptPath. |
typescript.mode |
mode. The same four values, and the same meaning; the default under build is write-dts rather than write-tsbuildinfo, because a referenced project is read through the declarations it publishes. |
typescript.memoryLimit, profile |
Nothing to set — the check is not a forked process. |
typescript.diagnosticOptions |
diagnosticOptions, spelled the same way. Every kind is reported here unless you turn one off; there, only semantic is on to begin with. |
issue.include, issue.exclude |
ignoreDiagnostics, which takes the same match of a file, a code and a severity, or a function of your own. It says what to leave out, so an include is written as the exclude of everything else. |
formatter |
formatter. Unset, TypeScript's own formatter is used, with color and the source line. |
logger |
Webpack's logger is what the plugin writes to; reportAs: "log" is what sends results there rather than onto the compilation. |
devServer |
Nothing to set: results reach the compilation, so a dev server overlays them, and reportAs: "log" keeps them off it. |
Running it next to a linter is one plugin rather than two, with one place to say how what they find is reported:
new DiagnosticsPlugin({
checks: [{ use: "eslint" }, { use: "typescript" }],
});We welcome all contributions!
If you're new here, please take a moment to review our contributing guidelines.