Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/files-outside-the-graph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"diagnostics-webpack-plugin": minor
---

Naming `files` now means every file they match is checked, whether or not webpack built it, for every check rather than only the ones that walk the file system by themselves. A module nothing imports yet was invisible to the ESLint check before. Left unset, a check still reads what it always read.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ Specify directories, files, or globs. Must be relative to `options.context`.
Directories are traversed recursively looking for files matching `options.extensions`.
File and glob patterns ignore `options.extensions`.

Naming them says what to check, so every file they match is checked whether or
not webpack built it — a module nothing imports yet, or one reached through a
loader webpack resolves differently, is checked all the same. Leave it unset and
a check reads whatever it reads by itself: ESLint the modules webpack built,
Stylelint a walk of the context.

In a watch run the folder a check takes its files from is watched, so a file
you add there is checked without anything else having to change — and for the
`typescript` check, so is every folder the `tsconfig.json` `include` covers. A
Expand Down
14 changes: 9 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
* @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
* @property {"modules" | "glob"} filesSource where the check's files come from
* @property {(file: string) => boolean} isWanted whether a path is one to lint
* @property {(file: string) => boolean} isExcluded whether a path is left out
*/
Expand Down Expand Up @@ -95,10 +96,10 @@ function contains(directory, path) {
* @param {ResolvedCheck} check the check to answer for
* @returns {string[]} the directories its globs are rooted at
*/
function globRoots({ adapter, wanted }) {
function globRoots({ filesSource, wanted }) {
// A check reading the files webpack built has nothing to say about one
// webpack does not build.
if (adapter.filesSource !== "glob") return [];
if (filesSource !== "glob") return [];

/** @type {Set<string>} */
const roots = new Set();
Expand Down Expand Up @@ -268,6 +269,9 @@ class DiagnosticsWebpackPlugin {
name,
adapter,
options: resolved,
// A check told which files to check looks at all of them, whether or not
// webpack built them, whatever the check reads by itself.
filesSource: options.files ? "glob" : adapter.filesSource,
wanted,
exclude,
// Compiled here rather than per call: the two run on every module of
Expand Down Expand Up @@ -342,7 +346,7 @@ class DiagnosticsWebpackPlugin {
// Globbing the file system does not depend on the module graph, so a
// child compilation would only lint what its parent already did.
const enabled = compilation.compiler.isChild()
? checks.filter(({ adapter }) => adapter.filesSource === "modules")
? checks.filter((check) => check.filesSource === "modules")
: checks;

if (enabled.length === 0) return;
Expand Down Expand Up @@ -404,7 +408,7 @@ class DiagnosticsWebpackPlugin {
});

const fromModules = runners.filter(
({ adapter }) => adapter.filesSource === "modules",
(check) => check.filesSource === "modules",
);

if (fromModules.length > 0) {
Expand Down Expand Up @@ -452,7 +456,7 @@ class DiagnosticsWebpackPlugin {

// Nothing globbed from the file system waits on the module graph.
for (const check of runners) {
if (check.adapter.filesSource === "modules") continue;
if (check.filesSource === "modules") continue;

const collected = collectFromFileSystem(compiler, check);

Expand Down
2 changes: 1 addition & 1 deletion src/shared-options.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
]
},
"files": {
"description": "Specify the files and/or directories to traverse. Must be relative to `options.context`.",
"description": "Specify directories, files, or globs to check. Naming them checks every file they match, whether or not webpack built it; left unset, a check reads whatever it reads by itself.",
"anyOf": [
{
"type": "string"
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/outside-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('./outside/good')
3 changes: 3 additions & 0 deletions test/fixtures/outside/good.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const used = 1;

module.exports = used;
1 change: 1 addition & 0 deletions test/fixtures/outside/orphan.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const neverImported = 1;
54 changes: 54 additions & 0 deletions test/outside-graph.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import { join } from "node:path";
import { describe, it } from "node:test";

import pack from "./utils/pack.js";

const require = createRequire(import.meta.url);

describe("files outside the module graph", () => {
it("should check only what webpack built when no files are named", async () => {
const stats = await pack("outside").runAsync();

assert.strictEqual(stats.hasErrors(), false);
assert.strictEqual(stats.hasWarnings(), false);
});

it("should check a file webpack never built when files are named", async () => {
const stats = await pack("outside", { files: "outside" }).runAsync();

assert.strictEqual(stats.hasErrors(), true);

const [{ message }] = stats.compilation.errors;

assert.match(message, /orphan\.js/u);
assert.match(message, /no-unused-vars/u);
});

it("should watch the folder it was told to check", async () => {
const stats = await pack("outside", { files: "outside" }).runAsync();
const watched = [...stats.compilation.contextDependencies];

// A check reading the module graph has nothing to say about a file that
// is not in it; one told where to look is watched there like any other.
assert.ok(
watched.includes(join(import.meta.dirname, "fixtures", "outside")),
);
});

it("should still check what webpack built when files are named", async () => {
const eslintPath = join(import.meta.dirname, "mock/eslint-recorder");

require(eslintPath)._reset();

// The walk covers the imported file as well as the orphan, rather than
// replacing the graph with whatever the graph left out.
await pack("outside", { eslintPath, files: "outside" }).runAsync();

const linted = require(eslintPath)._calls.flat().join();

assert.match(linted, /outside\/good\.js/u);
assert.match(linted, /outside\/orphan\.js/u);
});
});
4 changes: 4 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export type ResolvedCheck = {
* the globs of the files not to lint
*/
exclude: string[];
/**
* where the check's files come from
*/
filesSource: "modules" | "glob";
/**
* whether a path is one to lint
*/
Expand Down
Loading