rush-resolver-cache-plugin: add pnpm 10 / lockfile v9 compatibility#5749
rush-resolver-cache-plugin: add pnpm 10 / lockfile v9 compatibility#5749
Conversation
6034479 to
3ff472b
Compare
|
|
||
| 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"`; |
There was a problem hiding this comment.
What's the undefined coming from here?
| name: '@rushstack/ts-command-line' | ||
| } | ||
| ]) { | ||
| expect(getDescriptionFileRootFromKey(lockfileRoot, key, name)).toMatchSnapshot(`"${key}",${name}`); |
There was a problem hiding this comment.
name is only defined for one of these; merge key and name for the snapshot name? Or default to '' for the name?
| // 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: <name+ver>_<peer1>_<peer2>/node_modules/<name> | ||
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
Put this whole thing in a helper function.
| const pnpmSegment: string | undefined = context.descriptionFileRoot.split('/node_modules/.pnpm/')[1]; | ||
| const folderName: string = pnpmSegment ? pnpmSegment.split('/node_modules/')[0] : ''; |
There was a problem hiding this comment.
I'd prefer to avoid .split in this code; this is a hot path.
| 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: <name+ver>_<peer1>_<peer2>/node_modules/<name> | ||
| const pkgName: string = (context.name || '').replace(/\//g, '+'); |
There was a problem hiding this comment.
Let's just plumb the version that we extract from the key in the lockfile into the context object instead of recomputing it.
| : !Array.from(lockfile.packages.keys()) | ||
| .find((k) => !k.startsWith('file:')) | ||
| ?.startsWith('/'); |
There was a problem hiding this comment.
Create a helper function that early returns instead of cloning the entire key space.
| // 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) { |
There was a problem hiding this comment.
Let's put the versions on context so we don't have to compute them later.
| if (filename.length > 120 || (filename !== filename.toLowerCase() && !filename.startsWith('file+'))) { | ||
| return `${filename.substring(0, PNPM8_MAX_LENGTH_WITHOUT_HASH)}_${createBase32Hash(filename)}`; | ||
| } | ||
| } |
| // 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. |
There was a problem hiding this comment.
This statement is true in v6 as well; that's why there's the specifier.startsWith('/') branch.
I wonder if we should just try to see if the value is a specifier by checking it against the packages list?
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
The only difference between these branches is the presence of the leading /, which just adjusts the offsets by 1. The !key.startsWith('/') branch can just return key.slice(0, key.indexOf('@', 1)) instead of key.slice(1, key.indexOf('@', 2))
Summary
add pnpm 10 / lockfile v9 compatibility to rush-resolver-cache-plugin
Details
rush-resolver-cache-plugin does not support v9 pnpm lockfile format
support pnpm lockfile format v9 and update tests for v6 and v9 format.
How it was tested
unit tests and manually tested integration with pnpm 10 repo.
Impacted documentation