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] 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}`); + } + }); });