From 2507357e44f355bbca0f78d96ef6d15bf4b6e12c Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:36:48 +0000 Subject: [PATCH 1/2] feat!: union the emit and fail options `emitError` and `emitWarning` become one `emit`, and `failOnError` and `failOnWarning` one `failOn`. Each takes `"warning"`, `"error"` or `false`, naming the least severe result it takes in, so `"warning"` covers the errors above it and `false` covers nothing. Reporting the errors of a check while hiding its warnings is a threshold, not a set, so one value says it and no combination of two spells a state that is not one. `emitError: false` alone loses its spelling with them: showing a check's warnings while hiding its errors was never useful. Defaults carry the old behaviour over: `emit` is `"warning"`, and `failOn` is `"error"` outside `development` mode and `false` inside it. `quiet` is now exactly `emit: "error"`. Both options resolve through `??`, so one written out as `undefined` reads as the one left unwritten. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GzZci4NQeiqwdrVfd7dGXy --- .changeset/union-emit-and-fail-options.md | 5 ++ README.md | 65 +++++++---------- src/checks/eslint.js | 8 ++- src/checks/stylelint.js | 9 ++- src/index.js | 15 ++-- src/options.js | 35 +++++---- src/shared-options.json | 86 ++++++++++++++++------- src/utils.js | 15 ++++ test/emit-error.test.js | 46 ------------ test/emit-warning.test.js | 46 ------------ test/emit.test.js | 63 +++++++++++++++++ test/eslint-options.test.js | 6 +- test/fail-on-error.test.js | 26 ------- test/fail-on-warning.test.js | 19 ----- test/fail-on.test.js | 56 +++++++++++++++ test/multiple-instances.test.js | 12 ++-- test/stylelint/emit-error.test.js | 42 ----------- test/stylelint/emit-warning.test.js | 41 ----------- test/stylelint/emit.test.js | 42 +++++++++++ test/stylelint/fail-on-error.test.js | 24 ------- test/stylelint/fail-on-warning.test.js | 18 ----- test/stylelint/fail-on.test.js | 48 +++++++++++++ test/stylelint/stylelint-options.test.js | 8 +-- test/unified/unified.test.js | 8 +-- types/index.d.ts | 3 +- types/options.d.ts | 29 ++++---- types/utils.d.ts | 15 ++++ 27 files changed, 402 insertions(+), 388 deletions(-) create mode 100644 .changeset/union-emit-and-fail-options.md delete mode 100644 test/emit-error.test.js delete mode 100644 test/emit-warning.test.js create mode 100644 test/emit.test.js delete mode 100644 test/fail-on-error.test.js delete mode 100644 test/fail-on-warning.test.js create mode 100644 test/fail-on.test.js delete mode 100644 test/stylelint/emit-error.test.js delete mode 100644 test/stylelint/emit-warning.test.js create mode 100644 test/stylelint/emit.test.js delete mode 100644 test/stylelint/fail-on-error.test.js delete mode 100644 test/stylelint/fail-on-warning.test.js create mode 100644 test/stylelint/fail-on.test.js diff --git a/.changeset/union-emit-and-fail-options.md b/.changeset/union-emit-and-fail-options.md new file mode 100644 index 0000000..dacab37 --- /dev/null +++ b/.changeset/union-emit-and-fail-options.md @@ -0,0 +1,5 @@ +--- +"diagnostics-webpack-plugin": major +--- + +`emitError` and `emitWarning` are one `emit` option, and `failOnError` and `failOnWarning` one `failOn`. Each takes `"warning"`, `"error"` or `false`, naming the least severe result it takes in, so `"warning"` covers the errors above it. `emit` defaults to `"warning"` and `failOn` to `"error"`, or `false` in `development` mode. See the migration table in the README. diff --git a/README.md b/README.md index 7ff3a41..669cf0f 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ new DiagnosticsPlugin({ // Plugin options context: "src", // Shared options, every check uses them unless it says otherwise - failOnError: true, + failOn: "error", exclude: ["node_modules", "vendor"], // The checks to run, each with the options only it understands checks: [ @@ -235,55 +235,31 @@ See the [ESLint formatters](https://eslint.org/docs/user-guide/formatters/) and ### Errors and warnings -Every check reports its errors as webpack errors and its warnings as webpack warnings. `emitError` and `emitWarning` choose what is reported at all, and `failOnError` and `failOnWarning` choose whether the build is failed over it. +Every check reports its errors as webpack errors and its warnings as webpack warnings. `emit` chooses how much of that is reported at all, and `failOn` how much of it fails the build. Each names the least severe result it takes in, so `"warning"` covers the errors above it and `false` covers nothing. -#### `emitError` +#### `emit` - Type: ```ts -type emitError = boolean; +type emit = "error" | "warning" | false; ``` -- Default: `true` - -The errors found will always be emitted, to disable set to `false`. - -#### `emitWarning` - -- Type: - -```ts -type emitWarning = boolean; -``` - -- Default: `true` - -The warnings found will always be emitted, to disable set to `false`. +- Default: `"warning"` -#### `failOnError` +The least severe result that is reported: `"warning"` reports warnings and errors, `"error"` reports errors alone, and `false` reports nothing. -- Type: - -```ts -type failOnError = boolean; -``` - -- Default: `true`, `false` in `development` mode - -Will cause the module build to fail if any errors are found, to disable set to `false`. - -#### `failOnWarning` +#### `failOn` - Type: ```ts -type failOnWarning = boolean; +type failOn = "error" | "warning" | false; ``` -- Default: `false` +- Default: `"error"`, `false` in `development` mode -Will cause the module build to fail if any warnings are found, if set to `true`. +The least severe result that fails the build: `"warning"` fails on warnings and errors, `"error"` fails on errors alone, and `false` fails on nothing. A result `emit` does not report cannot fail the build either. #### `quiet` @@ -295,7 +271,7 @@ type quiet = boolean; - Default: `false` -Will process and report errors only and ignore warnings, if set to `true`. +Will process and report errors only and ignore warnings, if set to `true`. The same as [`emit`](#emit) set to `"error"`. #### `outputReport` @@ -483,7 +459,18 @@ Move the options you were passing into a `checks` entry: }; ``` -The shared options — `context`, `files`, `exclude`, `failOnError` and the rest of [Errors and warnings](#errors-and-warnings) — may stay at the top level instead. Everything else behaves as it did, and the default `cacheLocation` moved to `node_modules/.cache/diagnostics-webpack-plugin/.eslintcache`. +`emitError` and `emitWarning` are one [`emit`](#emit) option now, and `failOnError` and `failOnWarning` one [`failOn`](#failon): + +| Was | Is | +| :------------------------------------------ | :------------------ | +| `emitWarning: false` | `emit: "error"` | +| `emitError: false` and `emitWarning: false` | `emit: false` | +| `failOnError: true` | `failOn: "error"` | +| `failOnWarning: true` | `failOn: "warning"` | + +`emitError: false` on its own has no counterpart: reporting the warnings of a check while hiding its errors was never useful. + +The shared options — `context`, `files`, `exclude`, `failOn` and the rest of [Errors and warnings](#errors-and-warnings) — may stay at the top level instead. Everything else behaves as it did, and the default `cacheLocation` moved to `node_modules/.cache/diagnostics-webpack-plugin/.eslintcache`. ### From `stylelint-webpack-plugin` @@ -506,8 +493,8 @@ Move the options you were passing into a `checks` entry: Three things changed beyond the option shape: - **Stylelint 17 or later is required.** `stylelint-webpack-plugin` accepted `13` through `17`; 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.** Errors are reported as webpack errors and warnings as webpack warnings, whatever `failOnError` and `failOnWarning` say; those two now decide whether the build is failed, not how a problem is reported. Previously `failOnError: false` turned errors into warnings, and `failOnWarning: true` turned warnings into errors. -- **`failOnError` defaults to `false` in `development` mode**, matching the rest of the plugin, rather than being `true` everywhere. +- **Errors and warnings are no longer swapped.** Errors are reported as webpack errors and warnings as webpack warnings, whatever [`failOn`](#failon) says; it decides whether the build is failed, not how a problem is reported. Previously `failOnError: false` turned errors into warnings, and `failOnWarning: true` turned warnings into errors. +- **Failing the build is off in `development` mode**, matching the rest of the plugin, rather than `failOnError` being `true` everywhere. The default `cacheLocation` moved to `node_modules/.cache/diagnostics-webpack-plugin/.stylelintcache`. @@ -522,7 +509,7 @@ The two plugins become one instance, and options they had in common are written - new StylelintPlugin({ context: "src", failOnError: true, extensions: ["css"] }), + new DiagnosticsPlugin({ + context: "src", -+ failOnError: true, ++ failOn: "error", + checks: [ + { use: "eslint", extensions: ["js"] }, + { use: "stylelint", extensions: ["css"] }, diff --git a/src/checks/eslint.js b/src/checks/eslint.js index 4e751d6..1248ab1 100644 --- a/src/checks/eslint.js +++ b/src/checks/eslint.js @@ -5,7 +5,7 @@ import { createRequire } from "node:module"; import { isAbsolute, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; -import { importFrom, omitPluginOptions } from "../utils.js"; +import { coversSeverity, importFrom, omitPluginOptions } from "../utils.js"; const nodeRequire = createRequire(import.meta.url); @@ -223,7 +223,8 @@ async function create({ options }) { for (const file of /** @type {LintResult[]} */ (results)) { if (file.errorCount > 0) { const messages = file.messages.filter( - (message) => options.emitError && message.severity === 2, + (message) => + coversSeverity(options.emit, "error") && message.severity === 2, ); if (messages.length > 0) { @@ -233,7 +234,8 @@ async function create({ options }) { if (file.warningCount > 0) { const messages = file.messages.filter( - (message) => options.emitWarning && message.severity === 1, + (message) => + coversSeverity(options.emit, "warning") && message.severity === 1, ); if (messages.length > 0) { diff --git a/src/checks/stylelint.js b/src/checks/stylelint.js index 0f07c72..9b2d616 100644 --- a/src/checks/stylelint.js +++ b/src/checks/stylelint.js @@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url"; import { Worker as JestWorker } from "jest-worker"; import { + coversSeverity, jsonStringifyReplacerSortKeys, omitPluginOptions, parseFiles, @@ -273,7 +274,9 @@ async function create({ key, options, compilation }) { for (const file of /** @type {LintResult[]} */ (results)) { const fileErrors = file.warnings.filter( - (message) => options.emitError && message.severity === "error", + (message) => + coversSeverity(options.emit, "error") && + message.severity === "error", ); if (fileErrors.length > 0) { @@ -281,7 +284,9 @@ async function create({ key, options, compilation }) { } const fileWarnings = file.warnings.filter( - (message) => options.emitWarning && message.severity === "warning", + (message) => + coversSeverity(options.emit, "warning") && + message.severity === "warning", ); if (fileWarnings.length > 0) { diff --git a/src/index.js b/src/index.js index 433add1..f472b20 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ import createCheckRunner from "./check.js"; import { getOptions, validateOptions } from "./options.js"; import { arrify, + coversSeverity, parseFiles, parseFoldersToGlobs, writeOutputFile, @@ -23,13 +24,14 @@ const { isMatch } = micromatch; /** @typedef {import("./checks/index.js").CheckAdapter} CheckAdapter */ /** @typedef {import("./options.js").EnabledCheck} EnabledCheck */ /** @typedef {import("./options.js").CheckOptions} CheckOptions */ +/** @typedef {import("./options.js").ResolvedCheckOptions} ResolvedCheckOptions */ /** @typedef {import("./options.js").Options} Options */ /** * @typedef {object} ResolvedCheck * @property {string} name check name * @property {CheckAdapter} adapter the adapter running it - * @property {CheckOptions} options options resolved for this check + * @property {ResolvedCheckOptions} options options resolved for this check * @property {string[]} wanted the globs of the files to lint * @property {string[]} exclude the globs of the files not to lint */ @@ -134,12 +136,13 @@ class DiagnosticsWebpackPlugin { resolveCheck(compiler, context, { name, adapter, options }) { const resourceQueries = arrify(options.resourceQueryExclude || []); - /** @type {CheckOptions} */ + /** @type {ResolvedCheckOptions} */ const resolved = { ...options, context, - failOnError: - options.failOnError ?? compiler.options.mode !== "development", + failOn: + options.failOn ?? + (compiler.options.mode === "development" ? false : "error"), exclude: options.exclude ? parseFiles(options.exclude, context) : adapter.defaultExclude(compiler), @@ -271,9 +274,9 @@ class DiagnosticsWebpackPlugin { } if (!failure) { - if (warnings && options.failOnWarning) { + if (warnings && coversSeverity(options.failOn, "warning")) { failure = warnings; - } else if (errors && options.failOnError) { + } else if (errors && coversSeverity(options.failOn, "error")) { failure = errors; } } diff --git a/src/options.js b/src/options.js index 534604b..0a0a859 100644 --- a/src/options.js +++ b/src/options.js @@ -13,6 +13,8 @@ const nodeRequire = createRequire(import.meta.url); const PLUGIN_NAME = "Diagnostics Webpack Plugin"; /** @typedef {import("webpack").Compiler} Compiler */ +/** @typedef {"error" | "warning"} Severity */ +/** @typedef {Severity | false} SeverityLevel */ /** @typedef {import("./checks/index.js").FormatterOption} FormatterOption */ /** @typedef {import("./checks/index.js").CheckAdapter} CheckAdapter */ /** @typedef {import("./checks/index.js").CheckAdapterInput} CheckAdapterInput */ @@ -27,12 +29,10 @@ const PLUGIN_NAME = "Diagnostics Webpack Plugin"; * @typedef {object} SharedOptions * @property {boolean=} cache enable the tool's cache to decrease execution time * @property {string=} cacheLocation specify the path to the cache location - * @property {boolean=} emitError the errors found will always be emitted - * @property {boolean=} emitWarning the warnings found will always be emitted + * @property {SeverityLevel=} emit the least severe result that is reported * @property {string | string[]=} exclude specify the files and/or directories to exclude * @property {string | string[]=} extensions specify the extensions that should be checked - * @property {boolean=} failOnError will cause the module build to fail if there are any errors - * @property {boolean=} failOnWarning will cause the module build to fail if there are any warnings + * @property {SeverityLevel=} failOn the least severe result that fails the build * @property {string | string[]=} files specify directories, files, or globs * @property {boolean=} fix apply fixes * @property {FormatterOption=} formatter specify the formatter you would like to use to format your results @@ -46,7 +46,12 @@ const PLUGIN_NAME = "Diagnostics Webpack Plugin"; */ /** - * @typedef {SharedOptions & { [option: string]: EXPECTED_ANY }} CheckOptions + * @typedef {Omit & { emit: SeverityLevel, [option: string]: EXPECTED_ANY }} CheckOptions + */ + +/** + * What a check reads once the plugin has resolved it against a compiler. + * @typedef {Omit & { emit: SeverityLevel, failOn: SeverityLevel, [option: string]: EXPECTED_ANY }} ResolvedCheckOptions */ /** @@ -72,10 +77,7 @@ const PLUGIN_NAME = "Diagnostics Webpack Plugin"; * @property {EnabledCheck[]} checks the checks to run */ -const SHARED_DEFAULTS = { - emitError: true, - emitWarning: true, -}; +const DEFAULT_EMIT = /** @type {SeverityLevel} */ ("warning"); const DEFAULT_FOLDER_TO_EXCLUDE = "**/node_modules/**"; @@ -175,19 +177,16 @@ function getOptions(pluginOptions) { const { use, ...own } = entry; const adapter = toAdapter(use); + const merged = { ...adapter.defaults, ...shared, ...own }; + + // `??`, not a default merged under them, so that an option written out as + // `undefined` reads as the one left unwritten. /** @type {CheckOptions} */ const options = { - ...SHARED_DEFAULTS, - ...adapter.defaults, - ...shared, - ...own, + ...merged, + emit: merged.quiet ? "error" : (merged.emit ?? DEFAULT_EMIT), }; - if (options.quiet) { - options.emitError = true; - options.emitWarning = false; - } - return { name: adapter.name, adapter, options }; }); diff --git a/src/shared-options.json b/src/shared-options.json index bc65e50..04fbd29 100644 --- a/src/shared-options.json +++ b/src/shared-options.json @@ -10,33 +10,46 @@ "description": "Specify the path to the cache location, it can be a file or a directory.", "type": "string" }, - "emitError": { - "description": "The errors found will always be emitted, to disable set to `false`.", - "type": "boolean" - }, - "emitWarning": { - "description": "The warnings found will always be emitted, to disable set to `false`.", - "type": "boolean" + "emit": { + "description": "The least severe result that is reported: `\"warning\"` reports warnings and errors, `\"error\"` reports errors alone, `false` reports nothing.", + "enum": ["error", "warning", false] }, "exclude": { "description": "Specify the files and/or directories to exclude. Must be relative to `options.context`.", - "anyOf": [{ "type": "string" }, { "type": "array" }] + "anyOf": [ + { + "type": "string" + }, + { + "type": "array" + } + ] }, "extensions": { "description": "Specify extensions that should be checked.", - "anyOf": [{ "type": "string" }, { "type": "array" }] - }, - "failOnError": { - "description": "Will cause the module build to fail if there are any errors, to disable set to `false`.", - "type": "boolean" + "anyOf": [ + { + "type": "string" + }, + { + "type": "array" + } + ] }, - "failOnWarning": { - "description": "Will cause the module build to fail if there are any warnings, if set to `true`.", - "type": "boolean" + "failOn": { + "description": "The least severe result that fails the build: `\"warning\"` fails on warnings and errors, `\"error\"` fails on errors alone, `false` fails on nothing.", + "enum": ["error", "warning", false] }, "files": { "description": "Specify the files and/or directories to traverse. Must be relative to `options.context`.", - "anyOf": [{ "type": "string" }, { "type": "array" }] + "anyOf": [ + { + "type": "string" + }, + { + "type": "array" + } + ] }, "fix": { "description": "Will enable the autofix feature of the tool.", @@ -45,9 +58,15 @@ "formatter": { "description": "Specify the formatter that you would like to use to format your results.", "anyOf": [ - { "type": "string" }, - { "instanceof": "Function" }, - { "instanceof": "Promise" } + { + "type": "string" + }, + { + "instanceof": "Function" + }, + { + "instanceof": "Promise" + } ] }, "outputReport": { @@ -62,14 +81,24 @@ "properties": { "filePath": { "description": "The `filePath` is relative to the webpack config: `output.path`.", - "anyOf": [{ "type": "string" }] + "anyOf": [ + { + "type": "string" + } + ] }, "formatter": { "description": "You can pass in a different formatter for the output file, if none is passed in the default/configured formatter will be used.", "anyOf": [ - { "type": "string" }, - { "instanceof": "Function" }, - { "instanceof": "Promise" } + { + "type": "string" + }, + { + "instanceof": "Function" + }, + { + "instanceof": "Promise" + } ] } } @@ -82,7 +111,14 @@ }, "resourceQueryExclude": { "description": "Specify the resource query to exclude.", - "anyOf": [{ "instanceof": "RegExp" }, { "type": "array" }] + "anyOf": [ + { + "instanceof": "RegExp" + }, + { + "type": "array" + } + ] } } } diff --git a/src/utils.js b/src/utils.js index 3526c0f..b6fd00d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -73,6 +73,20 @@ async function importFrom(specifier) { } } +/** @typedef {import("./options.js").Severity} Severity */ +/** @typedef {import("./options.js").SeverityLevel} SeverityLevel */ + +/** + * `emit` and `failOn` each name the least severe result they take in, so + * `"warning"` covers the errors above it and `false` covers nothing. + * @param {SeverityLevel} level the level an option is set to + * @param {Severity} severity the severity to test against it + * @returns {boolean} whether the level covers the severity + */ +function coversSeverity(level, severity) { + return level === "warning" || (level === "error" && severity === "error"); +} + /** * @param {string | string[]} files files * @param {string} context context @@ -188,6 +202,7 @@ function writeOutputFile(compiler, name, content) { export { arrify, + coversSeverity, importFrom, jsonStringifyReplacerSortKeys, omitPluginOptions, diff --git a/test/emit-error.test.js b/test/emit-error.test.js deleted file mode 100644 index 4ab6a77..0000000 --- a/test/emit-error.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("emit error", () => { - it("should not emit errors if emitError is false", async () => { - const compiler = pack("error", { emitError: false }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), false); - }); - - it("should emit errors if emitError is undefined", async () => { - const compiler = pack("error", {}); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should emit errors if emitError is true", async () => { - const compiler = pack("error", { emitError: true }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should emit errors, but not warnings if emitError is true and emitWarning is false", async () => { - const compiler = pack("full-of-problems", { - emitError: true, - emitWarning: false, - }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), false); - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should emit errors and warnings if emitError is true and emitWarning is undefined", async () => { - const compiler = pack("full-of-problems", { emitError: true }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - assert.strictEqual(stats.hasErrors(), true); - }); -}); diff --git a/test/emit-warning.test.js b/test/emit-warning.test.js deleted file mode 100644 index 17b8484..0000000 --- a/test/emit-warning.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("emit warning", () => { - it("should not emit warnings if emitWarning is false", async () => { - const compiler = pack("warn", { emitWarning: false }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), false); - }); - - it("should emit warnings if emitWarning is undefined", async () => { - const compiler = pack("warn", {}); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should emit warnings if emitWarning is true", async () => { - const compiler = pack("warn", { emitWarning: true }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should emit warnings, but not warnings if emitWarning is true and emitError is false", async () => { - const compiler = pack("full-of-problems", { - emitWarning: true, - emitError: false, - }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - assert.strictEqual(stats.hasErrors(), false); - }); - - it("should emit warnings and errors if emitWarning is true and emitError is undefined", async () => { - const compiler = pack("full-of-problems", { emitWarning: true }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - assert.strictEqual(stats.hasErrors(), true); - }); -}); diff --git a/test/emit.test.js b/test/emit.test.js new file mode 100644 index 0000000..f87e065 --- /dev/null +++ b/test/emit.test.js @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import pack from "./utils/pack.js"; + +describe("emit", () => { + it("should report both severities by default", async () => { + const compiler = pack("full-of-problems", {}); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should report both severities when set to the warnings", async () => { + const compiler = pack("full-of-problems", { emit: "warning" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should report the errors alone when set to them", async () => { + const compiler = pack("full-of-problems", { emit: "error" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should report neither severity when set to false", async () => { + const compiler = pack("full-of-problems", { emit: false }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should keep the default when written out as undefined", async () => { + const compiler = pack("full-of-problems", { + emit: undefined, + failOn: undefined, + }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should reject anything but a severity or false", () => { + for (const emit of [true, "info", ["error", "warning"]]) { + assert.throws( + () => pack("full-of-problems", { emit }), + /emit should be one of these:\n *"error" \| "warning" \| false/u, + ); + } + }); +}); diff --git a/test/eslint-options.test.js b/test/eslint-options.test.js index 426c88a..92cfd41 100644 --- a/test/eslint-options.test.js +++ b/test/eslint-options.test.js @@ -20,10 +20,8 @@ describe("eslint options", () => { eslintPath: "some/place/where/eslint/lives", formatter: "table", fix: true, - emitError: false, - emitWarning: false, - failOnError: true, - failOnWarning: true, + emit: false, + failOn: ["error"], quiet: false, outputReport: true, }; diff --git a/test/fail-on-error.test.js b/test/fail-on-error.test.js deleted file mode 100644 index f0c28fb..0000000 --- a/test/fail-on-error.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("fail on error", () => { - it("should emits errors", async () => { - const compiler = pack("error", { failOnError: true }); - - await assert.rejects(compiler.runAsync(), /error/u); - }); - - it("should emit warnings when disabled", async () => { - const compiler = pack("error", { failOnError: false }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should correctly identifies a success", async () => { - const compiler = pack("good", { failOnError: true }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), false); - }); -}); diff --git a/test/fail-on-warning.test.js b/test/fail-on-warning.test.js deleted file mode 100644 index 62690dc..0000000 --- a/test/fail-on-warning.test.js +++ /dev/null @@ -1,19 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("fail on warning", () => { - it("should emits errors", async () => { - const compiler = pack("warn", { failOnWarning: true }); - - await assert.rejects(compiler.runAsync(), /warning/u); - }); - - it("should correctly identifies a success", async () => { - const compiler = pack("good", { failOnWarning: true }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), false); - }); -}); diff --git a/test/fail-on.test.js b/test/fail-on.test.js new file mode 100644 index 0000000..7c3aa1b --- /dev/null +++ b/test/fail-on.test.js @@ -0,0 +1,56 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import pack from "./utils/pack.js"; + +describe("fail on", () => { + it("should fail on an error when set to the errors", async () => { + const compiler = pack("error", { failOn: "error" }); + + await assert.rejects(compiler.runAsync(), /error/u); + }); + + it("should not fail on an error when set to false", async () => { + const compiler = pack("error", { failOn: false }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + }); + + it("should not fail on a warning when set to the errors", async () => { + const compiler = pack("warn", { failOn: "error" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should fail on either severity when set to the warnings", async () => { + await assert.rejects( + pack("warn", { failOn: "warning" }).runAsync(), + /warning/u, + ); + await assert.rejects( + pack("error", { failOn: "warning" }).runAsync(), + /error/u, + ); + }); + + it("should not fail on what emit does not report", async () => { + const compiler = pack("warn", { emit: "error", failOn: "warning" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should let a clean build pass whatever it is set to", async () => { + const compiler = pack("good", { failOn: "warning" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + }); +}); diff --git a/test/multiple-instances.test.js b/test/multiple-instances.test.js index 90e19d2..358dbb2 100644 --- a/test/multiple-instances.test.js +++ b/test/multiple-instances.test.js @@ -14,7 +14,7 @@ describe("multiple instances", () => { { plugins: [ new DiagnosticsPlugin({ - failOnError: true, + failOn: "error", exclude: "error.js", checks: [ { @@ -28,7 +28,7 @@ describe("multiple instances", () => { ], }), new DiagnosticsPlugin({ - failOnError: true, + failOn: "error", exclude: "error.js", checks: [ { @@ -57,7 +57,7 @@ describe("multiple instances", () => { { plugins: [ new DiagnosticsPlugin({ - failOnError: true, + failOn: "error", exclude: "good.js", checks: [ { @@ -71,7 +71,7 @@ describe("multiple instances", () => { ], }), new DiagnosticsPlugin({ - failOnError: true, + failOn: "error", exclude: "error.js", checks: [ { @@ -98,7 +98,7 @@ describe("multiple instances", () => { { plugins: [ new DiagnosticsPlugin({ - failOnError: true, + failOn: "error", exclude: "error.js", checks: [ { @@ -112,7 +112,7 @@ describe("multiple instances", () => { ], }), new DiagnosticsPlugin({ - failOnError: true, + failOn: "error", exclude: "good.js", checks: [ { diff --git a/test/stylelint/emit-error.test.js b/test/stylelint/emit-error.test.js deleted file mode 100644 index f13e2db..0000000 --- a/test/stylelint/emit-error.test.js +++ /dev/null @@ -1,42 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("emit error", () => { - it("should not emit errors if emitError is false", async () => { - const compiler = pack("error", { emitError: false }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), false); - }); - - it("should emit errors if emitError is undefined", async () => { - const compiler = pack("error"); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should emit errors if emitError is true", async () => { - const compiler = pack("error", { emitError: true }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should emit errors, but not warnings if emitError is true and emitWarning is false", async () => { - const compiler = pack("full-of-problems", { - emitError: true, - emitWarning: false, - }); - - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), false); - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should emit errors and warnings if emitError is true and emitWarning is undefined", async () => { - const compiler = pack("full-of-problems", { emitError: true }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - assert.strictEqual(stats.hasErrors(), true); - }); -}); diff --git a/test/stylelint/emit-warning.test.js b/test/stylelint/emit-warning.test.js deleted file mode 100644 index a526c3a..0000000 --- a/test/stylelint/emit-warning.test.js +++ /dev/null @@ -1,41 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("emit warning", () => { - it("should not emit warnings if emitWarning is false", async () => { - const compiler = pack("warning", { emitWarning: false }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), false); - }); - - it("should emit warnings if emitWarning is undefined", async () => { - const compiler = pack("warning"); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should emit warnings if emitWarning is true", async () => { - const compiler = pack("warning", { emitWarning: true }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should emit warnings, but not warnings if emitWarning is true and emitError is false", async () => { - const compiler = pack("full-of-problems", { - emitWarning: true, - emitError: false, - }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - assert.strictEqual(stats.hasErrors(), false); - }); - - it("should emit warnings and errors if emitWarning is true and emitError is undefined", async () => { - const compiler = pack("full-of-problems", { emitWarning: true }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasWarnings(), true); - assert.strictEqual(stats.hasErrors(), true); - }); -}); diff --git a/test/stylelint/emit.test.js b/test/stylelint/emit.test.js new file mode 100644 index 0000000..a2e2a9c --- /dev/null +++ b/test/stylelint/emit.test.js @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import pack from "./utils/pack.js"; + +describe("emit", () => { + it("should report both severities by default", async () => { + const compiler = pack("full-of-problems", {}); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should report both severities when set to the warnings", async () => { + const compiler = pack("full-of-problems", { emit: "warning" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should report the errors alone when set to them", async () => { + const compiler = pack("full-of-problems", { emit: "error" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should report neither severity when set to false", async () => { + const compiler = pack("full-of-problems", { emit: false }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + }); +}); diff --git a/test/stylelint/fail-on-error.test.js b/test/stylelint/fail-on-error.test.js deleted file mode 100644 index 66f661d..0000000 --- a/test/stylelint/fail-on-error.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("fail on error", () => { - it("should fail the build", async () => { - const compiler = pack("error", { failOnError: true }); - - await assert.rejects(compiler.runAsync(), /color-named/u); - }); - - it("should report errors without failing when disabled", async () => { - const compiler = pack("error", { failOnError: false }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should correctly identify a success", async () => { - const compiler = pack("good", { failOnError: true }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), false); - }); -}); diff --git a/test/stylelint/fail-on-warning.test.js b/test/stylelint/fail-on-warning.test.js deleted file mode 100644 index 90490b5..0000000 --- a/test/stylelint/fail-on-warning.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("fail on warning", () => { - it("should fail the build", async () => { - const compiler = pack("warning", { failOnWarning: true }); - - await assert.rejects(compiler.runAsync(), /color-hex-length/u); - }); - - it("should correctly identify a success", async () => { - const compiler = pack("good", { failOnWarning: true }); - const stats = await compiler.runAsync(); - assert.strictEqual(stats.hasErrors(), false); - }); -}); diff --git a/test/stylelint/fail-on.test.js b/test/stylelint/fail-on.test.js new file mode 100644 index 0000000..9f782b1 --- /dev/null +++ b/test/stylelint/fail-on.test.js @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import pack from "./utils/pack.js"; + +describe("fail on", () => { + it("should fail on an error when set to the errors", async () => { + const compiler = pack("error", { failOn: "error" }); + + await assert.rejects(compiler.runAsync(), /error/u); + }); + + it("should not fail on an error when set to false", async () => { + const compiler = pack("error", { failOn: false }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + }); + + it("should not fail on a warning when set to the errors", async () => { + const compiler = pack("warning", { failOn: "error" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should fail on either severity when set to the warnings", async () => { + await assert.rejects( + pack("warning", { failOn: "warning" }).runAsync(), + /warning/u, + ); + await assert.rejects( + pack("error", { failOn: "warning" }).runAsync(), + /error/u, + ); + }); + + it("should let a clean build pass whatever it is set to", async () => { + const compiler = pack("good", { failOn: "warning" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + }); +}); diff --git a/test/stylelint/stylelint-options.test.js b/test/stylelint/stylelint-options.test.js index 4957538..f78db79 100644 --- a/test/stylelint/stylelint-options.test.js +++ b/test/stylelint/stylelint-options.test.js @@ -7,7 +7,7 @@ describe("eslint options", () => { it("should filter plugin options", () => { const options = { formatter: "json", - emitError: false, + emit: false, }; assert.deepStrictEqual(getStylelintOptions(options), { formatter: "json", @@ -19,10 +19,8 @@ describe("eslint options", () => { stylelintPath: "some/place/where/stylelint/lives", formatter: "json", files: ["file.scss"], - emitError: false, - emitWarning: false, - failOnError: true, - failOnWarning: true, + emit: false, + failOn: ["error"], quiet: false, outputReport: true, }; diff --git a/test/unified/unified.test.js b/test/unified/unified.test.js index ecded34..283c07a 100644 --- a/test/unified/unified.test.js +++ b/test/unified/unified.test.js @@ -116,8 +116,8 @@ describe("unified plugin", () => { it("should let a check override a shared option", async () => { const compiler = pack("both", { - emitError: false, - checks: [eslint, { ...stylelint, emitError: true }], + emit: false, + checks: [eslint, { ...stylelint, emit: "error" }], }); const stats = await compiler.runAsync(); const [error] = stats.compilation.errors; @@ -167,8 +167,8 @@ describe("unified plugin", () => { ); }); - it("should fail the build when a shared failOnError is set", async () => { - const compiler = pack("both", { failOnError: true, checks }); + it("should fail the build when a shared failOn is set", async () => { + const compiler = pack("both", { failOn: "error", checks }); await assert.rejects(compiler.runAsync(), /bad\.js/u); }); diff --git a/types/index.d.ts b/types/index.d.ts index f211917..c1d2edf 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,6 +7,7 @@ export type Runner = import("./check.js").Runner; export type CheckAdapter = import("./checks/index.js").CheckAdapter; export type EnabledCheck = import("./options.js").EnabledCheck; export type CheckOptions = import("./options.js").CheckOptions; +export type ResolvedCheckOptions = import("./options.js").ResolvedCheckOptions; export type Options = import("./options.js").Options; export type ResolvedCheck = { /** @@ -20,7 +21,7 @@ export type ResolvedCheck = { /** * options resolved for this check */ - options: CheckOptions; + options: ResolvedCheckOptions; /** * the globs of the files to lint */ diff --git a/types/options.d.ts b/types/options.d.ts index c5614ce..d75f505 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -1,5 +1,7 @@ export type EXPECTED_ANY = any; export type Compiler = import("webpack").Compiler; +export type Severity = "error" | "warning"; +export type SeverityLevel = Severity | false; export type FormatterOption = import("./checks/index.js").FormatterOption; export type CheckAdapter = import("./checks/index.js").CheckAdapter; export type CheckAdapterInput = import("./checks/index.js").CheckAdapterInput; @@ -23,13 +25,9 @@ export type SharedOptions = { */ cacheLocation?: string | undefined; /** - * the errors found will always be emitted + * the least severe result that is reported */ - emitError?: boolean | undefined; - /** - * the warnings found will always be emitted - */ - emitWarning?: boolean | undefined; + emit?: SeverityLevel | undefined; /** * specify the files and/or directories to exclude */ @@ -39,13 +37,9 @@ export type SharedOptions = { */ extensions?: (string | string[]) | undefined; /** - * will cause the module build to fail if there are any errors - */ - failOnError?: boolean | undefined; - /** - * will cause the module build to fail if there are any warnings + * the least severe result that fails the build */ - failOnWarning?: boolean | undefined; + failOn?: SeverityLevel | undefined; /** * specify directories, files, or globs */ @@ -75,7 +69,16 @@ export type CheckEntry = SharedOptions & { use: string | CheckAdapterInput; [option: string]: EXPECTED_ANY; }; -export type CheckOptions = SharedOptions & { +export type CheckOptions = Omit & { + emit: SeverityLevel; + [option: string]: EXPECTED_ANY; +}; +/** + * What a check reads once the plugin has resolved it against a compiler. + */ +export type ResolvedCheckOptions = Omit & { + emit: SeverityLevel; + failOn: SeverityLevel; [option: string]: EXPECTED_ANY; }; export type PluginOptions = { diff --git a/types/utils.d.ts b/types/utils.d.ts index 3cbddf1..cc72962 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -8,6 +8,8 @@ export type ArrifyResult = T extends null | undefined : T extends Iterable ? T_1[] : [T]; +export type Severity = import("./options.js").Severity; +export type SeverityLevel = import("./options.js").SeverityLevel; export type EXPECTED_ANY = any; /** @typedef {import("webpack").Compiler} Compiler */ /** @@ -28,6 +30,19 @@ export type EXPECTED_ANY = any; * @returns {ArrifyResult} array of values */ export function arrify(value: T): ArrifyResult; +/** @typedef {import("./options.js").Severity} Severity */ +/** @typedef {import("./options.js").SeverityLevel} SeverityLevel */ +/** + * `emit` and `failOn` each name the least severe result they take in, so + * `"warning"` covers the errors above it and `false` covers nothing. + * @param {SeverityLevel} level the level an option is set to + * @param {Severity} severity the severity to test against it + * @returns {boolean} whether the level covers the severity + */ +export function coversSeverity( + level: SeverityLevel, + severity: Severity, +): boolean; /** * A package name is imported as it is, so a test can still mock it. A path may * name a directory, which `import.meta.resolve` returns unchanged for From 4815bcff330e2d07b5380aa6822df2673669c87f Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:05:02 +0000 Subject: [PATCH 2/2] feat!: union emit and failOn into one reportAs option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `emitError`, `emitWarning`, `failOnError` and `failOnWarning` become one option taking `"error"`, `"warning"` or `false`. It says what a check reports its results as, and reporting one as a webpack error is what fails the build, so there is nothing left for a second option to say. reportAs: undefined // each result at its own severity: errors fail, warnings do not reportAs: "error" // everything fails the build, warnings included reportAs: "warning" // nothing fails the build reportAs: false // nothing is reported The name answers the question the option asks. `emit` was inherited from `emitError` and `emitWarning`, where the answer was whether to report at all; this one answers what to report a result as, and a name that says "whether" invites reading `"error"` as "errors only". Left unset the default is unchanged, so a check no longer reports into errors and warnings by anyone's instruction — it does so because that is what its results are, and one value overrides it. `quiet` keeps dropping the warnings before that, which is how the errors are reported alone. The plugin no longer aborts the compilation. A result reported as a webpack error fails the build the way every other webpack error does, with the assets still written; webpack's own `bail` does not reach an error pushed this late, so the abort was the plugin's alone and is gone with `failOn`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GzZci4NQeiqwdrVfd7dGXy --- .../merge-eslint-and-stylelint-plugins.md | 2 +- .changeset/union-emit-and-fail-options.md | 5 - .changeset/union-report-options.md | 5 + README.md | 61 ++++++------ src/check.js | 16 +-- src/checks/eslint.js | 16 +-- src/checks/index.js | 2 +- src/checks/stylelint.js | 9 +- src/index.js | 37 ++++--- src/options.js | 26 ++--- src/shared-options.json | 12 +-- src/utils.js | 15 --- test/emit.test.js | 63 ------------ test/eslint-options.test.js | 3 +- test/fail-on.test.js | 56 ----------- test/multiple-instances.test.js | 16 +-- test/report-as.test.js | 97 +++++++++++++++++++ test/stylelint/emit.test.js | 42 -------- test/stylelint/fail-on.test.js | 48 --------- test/stylelint/report-as.test.js | 51 ++++++++++ test/stylelint/stylelint-options.test.js | 5 +- test/unified/unified.test.js | 39 +++++++- types/checks/index.d.ts | 4 +- types/index.d.ts | 3 +- types/options.d.ts | 22 ++--- types/utils.d.ts | 15 --- 26 files changed, 286 insertions(+), 384 deletions(-) delete mode 100644 .changeset/union-emit-and-fail-options.md create mode 100644 .changeset/union-report-options.md delete mode 100644 test/emit.test.js delete mode 100644 test/fail-on.test.js create mode 100644 test/report-as.test.js delete mode 100644 test/stylelint/emit.test.js delete mode 100644 test/stylelint/fail-on.test.js create mode 100644 test/stylelint/report-as.test.js diff --git a/.changeset/merge-eslint-and-stylelint-plugins.md b/.changeset/merge-eslint-and-stylelint-plugins.md index 277dd88..c20d08c 100644 --- a/.changeset/merge-eslint-and-stylelint-plugins.md +++ b/.changeset/merge-eslint-and-stylelint-plugins.md @@ -2,4 +2,4 @@ "diagnostics-webpack-plugin": major --- -Renamed from `eslint-webpack-plugin` and merged with `stylelint-webpack-plugin`: one plugin runs every linter through the `checks` option, `new DiagnosticsPlugin({ checks: [{ use: "eslint" }, { use: "stylelint" }] })`. Errors are reported as webpack errors and warnings as webpack warnings, with `failOnError` and `failOnWarning` deciding whether the build fails; `stylelint` must be 17 or later. See the migration guides in the README. +Renamed from `eslint-webpack-plugin` and merged with `stylelint-webpack-plugin`: one plugin runs every linter through the `checks` option, `new DiagnosticsPlugin({ checks: [{ use: "eslint" }, { use: "stylelint" }] })`. Errors are reported as webpack errors and warnings as webpack warnings, with `reportAs` deciding what each is reported as; `stylelint` must be 17 or later. See the migration guides in the README. diff --git a/.changeset/union-emit-and-fail-options.md b/.changeset/union-emit-and-fail-options.md deleted file mode 100644 index dacab37..0000000 --- a/.changeset/union-emit-and-fail-options.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"diagnostics-webpack-plugin": major ---- - -`emitError` and `emitWarning` are one `emit` option, and `failOnError` and `failOnWarning` one `failOn`. Each takes `"warning"`, `"error"` or `false`, naming the least severe result it takes in, so `"warning"` covers the errors above it. `emit` defaults to `"warning"` and `failOn` to `"error"`, or `false` in `development` mode. See the migration table in the README. diff --git a/.changeset/union-report-options.md b/.changeset/union-report-options.md new file mode 100644 index 0000000..d6e302a --- /dev/null +++ b/.changeset/union-report-options.md @@ -0,0 +1,5 @@ +--- +"diagnostics-webpack-plugin": major +--- + +`emitError`, `emitWarning`, `failOnError` and `failOnWarning` are one `reportAs` option taking `"error"`, `"warning"` or `false`: it says what a check reports its results as, and reporting one as a webpack error is what fails the build. Left unset each result keeps its own severity, `quiet` still drops the warnings, and the build is no longer aborted from inside the plugin. See the migration table in the README. diff --git a/README.md b/README.md index 669cf0f..2d51301 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ new DiagnosticsPlugin({ // Plugin options context: "src", // Shared options, every check uses them unless it says otherwise - failOn: "error", + 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: [ @@ -235,32 +235,37 @@ See the [ESLint formatters](https://eslint.org/docs/user-guide/formatters/) and ### Errors and warnings -Every check reports its errors as webpack errors and its warnings as webpack warnings. `emit` chooses how much of that is reported at all, and `failOn` how much of it fails the build. Each names the least severe result it takes in, so `"warning"` covers the errors above it and `false` covers nothing. +Every check reports its errors as webpack errors and its warnings as webpack warnings, which is what fails the build. `reportAs` overrides that, and `quiet` drops the warnings. -#### `emit` +#### `reportAs` - Type: ```ts -type emit = "error" | "warning" | false; +type reportAs = "error" | "warning" | false; ``` -- Default: `"warning"` +- Default: unset — each result stays at the severity the check gave it -The least severe result that is reported: `"warning"` reports warnings and errors, `"error"` reports errors alone, and `false` reports nothing. +What a check reports its results as. Left unset, an error is a webpack error and a warning a webpack warning; naming one severity reports every result as that one, and `false` reports nothing. -#### `failOn` +| 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. | +| `false` | Nothing is reported. An `outputReport` is still written. | -- Type: +Together with [`quiet`](#quiet), which drops the warnings before any of this, that covers reporting and failing in one option: -```ts -type failOn = "error" | "warning" | false; +```js +new DiagnosticsPlugin({ + reportAs: "warning", // report everything without failing the build + quiet: true, // and leave the warnings out of it + checks: [{ use: "eslint" }], +}); ``` -- Default: `"error"`, `false` in `development` mode - -The least severe result that fails the build: `"warning"` fails on warnings and errors, `"error"` fails on errors alone, and `false` fails on nothing. A result `emit` does not report cannot fail the build either. - #### `quiet` - Type: @@ -271,7 +276,7 @@ type quiet = boolean; - Default: `false` -Will process and report errors only and ignore warnings, if set to `true`. The same as [`emit`](#emit) set to `"error"`. +Will process and report errors only and ignore warnings, if set to `true`. It drops the warnings before [`reportAs`](#reportas) decides what the rest is reported as. #### `outputReport` @@ -416,7 +421,7 @@ new DiagnosticsPlugin({ }); ``` -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: +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`](#reportas) and [`quiet`](#quiet) are applied to what it returns: ```js module.exports = { @@ -459,18 +464,19 @@ Move the options you were passing into a `checks` entry: }; ``` -`emitError` and `emitWarning` are one [`emit`](#emit) option now, and `failOnError` and `failOnWarning` one [`failOn`](#failon): +`emitError`, `emitWarning`, `failOnError` and `failOnWarning` are one [`reportAs`](#reportas) option now, because reporting a result as a webpack error is what fails the build — there is nothing left for a second option to say: -| Was | Is | -| :------------------------------------------ | :------------------ | -| `emitWarning: false` | `emit: "error"` | -| `emitError: false` and `emitWarning: false` | `emit: false` | -| `failOnError: true` | `failOn: "error"` | -| `failOnWarning: true` | `failOn: "warning"` | +| Was | Is | +| :------------------------------------------ | :-------------------- | +| `emitWarning: false` | `quiet: true` | +| `emitError: false` and `emitWarning: false` | `reportAs: false` | +| `failOnError: true` | the default | +| `failOnError: false` | `reportAs: "warning"` | +| `failOnWarning: true` | `reportAs: "error"` | -`emitError: false` on its own has no counterpart: reporting the warnings of a check while hiding its errors was never useful. +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, and the assets are still written. `emitError: false` on its own has no counterpart — reporting the warnings of a check while hiding its errors was never useful. -The shared options — `context`, `files`, `exclude`, `failOn` and the rest of [Errors and warnings](#errors-and-warnings) — may stay at the top level instead. Everything else behaves as it did, and the default `cacheLocation` moved to `node_modules/.cache/diagnostics-webpack-plugin/.eslintcache`. +The shared options — `context`, `files`, `exclude`, `reportAs` and the rest of [Errors and warnings](#errors-and-warnings) — may stay at the top level instead. Everything else behaves as it did, and the default `cacheLocation` moved to `node_modules/.cache/diagnostics-webpack-plugin/.eslintcache`. ### From `stylelint-webpack-plugin` @@ -493,8 +499,8 @@ Move the options you were passing into a `checks` entry: Three things changed beyond the option shape: - **Stylelint 17 or later is required.** `stylelint-webpack-plugin` accepted `13` through `17`; 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.** Errors are reported as webpack errors and warnings as webpack warnings, whatever [`failOn`](#failon) says; it decides whether the build is failed, not how a problem is reported. Previously `failOnError: false` turned errors into warnings, and `failOnWarning: true` turned warnings into errors. -- **Failing the build is off in `development` mode**, matching the rest of the plugin, rather than `failOnError` being `true` everywhere. +- **Errors and warnings are no longer swapped.** Errors are reported as webpack errors and warnings as webpack warnings, unless [`reportAs`](#reportas) names one severity for all of them. Previously `failOnError: false` turned errors into warnings, and `failOnWarning: true` turned warnings into errors. +- **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, and the assets are still written. The default `cacheLocation` moved to `node_modules/.cache/diagnostics-webpack-plugin/.stylelintcache`. @@ -509,7 +515,6 @@ The two plugins become one instance, and options they had in common are written - new StylelintPlugin({ context: "src", failOnError: true, extensions: ["css"] }), + new DiagnosticsPlugin({ + context: "src", -+ failOn: "error", + checks: [ + { use: "eslint", extensions: ["js"] }, + { use: "stylelint", extensions: ["css"] }, diff --git a/src/check.js b/src/check.js index e07a257..09b0fac 100644 --- a/src/check.js +++ b/src/check.js @@ -82,12 +82,16 @@ function createCheckRunner(key, { name, adapter, options }, compilation) { /** @type {Report} */ const report = {}; - if (warnings.length > 0) { - report.warnings = new DiagnosticError(name, await format(warnings)); - } - - if (errors.length > 0) { - report.errors = new DiagnosticError(name, await format(errors)); + // `quiet` drops the warnings and `reportAs: false` everything, but an + // `outputReport` is still written from all of the results below. + if (options.reportAs !== false) { + if (warnings.length > 0 && !options.quiet) { + report.warnings = new DiagnosticError(name, await format(warnings)); + } + + if (errors.length > 0) { + report.errors = new DiagnosticError(name, await format(errors)); + } } const { outputReport } = options; diff --git a/src/checks/eslint.js b/src/checks/eslint.js index 1248ab1..675a8d4 100644 --- a/src/checks/eslint.js +++ b/src/checks/eslint.js @@ -5,7 +5,7 @@ import { createRequire } from "node:module"; import { isAbsolute, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; -import { coversSeverity, importFrom, omitPluginOptions } from "../utils.js"; +import { importFrom, omitPluginOptions } from "../utils.js"; const nodeRequire = createRequire(import.meta.url); @@ -223,24 +223,18 @@ async function create({ options }) { for (const file of /** @type {LintResult[]} */ (results)) { if (file.errorCount > 0) { const messages = file.messages.filter( - (message) => - coversSeverity(options.emit, "error") && message.severity === 2, + (message) => message.severity === 2, ); - if (messages.length > 0) { - errors.push({ ...file, messages }); - } + if (messages.length > 0) errors.push({ ...file, messages }); } if (file.warningCount > 0) { const messages = file.messages.filter( - (message) => - coversSeverity(options.emit, "warning") && message.severity === 1, + (message) => message.severity === 1, ); - if (messages.length > 0) { - warnings.push({ ...file, messages }); - } + if (messages.length > 0) warnings.push({ ...file, messages }); } } diff --git a/src/checks/index.js b/src/checks/index.js index b5a5817..4e567d3 100644 --- a/src/checks/index.js +++ b/src/checks/index.js @@ -30,7 +30,7 @@ import stylelint from "./stylelint.js"; * @typedef {object} CheckInstance * @property {(files: string[]) => Promise} lintFiles lints the given files * @property {(results: CheckResult[]) => Promise} getResults turns the raw results of every `lintFiles` call into the results to report - * @property {(results: CheckResult[]) => { errors: CheckResult[], warnings: CheckResult[] }} splitResults splits results into the ones reported as errors and as warnings + * @property {(results: CheckResult[]) => { errors: CheckResult[], warnings: CheckResult[] }} splitResults splits the results by their own severity, leaving `reportAs` and `quiet` to the plugin * @property {(formatter?: FormatterOption) => Promise} getFormatter loads a formatter, falling back to the tool's default one * @property {() => Promise} cleanup releases whatever the tool holds after a run */ diff --git a/src/checks/stylelint.js b/src/checks/stylelint.js index 9b2d616..7794b05 100644 --- a/src/checks/stylelint.js +++ b/src/checks/stylelint.js @@ -8,7 +8,6 @@ import { fileURLToPath } from "node:url"; import { Worker as JestWorker } from "jest-worker"; import { - coversSeverity, jsonStringifyReplacerSortKeys, omitPluginOptions, parseFiles, @@ -274,9 +273,7 @@ async function create({ key, options, compilation }) { for (const file of /** @type {LintResult[]} */ (results)) { const fileErrors = file.warnings.filter( - (message) => - coversSeverity(options.emit, "error") && - message.severity === "error", + (message) => message.severity === "error", ); if (fileErrors.length > 0) { @@ -284,9 +281,7 @@ async function create({ key, options, compilation }) { } const fileWarnings = file.warnings.filter( - (message) => - coversSeverity(options.emit, "warning") && - message.severity === "warning", + (message) => message.severity === "warning", ); if (fileWarnings.length > 0) { diff --git a/src/index.js b/src/index.js index f472b20..c9d6029 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,6 @@ import createCheckRunner from "./check.js"; import { getOptions, validateOptions } from "./options.js"; import { arrify, - coversSeverity, parseFiles, parseFoldersToGlobs, writeOutputFile, @@ -24,14 +23,13 @@ const { isMatch } = micromatch; /** @typedef {import("./checks/index.js").CheckAdapter} CheckAdapter */ /** @typedef {import("./options.js").EnabledCheck} EnabledCheck */ /** @typedef {import("./options.js").CheckOptions} CheckOptions */ -/** @typedef {import("./options.js").ResolvedCheckOptions} ResolvedCheckOptions */ /** @typedef {import("./options.js").Options} Options */ /** * @typedef {object} ResolvedCheck * @property {string} name check name * @property {CheckAdapter} adapter the adapter running it - * @property {ResolvedCheckOptions} options options resolved for this check + * @property {CheckOptions} options options resolved for this check * @property {string[]} wanted the globs of the files to lint * @property {string[]} exclude the globs of the files not to lint */ @@ -136,13 +134,10 @@ class DiagnosticsWebpackPlugin { resolveCheck(compiler, context, { name, adapter, options }) { const resourceQueries = arrify(options.resourceQueryExclude || []); - /** @type {ResolvedCheckOptions} */ + /** @type {CheckOptions} */ const resolved = { ...options, context, - failOn: - options.failOn ?? - (compiler.options.mode === "development" ? false : "error"), exclude: options.exclude ? parseFiles(options.exclude, context) : adapter.defaultExclude(compiler), @@ -252,18 +247,28 @@ class DiagnosticsWebpackPlugin { async (_, callback) => { /** @type {Map} */ const outputReports = new Map(); - /** @type {Error | undefined} */ - let failure; for (const { options, runner } of runners) { const { errors, warnings, outputReport } = await runner.report(); + // `reportAs` names the one place every result goes; left unset, each + // stays at the severity the check gave it. if (warnings) { - compilation.warnings.push(warnings); + const reported = + options.reportAs === "error" + ? compilation.errors + : compilation.warnings; + + reported.push(warnings); } if (errors) { - compilation.errors.push(errors); + const reported = + options.reportAs === "warning" + ? compilation.warnings + : compilation.errors; + + reported.push(errors); } if (outputReport) { @@ -272,14 +277,6 @@ class DiagnosticsWebpackPlugin { contents.push(outputReport.content); outputReports.set(outputReport.filePath, contents); } - - if (!failure) { - if (warnings && coversSeverity(options.failOn, "warning")) { - failure = warnings; - } else if (errors && coversSeverity(options.failOn, "error")) { - failure = errors; - } - } } await Promise.all( @@ -288,7 +285,7 @@ class DiagnosticsWebpackPlugin { ), ); - callback(failure); + callback(); }, ); }); diff --git a/src/options.js b/src/options.js index 0a0a859..0ce4c11 100644 --- a/src/options.js +++ b/src/options.js @@ -13,8 +13,7 @@ const nodeRequire = createRequire(import.meta.url); const PLUGIN_NAME = "Diagnostics Webpack Plugin"; /** @typedef {import("webpack").Compiler} Compiler */ -/** @typedef {"error" | "warning"} Severity */ -/** @typedef {Severity | false} SeverityLevel */ +/** @typedef {"error" | "warning" | false} ReportAs */ /** @typedef {import("./checks/index.js").FormatterOption} FormatterOption */ /** @typedef {import("./checks/index.js").CheckAdapter} CheckAdapter */ /** @typedef {import("./checks/index.js").CheckAdapterInput} CheckAdapterInput */ @@ -29,10 +28,9 @@ const PLUGIN_NAME = "Diagnostics Webpack Plugin"; * @typedef {object} SharedOptions * @property {boolean=} cache enable the tool's cache to decrease execution time * @property {string=} cacheLocation specify the path to the cache location - * @property {SeverityLevel=} emit the least severe result that is reported + * @property {ReportAs=} reportAs what a check reports its results as * @property {string | string[]=} exclude specify the files and/or directories to exclude * @property {string | string[]=} extensions specify the extensions that should be checked - * @property {SeverityLevel=} failOn the least severe result that fails the build * @property {string | string[]=} files specify directories, files, or globs * @property {boolean=} fix apply fixes * @property {FormatterOption=} formatter specify the formatter you would like to use to format your results @@ -46,12 +44,9 @@ const PLUGIN_NAME = "Diagnostics Webpack Plugin"; */ /** - * @typedef {Omit & { emit: SeverityLevel, [option: string]: EXPECTED_ANY }} CheckOptions - */ - -/** - * What a check reads once the plugin has resolved it against a compiler. - * @typedef {Omit & { emit: SeverityLevel, failOn: SeverityLevel, [option: string]: EXPECTED_ANY }} ResolvedCheckOptions + * The options of one check, as given and then as the plugin resolves them + * against a compiler. + * @typedef {SharedOptions & { [option: string]: EXPECTED_ANY }} CheckOptions */ /** @@ -77,8 +72,6 @@ const PLUGIN_NAME = "Diagnostics Webpack Plugin"; * @property {EnabledCheck[]} checks the checks to run */ -const DEFAULT_EMIT = /** @type {SeverityLevel} */ ("warning"); - const DEFAULT_FOLDER_TO_EXCLUDE = "**/node_modules/**"; /** @type {{ schema: EXPECTED_ANY, entrySchema: EXPECTED_ANY } | undefined} */ @@ -177,15 +170,8 @@ function getOptions(pluginOptions) { const { use, ...own } = entry; const adapter = toAdapter(use); - const merged = { ...adapter.defaults, ...shared, ...own }; - - // `??`, not a default merged under them, so that an option written out as - // `undefined` reads as the one left unwritten. /** @type {CheckOptions} */ - const options = { - ...merged, - emit: merged.quiet ? "error" : (merged.emit ?? DEFAULT_EMIT), - }; + const options = { ...adapter.defaults, ...shared, ...own }; return { name: adapter.name, adapter, options }; }); diff --git a/src/shared-options.json b/src/shared-options.json index 04fbd29..7f01d45 100644 --- a/src/shared-options.json +++ b/src/shared-options.json @@ -10,10 +10,6 @@ "description": "Specify the path to the cache location, it can be a file or a directory.", "type": "string" }, - "emit": { - "description": "The least severe result that is reported: `\"warning\"` reports warnings and errors, `\"error\"` reports errors alone, `false` reports nothing.", - "enum": ["error", "warning", false] - }, "exclude": { "description": "Specify the files and/or directories to exclude. Must be relative to `options.context`.", "anyOf": [ @@ -36,10 +32,6 @@ } ] }, - "failOn": { - "description": "The least severe result that fails the build: `\"warning\"` fails on warnings and errors, `\"error\"` fails on errors alone, `false` fails on nothing.", - "enum": ["error", "warning", false] - }, "files": { "description": "Specify the files and/or directories to traverse. Must be relative to `options.context`.", "anyOf": [ @@ -109,6 +101,10 @@ "description": "Will process and report errors only and ignore warnings, if set to `true`.", "type": "boolean" }, + "reportAs": { + "description": "What a check reports its results as: `\"error\"` reports them as webpack errors and fails the build, `\"warning\"` reports them as webpack warnings, `false` reports nothing.", + "enum": ["error", "warning", false] + }, "resourceQueryExclude": { "description": "Specify the resource query to exclude.", "anyOf": [ diff --git a/src/utils.js b/src/utils.js index b6fd00d..3526c0f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -73,20 +73,6 @@ async function importFrom(specifier) { } } -/** @typedef {import("./options.js").Severity} Severity */ -/** @typedef {import("./options.js").SeverityLevel} SeverityLevel */ - -/** - * `emit` and `failOn` each name the least severe result they take in, so - * `"warning"` covers the errors above it and `false` covers nothing. - * @param {SeverityLevel} level the level an option is set to - * @param {Severity} severity the severity to test against it - * @returns {boolean} whether the level covers the severity - */ -function coversSeverity(level, severity) { - return level === "warning" || (level === "error" && severity === "error"); -} - /** * @param {string | string[]} files files * @param {string} context context @@ -202,7 +188,6 @@ function writeOutputFile(compiler, name, content) { export { arrify, - coversSeverity, importFrom, jsonStringifyReplacerSortKeys, omitPluginOptions, diff --git a/test/emit.test.js b/test/emit.test.js deleted file mode 100644 index f87e065..0000000 --- a/test/emit.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("emit", () => { - it("should report both severities by default", async () => { - const compiler = pack("full-of-problems", {}); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should report both severities when set to the warnings", async () => { - const compiler = pack("full-of-problems", { emit: "warning" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should report the errors alone when set to them", async () => { - const compiler = pack("full-of-problems", { emit: "error" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - assert.strictEqual(stats.hasWarnings(), false); - }); - - it("should report neither severity when set to false", async () => { - const compiler = pack("full-of-problems", { emit: false }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), false); - assert.strictEqual(stats.hasWarnings(), false); - }); - - it("should keep the default when written out as undefined", async () => { - const compiler = pack("full-of-problems", { - emit: undefined, - failOn: undefined, - }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should reject anything but a severity or false", () => { - for (const emit of [true, "info", ["error", "warning"]]) { - assert.throws( - () => pack("full-of-problems", { emit }), - /emit should be one of these:\n *"error" \| "warning" \| false/u, - ); - } - }); -}); diff --git a/test/eslint-options.test.js b/test/eslint-options.test.js index 92cfd41..c52edb6 100644 --- a/test/eslint-options.test.js +++ b/test/eslint-options.test.js @@ -20,8 +20,7 @@ describe("eslint options", () => { eslintPath: "some/place/where/eslint/lives", formatter: "table", fix: true, - emit: false, - failOn: ["error"], + reportAs: false, quiet: false, outputReport: true, }; diff --git a/test/fail-on.test.js b/test/fail-on.test.js deleted file mode 100644 index 7c3aa1b..0000000 --- a/test/fail-on.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("fail on", () => { - it("should fail on an error when set to the errors", async () => { - const compiler = pack("error", { failOn: "error" }); - - await assert.rejects(compiler.runAsync(), /error/u); - }); - - it("should not fail on an error when set to false", async () => { - const compiler = pack("error", { failOn: false }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should not fail on a warning when set to the errors", async () => { - const compiler = pack("warn", { failOn: "error" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should fail on either severity when set to the warnings", async () => { - await assert.rejects( - pack("warn", { failOn: "warning" }).runAsync(), - /warning/u, - ); - await assert.rejects( - pack("error", { failOn: "warning" }).runAsync(), - /error/u, - ); - }); - - it("should not fail on what emit does not report", async () => { - const compiler = pack("warn", { emit: "error", failOn: "warning" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasWarnings(), false); - }); - - it("should let a clean build pass whatever it is set to", async () => { - const compiler = pack("good", { failOn: "warning" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), false); - assert.strictEqual(stats.hasWarnings(), false); - }); -}); diff --git a/test/multiple-instances.test.js b/test/multiple-instances.test.js index 358dbb2..b98d6ab 100644 --- a/test/multiple-instances.test.js +++ b/test/multiple-instances.test.js @@ -14,7 +14,6 @@ describe("multiple instances", () => { { plugins: [ new DiagnosticsPlugin({ - failOn: "error", exclude: "error.js", checks: [ { @@ -28,7 +27,6 @@ describe("multiple instances", () => { ], }), new DiagnosticsPlugin({ - failOn: "error", exclude: "error.js", checks: [ { @@ -57,7 +55,6 @@ describe("multiple instances", () => { { plugins: [ new DiagnosticsPlugin({ - failOn: "error", exclude: "good.js", checks: [ { @@ -71,7 +68,6 @@ describe("multiple instances", () => { ], }), new DiagnosticsPlugin({ - failOn: "error", exclude: "error.js", checks: [ { @@ -88,7 +84,10 @@ describe("multiple instances", () => { }, ); - await assert.rejects(compiler.runAsync(), /error\.js/u); + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.match(stats.compilation.errors[0].message, /error\.js/u); }); it("should fail on second instance", async () => { @@ -98,7 +97,6 @@ describe("multiple instances", () => { { plugins: [ new DiagnosticsPlugin({ - failOn: "error", exclude: "error.js", checks: [ { @@ -112,7 +110,6 @@ describe("multiple instances", () => { ], }), new DiagnosticsPlugin({ - failOn: "error", exclude: "good.js", checks: [ { @@ -129,6 +126,9 @@ describe("multiple instances", () => { }, ); - await assert.rejects(compiler.runAsync(), /error\.js/u); + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.match(stats.compilation.errors[0].message, /error\.js/u); }); }); diff --git a/test/report-as.test.js b/test/report-as.test.js new file mode 100644 index 0000000..046b454 --- /dev/null +++ b/test/report-as.test.js @@ -0,0 +1,97 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import pack from "./utils/pack.js"; + +describe("report as", () => { + it("should leave each result at its own severity by default", async () => { + const compiler = pack("full-of-problems", {}); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should report everything as errors when set to the errors", async () => { + const compiler = pack("full-of-problems", { reportAs: "error" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should report everything as warnings when set to the warnings", async () => { + const compiler = pack("full-of-problems", { reportAs: "warning" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should report nothing when set to false", async () => { + const compiler = pack("full-of-problems", { reportAs: false }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should keep the default when written out as undefined", async () => { + const compiler = pack("full-of-problems", { reportAs: undefined }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should fail the build on the errors it reports", async () => { + const failing = await pack("error", {}).runAsync(); + const passing = await pack("error", { reportAs: "warning" }).runAsync(); + + assert.strictEqual(failing.hasErrors(), true); + assert.strictEqual(passing.hasErrors(), false); + assert.strictEqual(passing.hasWarnings(), true); + }); + + it("should fail the build on a warning set to the errors", async () => { + const stats = await pack("warn", { reportAs: "error" }).runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + }); + + it("should ignore the warnings when quiet is set", async () => { + const stats = await pack("full-of-problems", { + reportAs: "error", + quiet: true, + }).runAsync(); + + assert.strictEqual(stats.compilation.errors.length, 1); + assert.doesNotMatch( + stats.compilation.errors[0].message, + /^\s+\d+:\d+\s+warning/mu, + ); + }); + + it("should let a clean build pass whatever it is set to", async () => { + for (const reportAs of ["error", "warning", false]) { + const stats = await pack("good", { reportAs }).runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + } + }); + + it("should reject anything but a severity or false", () => { + for (const reportAs of [true, "info", [], ["error"], { error: "error" }]) { + assert.throws( + () => pack("full-of-problems", { reportAs }), + /reportAs should be one of these:\n *"error" \| "warning" \| false/u, + ); + } + }); +}); diff --git a/test/stylelint/emit.test.js b/test/stylelint/emit.test.js deleted file mode 100644 index a2e2a9c..0000000 --- a/test/stylelint/emit.test.js +++ /dev/null @@ -1,42 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("emit", () => { - it("should report both severities by default", async () => { - const compiler = pack("full-of-problems", {}); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should report both severities when set to the warnings", async () => { - const compiler = pack("full-of-problems", { emit: "warning" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should report the errors alone when set to them", async () => { - const compiler = pack("full-of-problems", { emit: "error" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - assert.strictEqual(stats.hasWarnings(), false); - }); - - it("should report neither severity when set to false", async () => { - const compiler = pack("full-of-problems", { emit: false }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), false); - assert.strictEqual(stats.hasWarnings(), false); - }); -}); diff --git a/test/stylelint/fail-on.test.js b/test/stylelint/fail-on.test.js deleted file mode 100644 index 9f782b1..0000000 --- a/test/stylelint/fail-on.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import pack from "./utils/pack.js"; - -describe("fail on", () => { - it("should fail on an error when set to the errors", async () => { - const compiler = pack("error", { failOn: "error" }); - - await assert.rejects(compiler.runAsync(), /error/u); - }); - - it("should not fail on an error when set to false", async () => { - const compiler = pack("error", { failOn: false }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), true); - }); - - it("should not fail on a warning when set to the errors", async () => { - const compiler = pack("warning", { failOn: "error" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasWarnings(), true); - }); - - it("should fail on either severity when set to the warnings", async () => { - await assert.rejects( - pack("warning", { failOn: "warning" }).runAsync(), - /warning/u, - ); - await assert.rejects( - pack("error", { failOn: "warning" }).runAsync(), - /error/u, - ); - }); - - it("should let a clean build pass whatever it is set to", async () => { - const compiler = pack("good", { failOn: "warning" }); - - const stats = await compiler.runAsync(); - - assert.strictEqual(stats.hasErrors(), false); - assert.strictEqual(stats.hasWarnings(), false); - }); -}); diff --git a/test/stylelint/report-as.test.js b/test/stylelint/report-as.test.js new file mode 100644 index 0000000..73ece28 --- /dev/null +++ b/test/stylelint/report-as.test.js @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import pack from "./utils/pack.js"; + +describe("report as", () => { + it("should leave each result at its own severity by default", async () => { + const compiler = pack("full-of-problems", {}); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should report everything as errors when set to the errors", async () => { + const compiler = pack("full-of-problems", { reportAs: "error" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should report everything as warnings when set to the warnings", async () => { + const compiler = pack("full-of-problems", { reportAs: "warning" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), true); + }); + + it("should report nothing when set to false", async () => { + const compiler = pack("full-of-problems", { reportAs: false }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + }); + + it("should let a clean build pass whatever it is set to", async () => { + const compiler = pack("good", { reportAs: "error" }); + + const stats = await compiler.runAsync(); + + assert.strictEqual(stats.hasErrors(), false); + assert.strictEqual(stats.hasWarnings(), false); + }); +}); diff --git a/test/stylelint/stylelint-options.test.js b/test/stylelint/stylelint-options.test.js index f78db79..de4afe1 100644 --- a/test/stylelint/stylelint-options.test.js +++ b/test/stylelint/stylelint-options.test.js @@ -7,7 +7,7 @@ describe("eslint options", () => { it("should filter plugin options", () => { const options = { formatter: "json", - emit: false, + reportAs: false, }; assert.deepStrictEqual(getStylelintOptions(options), { formatter: "json", @@ -19,8 +19,7 @@ describe("eslint options", () => { stylelintPath: "some/place/where/stylelint/lives", formatter: "json", files: ["file.scss"], - emit: false, - failOn: ["error"], + reportAs: false, quiet: false, outputReport: true, }; diff --git a/test/unified/unified.test.js b/test/unified/unified.test.js index 283c07a..8cd2cec 100644 --- a/test/unified/unified.test.js +++ b/test/unified/unified.test.js @@ -116,8 +116,8 @@ describe("unified plugin", () => { it("should let a check override a shared option", async () => { const compiler = pack("both", { - emit: false, - checks: [eslint, { ...stylelint, emit: "error" }], + reportAs: false, + checks: [eslint, { ...stylelint, reportAs: "error" }], }); const stats = await compiler.runAsync(); const [error] = stats.compilation.errors; @@ -167,10 +167,39 @@ describe("unified plugin", () => { ); }); - it("should fail the build when a shared failOn is set", async () => { - const compiler = pack("both", { failOn: "error", checks }); + it("should apply reportAs and quiet to a check that implements neither", async () => { + const adapter = { + name: "made-up", + create: async () => ({ + lintFiles: async () => [{ file: "checked" }], + getResults: async (results) => results, + splitResults: (results) => ({ errors: [], warnings: results }), + getFormatter: async () => async () => "made up problem", + cleanup: async () => {}, + }), + }; + const run = (options) => + pack("good", { ...options, checks: [{ use: adapter }] }).runAsync(); + + const reported = await run({}); + const asErrors = await run({ reportAs: "error" }); + const silent = await run({ reportAs: false }); + const quiet = await run({ quiet: true }); + + assert.strictEqual(reported.compilation.warnings.length, 1); + assert.strictEqual(asErrors.compilation.errors.length, 1); + assert.strictEqual(silent.hasWarnings(), false); + assert.strictEqual(quiet.hasWarnings(), false); + }); - await assert.rejects(compiler.runAsync(), /bad\.js/u); + it("should report every check where a shared reportAs says", async () => { + const compiler = pack("both", { reportAs: "warning", checks }); + const stats = await compiler.runAsync(); + const messages = stats.compilation.warnings.map(({ message }) => message); + + assert.strictEqual(stats.hasErrors(), false); + assert.match(messages.join("\n"), /bad\.js/u); + assert.match(messages.join("\n"), /bad\.scss/u); }); it("should join the reports of every check into one output report", async () => { diff --git a/types/checks/index.d.ts b/types/checks/index.d.ts index 7222092..bb98ddd 100644 --- a/types/checks/index.d.ts +++ b/types/checks/index.d.ts @@ -38,7 +38,7 @@ export type CheckInstance = { */ getResults: (results: CheckResult[]) => Promise; /** - * splits results into the ones reported as errors and as warnings + * splits the results by their own severity, leaving `reportAs` and `quiet` to the plugin */ splitResults: (results: CheckResult[]) => { errors: CheckResult[]; @@ -154,7 +154,7 @@ export type CheckAdapter = { * @typedef {object} CheckInstance * @property {(files: string[]) => Promise} lintFiles lints the given files * @property {(results: CheckResult[]) => Promise} getResults turns the raw results of every `lintFiles` call into the results to report - * @property {(results: CheckResult[]) => { errors: CheckResult[], warnings: CheckResult[] }} splitResults splits results into the ones reported as errors and as warnings + * @property {(results: CheckResult[]) => { errors: CheckResult[], warnings: CheckResult[] }} splitResults splits the results by their own severity, leaving `reportAs` and `quiet` to the plugin * @property {(formatter?: FormatterOption) => Promise} getFormatter loads a formatter, falling back to the tool's default one * @property {() => Promise} cleanup releases whatever the tool holds after a run */ diff --git a/types/index.d.ts b/types/index.d.ts index c1d2edf..f211917 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,7 +7,6 @@ export type Runner = import("./check.js").Runner; export type CheckAdapter = import("./checks/index.js").CheckAdapter; export type EnabledCheck = import("./options.js").EnabledCheck; export type CheckOptions = import("./options.js").CheckOptions; -export type ResolvedCheckOptions = import("./options.js").ResolvedCheckOptions; export type Options = import("./options.js").Options; export type ResolvedCheck = { /** @@ -21,7 +20,7 @@ export type ResolvedCheck = { /** * options resolved for this check */ - options: ResolvedCheckOptions; + options: CheckOptions; /** * the globs of the files to lint */ diff --git a/types/options.d.ts b/types/options.d.ts index d75f505..d314bce 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -1,7 +1,6 @@ export type EXPECTED_ANY = any; export type Compiler = import("webpack").Compiler; -export type Severity = "error" | "warning"; -export type SeverityLevel = Severity | false; +export type ReportAs = "error" | "warning" | false; export type FormatterOption = import("./checks/index.js").FormatterOption; export type CheckAdapter = import("./checks/index.js").CheckAdapter; export type CheckAdapterInput = import("./checks/index.js").CheckAdapterInput; @@ -25,9 +24,9 @@ export type SharedOptions = { */ cacheLocation?: string | undefined; /** - * the least severe result that is reported + * what a check reports its results as */ - emit?: SeverityLevel | undefined; + reportAs?: ReportAs | undefined; /** * specify the files and/or directories to exclude */ @@ -36,10 +35,6 @@ export type SharedOptions = { * specify the extensions that should be checked */ extensions?: (string | string[]) | undefined; - /** - * the least severe result that fails the build - */ - failOn?: SeverityLevel | undefined; /** * specify directories, files, or globs */ @@ -69,16 +64,11 @@ export type CheckEntry = SharedOptions & { use: string | CheckAdapterInput; [option: string]: EXPECTED_ANY; }; -export type CheckOptions = Omit & { - emit: SeverityLevel; - [option: string]: EXPECTED_ANY; -}; /** - * What a check reads once the plugin has resolved it against a compiler. + * The options of one check, as given and then as the plugin resolves them + * against a compiler. */ -export type ResolvedCheckOptions = Omit & { - emit: SeverityLevel; - failOn: SeverityLevel; +export type CheckOptions = SharedOptions & { [option: string]: EXPECTED_ANY; }; export type PluginOptions = { diff --git a/types/utils.d.ts b/types/utils.d.ts index cc72962..3cbddf1 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -8,8 +8,6 @@ export type ArrifyResult = T extends null | undefined : T extends Iterable ? T_1[] : [T]; -export type Severity = import("./options.js").Severity; -export type SeverityLevel = import("./options.js").SeverityLevel; export type EXPECTED_ANY = any; /** @typedef {import("webpack").Compiler} Compiler */ /** @@ -30,19 +28,6 @@ export type EXPECTED_ANY = any; * @returns {ArrifyResult} array of values */ export function arrify(value: T): ArrifyResult; -/** @typedef {import("./options.js").Severity} Severity */ -/** @typedef {import("./options.js").SeverityLevel} SeverityLevel */ -/** - * `emit` and `failOn` each name the least severe result they take in, so - * `"warning"` covers the errors above it and `false` covers nothing. - * @param {SeverityLevel} level the level an option is set to - * @param {Severity} severity the severity to test against it - * @returns {boolean} whether the level covers the severity - */ -export function coversSeverity( - level: SeverityLevel, - severity: Severity, -): boolean; /** * A package name is imported as it is, so a test can still mock it. A path may * name a directory, which `import.meta.resolve` returns unchanged for