diff --git a/.changeset/lint-on-start.md b/.changeset/lint-on-start.md new file mode 100644 index 0000000..f8ff0f1 --- /dev/null +++ b/.changeset/lint-on-start.md @@ -0,0 +1,5 @@ +--- +"diagnostics-webpack-plugin": major +--- + +`lintDirtyModulesOnly` is `lintOnStart`, inverted and defaulting to `true`. It says whether the first compilation lints every file it covers, and a build — which is nothing but a first compilation — now lints whatever it is set to, where `lintDirtyModulesOnly: true` used to leave a build silently unlinted. diff --git a/README.md b/README.md index b46a898..497ade5 100644 --- a/README.md +++ b/README.md @@ -115,17 +115,23 @@ type context = string; Base directory for linting. Every relative `files` and `exclude` pattern is resolved against it. -#### `lintDirtyModulesOnly` +#### `lintOnStart` - Type: ```ts -type lintDirtyModulesOnly = boolean; +type lintOnStart = boolean; ``` -- Default: `false` +- Default: `true` + +Whether the first compilation lints every file it covers. Leave it alone and a +build lints everything while a watch run lints everything once and then only +what webpack rebuilds. -Lint only changed files, skipping the initial lint on build start. +Set it to `false` to start a watch run quiet: nothing is linted until you touch +a file, and only the modules webpack rebuilds are reported. A build is nothing +but a first compilation, so it lints either way — the option cannot silence one. ### Shared options @@ -310,7 +316,7 @@ Run with `{ use: "eslint" }`. It lints the files webpack builds, so only the mod Alongside the shared options you can pass any [ESLint Node.js API option](https://eslint.org/docs/latest/integrate/nodejs-api#-new-eslintoptions) — they are handed to the `ESLint` class as they are. `concurrency` is worth knowing about: it spreads a lint across worker threads, and ESLint warns on the runs where doing so costs more than it saves, so measure your own project rather than turning it on by default. -A rebuild lints only the files webpack rebuilt and reports the rest from the previous run, so `lintDirtyModulesOnly` is only worth setting to skip the first lint entirely. +A rebuild lints only the files webpack rebuilt and reports the rest from the previous run, so [`lintOnStart`](#lintonstart) is only worth setting to start a watch run quiet. ### `configType` @@ -439,7 +445,7 @@ module.exports = { Both plugins become one, and every option they had is still here. What changed is where an option is written and how the four that decided severity are spelled. -**Where an option goes.** `context`, `lintDirtyModulesOnly` and `checks` are the plugin's own and stay at the top level. Everything else is shared: write it at the top level to cover every check, or inside a `checks` entry to cover that one. `configType`, `eslintPath`, `stylelintPath` and `threads` belong to a single check and go in its entry. +**Where an option goes.** `context`, `lintOnStart` and `checks` are the plugin's own and stay at the top level. Everything else is shared: write it at the top level to cover every check, or inside a `checks` entry to cover that one. `configType`, `eslintPath`, `stylelintPath` and `threads` belong to a single check and go in its entry. **Severity is one option.** `emitError`, `emitWarning`, `failOnError`, `failOnWarning` and `quiet` are [`reportAs`](#reportas), because reporting a result as a webpack error is what fails the build: @@ -490,7 +496,7 @@ Every option `eslint-webpack-plugin` accepted, and where it is now: | `files` | Unchanged, shared. | | `fix` | Unchanged, shared. | | `formatter` | Unchanged, shared. | -| `lintDirtyModulesOnly` | Unchanged, top level. It covers every check and cannot be set per check. | +| `lintDirtyModulesOnly` | [`lintOnStart`](#lintonstart), inverted: `lintDirtyModulesOnly: true` is `lintOnStart: false`. Top level. | | `outputReport` | Unchanged, shared. It is still written even when `reportAs` is `false`. | | `quiet` | `reportAs: { warnings: false }`. | | `resourceQueryExclude` | Unchanged, shared. | @@ -528,7 +534,7 @@ Every option `stylelint-webpack-plugin` accepted, and where it is now: | `failOnWarning` | [`reportAs`](#reportas), see the table above. | | `files` | Unchanged, shared. | | `formatter` | Unchanged, shared. | -| `lintDirtyModulesOnly` | Unchanged, top level. It covers every check and cannot be set per check. | +| `lintDirtyModulesOnly` | [`lintOnStart`](#lintonstart), inverted: `lintDirtyModulesOnly: true` is `lintOnStart: false`. Top level. | | `outputReport` | Unchanged, shared. It is still written even when `reportAs` is `false`. | | `quiet` | `reportAs: { warnings: false }`. | | `stylelintPath` | Unchanged, in the `stylelint` entry. | diff --git a/src/index.js b/src/index.js index 33df824..b48a0ae 100644 --- a/src/index.js +++ b/src/index.js @@ -104,24 +104,24 @@ class DiagnosticsWebpackPlugin { return checks; }; - // If `lintDirtyModulesOnly` is disabled, - // execute the checks on the build - if (!this.options.lintDirtyModulesOnly) { - compiler.hooks.run.tapPromise(this.key, (compiler) => - this.run(compiler, getChecks()), - ); - } + // A build is nothing but a first compilation, so `lintOnStart` cannot + // silence one without silencing the plugin. + compiler.hooks.run.tapPromise(this.key, (compiler) => + this.run(compiler, getChecks()), + ); - let hasCompilerRunByDirtyModule = this.options.lintDirtyModulesOnly; + // A lint integration whose bundler reaches a file only once something + // requests it defaults this off; webpack's first build walks all of them. + let skipping = !this.options.lintOnStart; compiler.hooks.watchRun.tapPromise(this.key, (compiler) => { - if (!hasCompilerRunByDirtyModule) { - return this.run(compiler, getChecks()); - } + if (skipping) { + skipping = false; - hasCompilerRunByDirtyModule = false; + return Promise.resolve(); + } - return Promise.resolve(); + return this.run(compiler, getChecks()); }); } @@ -270,7 +270,7 @@ class DiagnosticsWebpackPlugin { ); // A module webpack did not rebuild is reported from the last run. - if (!this.options.lintDirtyModulesOnly) { + if (this.options.lintOnStart) { compilation.hooks.stillValidModule.tap(this.key, (module) => addFile(module, false), ); diff --git a/src/options.js b/src/options.js index 6aded71..aebf5ea 100644 --- a/src/options.js +++ b/src/options.js @@ -53,7 +53,7 @@ const PLUGIN_NAME = "Diagnostics Webpack Plugin"; /** * @typedef {object} PluginOptions * @property {string=} context a string indicating the root of your files - * @property {boolean=} lintDirtyModulesOnly lint only changed files, skip linting on start + * @property {boolean=} lintOnStart whether the first compilation lints everything * @property {CheckEntry[]} checks the checks to run */ @@ -69,7 +69,7 @@ const PLUGIN_NAME = "Diagnostics Webpack Plugin"; /** * @typedef {object} NormalizedOptions * @property {string=} context a string indicating the root of your files - * @property {boolean=} lintDirtyModulesOnly lint only changed files, skip linting on start + * @property {boolean} lintOnStart whether the first compilation lints everything * @property {EnabledCheck[]} checks the checks to run */ @@ -180,7 +180,7 @@ function toAdapter(use) { function getOptions(pluginOptions) { const { context, - lintDirtyModulesOnly, + lintOnStart = true, checks: entries = [], ...shared } = pluginOptions; @@ -195,7 +195,7 @@ function getOptions(pluginOptions) { return { name: adapter.name, adapter, options }; }); - return { context, lintDirtyModulesOnly, checks: enabled }; + return { context, lintOnStart, checks: enabled }; } /** diff --git a/src/options.json b/src/options.json index ebf6ed9..64870af 100644 --- a/src/options.json +++ b/src/options.json @@ -6,8 +6,8 @@ "description": "A string indicating the root of your files.", "type": "string" }, - "lintDirtyModulesOnly": { - "description": "Lint only changed files, skip lint on start.", + "lintOnStart": { + "description": "Whether the first compilation lints every file it covers. `false` leaves a watch run reporting only the modules webpack rebuilds; a build has nothing but a first compilation, so it always lints.", "type": "boolean" }, "checks": { diff --git a/test/lint-dirty-modules-only.test.js b/test/lint-on-start.test.js similarity index 70% rename from test/lint-dirty-modules-only.test.js rename to test/lint-on-start.test.js index b123de3..9040f63 100644 --- a/test/lint-dirty-modules-only.test.js +++ b/test/lint-on-start.test.js @@ -5,13 +5,9 @@ import { afterEach, describe, it } from "node:test"; import pack from "./utils/pack.js"; -const target = join( - import.meta.dirname, - "fixtures", - "lint-dirty-modules-only-entry.js", -); +const target = join(import.meta.dirname, "fixtures", "lint-on-start-entry.js"); -describe("lint dirty modules only", () => { +describe("lint on start", () => { let watch; afterEach(() => { @@ -26,8 +22,8 @@ describe("lint dirty modules only", () => { // eslint-disable-next-line no-use-before-define let next = firstPass; - const compiler = pack("lint-dirty-modules-only", { - lintDirtyModulesOnly: true, + const compiler = pack("lint-on-start", { + lintOnStart: false, }); watch = compiler.watch({}, (err, stats) => next(err, stats)); @@ -51,4 +47,15 @@ describe("lint dirty modules only", () => { writeFileSync(target, "const bar = false;\n"); } }); + + it("still lints a build, which is nothing but a start", async () => { + writeFileSync(target, "const foo = false\n"); + + const stats = await pack("lint-on-start", { + lintOnStart: false, + }).runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.match(stats.compilation.errors[0].message, /no-unused-vars/u); + }); }); diff --git a/test/stylelint/fixtures/lint-dirty-modules-only/index.js b/test/stylelint/fixtures/lint-on-start/index.js similarity index 100% rename from test/stylelint/fixtures/lint-dirty-modules-only/index.js rename to test/stylelint/fixtures/lint-on-start/index.js diff --git a/test/stylelint/lint-dirty-modules-only.test.js b/test/stylelint/lint-on-start.test.js similarity index 70% rename from test/stylelint/lint-dirty-modules-only.test.js rename to test/stylelint/lint-on-start.test.js index ee4f9b5..8d0916e 100644 --- a/test/stylelint/lint-dirty-modules-only.test.js +++ b/test/stylelint/lint-on-start.test.js @@ -5,12 +5,9 @@ import { afterEach, describe, it } from "node:test"; import pack from "./utils/pack.js"; -const target = join( - import.meta.dirname, - "fixtures/lint-dirty-modules-only/test.scss", -); +const target = join(import.meta.dirname, "fixtures/lint-on-start/test.scss"); -describe("lint dirty modules only", () => { +describe("lint on start", () => { let watch; afterEach(() => { @@ -25,8 +22,8 @@ describe("lint dirty modules only", () => { // eslint-disable-next-line no-use-before-define let next = firstPass; - const compiler = pack("lint-dirty-modules-only", { - lintDirtyModulesOnly: true, + const compiler = pack("lint-on-start", { + lintOnStart: false, }); watch = compiler.watch({}, (err, stats) => next(err, stats)); @@ -51,4 +48,15 @@ describe("lint dirty modules only", () => { writeFileSync(target, "#stuff { background: black; }\n"); } }); + + it("still lints a build, which is nothing but a start", async () => { + writeFileSync(target, "#stuff { background: black; }\n"); + + const stats = await pack("lint-on-start", { + lintOnStart: false, + }).runAsync(); + + assert.strictEqual(stats.hasErrors(), true); + assert.match(stats.compilation.errors[0].message, /color-named/u); + }); }); diff --git a/test/stylelint/utils/conf.js b/test/stylelint/utils/conf.js index ef5f265..7fd28d0 100644 --- a/test/stylelint/utils/conf.js +++ b/test/stylelint/utils/conf.js @@ -3,7 +3,7 @@ import { join } from "node:path"; import DiagnosticsPlugin from "../../../src/index.js"; // Options the plugin only accepts next to the check entries, not inside one. -const PLUGIN_OPTIONS = ["context", "lintDirtyModulesOnly"]; +const PLUGIN_OPTIONS = ["context", "lintOnStart"]; export default (context, pluginConf = {}, webpackConf = {}) => { const testDir = join(import.meta.dirname, ".."); diff --git a/test/utils/conf.js b/test/utils/conf.js index 66e809a..2c05386 100644 --- a/test/utils/conf.js +++ b/test/utils/conf.js @@ -3,7 +3,7 @@ import { join } from "node:path"; import DiagnosticsPlugin from "../../src/index.js"; // Options the plugin only accepts next to the check entries, not inside one. -const PLUGIN_OPTIONS = ["context", "lintDirtyModulesOnly"]; +const PLUGIN_OPTIONS = ["context", "lintOnStart"]; export default (entry, pluginConf = {}, webpackConf = {}) => { const testDir = join(import.meta.dirname, ".."); diff --git a/types/options.d.ts b/types/options.d.ts index b4fae79..232583b 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -80,9 +80,9 @@ export type PluginOptions = { */ context?: string | undefined; /** - * lint only changed files, skip linting on start + * whether the first compilation lints everything */ - lintDirtyModulesOnly?: boolean | undefined; + lintOnStart?: boolean | undefined; /** * the checks to run */ @@ -109,9 +109,9 @@ export type NormalizedOptions = { */ context?: string | undefined; /** - * lint only changed files, skip linting on start + * whether the first compilation lints everything */ - lintDirtyModulesOnly?: boolean | undefined; + lintOnStart: boolean; /** * the checks to run */