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/asset-imports-keep-graph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"diagnostics-webpack-plugin": patch
---

Keep linting again only what a change reaches in a project importing stylesheets, images or other assets: such an import, which TypeScript resolves to nothing, counted as an import yet to resolve and made every rebuild lint every file again.
4 changes: 3 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"stylelint's",
"stylelintcache",
"stylelintignore",
"stylelintrc"
"stylelintrc",
"pcss",
"styl"
],
"ignorePaths": [
"**/__snapshots__/**",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,8 @@ Which file imports which is read with the project's own TypeScript, through the
drops it before webpack sees it — and this holds for every check, whatever tool
it runs. A change that could reach files some other way — a declaration file
nothing imports, a `declare global`, a config file, an import that resolves to
nothing — lints every file again, as does a project without TypeScript. There,
nothing (a stylesheet, an image or another asset webpack bundles aside) — lints
every file again, as does a project without TypeScript. There,
an ESLint check with `cache: true` written down keeps ESLint's own cache, which
answers for a file whose import changed from before the change.

Expand Down
26 changes: 22 additions & 4 deletions src/import-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { toPosixPath } from "./utils.js";
* @property {string[]} resolved the files its imports resolve to
* @property {boolean} unresolved whether a relative import resolved to nothing
* @property {boolean} global whether it declares something every file can see
* @property {string[]} missing the relative imports that resolve to nothing, as written
* @property {string[]} missing the relative imports that resolve to nothing, as written, assets included
*/

/**
Expand All @@ -41,6 +41,10 @@ import { toPosixPath } from "./utils.js";
* @property {Map<string, string>} configHashes each config file and what it extends, hashed, as the signatures were last read under
*/

// What a file webpack bundles and TypeScript has no types for ends in.
const ASSET =
/\.(?:css|scss|sass|less|styl|stylus|pcss|postcss|svg|png|jpe?g|gif|webp|avif|ico|bmp|tiff?|woff2?|ttf|otf|eot|txt|md|html?|wasm|mp4|webm|mp3|wav|ogg|flac|aac)(?:\?.*)?$/iu;

// What the options of a directory no config file covers are taken as: what a
// bundler resolves, JavaScript included.
const FALLBACK_OPTIONS = { allowJs: true, moduleResolution: 100 };
Expand Down Expand Up @@ -113,6 +117,9 @@ function importsOf(graph, file, text) {
/** @type {string[]} */
const missing = [];

/** @type {string[]} */
const assets = [];

for (const { fileName } of info.importedFiles) {
const found = ts.resolveModuleName(
fileName,
Expand All @@ -121,8 +128,18 @@ function importsOf(graph, file, text) {
ts.sys,
).resolvedModule;

if (found) resolved.push(toPosixPath(resolve(found.resolvedFileName)));
else if (fileName.startsWith(".")) missing.push(fileName);
if (found) {
resolved.push(toPosixPath(resolve(found.resolvedFileName)));
} else if (!fileName.startsWith(".")) {
continue;
} else if (ASSET.test(fileName) && !options.allowArbitraryExtensions) {
// A stylesheet or an image is a module to webpack and nothing to
// TypeScript, which no file written later turns into a type — unless
// `allowArbitraryExtensions` reads one through a `.d.css.ts` beside it.
assets.push(fileName);
} else {
missing.push(fileName);
}
}

for (const { fileName } of info.referencedFiles) {
Expand All @@ -132,7 +149,8 @@ function importsOf(graph, file, text) {
return {
resolved,
unresolved: missing.length > 0,
missing,
// Still part of what a signature is read from, and of nothing else.
missing: [...missing, ...assets],
// `declare global` and `declare module` reach files that never import the
// one declaring them.
global:
Expand Down
31 changes: 31 additions & 0 deletions test/import-graph.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,37 @@ describe("import graph", () => {

assert.strictEqual(affected(all, ["leaf.ts"]), undefined);
});

it("should keep answering for what a change reaches where a stylesheet is imported", () => {
// Webpack bundles it; TypeScript resolves it to nothing, and nothing
// written later turns it into a type.
write(
"alone.ts",
'import "./styles.css";\nimport logo from "./logo.svg?url";\nexport const a = logo;\n',
);

assert.deepStrictEqual(affected(all, ["leaf.ts"]), [
"leaf.ts",
"middle.ts",
"top.ts",
]);
});

it("should answer for every file where a stylesheet can be read through a declaration beside it", () => {
write(
"tsconfig.json",
JSON.stringify({
compilerOptions: {
strict: true,
moduleResolution: "bundler",
allowArbitraryExtensions: true,
},
}),
);
write("alone.ts", 'import "./styles.css";\nexport const a = 1;\n');

assert.strictEqual(affected(all, ["leaf.ts"]), undefined);
});
});

describe("the graph a compiler keeps", () => {
Expand Down
2 changes: 1 addition & 1 deletion types/import-graph.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type Imports = {
*/
global: boolean;
/**
* the relative imports that resolve to nothing, as written
* the relative imports that resolve to nothing, as written, assets included
*/
missing: string[];
};
Expand Down
Loading