From 118532f5ca84d1ff251c41cff927c64a70cc501b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 13:31:32 +0000 Subject: [PATCH] perf: resolve a pass of the import graph through one resolution cache `ts.resolveModuleName` was called with no cache, so every file importing a package walked `node_modules` and read its `package.json` again, and every file importing a shared module looked it up again. A pass of `affectedFiles` or `signatures` now resolves through one `createModuleResolutionCache` for each set of compiler options. It is dropped when the pass ends: a file written since can make an import that resolved to nothing resolve. 1,500 files in 30 folders, each importing two packages, a shared module and a sibling; first pass: affectedFiles 151-240 ms -> 60-105 ms, signatures 198-272 ms -> 94-122 ms. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf --- .changeset/import-graph-resolution-cache.md | 5 ++++ src/import-graph.js | 33 +++++++++++++++++++++ types/import-graph.d.ts | 4 +++ 3 files changed, 42 insertions(+) create mode 100644 .changeset/import-graph-resolution-cache.md diff --git a/.changeset/import-graph-resolution-cache.md b/.changeset/import-graph-resolution-cache.md new file mode 100644 index 0000000..21c150d --- /dev/null +++ b/.changeset/import-graph-resolution-cache.md @@ -0,0 +1,5 @@ +--- +"diagnostics-webpack-plugin": patch +--- + +Resolve the imports of every file in a pass of the import graph through one module resolution cache per set of compiler options, rather than looking up the same package or shared module again for every file importing it. diff --git a/src/import-graph.js b/src/import-graph.js index 8933662..324da4b 100644 --- a/src/import-graph.js +++ b/src/import-graph.js @@ -39,6 +39,7 @@ import { toPosixPath } from "./utils.js"; * @property {Map} options the compiler options of each config file * @property {Map} reads every file read for a signature, as it was read * @property {Map} configHashes each config file and what it extends, hashed, as the signatures were last read under + * @property {Map} resolutions what each set of compiler options resolved this pass, so that every file in a folder does not look its imports up again */ // What the options of a directory no config file covers are taken as: what a @@ -57,6 +58,7 @@ function createGraph(ts) { options: new Map(), reads: new Map(), configHashes: new Map(), + resolutions: new Map(), }; } @@ -95,6 +97,32 @@ function optionsFor(graph, file) { return graph.options.get(config); } +/** + * What the files of one set of compiler options resolved this pass. Kept for + * no longer than a pass: a file written since can make an import that resolved + * to nothing resolve, which an answer kept from before would hide. + * @param {Graph} graph the graph + * @param {EXPECTED_ANY} options the compiler options the imports are resolved under + * @returns {EXPECTED_ANY} the cache TypeScript resolves them through + */ +function resolutionsFor(graph, options) { + let cache = graph.resolutions.get(options); + + if (!cache) { + const { ts } = graph; + + cache = ts.createModuleResolutionCache( + ts.sys.getCurrentDirectory(), + (/** @type {string} */ name) => + ts.sys.useCaseSensitiveFileNames ? name : name.toLowerCase(), + options, + ); + graph.resolutions.set(options, cache); + } + + return cache; +} + /** * @param {Graph} graph the graph * @param {string} file the file the text is of @@ -119,6 +147,7 @@ function importsOf(graph, file, text) { file, options, ts.sys, + resolutionsFor(graph, options), ).resolvedModule; if (found) resolved.push(toPosixPath(resolve(found.resolvedFileName))); @@ -163,6 +192,8 @@ function readImports(graph, file) { * @returns {Set | undefined} the files to lint again, as posix paths, or nothing when every file has to be */ function affectedFiles(graph, files, changed) { + graph.resolutions.clear(); + const covered = new Set(files.map((file) => toPosixPath(resolve(file)))); const touched = changed.map((file) => toPosixPath(resolve(file))); @@ -420,6 +451,8 @@ function projectSignature(graph, configs) { * @returns {Map} the signature of each of them that can be read, by its posix path */ function signatures(graph, files) { + graph.resolutions.clear(); + const wanted = files.map((file) => toPosixPath(resolve(file))); // Read first, so that the config files they resolve through are known. diff --git a/types/import-graph.d.ts b/types/import-graph.d.ts index efb5f66..19dfd71 100644 --- a/types/import-graph.d.ts +++ b/types/import-graph.d.ts @@ -71,6 +71,10 @@ export type Graph = { * each config file and what it extends, hashed, as the signatures were last read under */ configHashes: Map; + /** + * what each set of compiler options resolved this pass, so that every file in a folder does not look its imports up again + */ + resolutions: Map; }; /** * Which files a change can have changed what typed linting says of: the ones