From 3ff472b0282b5ed627f5dcd017a08fa2270c90e0 Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:11:51 +0000 Subject: [PATCH 1/5] rush-resolver-cache-plugin: add pnpm 10 / lockfile v9 compatibility --- ...solver-cache-pnpm-10_2026-04-07-06-16.json | 10 +++ .../src/afterInstallAsync.ts | 53 +++++++++++++- .../computeResolverCacheFromLockfileAsync.ts | 28 ++++++-- .../rush-resolver-cache-plugin/src/helpers.ts | 70 +++++++++++++++---- .../test/__snapshots__/helpers.test.ts.snap | 62 +++++++++++----- .../src/test/helpers.test.ts | 51 +++++++++++++- 6 files changed, 234 insertions(+), 40 deletions(-) create mode 100644 common/changes/@microsoft/rush/bmiddha-resolver-cache-pnpm-10_2026-04-07-06-16.json diff --git a/common/changes/@microsoft/rush/bmiddha-resolver-cache-pnpm-10_2026-04-07-06-16.json b/common/changes/@microsoft/rush/bmiddha-resolver-cache-pnpm-10_2026-04-07-06-16.json new file mode 100644 index 00000000000..813779a5a3c --- /dev/null +++ b/common/changes/@microsoft/rush/bmiddha-resolver-cache-pnpm-10_2026-04-07-06-16.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "rush-resolver-cache-plugin: add pnpm 10 / lockfile v9 compatibility", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts index 9c675ca96b8..0191dd1cc1e 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { existsSync, readdirSync } from 'node:fs'; + import type { RushSession, RushConfiguration, @@ -79,7 +81,12 @@ export async function afterInstallAsync( const lockFilePath: string = subspace.getCommittedShrinkwrapFilePath(variant); - const pnpmStoreDir: string = `${rushConfiguration.pnpmOptions.pnpmStorePath}/v3/files/`; + const pnpmStorePath: string = rushConfiguration.pnpmOptions.pnpmStorePath; + // pnpm 10 uses v10/index/ for index files; pnpm 8 uses v3/files/ + const pnpmStoreV10IndexDir: string = `${pnpmStorePath}/v10/index/`; + const pnpmStoreV3FilesDir: string = `${pnpmStorePath}/v3/files/`; + const useV10Store: boolean = existsSync(pnpmStoreV10IndexDir); + const pnpmStoreDir: string = useV10Store ? pnpmStoreV10IndexDir : pnpmStoreV3FilesDir; terminal.writeLine(`Using pnpm-lock from: ${lockFilePath}`); terminal.writeLine(`Using pnpm store folder: ${pnpmStoreDir}`); @@ -167,9 +174,49 @@ export async function afterInstallAsync( const hash: string = Buffer.from(descriptionFileHash.slice(prefixIndex + 1), 'base64').toString('hex'); // The pnpm store directory has index files of package contents at paths: - // /v3/files//-index.json + // pnpm 8: /v3/files//-index.json + // pnpm 10: /v10/index//-@.json // See https://github.com/pnpm/pnpm/blob/f394cfccda7bc519ceee8c33fc9b68a0f4235532/store/cafs/src/getFilePathInCafs.ts#L33 - const indexPath: string = `${pnpmStoreDir}${hash.slice(0, 2)}/${hash.slice(2)}-index.json`; + let indexPath: string; + if (useV10Store) { + // pnpm 10 truncates integrity hashes to 32 bytes (64 hex chars) for index paths. + const truncHash: string = hash.length > 64 ? hash.slice(0, 64) : hash; + const hashDir: string = truncHash.slice(0, 2); + const hashRest: string = truncHash.slice(2); + // Build the bare name@version using context.name and version from the .pnpm folder path. + // The .pnpm folder name format is: __/node_modules/ + const pkgName: string = (context.name || '').replace(/\//g, '+'); + const pnpmSegment: string | undefined = context.descriptionFileRoot.split('/node_modules/.pnpm/')[1]; + const folderName: string = pnpmSegment ? pnpmSegment.split('/node_modules/')[0] : ''; + const namePrefix: string = `${pkgName}@`; + const nameStart: number = folderName.indexOf(namePrefix); + let nameVer: string = folderName; + if (nameStart !== -1) { + const afterName: string = folderName.slice(nameStart + namePrefix.length); + // Version ends at first _ followed by a letter/@ (peer dep separator) + const peerSep: number = afterName.search(/_[a-zA-Z@]/); + const version: string = peerSep !== -1 ? afterName.slice(0, peerSep) : afterName; + nameVer = `${pkgName}@${version}`; + } + indexPath = `${pnpmStoreDir}${hashDir}/${hashRest}-${nameVer}.json`; + // For truncated/hashed folder names, nameVer from the folder path may be wrong. + // Fallback: scan the directory for a file matching the hash prefix. + if (!existsSync(indexPath)) { + const dir: string = `${pnpmStoreDir}${hashDir}/`; + const filePrefix: string = `${hashRest}-`; + try { + const files: string[] = readdirSync(dir); + const match: string | undefined = files.find((f) => f.startsWith(filePrefix)); + if (match) { + indexPath = dir + match; + } + } catch { + // ignore + } + } + } else { + indexPath = `${pnpmStoreDir}${hash.slice(0, 2)}/${hash.slice(2)}-index.json`; + } try { const indexContent: string = await FileSystem.readFileAsync(indexPath); diff --git a/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts index e0aff3acbf3..2a5e3f203f9 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts @@ -169,6 +169,15 @@ export async function computeResolverCacheFromLockfileAsync( const contexts: Map = new Map(); const missingOptionalDependencies: Set = new Set(); + // Detect v9+ lockfile format by checking if the lockfile has shrinkwrapFileMajorVersion >= 9, + // or by checking if the first package key lacks a leading '/' (v6 keys always start with '/'). + const isV9Lockfile: boolean = + (lockfile as { shrinkwrapFileMajorVersion?: number }).shrinkwrapFileMajorVersion !== undefined + ? (lockfile as { shrinkwrapFileMajorVersion?: number }).shrinkwrapFileMajorVersion! >= 9 + : !Array.from(lockfile.packages.keys()) + .find((k) => !k.startsWith('file:')) + ?.startsWith('/'); + // Enumerate external dependencies first, to simplify looping over them for store data for (const [key, pack] of lockfile.packages) { let name: string | undefined = pack.name; @@ -187,6 +196,15 @@ export async function computeResolverCacheFromLockfileAsync( name = key.slice(1, versionIndex); } + if (!name) { + // Handle v9 lockfile keys: @scope/name@version or name@version + const searchStart: number = key.startsWith('@') ? key.indexOf('/') + 1 : 0; + const versionIndex: number = key.indexOf('@', searchStart); + if (versionIndex !== -1) { + name = key.slice(0, versionIndex); + } + } + if (!name) { throw new Error(`Missing name for ${key}`); } @@ -204,10 +222,10 @@ export async function computeResolverCacheFromLockfileAsync( contexts.set(descriptionFileRoot, context); if (pack.dependencies) { - resolveDependencies(workspaceRoot, pack.dependencies, context); + resolveDependencies(workspaceRoot, pack.dependencies, context, isV9Lockfile); } if (pack.optionalDependencies) { - resolveDependencies(workspaceRoot, pack.optionalDependencies, context); + resolveDependencies(workspaceRoot, pack.optionalDependencies, context, isV9Lockfile); } } @@ -248,13 +266,13 @@ export async function computeResolverCacheFromLockfileAsync( contexts.set(descriptionFileRoot, context); if (importer.dependencies) { - resolveDependencies(workspaceRoot, importer.dependencies, context); + resolveDependencies(workspaceRoot, importer.dependencies, context, isV9Lockfile); } if (importer.devDependencies) { - resolveDependencies(workspaceRoot, importer.devDependencies, context); + resolveDependencies(workspaceRoot, importer.devDependencies, context, isV9Lockfile); } if (importer.optionalDependencies) { - resolveDependencies(workspaceRoot, importer.optionalDependencies, context); + resolveDependencies(workspaceRoot, importer.optionalDependencies, context, isV9Lockfile); } } diff --git a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts index 99ade3da188..52e5dce2588 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts @@ -8,7 +8,9 @@ import type { ISerializedResolveContext } from '@rushstack/webpack-workspace-res import type { IDependencyEntry, IResolverContext } from './types'; -const MAX_LENGTH_WITHOUT_HASH: number = 120 - 26 - 1; +const PNPM8_MAX_LENGTH_WITHOUT_HASH: number = 120 - 26 - 1; +// pnpm 10 uses SHA-256 hex (32 chars) + underscore separator +const PNPM10_MAX_LENGTH_WITHOUT_HASH: number = 120 - 32 - 1; const BASE32: string[] = 'abcdefghijklmnopqrstuvwxyz234567'.split(''); // https://github.com/swansontec/rfc4648.js/blob/ead9c9b4b68e5d4a529f32925da02c02984e772c/src/codec.ts#L82-L118 @@ -42,14 +44,32 @@ export function createBase32Hash(input: string): string { return out; } +/** + * Creates a short SHA-256 hex hash, matching pnpm 10's createShortHash. + */ +export function createShortSha256Hash(input: string): string { + return createHash('sha256').update(input).digest('hex').substring(0, 32); +} + // https://github.com/pnpm/pnpm/blob/f394cfccda7bc519ceee8c33fc9b68a0f4235532/packages/dependency-path/src/index.ts#L167-L189 -export function depPathToFilename(depPath: string): string { +export function depPathToFilename(depPath: string, usePnpm10Hashing?: boolean): string { let filename: string = depPathToFilenameUnescaped(depPath).replace(/[\\/:*?"<>|]/g, '+'); - if (filename.includes('(')) { - filename = filename.replace(/(\)\()|\(/g, '_').replace(/\)$/, ''); - } - if (filename.length > 120 || (filename !== filename.toLowerCase() && !filename.startsWith('file+'))) { - return `${filename.substring(0, MAX_LENGTH_WITHOUT_HASH)}_${createBase32Hash(filename)}`; + if (usePnpm10Hashing) { + // pnpm 10 also replaces `#` and handles parentheses differently + filename = filename.replace(/#/g, '+'); + if (filename.includes('(')) { + filename = filename.replace(/\)$/, '').replace(/\)\(|\(|\)/g, '_'); + } + if (filename.length > 120 || (filename !== filename.toLowerCase() && !filename.startsWith('file+'))) { + return `${filename.substring(0, PNPM10_MAX_LENGTH_WITHOUT_HASH)}_${createShortSha256Hash(filename)}`; + } + } else { + if (filename.includes('(')) { + filename = filename.replace(/(\)\()|\(/g, '_').replace(/\)$/, ''); + } + if (filename.length > 120 || (filename !== filename.toLowerCase() && !filename.startsWith('file+'))) { + return `${filename.substring(0, PNPM8_MAX_LENGTH_WITHOUT_HASH)}_${createBase32Hash(filename)}`; + } } return filename; } @@ -66,7 +86,8 @@ export function resolveDependencyKey( lockfileFolder: string, key: string, specifier: string, - context: IResolverContext + context: IResolverContext, + isV9Lockfile?: boolean ): string { if (specifier.startsWith('/')) { return getDescriptionFileRootFromKey(lockfileFolder, specifier); @@ -79,7 +100,16 @@ export function resolveDependencyKey( } else if (specifier.startsWith('file:')) { return getDescriptionFileRootFromKey(lockfileFolder, specifier, key); } else { - return getDescriptionFileRootFromKey(lockfileFolder, `/${key}@${specifier}`); + // In v9 lockfiles, aliased dependency values use the full package key format + // (e.g., 'string-width@4.2.3' or '@types/events@3.0.0') instead of bare versions. + // A bare version starts with a digit; a full key starts with a letter or @. + if (/^[a-zA-Z@]/.test(specifier)) { + return getDescriptionFileRootFromKey(lockfileFolder, specifier); + } + // Construct the full dependency key from package name and version specifier. + // v6 keys use '/' prefix; v9 keys don't. + const fullKey: string = isV9Lockfile ? `${key}@${specifier}` : `/${key}@${specifier}`; + return getDescriptionFileRootFromKey(lockfileFolder, fullKey); } } @@ -91,14 +121,27 @@ export function resolveDependencyKey( * @returns The physical path to the dependency */ export function getDescriptionFileRootFromKey(lockfileFolder: string, key: string, name?: string): string { + // Detect lockfile version: v6 keys start with '/', v9 keys don't + const isV9Key: boolean = !key.startsWith('/') && !key.startsWith('file:'); + if (!key.startsWith('file:')) { - name = key.slice(1, key.indexOf('@', 2)); + if (key.startsWith('/')) { + // v6 format: /name@version or /@scope/name@version + name = key.slice(1, key.indexOf('@', 2)); + } else if (!name) { + // v9 format: name@version or @scope/name@version + const searchStart: number = key.startsWith('@') ? key.indexOf('/') + 1 : 0; + const versionIndex: number = key.indexOf('@', searchStart); + if (versionIndex !== -1) { + name = key.slice(0, versionIndex); + } + } } if (!name) { throw new Error(`Missing package name for ${key}`); } - const originFolder: string = `${lockfileFolder}/node_modules/.pnpm/${depPathToFilename(key)}/node_modules`; + const originFolder: string = `${lockfileFolder}/node_modules/.pnpm/${depPathToFilename(key, isV9Key)}/node_modules`; const descriptionFileRoot: string = `${originFolder}/${name}`; return descriptionFileRoot; } @@ -106,11 +149,12 @@ export function getDescriptionFileRootFromKey(lockfileFolder: string, key: strin export function resolveDependencies( lockfileFolder: string, collection: Record, - context: IResolverContext + context: IResolverContext, + isV9Lockfile?: boolean ): void { for (const [key, value] of Object.entries(collection)) { const version: string = typeof value === 'string' ? value : value.version; - const resolved: string = resolveDependencyKey(lockfileFolder, key, version, context); + const resolved: string = resolveDependencyKey(lockfileFolder, key, version, context, isV9Lockfile); context.deps.set(key, resolved); } diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap index 7c4d7cb6eb6..a5d81076ea7 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap @@ -6,34 +6,64 @@ exports[`createBase32Hash hashes: a 1`] = `"btaxlooa6g3kqmodthrgs5zgme"`; exports[`createBase32Hash hashes: abracadabra 1`] = `"5rjiprc7bzyoyiwvf2f4x3vwia"`; -exports[`depPathToFilename formats: /@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1) 1`] = `"@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge-style_yt7yh6tpppbzu7nx3lzx3f3ife"`; +exports[`createShortSha256Hash hashes: (eslint@8.57.0)(typescript@5.4.5) 1`] = `"395951816c5613fa894c6f81441c9d08"`; -exports[`depPathToFilename formats: /@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1) 1`] = `"@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6.5.15__wnqw6tyxzeiksxiymflshxscdq"`; +exports[`createShortSha256Hash hashes: a 1`] = `"ca978112ca1bbdcafac231b39a23dc4d"`; -exports[`depPathToFilename formats: /@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2) 1`] = `"@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2"`; +exports[`createShortSha256Hash hashes: abracadabra 1`] = `"045babdcd2118960e8c8b8e0ecf65b73"`; -exports[`depPathToFilename formats: /autoprefixer@9.8.8 1`] = `"autoprefixer@9.8.8"`; +exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1) 1`] = `"@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge-style_yt7yh6tpppbzu7nx3lzx3f3ife"`; -exports[`depPathToFilename formats: /autoprefixer@10.4.18(postcss@8.4.36) 1`] = `"autoprefixer@10.4.18_postcss@8.4.36"`; +exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1) 1`] = `"@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6.5.15__wnqw6tyxzeiksxiymflshxscdq"`; -exports[`depPathToFilename formats: /react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2) 1`] = `"react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2"`; +exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2) 1`] = `"@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2"`; -exports[`depPathToFilename formats: file:../../../libraries/ts-command-line(@types/node@18.17.15) 1`] = `"file+..+..+..+libraries+ts-command-line_@types+node@18.17.15"`; +exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /autoprefixer@9.8.8 1`] = `"autoprefixer@9.8.8"`; -exports[`depPathToFilename formats: file:../../../rigs/local-node-rig 1`] = `"file+..+..+..+rigs+local-node-rig"`; +exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /autoprefixer@10.4.18(postcss@8.4.36) 1`] = `"autoprefixer@10.4.18_postcss@8.4.36"`; -exports[`getDescriptionFileRootFromKey parses: "/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)",undefined 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge-style_yt7yh6tpppbzu7nx3lzx3f3ife/node_modules/@some/package"`; +exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2) 1`] = `"react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2"`; -exports[`getDescriptionFileRootFromKey parses: "/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)",undefined 1`] = `"/$/node_modules/.pnpm/@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6.5.15__wnqw6tyxzeiksxiymflshxscdq/node_modules/@storybook/core"`; +exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: file:../../../libraries/ts-command-line(@types/node@18.17.15) 1`] = `"file+..+..+..+libraries+ts-command-line_@types+node@18.17.15"`; -exports[`getDescriptionFileRootFromKey parses: "/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)",undefined 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; +exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: file:../../../rigs/local-node-rig 1`] = `"file+..+..+..+rigs+local-node-rig"`; -exports[`getDescriptionFileRootFromKey parses: "/autoprefixer@9.8.8",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; +exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: @fluentui/react-migration-v8-v9@9.9.7(@types/react-dom@17.0.17)(@types/react@17.0.45)(react-dom@17.0.1)(react@17.0.1) 1`] = `"@fluentui+react-migration-v8-v9@9.9.7_@types+react-dom@17.0.17_@types+react@17.0.45_react-dom@17.0.1_react@17.0.1"`; -exports[`getDescriptionFileRootFromKey parses: "/autoprefixer@10.4.18(postcss@8.4.36)",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; +exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: @some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0) 1`] = `"@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0"`; -exports[`getDescriptionFileRootFromKey parses: "/react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)",undefined 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; +exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: @typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2) 1`] = `"@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2"`; -exports[`getDescriptionFileRootFromKey parses: "file:../../../libraries/ts-command-line(@types/node@18.17.15)",@rushstack/ts-command-line 1`] = `"/$/node_modules/.pnpm/file+..+..+..+libraries+ts-command-line_@types+node@18.17.15/node_modules/@rushstack/ts-command-line"`; +exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: autoprefixer@9.8.8 1`] = `"autoprefixer@9.8.8"`; -exports[`getDescriptionFileRootFromKey parses: "file:../../../rigs/local-node-rig",local-node-rig 1`] = `"/$/node_modules/.pnpm/file+..+..+..+rigs+local-node-rig/node_modules/local-node-rig"`; +exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: autoprefixer@10.4.18(postcss@8.4.36) 1`] = `"autoprefixer@10.4.18_postcss@8.4.36"`; + +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)",undefined 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge-style_yt7yh6tpppbzu7nx3lzx3f3ife/node_modules/@some/package"`; + +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)",undefined 1`] = `"/$/node_modules/.pnpm/@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6.5.15__wnqw6tyxzeiksxiymflshxscdq/node_modules/@storybook/core"`; + +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)",undefined 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; + +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/autoprefixer@9.8.8",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; + +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/autoprefixer@10.4.18(postcss@8.4.36)",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; + +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)",undefined 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; + +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "file:../../../libraries/ts-command-line(@types/node@18.17.15)",@rushstack/ts-command-line 1`] = `"/$/node_modules/.pnpm/file+..+..+..+libraries+ts-command-line_@types+node@18.17.15/node_modules/@rushstack/ts-command-line"`; + +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "file:../../../rigs/local-node-rig",local-node-rig 1`] = `"/$/node_modules/.pnpm/file+..+..+..+rigs+local-node-rig/node_modules/local-node-rig"`; + +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)",undefined 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0/node_modules/@some/package"`; + +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)",undefined 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; + +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "autoprefixer@9.8.8",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; + +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "autoprefixer@10.4.18(postcss@8.4.36)",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; + +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "file:../../../libraries/ts-command-line(@types/node@18.17.15)",@rushstack/ts-command-line 1`] = `"/$/node_modules/.pnpm/file+..+..+..+libraries+ts-command-line_@types+node@18.17.15/node_modules/@rushstack/ts-command-line"`; + +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "file:../../../rigs/local-node-rig",local-node-rig 1`] = `"/$/node_modules/.pnpm/file+..+..+..+rigs+local-node-rig/node_modules/local-node-rig"`; + +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)",undefined 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts index c26e3eba9b6..b9a171abec2 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts @@ -1,7 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { createBase32Hash, depPathToFilename, getDescriptionFileRootFromKey } from '../helpers'; +import { + createBase32Hash, + createShortSha256Hash, + depPathToFilename, + getDescriptionFileRootFromKey +} from '../helpers'; describe(createBase32Hash.name, () => { it('hashes', () => { @@ -11,8 +16,16 @@ describe(createBase32Hash.name, () => { }); }); +describe(createShortSha256Hash.name, () => { + it('hashes', () => { + for (const input of ['a', 'abracadabra', '(eslint@8.57.0)(typescript@5.4.5)']) { + expect(createShortSha256Hash(input)).toMatchSnapshot(input); + } + }); +}); + describe(depPathToFilename.name, () => { - it('formats', () => { + it('formats v6 keys (leading /) with pnpm 8 hashing', () => { for (const input of [ '/autoprefixer@9.8.8', '/autoprefixer@10.4.18(postcss@8.4.36)', @@ -26,10 +39,22 @@ describe(depPathToFilename.name, () => { expect(depPathToFilename(input)).toMatchSnapshot(input); } }); + + it('formats v9 keys (no leading /) with pnpm 10 hashing', () => { + for (const input of [ + 'autoprefixer@9.8.8', + 'autoprefixer@10.4.18(postcss@8.4.36)', + '@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)', + '@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)', + '@fluentui/react-migration-v8-v9@9.9.7(@types/react-dom@17.0.17)(@types/react@17.0.45)(react-dom@17.0.1)(react@17.0.1)' + ]) { + expect(depPathToFilename(input, true)).toMatchSnapshot(input); + } + }); }); describe(getDescriptionFileRootFromKey.name, () => { - it('parses', () => { + it('parses v6 keys (leading /)', () => { const lockfileRoot: string = '/$'; for (const { key, name } of [ { key: '/autoprefixer@9.8.8' }, @@ -51,4 +76,24 @@ describe(getDescriptionFileRootFromKey.name, () => { expect(getDescriptionFileRootFromKey(lockfileRoot, key, name)).toMatchSnapshot(`"${key}",${name}`); } }); + + it('parses v9 keys (no leading /)', () => { + const lockfileRoot: string = '/$'; + for (const { key, name } of [ + { key: 'autoprefixer@9.8.8' }, + { key: 'autoprefixer@10.4.18(postcss@8.4.36)' }, + { key: 'react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)' }, + { + key: '@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)' + }, + { key: '@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)' }, + { key: 'file:../../../rigs/local-node-rig', name: 'local-node-rig' }, + { + key: 'file:../../../libraries/ts-command-line(@types/node@18.17.15)', + name: '@rushstack/ts-command-line' + } + ]) { + expect(getDescriptionFileRootFromKey(lockfileRoot, key, name)).toMatchSnapshot(`"${key}",${name}`); + } + }); }); From 44fb50b759b4e1d27b24f5d4f8a3e08c7d9818c5 Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:40:49 +0000 Subject: [PATCH 2/5] Address PR #5749 review feedback - Hoist regexes in depPathToFilename to module-level constants - Unify v6/v9 branches in getDescriptionFileRootFromKey using offset - Check specifier against packages list instead of regex heuristic - Extract detectV9Lockfile helper (iterates with early return, no clone) - Store version on IResolverContext when parsing lockfile keys - Extract getStoreIndexPath helper in afterInstallAsync - Use context.version instead of .split() on hot path - Fix undefined in snapshot test names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/afterInstallAsync.ts | 79 ++++++++----------- .../computeResolverCacheFromLockfileAsync.ts | 67 ++++++++++------ .../rush-resolver-cache-plugin/src/helpers.ts | 57 ++++++------- .../test/__snapshots__/helpers.test.ts.snap | 22 +++--- .../src/test/helpers.test.ts | 8 +- .../rush-resolver-cache-plugin/src/types.ts | 1 + 6 files changed, 125 insertions(+), 109 deletions(-) diff --git a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts index 0191dd1cc1e..cc4e75d37d0 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts @@ -143,6 +143,40 @@ export async function afterInstallAsync( // Ignore } + /** + * Computes the pnpm store index file path for a given package integrity hash. + */ + function getStoreIndexPath(context: IResolverContext, hash: string): string { + if (!useV10Store) { + return `${pnpmStoreDir}${hash.slice(0, 2)}/${hash.slice(2)}-index.json`; + } + + // pnpm 10 truncates integrity hashes to 32 bytes (64 hex chars) for index paths. + const truncHash: string = hash.length > 64 ? hash.slice(0, 64) : hash; + const hashDir: string = truncHash.slice(0, 2); + const hashRest: string = truncHash.slice(2); + // pnpm 10 index path format: /-@.json + const pkgName: string = (context.name || '').replace(/\//g, '+'); + const nameVer: string = context.version ? `${pkgName}@${context.version}` : pkgName; + let indexPath: string = `${pnpmStoreDir}${hashDir}/${hashRest}-${nameVer}.json`; + // For truncated/hashed folder names, nameVer from the key may be wrong. + // Fallback: scan the directory for a file matching the hash prefix. + if (!existsSync(indexPath)) { + const dir: string = `${pnpmStoreDir}${hashDir}/`; + const filePrefix: string = `${hashRest}-`; + try { + const files: string[] = readdirSync(dir); + const match: string | undefined = files.find((f) => f.startsWith(filePrefix)); + if (match) { + indexPath = dir + match; + } + } catch { + // ignore + } + } + return indexPath; + } + async function afterExternalPackagesAsync( contexts: Map, missingOptionalDependencies: Set @@ -173,50 +207,7 @@ export async function afterInstallAsync( const prefixIndex: number = descriptionFileHash.indexOf('-'); const hash: string = Buffer.from(descriptionFileHash.slice(prefixIndex + 1), 'base64').toString('hex'); - // The pnpm store directory has index files of package contents at paths: - // pnpm 8: /v3/files//-index.json - // pnpm 10: /v10/index//-@.json - // See https://github.com/pnpm/pnpm/blob/f394cfccda7bc519ceee8c33fc9b68a0f4235532/store/cafs/src/getFilePathInCafs.ts#L33 - let indexPath: string; - if (useV10Store) { - // pnpm 10 truncates integrity hashes to 32 bytes (64 hex chars) for index paths. - const truncHash: string = hash.length > 64 ? hash.slice(0, 64) : hash; - const hashDir: string = truncHash.slice(0, 2); - const hashRest: string = truncHash.slice(2); - // Build the bare name@version using context.name and version from the .pnpm folder path. - // The .pnpm folder name format is: __/node_modules/ - const pkgName: string = (context.name || '').replace(/\//g, '+'); - const pnpmSegment: string | undefined = context.descriptionFileRoot.split('/node_modules/.pnpm/')[1]; - const folderName: string = pnpmSegment ? pnpmSegment.split('/node_modules/')[0] : ''; - const namePrefix: string = `${pkgName}@`; - const nameStart: number = folderName.indexOf(namePrefix); - let nameVer: string = folderName; - if (nameStart !== -1) { - const afterName: string = folderName.slice(nameStart + namePrefix.length); - // Version ends at first _ followed by a letter/@ (peer dep separator) - const peerSep: number = afterName.search(/_[a-zA-Z@]/); - const version: string = peerSep !== -1 ? afterName.slice(0, peerSep) : afterName; - nameVer = `${pkgName}@${version}`; - } - indexPath = `${pnpmStoreDir}${hashDir}/${hashRest}-${nameVer}.json`; - // For truncated/hashed folder names, nameVer from the folder path may be wrong. - // Fallback: scan the directory for a file matching the hash prefix. - if (!existsSync(indexPath)) { - const dir: string = `${pnpmStoreDir}${hashDir}/`; - const filePrefix: string = `${hashRest}-`; - try { - const files: string[] = readdirSync(dir); - const match: string | undefined = files.find((f) => f.startsWith(filePrefix)); - if (match) { - indexPath = dir + match; - } - } catch { - // ignore - } - } - } else { - indexPath = `${pnpmStoreDir}${hash.slice(0, 2)}/${hash.slice(2)}-index.json`; - } + const indexPath: string = getStoreIndexPath(context, hash); try { const indexContent: string = await FileSystem.readFileAsync(indexPath); diff --git a/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts index 2a5e3f203f9..4575f09cb38 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts @@ -152,6 +152,24 @@ function convertToSlashes(path: string): string { return path.replace(/\\/g, '/'); } +/** + * Detects whether the lockfile uses v9+ format by checking the major version + * or inspecting the first non-file package key. + */ +function detectV9Lockfile(lockfile: PnpmShrinkwrapFile): boolean { + const majorVersion: number | undefined = (lockfile as { shrinkwrapFileMajorVersion?: number }) + .shrinkwrapFileMajorVersion; + if (majorVersion !== undefined) { + return majorVersion >= 9; + } + for (const key of lockfile.packages.keys()) { + if (!key.startsWith('file:')) { + return !key.startsWith('/'); + } + } + return false; +} + /** * Given a lockfile and information about the workspace and platform, computes the resolver cache file. * @param params - The options for computing the resolver cache @@ -169,14 +187,7 @@ export async function computeResolverCacheFromLockfileAsync( const contexts: Map = new Map(); const missingOptionalDependencies: Set = new Set(); - // Detect v9+ lockfile format by checking if the lockfile has shrinkwrapFileMajorVersion >= 9, - // or by checking if the first package key lacks a leading '/' (v6 keys always start with '/'). - const isV9Lockfile: boolean = - (lockfile as { shrinkwrapFileMajorVersion?: number }).shrinkwrapFileMajorVersion !== undefined - ? (lockfile as { shrinkwrapFileMajorVersion?: number }).shrinkwrapFileMajorVersion! >= 9 - : !Array.from(lockfile.packages.keys()) - .find((k) => !k.startsWith('file:')) - ?.startsWith('/'); + const isV9Lockfile: boolean = detectV9Lockfile(lockfile); // Enumerate external dependencies first, to simplify looping over them for store data for (const [key, pack] of lockfile.packages) { @@ -191,17 +202,18 @@ export async function computeResolverCacheFromLockfileAsync( const integrity: string | undefined = pack.resolution?.integrity; - if (!name && key.startsWith('/')) { - const versionIndex: number = key.indexOf('@', 2); - name = key.slice(1, versionIndex); - } - - if (!name) { - // Handle v9 lockfile keys: @scope/name@version or name@version - const searchStart: number = key.startsWith('@') ? key.indexOf('/') + 1 : 0; - const versionIndex: number = key.indexOf('@', searchStart); - if (versionIndex !== -1) { - name = key.slice(0, versionIndex); + // Extract name and version from the key if not already provided + let version: string | undefined; + if (!key.startsWith('file:')) { + const offset: number = key.startsWith('/') ? 1 : 0; + const versionAtIndex: number = key.indexOf('@', offset + 1); + if (versionAtIndex !== -1) { + if (!name) { + name = key.slice(offset, versionAtIndex); + } + const parenIndex: number = key.indexOf('(', versionAtIndex); + version = + parenIndex !== -1 ? key.slice(versionAtIndex + 1, parenIndex) : key.slice(versionAtIndex + 1); } } @@ -214,6 +226,7 @@ export async function computeResolverCacheFromLockfileAsync( descriptionFileHash: integrity, isProject: false, name, + version, deps: new Map(), ordinal: -1, optional: pack.optional @@ -222,10 +235,10 @@ export async function computeResolverCacheFromLockfileAsync( contexts.set(descriptionFileRoot, context); if (pack.dependencies) { - resolveDependencies(workspaceRoot, pack.dependencies, context, isV9Lockfile); + resolveDependencies(workspaceRoot, pack.dependencies, context, isV9Lockfile, lockfile.packages); } if (pack.optionalDependencies) { - resolveDependencies(workspaceRoot, pack.optionalDependencies, context, isV9Lockfile); + resolveDependencies(workspaceRoot, pack.optionalDependencies, context, isV9Lockfile, lockfile.packages); } } @@ -266,13 +279,19 @@ export async function computeResolverCacheFromLockfileAsync( contexts.set(descriptionFileRoot, context); if (importer.dependencies) { - resolveDependencies(workspaceRoot, importer.dependencies, context, isV9Lockfile); + resolveDependencies(workspaceRoot, importer.dependencies, context, isV9Lockfile, lockfile.packages); } if (importer.devDependencies) { - resolveDependencies(workspaceRoot, importer.devDependencies, context, isV9Lockfile); + resolveDependencies(workspaceRoot, importer.devDependencies, context, isV9Lockfile, lockfile.packages); } if (importer.optionalDependencies) { - resolveDependencies(workspaceRoot, importer.optionalDependencies, context, isV9Lockfile); + resolveDependencies( + workspaceRoot, + importer.optionalDependencies, + context, + isV9Lockfile, + lockfile.packages + ); } } diff --git a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts index 52e5dce2588..65fbf23995a 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts @@ -13,6 +13,12 @@ const PNPM8_MAX_LENGTH_WITHOUT_HASH: number = 120 - 26 - 1; const PNPM10_MAX_LENGTH_WITHOUT_HASH: number = 120 - 32 - 1; const BASE32: string[] = 'abcdefghijklmnopqrstuvwxyz234567'.split(''); +const SPECIAL_CHARS_REGEX: RegExp = /[\\/:*?"<>|]/g; +const HASH_CHAR_REGEX: RegExp = /#/g; +const TRAILING_PAREN_REGEX: RegExp = /\)$/; +const PNPM10_PARENS_REGEX: RegExp = /\)\(|\(|\)/g; +const PNPM8_PARENS_REGEX: RegExp = /(\)\()|\(/g; + // https://github.com/swansontec/rfc4648.js/blob/ead9c9b4b68e5d4a529f32925da02c02984e772c/src/codec.ts#L82-L118 export function createBase32Hash(input: string): string { const data: Buffer = createHash('md5').update(input).digest(); @@ -53,19 +59,19 @@ export function createShortSha256Hash(input: string): string { // https://github.com/pnpm/pnpm/blob/f394cfccda7bc519ceee8c33fc9b68a0f4235532/packages/dependency-path/src/index.ts#L167-L189 export function depPathToFilename(depPath: string, usePnpm10Hashing?: boolean): string { - let filename: string = depPathToFilenameUnescaped(depPath).replace(/[\\/:*?"<>|]/g, '+'); + let filename: string = depPathToFilenameUnescaped(depPath).replace(SPECIAL_CHARS_REGEX, '+'); if (usePnpm10Hashing) { // pnpm 10 also replaces `#` and handles parentheses differently - filename = filename.replace(/#/g, '+'); + filename = filename.replace(HASH_CHAR_REGEX, '+'); if (filename.includes('(')) { - filename = filename.replace(/\)$/, '').replace(/\)\(|\(|\)/g, '_'); + filename = filename.replace(TRAILING_PAREN_REGEX, '').replace(PNPM10_PARENS_REGEX, '_'); } if (filename.length > 120 || (filename !== filename.toLowerCase() && !filename.startsWith('file+'))) { return `${filename.substring(0, PNPM10_MAX_LENGTH_WITHOUT_HASH)}_${createShortSha256Hash(filename)}`; } } else { if (filename.includes('(')) { - filename = filename.replace(/(\)\()|\(/g, '_').replace(/\)$/, ''); + filename = filename.replace(PNPM8_PARENS_REGEX, '_').replace(TRAILING_PAREN_REGEX, ''); } if (filename.length > 120 || (filename !== filename.toLowerCase() && !filename.startsWith('file+'))) { return `${filename.substring(0, PNPM8_MAX_LENGTH_WITHOUT_HASH)}_${createBase32Hash(filename)}`; @@ -87,11 +93,10 @@ export function resolveDependencyKey( key: string, specifier: string, context: IResolverContext, - isV9Lockfile?: boolean + isV9Lockfile?: boolean, + packageKeys?: { has(key: string): boolean } ): string { - if (specifier.startsWith('/')) { - return getDescriptionFileRootFromKey(lockfileFolder, specifier); - } else if (specifier.startsWith('link:')) { + if (specifier.startsWith('link:')) { if (context.isProject) { return path.posix.join(context.descriptionFileRoot, specifier.slice(5)); } else { @@ -99,13 +104,10 @@ export function resolveDependencyKey( } } else if (specifier.startsWith('file:')) { return getDescriptionFileRootFromKey(lockfileFolder, specifier, key); + } else if (packageKeys?.has(specifier)) { + // The specifier is a full package key (v6: '/pkg@ver', v9: 'pkg@ver') + return getDescriptionFileRootFromKey(lockfileFolder, specifier); } else { - // In v9 lockfiles, aliased dependency values use the full package key format - // (e.g., 'string-width@4.2.3' or '@types/events@3.0.0') instead of bare versions. - // A bare version starts with a digit; a full key starts with a letter or @. - if (/^[a-zA-Z@]/.test(specifier)) { - return getDescriptionFileRootFromKey(lockfileFolder, specifier); - } // Construct the full dependency key from package name and version specifier. // v6 keys use '/' prefix; v9 keys don't. const fullKey: string = isV9Lockfile ? `${key}@${specifier}` : `/${key}@${specifier}`; @@ -124,18 +126,9 @@ export function getDescriptionFileRootFromKey(lockfileFolder: string, key: strin // Detect lockfile version: v6 keys start with '/', v9 keys don't const isV9Key: boolean = !key.startsWith('/') && !key.startsWith('file:'); - if (!key.startsWith('file:')) { - if (key.startsWith('/')) { - // v6 format: /name@version or /@scope/name@version - name = key.slice(1, key.indexOf('@', 2)); - } else if (!name) { - // v9 format: name@version or @scope/name@version - const searchStart: number = key.startsWith('@') ? key.indexOf('/') + 1 : 0; - const versionIndex: number = key.indexOf('@', searchStart); - if (versionIndex !== -1) { - name = key.slice(0, versionIndex); - } - } + if (!key.startsWith('file:') && !name) { + const offset: number = key.startsWith('/') ? 1 : 0; + name = key.slice(offset, key.indexOf('@', offset + 1)); } if (!name) { throw new Error(`Missing package name for ${key}`); @@ -150,11 +143,19 @@ export function resolveDependencies( lockfileFolder: string, collection: Record, context: IResolverContext, - isV9Lockfile?: boolean + isV9Lockfile?: boolean, + packageKeys?: { has(key: string): boolean } ): void { for (const [key, value] of Object.entries(collection)) { const version: string = typeof value === 'string' ? value : value.version; - const resolved: string = resolveDependencyKey(lockfileFolder, key, version, context, isV9Lockfile); + const resolved: string = resolveDependencyKey( + lockfileFolder, + key, + version, + context, + isV9Lockfile, + packageKeys + ); context.deps.set(key, resolved); } diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap index a5d81076ea7..024dd750159 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap @@ -38,32 +38,32 @@ exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: autoprefixer@10.4.18(postcss@8.4.36) 1`] = `"autoprefixer@10.4.18_postcss@8.4.36"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)",undefined 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge-style_yt7yh6tpppbzu7nx3lzx3f3ife/node_modules/@some/package"`; +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)", 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge-style_yt7yh6tpppbzu7nx3lzx3f3ife/node_modules/@some/package"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)",undefined 1`] = `"/$/node_modules/.pnpm/@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6.5.15__wnqw6tyxzeiksxiymflshxscdq/node_modules/@storybook/core"`; +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)", 1`] = `"/$/node_modules/.pnpm/@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6.5.15__wnqw6tyxzeiksxiymflshxscdq/node_modules/@storybook/core"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)",undefined 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)", 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/autoprefixer@9.8.8",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/autoprefixer@9.8.8", 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/autoprefixer@10.4.18(postcss@8.4.36)",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/autoprefixer@10.4.18(postcss@8.4.36)", 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)",undefined 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)", 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "file:../../../libraries/ts-command-line(@types/node@18.17.15)",@rushstack/ts-command-line 1`] = `"/$/node_modules/.pnpm/file+..+..+..+libraries+ts-command-line_@types+node@18.17.15/node_modules/@rushstack/ts-command-line"`; exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "file:../../../rigs/local-node-rig",local-node-rig 1`] = `"/$/node_modules/.pnpm/file+..+..+..+rigs+local-node-rig/node_modules/local-node-rig"`; -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)",undefined 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0/node_modules/@some/package"`; +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)", 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0/node_modules/@some/package"`; -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)",undefined 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)", 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "autoprefixer@9.8.8",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "autoprefixer@9.8.8", 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "autoprefixer@10.4.18(postcss@8.4.36)",undefined 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "autoprefixer@10.4.18(postcss@8.4.36)", 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "file:../../../libraries/ts-command-line(@types/node@18.17.15)",@rushstack/ts-command-line 1`] = `"/$/node_modules/.pnpm/file+..+..+..+libraries+ts-command-line_@types+node@18.17.15/node_modules/@rushstack/ts-command-line"`; exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "file:../../../rigs/local-node-rig",local-node-rig 1`] = `"/$/node_modules/.pnpm/file+..+..+..+rigs+local-node-rig/node_modules/local-node-rig"`; -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)",undefined 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; +exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)", 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts index b9a171abec2..be5aa5b59b8 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts @@ -73,7 +73,9 @@ describe(getDescriptionFileRootFromKey.name, () => { name: '@rushstack/ts-command-line' } ]) { - expect(getDescriptionFileRootFromKey(lockfileRoot, key, name)).toMatchSnapshot(`"${key}",${name}`); + expect(getDescriptionFileRootFromKey(lockfileRoot, key, name)).toMatchSnapshot( + `"${key}",${name || ''}` + ); } }); @@ -93,7 +95,9 @@ describe(getDescriptionFileRootFromKey.name, () => { name: '@rushstack/ts-command-line' } ]) { - expect(getDescriptionFileRootFromKey(lockfileRoot, key, name)).toMatchSnapshot(`"${key}",${name}`); + expect(getDescriptionFileRootFromKey(lockfileRoot, key, name)).toMatchSnapshot( + `"${key}",${name || ''}` + ); } }); }); diff --git a/rush-plugins/rush-resolver-cache-plugin/src/types.ts b/rush-plugins/rush-resolver-cache-plugin/src/types.ts index c58938042d4..f32953fd67b 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/types.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/types.ts @@ -5,6 +5,7 @@ export interface IResolverContext { descriptionFileRoot: string; descriptionFileHash: string | undefined; name: string; + version?: string; deps: Map; isProject: boolean; ordinal: number; From de5dad1a16f77a667b5d736cdd59a1413adb7f9d Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:15:04 +0000 Subject: [PATCH 3/5] Address second round of PR #5749 review feedback - Use readdirSync with { withFileTypes: true } to match only files - Use lockfile.shrinkwrapFileMajorVersion directly (no cast needed) - Extract extractNameAndVersionFromKey helper with unit tests - Add comment explaining why depPathToFilename is inlined vs @pnpm/dependency-path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/afterInstallAsync.ts | 8 +-- .../computeResolverCacheFromLockfileAsync.ts | 31 +++++------ .../rush-resolver-cache-plugin/src/helpers.ts | 24 +++++++++ .../src/test/helpers.test.ts | 54 ++++++++++++++++++- 4 files changed, 96 insertions(+), 21 deletions(-) diff --git a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts index cc4e75d37d0..71e752a8260 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts @@ -165,10 +165,12 @@ export async function afterInstallAsync( const dir: string = `${pnpmStoreDir}${hashDir}/`; const filePrefix: string = `${hashRest}-`; try { - const files: string[] = readdirSync(dir); - const match: string | undefined = files.find((f) => f.startsWith(filePrefix)); + const entries: import('node:fs').Dirent[] = readdirSync(dir, { withFileTypes: true }); + const match: import('node:fs').Dirent | undefined = entries.find( + (e) => e.isFile() && e.name.startsWith(filePrefix) + ); if (match) { - indexPath = dir + match; + indexPath = dir + match.name; } } catch { // ignore diff --git a/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts index 4575f09cb38..094aeda16f8 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts @@ -9,7 +9,12 @@ import type { } from '@rushstack/webpack-workspace-resolve-plugin'; import type { PnpmShrinkwrapFile } from './externals'; -import { getDescriptionFileRootFromKey, resolveDependencies, createContextSerializer } from './helpers'; +import { + getDescriptionFileRootFromKey, + resolveDependencies, + createContextSerializer, + extractNameAndVersionFromKey +} from './helpers'; import type { IResolverContext } from './types'; /** @@ -157,11 +162,10 @@ function convertToSlashes(path: string): string { * or inspecting the first non-file package key. */ function detectV9Lockfile(lockfile: PnpmShrinkwrapFile): boolean { - const majorVersion: number | undefined = (lockfile as { shrinkwrapFileMajorVersion?: number }) - .shrinkwrapFileMajorVersion; - if (majorVersion !== undefined) { - return majorVersion >= 9; + if (lockfile.shrinkwrapFileMajorVersion > 0) { + return lockfile.shrinkwrapFileMajorVersion >= 9; } + // Fallback for lockfiles where version parsing failed: inspect the first non-file package key. for (const key of lockfile.packages.keys()) { if (!key.startsWith('file:')) { return !key.startsWith('/'); @@ -203,17 +207,10 @@ export async function computeResolverCacheFromLockfileAsync( const integrity: string | undefined = pack.resolution?.integrity; // Extract name and version from the key if not already provided - let version: string | undefined; - if (!key.startsWith('file:')) { - const offset: number = key.startsWith('/') ? 1 : 0; - const versionAtIndex: number = key.indexOf('@', offset + 1); - if (versionAtIndex !== -1) { - if (!name) { - name = key.slice(offset, versionAtIndex); - } - const parenIndex: number = key.indexOf('(', versionAtIndex); - version = - parenIndex !== -1 ? key.slice(versionAtIndex + 1, parenIndex) : key.slice(versionAtIndex + 1); + const parsed: { name: string; version: string } | undefined = extractNameAndVersionFromKey(key); + if (parsed) { + if (!name) { + name = parsed.name; } } @@ -226,7 +223,7 @@ export async function computeResolverCacheFromLockfileAsync( descriptionFileHash: integrity, isProject: false, name, - version, + version: parsed?.version, deps: new Map(), ordinal: -1, optional: pack.optional diff --git a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts index 65fbf23995a..0202caf0a51 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts @@ -57,6 +57,9 @@ export function createShortSha256Hash(input: string): string { return createHash('sha256').update(input).digest('hex').substring(0, 32); } +// Mirrors @pnpm/dependency-path depPathToFilename. +// We inline it rather than importing two aliased versions of the package (v8 and v10) +// since each brings transitive deps (@pnpm/crypto.hash, @pnpm/types, semver). // https://github.com/pnpm/pnpm/blob/f394cfccda7bc519ceee8c33fc9b68a0f4235532/packages/dependency-path/src/index.ts#L167-L189 export function depPathToFilename(depPath: string, usePnpm10Hashing?: boolean): string { let filename: string = depPathToFilenameUnescaped(depPath).replace(SPECIAL_CHARS_REGEX, '+'); @@ -161,6 +164,27 @@ export function resolveDependencies( } } +/** + * Extracts the package name and version from a lockfile package key. + * @param key - The lockfile package key (e.g. '/autoprefixer\@9.8.8', '\@scope/name\@1.0.0(peer\@2.0.0)') + * @returns The extracted name and version, or undefined for file: keys + */ +export function extractNameAndVersionFromKey(key: string): { name: string; version: string } | undefined { + if (key.startsWith('file:')) { + return undefined; + } + const offset: number = key.startsWith('/') ? 1 : 0; + const versionAtIndex: number = key.indexOf('@', offset + 1); + if (versionAtIndex === -1) { + return undefined; + } + const name: string = key.slice(offset, versionAtIndex); + const parenIndex: number = key.indexOf('(', versionAtIndex); + const version: string = + parenIndex !== -1 ? key.slice(versionAtIndex + 1, parenIndex) : key.slice(versionAtIndex + 1); + return { name, version }; +} + /** * * @param depPath - The path to the dependency diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts index be5aa5b59b8..717fb3ecf70 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts @@ -5,7 +5,8 @@ import { createBase32Hash, createShortSha256Hash, depPathToFilename, - getDescriptionFileRootFromKey + getDescriptionFileRootFromKey, + extractNameAndVersionFromKey } from '../helpers'; describe(createBase32Hash.name, () => { @@ -101,3 +102,54 @@ describe(getDescriptionFileRootFromKey.name, () => { } }); }); + +describe(extractNameAndVersionFromKey.name, () => { + it('extracts name and version from v6 keys (leading /)', () => { + expect(extractNameAndVersionFromKey('/autoprefixer@9.8.8')).toEqual({ + name: 'autoprefixer', + version: '9.8.8' + }); + expect(extractNameAndVersionFromKey('/autoprefixer@10.4.18(postcss@8.4.36)')).toEqual({ + name: 'autoprefixer', + version: '10.4.18' + }); + expect(extractNameAndVersionFromKey('/@some/package@1.2.3(@azure/msal-browser@2.28.1)')).toEqual({ + name: '@some/package', + version: '1.2.3' + }); + expect( + extractNameAndVersionFromKey('/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)') + ).toEqual({ + name: '@typescript-eslint/utils', + version: '6.19.1' + }); + }); + + it('extracts name and version from v9 keys (no leading /)', () => { + expect(extractNameAndVersionFromKey('autoprefixer@9.8.8')).toEqual({ + name: 'autoprefixer', + version: '9.8.8' + }); + expect(extractNameAndVersionFromKey('autoprefixer@10.4.18(postcss@8.4.36)')).toEqual({ + name: 'autoprefixer', + version: '10.4.18' + }); + expect(extractNameAndVersionFromKey('@some/package@1.2.3(@azure/msal-browser@2.28.1)')).toEqual({ + name: '@some/package', + version: '1.2.3' + }); + expect( + extractNameAndVersionFromKey('@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)') + ).toEqual({ + name: '@typescript-eslint/utils', + version: '6.19.1' + }); + }); + + it('returns undefined for file: keys', () => { + expect(extractNameAndVersionFromKey('file:../../../rigs/local-node-rig')).toBeUndefined(); + expect( + extractNameAndVersionFromKey('file:../../../libraries/ts-command-line(@types/node@18.17.15)') + ).toBeUndefined(); + }); +}); From 807778ebebb5677ddb24793d3952e8cf0afd58ab Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:35:35 +0000 Subject: [PATCH 4/5] Replace hand-rolled depPathToFilename with @pnpm/dependency-path package - Add @pnpm/dependency-path as a dependency - Remove custom depPathToFilename, createBase32Hash, createShortSha256Hash, depPathToFilenameUnescaped, and associated regex/hash constants - Remove isV9Lockfile parameter from resolveDependencyKey/resolveDependencies - Remove detectV9Lockfile helper (no longer needed) - Remove unit tests for removed custom hash/filename functions - Update snapshots to reflect pnpm 10 hashing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../config/subspaces/default/pnpm-lock.yaml | 3 + .../rush-resolver-cache-plugin/package.json | 1 + .../computeResolverCacheFromLockfileAsync.ts | 35 +----- .../rush-resolver-cache-plugin/src/helpers.ts | 114 ++---------------- ...esolverCacheFromLockfileAsync.test.ts.snap | 70 +++++------ .../test/__snapshots__/helpers.test.ts.snap | 42 +------ .../src/test/helpers.test.ts | 53 +------- 7 files changed, 54 insertions(+), 264 deletions(-) diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index af1789b7858..3df81a5c536 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -5151,6 +5151,9 @@ importers: ../../../rush-plugins/rush-resolver-cache-plugin: dependencies: + '@pnpm/dependency-path': + specifier: 1000.0.9 + version: 1000.0.9 '@rushstack/rush-sdk': specifier: workspace:* version: link:../../libraries/rush-sdk diff --git a/rush-plugins/rush-resolver-cache-plugin/package.json b/rush-plugins/rush-resolver-cache-plugin/package.json index 5ed67092bde..e6d40861d87 100644 --- a/rush-plugins/rush-resolver-cache-plugin/package.json +++ b/rush-plugins/rush-resolver-cache-plugin/package.json @@ -17,6 +17,7 @@ "_phase:test": "heft run --only test -- --clean" }, "dependencies": { + "@pnpm/dependency-path": "1000.0.9", "@rushstack/rush-sdk": "workspace:*" }, "devDependencies": { diff --git a/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts index 094aeda16f8..2e5eb6e1d3d 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/computeResolverCacheFromLockfileAsync.ts @@ -157,23 +157,6 @@ function convertToSlashes(path: string): string { return path.replace(/\\/g, '/'); } -/** - * Detects whether the lockfile uses v9+ format by checking the major version - * or inspecting the first non-file package key. - */ -function detectV9Lockfile(lockfile: PnpmShrinkwrapFile): boolean { - if (lockfile.shrinkwrapFileMajorVersion > 0) { - return lockfile.shrinkwrapFileMajorVersion >= 9; - } - // Fallback for lockfiles where version parsing failed: inspect the first non-file package key. - for (const key of lockfile.packages.keys()) { - if (!key.startsWith('file:')) { - return !key.startsWith('/'); - } - } - return false; -} - /** * Given a lockfile and information about the workspace and platform, computes the resolver cache file. * @param params - The options for computing the resolver cache @@ -191,8 +174,6 @@ export async function computeResolverCacheFromLockfileAsync( const contexts: Map = new Map(); const missingOptionalDependencies: Set = new Set(); - const isV9Lockfile: boolean = detectV9Lockfile(lockfile); - // Enumerate external dependencies first, to simplify looping over them for store data for (const [key, pack] of lockfile.packages) { let name: string | undefined = pack.name; @@ -232,10 +213,10 @@ export async function computeResolverCacheFromLockfileAsync( contexts.set(descriptionFileRoot, context); if (pack.dependencies) { - resolveDependencies(workspaceRoot, pack.dependencies, context, isV9Lockfile, lockfile.packages); + resolveDependencies(workspaceRoot, pack.dependencies, context, lockfile.packages); } if (pack.optionalDependencies) { - resolveDependencies(workspaceRoot, pack.optionalDependencies, context, isV9Lockfile, lockfile.packages); + resolveDependencies(workspaceRoot, pack.optionalDependencies, context, lockfile.packages); } } @@ -276,19 +257,13 @@ export async function computeResolverCacheFromLockfileAsync( contexts.set(descriptionFileRoot, context); if (importer.dependencies) { - resolveDependencies(workspaceRoot, importer.dependencies, context, isV9Lockfile, lockfile.packages); + resolveDependencies(workspaceRoot, importer.dependencies, context, lockfile.packages); } if (importer.devDependencies) { - resolveDependencies(workspaceRoot, importer.devDependencies, context, isV9Lockfile, lockfile.packages); + resolveDependencies(workspaceRoot, importer.devDependencies, context, lockfile.packages); } if (importer.optionalDependencies) { - resolveDependencies( - workspaceRoot, - importer.optionalDependencies, - context, - isV9Lockfile, - lockfile.packages - ); + resolveDependencies(workspaceRoot, importer.optionalDependencies, context, lockfile.packages); } } diff --git a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts index 0202caf0a51..f457742d2c3 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts @@ -1,87 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { createHash } from 'node:crypto'; import * as path from 'node:path'; +import { depPathToFilename } from '@pnpm/dependency-path-pnpm-v10'; + import type { ISerializedResolveContext } from '@rushstack/webpack-workspace-resolve-plugin'; import type { IDependencyEntry, IResolverContext } from './types'; -const PNPM8_MAX_LENGTH_WITHOUT_HASH: number = 120 - 26 - 1; -// pnpm 10 uses SHA-256 hex (32 chars) + underscore separator -const PNPM10_MAX_LENGTH_WITHOUT_HASH: number = 120 - 32 - 1; -const BASE32: string[] = 'abcdefghijklmnopqrstuvwxyz234567'.split(''); - -const SPECIAL_CHARS_REGEX: RegExp = /[\\/:*?"<>|]/g; -const HASH_CHAR_REGEX: RegExp = /#/g; -const TRAILING_PAREN_REGEX: RegExp = /\)$/; -const PNPM10_PARENS_REGEX: RegExp = /\)\(|\(|\)/g; -const PNPM8_PARENS_REGEX: RegExp = /(\)\()|\(/g; - -// https://github.com/swansontec/rfc4648.js/blob/ead9c9b4b68e5d4a529f32925da02c02984e772c/src/codec.ts#L82-L118 -export function createBase32Hash(input: string): string { - const data: Buffer = createHash('md5').update(input).digest(); - - const mask: 0x1f = 0x1f; - let out: string = ''; - - let bits: number = 0; // Number of bits currently in the buffer - let buffer: number = 0; // Bits waiting to be written out, MSB first - for (let i: number = 0; i < data.length; ++i) { - // eslint-disable-next-line no-bitwise - buffer = (buffer << 8) | (0xff & data[i]); - bits += 8; - - // Write out as much as we can: - while (bits > 5) { - bits -= 5; - // eslint-disable-next-line no-bitwise - out += BASE32[mask & (buffer >> bits)]; - } - } - - // Partial character: - if (bits) { - // eslint-disable-next-line no-bitwise - out += BASE32[mask & (buffer << (5 - bits))]; - } - - return out; -} - -/** - * Creates a short SHA-256 hex hash, matching pnpm 10's createShortHash. - */ -export function createShortSha256Hash(input: string): string { - return createHash('sha256').update(input).digest('hex').substring(0, 32); -} - -// Mirrors @pnpm/dependency-path depPathToFilename. -// We inline it rather than importing two aliased versions of the package (v8 and v10) -// since each brings transitive deps (@pnpm/crypto.hash, @pnpm/types, semver). -// https://github.com/pnpm/pnpm/blob/f394cfccda7bc519ceee8c33fc9b68a0f4235532/packages/dependency-path/src/index.ts#L167-L189 -export function depPathToFilename(depPath: string, usePnpm10Hashing?: boolean): string { - let filename: string = depPathToFilenameUnescaped(depPath).replace(SPECIAL_CHARS_REGEX, '+'); - if (usePnpm10Hashing) { - // pnpm 10 also replaces `#` and handles parentheses differently - filename = filename.replace(HASH_CHAR_REGEX, '+'); - if (filename.includes('(')) { - filename = filename.replace(TRAILING_PAREN_REGEX, '').replace(PNPM10_PARENS_REGEX, '_'); - } - if (filename.length > 120 || (filename !== filename.toLowerCase() && !filename.startsWith('file+'))) { - return `${filename.substring(0, PNPM10_MAX_LENGTH_WITHOUT_HASH)}_${createShortSha256Hash(filename)}`; - } - } else { - if (filename.includes('(')) { - filename = filename.replace(PNPM8_PARENS_REGEX, '_').replace(TRAILING_PAREN_REGEX, ''); - } - if (filename.length > 120 || (filename !== filename.toLowerCase() && !filename.startsWith('file+'))) { - return `${filename.substring(0, PNPM8_MAX_LENGTH_WITHOUT_HASH)}_${createBase32Hash(filename)}`; - } - } - return filename; -} +const PNPM_STORE_DIR_MAX_LENGTH: number = 120; /** * Computes the root folder for a dependency from a reference to it in another package @@ -96,7 +24,6 @@ export function resolveDependencyKey( key: string, specifier: string, context: IResolverContext, - isV9Lockfile?: boolean, packageKeys?: { has(key: string): boolean } ): string { if (specifier.startsWith('link:')) { @@ -108,12 +35,11 @@ export function resolveDependencyKey( } else if (specifier.startsWith('file:')) { return getDescriptionFileRootFromKey(lockfileFolder, specifier, key); } else if (packageKeys?.has(specifier)) { - // The specifier is a full package key (v6: '/pkg@ver', v9: 'pkg@ver') + // The specifier is a full package key return getDescriptionFileRootFromKey(lockfileFolder, specifier); } else { // Construct the full dependency key from package name and version specifier. - // v6 keys use '/' prefix; v9 keys don't. - const fullKey: string = isV9Lockfile ? `${key}@${specifier}` : `/${key}@${specifier}`; + const fullKey: string = `${key}@${specifier}`; return getDescriptionFileRootFromKey(lockfileFolder, fullKey); } } @@ -126,9 +52,6 @@ export function resolveDependencyKey( * @returns The physical path to the dependency */ export function getDescriptionFileRootFromKey(lockfileFolder: string, key: string, name?: string): string { - // Detect lockfile version: v6 keys start with '/', v9 keys don't - const isV9Key: boolean = !key.startsWith('/') && !key.startsWith('file:'); - if (!key.startsWith('file:') && !name) { const offset: number = key.startsWith('/') ? 1 : 0; name = key.slice(offset, key.indexOf('@', offset + 1)); @@ -137,7 +60,7 @@ export function getDescriptionFileRootFromKey(lockfileFolder: string, key: strin throw new Error(`Missing package name for ${key}`); } - const originFolder: string = `${lockfileFolder}/node_modules/.pnpm/${depPathToFilename(key, isV9Key)}/node_modules`; + const originFolder: string = `${lockfileFolder}/node_modules/.pnpm/${depPathToFilename(key, PNPM_STORE_DIR_MAX_LENGTH)}/node_modules`; const descriptionFileRoot: string = `${originFolder}/${name}`; return descriptionFileRoot; } @@ -146,19 +69,11 @@ export function resolveDependencies( lockfileFolder: string, collection: Record, context: IResolverContext, - isV9Lockfile?: boolean, packageKeys?: { has(key: string): boolean } ): void { for (const [key, value] of Object.entries(collection)) { const version: string = typeof value === 'string' ? value : value.version; - const resolved: string = resolveDependencyKey( - lockfileFolder, - key, - version, - context, - isV9Lockfile, - packageKeys - ); + const resolved: string = resolveDependencyKey(lockfileFolder, key, version, context, packageKeys); context.deps.set(key, resolved); } @@ -185,21 +100,6 @@ export function extractNameAndVersionFromKey(key: string): { name: string; versi return { name, version }; } -/** - * - * @param depPath - The path to the dependency - * @returns The folder name for the dependency - */ -export function depPathToFilenameUnescaped(depPath: string): string { - if (depPath.indexOf('file:') !== 0) { - if (depPath.startsWith('/')) { - depPath = depPath.slice(1); - } - return depPath; - } - return depPath.replace(':', '+'); -} - /** * * @param missingOptionalDependencies - The set of optional dependencies that were not installed diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/computeResolverCacheFromLockfileAsync.test.ts.snap b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/computeResolverCacheFromLockfileAsync.test.ts.snap index 401d0b21317..e1a0932ae3d 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/computeResolverCacheFromLockfileAsync.test.ts.snap +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/computeResolverCacheFromLockfileAsync.test.ts.snap @@ -6609,7 +6609,7 @@ Object { "constructs": 1048, }, "name": "@aws-cdk/aws-apigatewayv2-authorizers-alpha", - "root": "common/temp/default/node_modules/.pnpm/@aws-cdk+aws-apigatewayv2-authorizers-alpha@2.50.0-alpha.0_@aws-cdk+aws-apigatewayv2-alpha@2._2zjvdar6ml6w5rggykpucquwre/node_modules/@aws-cdk/aws-apigatewayv2-authorizers-alpha", + "root": "common/temp/default/node_modules/.pnpm/@aws-cdk+aws-apigatewayv2-authorizers-alpha@2.50.0-alpha.0_@aws-cdk+aws-apigatewayv2-al_767b83692d7cf4a6fac54c8e887397fb/node_modules/@aws-cdk/aws-apigatewayv2-authorizers-alpha", }, Object { "deps": Object { @@ -6618,7 +6618,7 @@ Object { "constructs": 1048, }, "name": "@aws-cdk/aws-apigatewayv2-integrations-alpha", - "root": "common/temp/default/node_modules/.pnpm/@aws-cdk+aws-apigatewayv2-integrations-alpha@2.50.0-alpha.0_@aws-cdk+aws-apigatewayv2-alpha@2_3hg7emzhhnzrnhew65j6d7q5ca/node_modules/@aws-cdk/aws-apigatewayv2-integrations-alpha", + "root": "common/temp/default/node_modules/.pnpm/@aws-cdk+aws-apigatewayv2-integrations-alpha@2.50.0-alpha.0_@aws-cdk+aws-apigatewayv2-a_ce351ac5742febe75c59346283737abf/node_modules/@aws-cdk/aws-apigatewayv2-integrations-alpha", }, Object { "deps": Object { @@ -9039,7 +9039,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-accordion", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-accordion@9.3.46_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0_3ny3g4kjxzahs6hhtn3wibkv6q/node_modules/@fluentui/react-accordion", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-accordion@9.3.46_@types+react-dom@17.0.25_@types+react@17.0.74_react-do_b71c344d9dc9dcb3c409830a4400f894/node_modules/@fluentui/react-accordion", }, Object { "deps": Object { @@ -9058,7 +9058,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-alert", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-alert@9.0.0-beta.63_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@1_mmbfibu2ulsox3tppd6lp5wbdm/node_modules/@fluentui/react-alert", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-alert@9.0.0-beta.63_@types+react-dom@17.0.25_@types+react@17.0.74_react_4c81f6da4726ebd5bbb0e65b18bd972f/node_modules/@fluentui/react-alert", }, Object { "deps": Object { @@ -9096,7 +9096,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-avatar", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-avatar@9.6.19_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2__h75vrb2iuccv4p2nlckeeute2y/node_modules/@fluentui/react-avatar", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-avatar@9.6.19_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@1_27a426bb76185e504b4fdeb701ad22ea/node_modules/@fluentui/react-avatar", }, Object { "deps": Object { @@ -9170,7 +9170,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-checkbox", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-checkbox@9.2.17_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0._6kdnysrlrg7gdc3chketcwjram/node_modules/@fluentui/react-checkbox", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-checkbox@9.2.17_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom_3fbeb3e0d1563cf978000f0d75ed4e36/node_modules/@fluentui/react-checkbox", }, Object { "deps": Object { @@ -9194,7 +9194,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-combobox", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-combobox@9.9.3_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2_k7vu2hkzucfaym7g4rveom44pu/node_modules/@fluentui/react-combobox", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-combobox@9.9.3_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@_53a75bfe6e677dab55db85cb40821abd/node_modules/@fluentui/react-combobox", }, Object { "deps": Object { @@ -9252,7 +9252,7 @@ Object { "scheduler": 2223, }, "name": "@fluentui/react-components", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-components@9.27.4_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17._rrc4dsgm5xlhpm2l2xvbhfv7ia/node_modules/@fluentui/react-components", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-components@9.27.4_@types+react-dom@17.0.25_@types+react@17.0.74_react-d_5f3451f05354e82e8cad62dc5fbb112a/node_modules/@fluentui/react-components", }, Object { "deps": Object { @@ -9265,7 +9265,7 @@ Object { "scheduler": 2223, }, "name": "@fluentui/react-context-selector", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-context-selector@9.1.56_@types+react-dom@17.0.25_@types+react@17.0.74_react-d_wlqzfltfi3vnmsbcjghtdc44yy/node_modules/@fluentui/react-context-selector", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-context-selector@9.1.56_@types+react-dom@17.0.25_@types+react@17.0.74_r_57d085b13b99ef72e3084b02a4170958/node_modules/@fluentui/react-context-selector", }, Object { "deps": Object { @@ -9288,7 +9288,7 @@ Object { "react-transition-group": 2118, }, "name": "@fluentui/react-dialog", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-dialog@9.9.15_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2__noytcrubbkx7t4ib2lltclb64m/node_modules/@fluentui/react-dialog", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-dialog@9.9.15_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@1_373f47b6901c33b74be299faf7ee4042/node_modules/@fluentui/react-dialog", }, Object { "deps": Object { @@ -9321,7 +9321,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-drawer", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-drawer@9.0.0-beta.12_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@_uvgv6vqgvzadd73d2xfo4yq2ie/node_modules/@fluentui/react-drawer", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-drawer@9.0.0-beta.12_@types+react-dom@17.0.25_@types+react@17.0.74_reac_66a8490b60c89bf3ff2f05a03b0912cf/node_modules/@fluentui/react-drawer", }, Object { "deps": Object { @@ -9339,7 +9339,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-field", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-field@9.1.58_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2_r_7tl65itnv6wduaxb7kvdbzmaf4/node_modules/@fluentui/react-field", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-field@9.1.58_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17_1d9dfe4007037db45102ca4eef2106cf/node_modules/@fluentui/react-field", }, Object { "deps": Object { @@ -9409,7 +9409,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-infobutton", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-infobutton@9.0.0-beta.47_@types+react-dom@17.0.25_@types+react@17.0.74_react-_akgxb7tovm7amtemx5vkxfmqau/node_modules/@fluentui/react-infobutton", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-infobutton@9.0.0-beta.47_@types+react-dom@17.0.25_@types+react@17.0.74__5344e9f74ff3c8a97cc32f0a35884769/node_modules/@fluentui/react-infobutton", }, Object { "deps": Object { @@ -9426,7 +9426,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-input", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-input@9.4.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2_r_iazqepwia3pya3wqujvfwc4ukm/node_modules/@fluentui/react-input", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-input@9.4.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17_271e0b44b7a95c2476e2819b7834004c/node_modules/@fluentui/react-input", }, Object { "deps": Object { @@ -9520,7 +9520,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-overflow", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-overflow@9.1.15_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0._uycc7uxxwhw6jgermro2bjuwf4/node_modules/@fluentui/react-overflow", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-overflow@9.1.15_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom_daf1e6830761ded1a694d6006f42dd92/node_modules/@fluentui/react-overflow", }, Object { "deps": Object { @@ -9538,7 +9538,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-persona", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-persona@9.2.78_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2_gwb2xsnjxwaw2yb4y4nx7q42dy/node_modules/@fluentui/react-persona", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-persona@9.2.78_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@_b6ca28010a44cce0a61119ec15fd9356/node_modules/@fluentui/react-persona", }, Object { "deps": Object { @@ -9560,7 +9560,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-popover", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-popover@9.9.2_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2__pyflnqu5iah7662njgikihywyy/node_modules/@fluentui/react-popover", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-popover@9.9.2_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@1_f965a1d06e7ff27f4fe85adb301227fa/node_modules/@fluentui/react-popover", }, Object { "deps": Object { @@ -9619,7 +9619,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-progress", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-progress@9.1.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0._4ddokn6i6qzorspkjr3c2mpx6y/node_modules/@fluentui/react-progress", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-progress@9.1.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom_71f10c73876613f9a5d001e6190ec4e0/node_modules/@fluentui/react-progress", }, Object { "deps": Object { @@ -9657,7 +9657,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-radio", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-radio@9.2.12_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2_r_mo4hkpmyd7oumz2d6i6qdjqtwy/node_modules/@fluentui/react-radio", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-radio@9.2.12_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17_96113638025aea78f54d39c550eae425/node_modules/@fluentui/react-radio", }, Object { "deps": Object { @@ -9675,7 +9675,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-select", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-select@9.1.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2__k3knfgz6zpux6raexbwvyzdvwa/node_modules/@fluentui/react-select", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-select@9.1.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@1_58a6dccf36774a9ff17b6ac5bab0aada/node_modules/@fluentui/react-select", }, Object { "deps": Object { @@ -9702,7 +9702,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-skeleton", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-skeleton@9.0.56_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0._sfeb7yrhm2jlx4aheiz6uzqvkm/node_modules/@fluentui/react-skeleton", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-skeleton@9.0.56_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom_e25a1878ea749cf675477c45e4fbcd5f/node_modules/@fluentui/react-skeleton", }, Object { "deps": Object { @@ -9720,7 +9720,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-slider", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-slider@9.1.74_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2__got4evyigylsgol63s3tpoea2e/node_modules/@fluentui/react-slider", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-slider@9.1.74_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@1_216d81f259a68daefa27fcaf031801d9/node_modules/@fluentui/react-slider", }, Object { "deps": Object { @@ -9739,7 +9739,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-spinbutton", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-spinbutton@9.2.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17._54ylukac4izqz4g7veps3e77au/node_modules/@fluentui/react-spinbutton", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-spinbutton@9.2.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-d_99b3278bd6bf3c4a338cc1867e5406b4/node_modules/@fluentui/react-spinbutton", }, Object { "deps": Object { @@ -9776,7 +9776,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-switch", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-switch@9.1.74_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2__bg3pcrh5kphefx5yqxdilpmufy/node_modules/@fluentui/react-switch", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-switch@9.1.74_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@1_ac087ab18e00753d54a8501c66f43426/node_modules/@fluentui/react-switch", }, Object { "deps": Object { @@ -9800,7 +9800,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-table", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-table@9.11.15_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2__4v6kpkif65ljt5aoerrbkek2ka/node_modules/@fluentui/react-table", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-table@9.11.15_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@1_4132706b27e132c2074c2d0389240754/node_modules/@fluentui/react-table", }, Object { "deps": Object { @@ -9868,7 +9868,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-textarea", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-textarea@9.3.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0._kzu73p535aaha7hycnv6cocfry/node_modules/@fluentui/react-textarea", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-textarea@9.3.68_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom_4e1db323ad742e7f1dbfdf78e7bc8aa8/node_modules/@fluentui/react-textarea", }, Object { "deps": Object { @@ -9919,7 +9919,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-toolbar", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-toolbar@9.1.75_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17.0.2_jmftdxha3zk5e2tghf6ehgwk5a/node_modules/@fluentui/react-toolbar", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-toolbar@9.1.75_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@_bf79a59e60f1e943a86a1ea6681a9faf/node_modules/@fluentui/react-toolbar", }, Object { "deps": Object { @@ -9965,7 +9965,7 @@ Object { "react-dom": 2099, }, "name": "@fluentui/react-tree", - "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-tree@9.0.0-beta.30_@types+react-dom@17.0.25_@types+react@17.0.74_react-dom@17_ekkwm54xwaasnfnesistyhtkhu/node_modules/@fluentui/react-tree", + "root": "common/temp/default/node_modules/.pnpm/@fluentui+react-tree@9.0.0-beta.30_@types+react-dom@17.0.25_@types+react@17.0.74_react-_c2379e1579767f7ceb9ca2d8cd1ddf2f/node_modules/@fluentui/react-tree", }, Object { "deps": Object { @@ -11442,7 +11442,7 @@ Object { "lodash": 1760, }, "name": "@rushstack/heft-jest-plugin", - "root": "common/temp/default/node_modules/.pnpm/@rushstack+heft-jest-plugin@0.11.38_@rushstack+heft@..+..+apps+heft_@types+node@18.17.15_jest_d4nwoquxsq3lena3jdncul4h6e/node_modules/@rushstack/heft-jest-plugin", + "root": "common/temp/default/node_modules/.pnpm/@rushstack+heft-jest-plugin@0.11.38_@rushstack+heft@..+..+apps+heft_@types+node@18.17.1_6b7ba8200f133ff5d0afcb8104308861/node_modules/@rushstack/heft-jest-plugin", }, Object { "deps": Object { @@ -11460,7 +11460,7 @@ Object { "lodash": 1760, }, "name": "@rushstack/heft-jest-plugin", - "root": "common/temp/default/node_modules/.pnpm/@rushstack+heft-jest-plugin@0.11.38_@rushstack+heft@0.66.17_@types+node@18.17.15_jest-environ_udyctmgs62iwhfjdgfh4tutvge/node_modules/@rushstack/heft-jest-plugin", + "root": "common/temp/default/node_modules/.pnpm/@rushstack+heft-jest-plugin@0.11.38_@rushstack+heft@0.66.17_@types+node@18.17.15_jest-e_5af72ff8d344dc65f255348b2dddc1d5/node_modules/@rushstack/heft-jest-plugin", }, Object { "deps": Object { @@ -12254,7 +12254,7 @@ Object { "webpack": 2558, }, "name": "@storybook/addon-docs", - "root": "common/temp/default/node_modules/.pnpm/@storybook+addon-docs@6.4.22_@storybook+react@6.4.22_@types+react@17.0.74_react-dom@17.0.2_re_cyh2kl4oxqqjzzvuae6ks2n4ji/node_modules/@storybook/addon-docs", + "root": "common/temp/default/node_modules/.pnpm/@storybook+addon-docs@6.4.22_@storybook+react@6.4.22_@types+react@17.0.74_react-dom@17._27e2daf1fedb051068d3de366f85f9ad/node_modules/@storybook/addon-docs", }, Object { "deps": Object { @@ -12279,7 +12279,7 @@ Object { "webpack": 2558, }, "name": "@storybook/addon-essentials", - "root": "common/temp/default/node_modules/.pnpm/@storybook+addon-essentials@6.4.22_@babel+core@7.20.12_@storybook+react@6.4.22_@types+react@1_dbhp3ql5ql4kiy4ubyky74xexq/node_modules/@storybook/addon-essentials", + "root": "common/temp/default/node_modules/.pnpm/@storybook+addon-essentials@6.4.22_@babel+core@7.20.12_@storybook+react@6.4.22_@types+r_04b8c5b92686db8c9b4e8e4e4c6540cd/node_modules/@storybook/addon-essentials", }, Object { "deps": Object { @@ -12971,7 +12971,7 @@ Object { "webpack": 2558, }, "name": "@storybook/react", - "root": "common/temp/default/node_modules/.pnpm/@storybook+react@6.4.22_@babel+core@7.20.12_@types+node@18.17.15_@types+react@17.0.74_eslint@_z6zmlpizzl5plaxmf6nthuwe34/node_modules/@storybook/react", + "root": "common/temp/default/node_modules/.pnpm/@storybook+react@6.4.22_@babel+core@7.20.12_@types+node@18.17.15_@types+react@17.0.74_e_c454de8dbcd6e438f211590f7ad1aeb0/node_modules/@storybook/react", }, Object { "deps": Object { @@ -13938,7 +13938,7 @@ Object { "typescript": 2459, }, "name": "@typescript-eslint/eslint-plugin", - "root": "common/temp/default/node_modules/.pnpm/@typescript-eslint+eslint-plugin@6.19.1_@typescript-eslint+parser@6.19.1_eslint@8.57.0_suppor_cdfp35n6xmy2avdo2soai22xhi/node_modules/@typescript-eslint/eslint-plugin", + "root": "common/temp/default/node_modules/.pnpm/@typescript-eslint+eslint-plugin@6.19.1_@typescript-eslint+parser@6.19.1_eslint@8.57.0__e1b210c04ffa5a358c3b24883ff74fff/node_modules/@typescript-eslint/eslint-plugin", }, Object { "deps": Object { @@ -24687,7 +24687,7 @@ Object { "use-sync-external-store": 2506, }, "name": "react-redux", - "root": "common/temp/default/node_modules/.pnpm/react-redux@8.0.7_@reduxjs+toolkit@1.8.6_@types+react-dom@17.0.25_@types+react@17.0.74_react-_fxbgebfgcimhtvtdsot4e7klsa/node_modules/react-redux", + "root": "common/temp/default/node_modules/.pnpm/react-redux@8.0.7_@reduxjs+toolkit@1.8.6_@types+react-dom@17.0.25_@types+react@17.0.74__fd6f85f05c0168c5ee0570fcb3a7e266/node_modules/react-redux", }, Object { "name": "react-refresh", diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap index 024dd750159..5edac6896eb 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap @@ -1,46 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`createBase32Hash hashes: (eslint@8.57.0)(typescript@5.4.5) 1`] = `"2nt7x5y4npubskzl4nixybkqqi"`; +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)", 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge_bd7d736e48cc924757a4893ec379e38f/node_modules/@some/package"`; -exports[`createBase32Hash hashes: a 1`] = `"btaxlooa6g3kqmodthrgs5zgme"`; - -exports[`createBase32Hash hashes: abracadabra 1`] = `"5rjiprc7bzyoyiwvf2f4x3vwia"`; - -exports[`createShortSha256Hash hashes: (eslint@8.57.0)(typescript@5.4.5) 1`] = `"395951816c5613fa894c6f81441c9d08"`; - -exports[`createShortSha256Hash hashes: a 1`] = `"ca978112ca1bbdcafac231b39a23dc4d"`; - -exports[`createShortSha256Hash hashes: abracadabra 1`] = `"045babdcd2118960e8c8b8e0ecf65b73"`; - -exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1) 1`] = `"@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge-style_yt7yh6tpppbzu7nx3lzx3f3ife"`; - -exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1) 1`] = `"@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6.5.15__wnqw6tyxzeiksxiymflshxscdq"`; - -exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2) 1`] = `"@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2"`; - -exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /autoprefixer@9.8.8 1`] = `"autoprefixer@9.8.8"`; - -exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /autoprefixer@10.4.18(postcss@8.4.36) 1`] = `"autoprefixer@10.4.18_postcss@8.4.36"`; - -exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: /react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2) 1`] = `"react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2"`; - -exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: file:../../../libraries/ts-command-line(@types/node@18.17.15) 1`] = `"file+..+..+..+libraries+ts-command-line_@types+node@18.17.15"`; - -exports[`depPathToFilename formats v6 keys (leading /) with pnpm 8 hashing: file:../../../rigs/local-node-rig 1`] = `"file+..+..+..+rigs+local-node-rig"`; - -exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: @fluentui/react-migration-v8-v9@9.9.7(@types/react-dom@17.0.17)(@types/react@17.0.45)(react-dom@17.0.1)(react@17.0.1) 1`] = `"@fluentui+react-migration-v8-v9@9.9.7_@types+react-dom@17.0.17_@types+react@17.0.45_react-dom@17.0.1_react@17.0.1"`; - -exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: @some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0) 1`] = `"@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0"`; - -exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: @typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2) 1`] = `"@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2"`; - -exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: autoprefixer@9.8.8 1`] = `"autoprefixer@9.8.8"`; - -exports[`depPathToFilename formats v9 keys (no leading /) with pnpm 10 hashing: autoprefixer@10.4.18(postcss@8.4.36) 1`] = `"autoprefixer@10.4.18_postcss@8.4.36"`; - -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)", 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge-style_yt7yh6tpppbzu7nx3lzx3f3ife/node_modules/@some/package"`; - -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)", 1`] = `"/$/node_modules/.pnpm/@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6.5.15__wnqw6tyxzeiksxiymflshxscdq/node_modules/@storybook/core"`; +exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)", 1`] = `"/$/node_modules/.pnpm/@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6_eb87600472da240f1b95d9cdd4a74603/node_modules/@storybook/core"`; exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)", 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts index 717fb3ecf70..a6bbf89e1b0 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts @@ -1,58 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { - createBase32Hash, - createShortSha256Hash, - depPathToFilename, - getDescriptionFileRootFromKey, - extractNameAndVersionFromKey -} from '../helpers'; - -describe(createBase32Hash.name, () => { - it('hashes', () => { - for (const input of ['a', 'abracadabra', '(eslint@8.57.0)(typescript@5.4.5)']) { - expect(createBase32Hash(input)).toMatchSnapshot(input); - } - }); -}); - -describe(createShortSha256Hash.name, () => { - it('hashes', () => { - for (const input of ['a', 'abracadabra', '(eslint@8.57.0)(typescript@5.4.5)']) { - expect(createShortSha256Hash(input)).toMatchSnapshot(input); - } - }); -}); - -describe(depPathToFilename.name, () => { - it('formats v6 keys (leading /) with pnpm 8 hashing', () => { - for (const input of [ - '/autoprefixer@9.8.8', - '/autoprefixer@10.4.18(postcss@8.4.36)', - '/react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)', - '/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)', - '/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)', - '/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)', - 'file:../../../rigs/local-node-rig', - 'file:../../../libraries/ts-command-line(@types/node@18.17.15)' - ]) { - expect(depPathToFilename(input)).toMatchSnapshot(input); - } - }); - - it('formats v9 keys (no leading /) with pnpm 10 hashing', () => { - for (const input of [ - 'autoprefixer@9.8.8', - 'autoprefixer@10.4.18(postcss@8.4.36)', - '@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)', - '@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)', - '@fluentui/react-migration-v8-v9@9.9.7(@types/react-dom@17.0.17)(@types/react@17.0.45)(react-dom@17.0.1)(react@17.0.1)' - ]) { - expect(depPathToFilename(input, true)).toMatchSnapshot(input); - } - }); -}); +import { getDescriptionFileRootFromKey, extractNameAndVersionFromKey } from '../helpers'; describe(getDescriptionFileRootFromKey.name, () => { it('parses v6 keys (leading /)', () => { From 396e5a506f005d48e1e133271ad96f86d9582fff Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:11:55 +0000 Subject: [PATCH 5/5] cleanup --- .../rush-resolver-cache-plugin/src/helpers.ts | 2 +- .../test/__snapshots__/helpers.test.ts.snap | 30 +++-------- .../src/test/helpers.test.ts | 50 +------------------ 3 files changed, 10 insertions(+), 72 deletions(-) diff --git a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts index f457742d2c3..895e5b3ca84 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/helpers.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; -import { depPathToFilename } from '@pnpm/dependency-path-pnpm-v10'; +import { depPathToFilename } from '@pnpm/dependency-path'; import type { ISerializedResolveContext } from '@rushstack/webpack-workspace-resolve-plugin'; diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap index 5edac6896eb..1592d18dca9 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/__snapshots__/helpers.test.ts.snap @@ -1,31 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)", 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0_@fluentui+merge_bd7d736e48cc924757a4893ec379e38f/node_modules/@some/package"`; +exports[`getDescriptionFileRootFromKey parses : "@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)", 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0/node_modules/@some/package"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)", 1`] = `"/$/node_modules/.pnpm/@storybook+core@6.5.15_@storybook+builder-webpack5@6.5.15_@storybook+manager-webpack5@6_eb87600472da240f1b95d9cdd4a74603/node_modules/@storybook/core"`; +exports[`getDescriptionFileRootFromKey parses : "@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)", 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)", 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; +exports[`getDescriptionFileRootFromKey parses : "autoprefixer@9.8.8", 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/autoprefixer@9.8.8", 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; +exports[`getDescriptionFileRootFromKey parses : "autoprefixer@10.4.18(postcss@8.4.36)", 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/autoprefixer@10.4.18(postcss@8.4.36)", 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; +exports[`getDescriptionFileRootFromKey parses : "file:../../../libraries/ts-command-line(@types/node@18.17.15)",@rushstack/ts-command-line 1`] = `"/$/node_modules/.pnpm/file+..+..+..+libraries+ts-command-line_@types+node@18.17.15/node_modules/@rushstack/ts-command-line"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "/react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)", 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; +exports[`getDescriptionFileRootFromKey parses : "file:../../../rigs/local-node-rig",local-node-rig 1`] = `"/$/node_modules/.pnpm/file+..+..+..+rigs+local-node-rig/node_modules/local-node-rig"`; -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "file:../../../libraries/ts-command-line(@types/node@18.17.15)",@rushstack/ts-command-line 1`] = `"/$/node_modules/.pnpm/file+..+..+..+libraries+ts-command-line_@types+node@18.17.15/node_modules/@rushstack/ts-command-line"`; - -exports[`getDescriptionFileRootFromKey parses v6 keys (leading /): "file:../../../rigs/local-node-rig",local-node-rig 1`] = `"/$/node_modules/.pnpm/file+..+..+..+rigs+local-node-rig/node_modules/local-node-rig"`; - -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)", 1`] = `"/$/node_modules/.pnpm/@some+package@1.2.3_@azure+msal-browser@2.28.1_@azure+msal-common@6.4.0/node_modules/@some/package"`; - -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)", 1`] = `"/$/node_modules/.pnpm/@typescript-eslint+utils@6.19.1_eslint@7.7.0_typescript@5.4.2/node_modules/@typescript-eslint/utils"`; - -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "autoprefixer@9.8.8", 1`] = `"/$/node_modules/.pnpm/autoprefixer@9.8.8/node_modules/autoprefixer"`; - -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "autoprefixer@10.4.18(postcss@8.4.36)", 1`] = `"/$/node_modules/.pnpm/autoprefixer@10.4.18_postcss@8.4.36/node_modules/autoprefixer"`; - -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "file:../../../libraries/ts-command-line(@types/node@18.17.15)",@rushstack/ts-command-line 1`] = `"/$/node_modules/.pnpm/file+..+..+..+libraries+ts-command-line_@types+node@18.17.15/node_modules/@rushstack/ts-command-line"`; - -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "file:../../../rigs/local-node-rig",local-node-rig 1`] = `"/$/node_modules/.pnpm/file+..+..+..+rigs+local-node-rig/node_modules/local-node-rig"`; - -exports[`getDescriptionFileRootFromKey parses v9 keys (no leading /): "react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)", 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; +exports[`getDescriptionFileRootFromKey parses : "react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)", 1`] = `"/$/node_modules/.pnpm/react-transition-group@4.4.5_react-dom@17.0.2_react@17.0.2/node_modules/react-transition-group"`; diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts index a6bbf89e1b0..d153d3ed10a 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/helpers.test.ts @@ -4,32 +4,7 @@ import { getDescriptionFileRootFromKey, extractNameAndVersionFromKey } from '../helpers'; describe(getDescriptionFileRootFromKey.name, () => { - it('parses v6 keys (leading /)', () => { - const lockfileRoot: string = '/$'; - for (const { key, name } of [ - { key: '/autoprefixer@9.8.8' }, - { key: '/autoprefixer@10.4.18(postcss@8.4.36)' }, - { key: '/react-transition-group@4.4.5(react-dom@17.0.2)(react@17.0.2)' }, - { - key: '/@some/package@1.2.3(@azure/msal-browser@2.28.1)(@azure/msal-common@6.4.0)(@fluentui/merge-styles@8.6.2)(@fluentui/react@8.117.5)(@fluentui/theme@2.6.45)(@fluentui/utilities@8.15.2)(chart.js@2.9.4)(lodash@4.17.21)(moment@2.29.4)(prop-types@15.8.1)(react-dnd-html5-backend@14.1.0)(react-dnd@14.0.5)(react-dom@17.0.1)(react-intersection-observer@8.34.0)(react@17.0.1)' - }, - { - key: '/@storybook/core@6.5.15(@storybook/builder-webpack5@6.5.15)(@storybook/manager-webpack5@6.5.15)(eslint@8.57.0)(react-dom@17.0.1)(react@17.0.1)(typescript@5.3.3)(webpack@5.88.1)' - }, - { key: '/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)' }, - { key: 'file:../../../rigs/local-node-rig', name: 'local-node-rig' }, - { - key: 'file:../../../libraries/ts-command-line(@types/node@18.17.15)', - name: '@rushstack/ts-command-line' - } - ]) { - expect(getDescriptionFileRootFromKey(lockfileRoot, key, name)).toMatchSnapshot( - `"${key}",${name || ''}` - ); - } - }); - - it('parses v9 keys (no leading /)', () => { + it('parses ', () => { const lockfileRoot: string = '/$'; for (const { key, name } of [ { key: 'autoprefixer@9.8.8' }, @@ -53,28 +28,7 @@ describe(getDescriptionFileRootFromKey.name, () => { }); describe(extractNameAndVersionFromKey.name, () => { - it('extracts name and version from v6 keys (leading /)', () => { - expect(extractNameAndVersionFromKey('/autoprefixer@9.8.8')).toEqual({ - name: 'autoprefixer', - version: '9.8.8' - }); - expect(extractNameAndVersionFromKey('/autoprefixer@10.4.18(postcss@8.4.36)')).toEqual({ - name: 'autoprefixer', - version: '10.4.18' - }); - expect(extractNameAndVersionFromKey('/@some/package@1.2.3(@azure/msal-browser@2.28.1)')).toEqual({ - name: '@some/package', - version: '1.2.3' - }); - expect( - extractNameAndVersionFromKey('/@typescript-eslint/utils@6.19.1(eslint@7.7.0)(typescript@5.4.2)') - ).toEqual({ - name: '@typescript-eslint/utils', - version: '6.19.1' - }); - }); - - it('extracts name and version from v9 keys (no leading /)', () => { + it('extracts name and version from', () => { expect(extractNameAndVersionFromKey('autoprefixer@9.8.8')).toEqual({ name: 'autoprefixer', version: '9.8.8'