From 136128d1abac20eaf503b49b6c2752d667080bd1 Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Tue, 30 Jun 2026 17:42:54 +0530 Subject: [PATCH 1/2] Build: Make check-installed-deps layout-agnostic Match installed packages by `name@version` and compare their `resolved` source instead of looking them up by path and comparing `integrity`. This keeps the check working under both the hoisted (default) and linked (`install-strategy=linked`) layouts: linked relocates packages under `node_modules/.store/...` and omits `integrity` from the hidden lockfile, so path/integrity matching no longer holds. For npm aliases the lockfile `name` is the real package (the linked `.store` key) while the path leaf is the alias install name (the hoisted key), so both keys are tried. --- tools/validation/check-installed-deps.mjs | 52 +++++++++++++++++------ 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/tools/validation/check-installed-deps.mjs b/tools/validation/check-installed-deps.mjs index d78fc8bfb3eda1..ca5baa470059e2 100644 --- a/tools/validation/check-installed-deps.mjs +++ b/tools/validation/check-installed-deps.mjs @@ -5,6 +5,8 @@ * `package-lock.json` against `node_modules/.package-lock.json` (npm's hidden * lockfile, written on every install to record the actual installed tree). * + * Works with both the hoisted and isolated layouts. + * * Exits non-zero with a hint to run `npm install` if the trees diverge. */ @@ -85,6 +87,28 @@ if ( needsCheck ) { const lockPkgs = lock.packages || {}; const hiddenPkgs = hidden.packages || {}; + // The package name is the path segment after the final `node_modules/`. + const NM = 'node_modules/'; + const packageName = ( pkgPath ) => { + const i = pkgPath.lastIndexOf( NM ); + return i === -1 ? pkgPath : pkgPath.slice( i + NM.length ); + }; + + /* + * Index installed packages by `name@version` → set of `resolved` sources. + */ + const installedByKey = new Map(); + for ( const [ pkgPath, info ] of Object.entries( hiddenPkgs ) ) { + if ( info.link || ! info.version ) { + continue; + } + const key = `${ packageName( pkgPath ) }@${ info.version }`; + if ( ! installedByKey.has( key ) ) { + installedByKey.set( key, new Set() ); + } + installedByKey.get( key ).add( info.resolved ); + } + const reportedMismatches = []; const MAX_REPORTED = 5; let totalMismatches = 0; @@ -100,22 +124,24 @@ if ( needsCheck ) { continue; } - const installed = hiddenPkgs[ pkgPath ]; + // Optional/extraneous deps may legitimately not be installed. + if ( info.optional || info.extraneous ) { + continue; + } + + /* + * Match by name@version. For aliases `info.name` is the real name + */ + const leaf = packageName( pkgPath ); + const resolvedSet = + installedByKey.get( `${ info.name || leaf }@${ info.version }` ) || + installedByKey.get( `${ leaf }@${ info.version }` ); let mismatch; - if ( ! installed ) { - /* - * Optional deps may be skipped by npm on the current platform - * (e.g. macOS-only fsevents on Linux). Don't flag them as - * missing. Real drift on an optional dep would still be caught - * below as an integrity mismatch. - */ - if ( info.optional ) { - continue; - } + if ( ! resolvedSet ) { mismatch = `missing: ${ pkgPath }`; - } else if ( installed.integrity !== info.integrity ) { - mismatch = `integrity mismatch: ${ pkgPath }`; + } else if ( info.resolved && ! resolvedSet.has( info.resolved ) ) { + mismatch = `source mismatch: ${ pkgPath }`; } if ( ! mismatch ) { From a89c0f14bf89181bb9bcee64f66da2c77a7f38c0 Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Tue, 30 Jun 2026 17:51:02 +0530 Subject: [PATCH 2/2] Build: Mention GUTENBERG_CHECK_INSTALLED_DEPS opt-out on check failure When the dependency check fails the build/dev startup, point developers at the `GUTENBERG_CHECK_INSTALLED_DEPS=NEVER` env var so they know the check can be skipped. --- tools/build-scripts/build.mjs | 4 +++- tools/build-scripts/dev.mjs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/build-scripts/build.mjs b/tools/build-scripts/build.mjs index b660db0b2191b2..8a87a9cf62b77c 100644 --- a/tools/build-scripts/build.mjs +++ b/tools/build-scripts/build.mjs @@ -112,7 +112,9 @@ async function build() { '@wordpress/validation-tools', '--silent', ] ).catch( () => { - throw new Error( 'Run `npm install` to update.' ); + throw new Error( + 'Run `npm install` to update, or set GUTENBERG_CHECK_INSTALLED_DEPS=NEVER to skip this check.' + ); } ); } diff --git a/tools/build-scripts/dev.mjs b/tools/build-scripts/dev.mjs index 0714e158ec7fde..593017c4c051dc 100644 --- a/tools/build-scripts/dev.mjs +++ b/tools/build-scripts/dev.mjs @@ -151,7 +151,9 @@ async function dev() { '@wordpress/validation-tools', '--silent', ] ).catch( () => { - throw new Error( 'Run `npm install` to update.' ); + throw new Error( + 'Run `npm install` to update, or set GUTENBERG_CHECK_INSTALLED_DEPS=NEVER to skip this check.' + ); } ); }