diff --git a/lib/modules/manager/npm/extract/locked-versions.spec.ts b/lib/modules/manager/npm/extract/locked-versions.spec.ts index bbac5fcacafef5619c5528fd5e874191a0beaa13..9891ec0ffeca5c4bdb8769b27e10887ad6cd59cd 100644 --- a/lib/modules/manager/npm/extract/locked-versions.spec.ts +++ b/lib/modules/manager/npm/extract/locked-versions.spec.ts @@ -492,10 +492,14 @@ describe('modules/manager/npm/extract/locked-versions', () => { it('uses pnpm-lock', async () => { pnpm.getPnpmLock.mockReturnValue({ - lockedVersions: { - a: '1.0.0', - b: '2.0.0', - c: '3.0.0', + lockedVersionsWithPath: { + '.': { + dependencies: { + a: '1.0.0', + b: '2.0.0', + c: '3.0.0', + }, + }, }, lockfileVersion: 6.0, }); @@ -510,14 +514,16 @@ describe('modules/manager/npm/extract/locked-versions', () => { deps: [ { depName: 'a', + depType: 'dependencies', currentValue: '1.0.0', }, { depName: 'b', + depType: 'dependencies', currentValue: '2.0.0', }, ], - packageFile: 'some-file', + packageFile: 'package.json', }, ]; await getLockedVersions(packageFiles); @@ -525,12 +531,22 @@ describe('modules/manager/npm/extract/locked-versions', () => { { extractedConstraints: { pnpm: '>=6.0.0' }, deps: [ - { currentValue: '1.0.0', depName: 'a', lockedVersion: '1.0.0' }, - { currentValue: '2.0.0', depName: 'b', lockedVersion: '2.0.0' }, + { + currentValue: '1.0.0', + depName: 'a', + lockedVersion: '1.0.0', + depType: 'dependencies', + }, + { + currentValue: '2.0.0', + depName: 'b', + lockedVersion: '2.0.0', + depType: 'dependencies', + }, ], lockFiles: ['pnpm-lock.yaml'], managerData: { pnpmShrinkwrap: 'pnpm-lock.yaml' }, - packageFile: 'some-file', + packageFile: 'package.json', }, ]); }); diff --git a/lib/modules/manager/npm/extract/locked-versions.ts b/lib/modules/manager/npm/extract/locked-versions.ts index 0515deab74de2d139ffe72c9c874b6b4665030f3..93ce3adb0e73d104cae288c99fe1aba572ae1c3e 100644 --- a/lib/modules/manager/npm/extract/locked-versions.ts +++ b/lib/modules/manager/npm/extract/locked-versions.ts @@ -1,3 +1,4 @@ +import is from '@sindresorhus/is'; import semver from 'semver'; import { logger } from '../../../../logger'; import type { PackageFile } from '../../types'; @@ -42,7 +43,7 @@ export async function getLockedVersions( } for (const dep of packageFile.deps) { dep.lockedVersion = - lockFileCache[yarnLock].lockedVersions[ + lockFileCache[yarnLock].lockedVersions?.[ // TODO: types (#7154) // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `${dep.depName}@${dep.currentValue}` @@ -96,7 +97,7 @@ export async function getLockedVersions( for (const dep of packageFile.deps) { // TODO: types (#7154) dep.lockedVersion = semver.valid( - lockFileCache[npmLock].lockedVersions[dep.depName!] + lockFileCache[npmLock].lockedVersions?.[dep.depName!] )!; } } else if (pnpmShrinkwrap) { @@ -107,11 +108,20 @@ export async function getLockedVersions( lockFileCache[pnpmShrinkwrap] = await getPnpmLock(pnpmShrinkwrap); } + const parentDir = packageFile.packageFile + .replace(/\/package\.json$/, '') + .replace(/^package\.json$/, '.'); for (const dep of packageFile.deps) { + const { depName, depType } = dep; // TODO: types (#7154) - dep.lockedVersion = semver.valid( - lockFileCache[pnpmShrinkwrap].lockedVersions[dep.depName!] - )!; + const lockedVersion = semver.valid( + lockFileCache[pnpmShrinkwrap].lockedVersionsWithPath?.[parentDir]?.[ + depType! + ]?.[depName!] + ); + if (is.string(lockedVersion)) { + dep.lockedVersion = lockedVersion; + } } } if (lockFiles.length) { diff --git a/lib/modules/manager/npm/extract/npm.spec.ts b/lib/modules/manager/npm/extract/npm.spec.ts index 321b56501dd0d7eb696ee78000dab192df0b815d..46a2787a0735fd8b71664955e255bbeebcd13047 100644 --- a/lib/modules/manager/npm/extract/npm.spec.ts +++ b/lib/modules/manager/npm/extract/npm.spec.ts @@ -9,7 +9,7 @@ describe('modules/manager/npm/extract/npm', () => { it('returns empty if failed to parse', async () => { fs.readLocalFile.mockResolvedValueOnce('abcd'); const res = await getNpmLock('package.json'); - expect(Object.keys(res.lockedVersions)).toHaveLength(0); + expect(Object.keys(res.lockedVersions!)).toHaveLength(0); }); it('extracts', async () => { @@ -17,7 +17,7 @@ describe('modules/manager/npm/extract/npm', () => { fs.readLocalFile.mockResolvedValueOnce(plocktest1Lock as never); const res = await getNpmLock('package.json'); expect(res).toMatchSnapshot(); - expect(Object.keys(res.lockedVersions)).toHaveLength(7); + expect(Object.keys(res.lockedVersions!)).toHaveLength(7); }); it('extracts npm 7 lockfile', async () => { @@ -25,14 +25,14 @@ describe('modules/manager/npm/extract/npm', () => { fs.readLocalFile.mockResolvedValueOnce(npm7Lock as never); const res = await getNpmLock('package.json'); expect(res).toMatchSnapshot(); - expect(Object.keys(res.lockedVersions)).toHaveLength(7); + expect(Object.keys(res.lockedVersions!)).toHaveLength(7); expect(res.lockfileVersion).toBe(2); }); it('returns empty if no deps', async () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const res = await getNpmLock('package.json'); - expect(Object.keys(res.lockedVersions)).toHaveLength(0); + expect(Object.keys(res.lockedVersions!)).toHaveLength(0); }); }); }); diff --git a/lib/modules/manager/npm/extract/pnpm.spec.ts b/lib/modules/manager/npm/extract/pnpm.spec.ts index 32259488514a59fa41b4e1b2473400f1f67a23de..c44a66257cf3ff68bc1ecd016d254d5fbb68a14e 100644 --- a/lib/modules/manager/npm/extract/pnpm.spec.ts +++ b/lib/modules/manager/npm/extract/pnpm.spec.ts @@ -236,33 +236,30 @@ describe('modules/manager/npm/extract/pnpm', () => { it('returns empty if failed to parse', async () => { readLocalFile.mockResolvedValueOnce(undefined as never); const res = await getPnpmLock('package.json'); - expect(Object.keys(res.lockedVersions)).toHaveLength(0); + expect(res.lockedVersionsWithPath).toBeUndefined(); }); - it('extracts', async () => { + it('extracts version from monorepo', async () => { const plocktest1Lock = Fixtures.get('pnpm-monorepo/pnpm-lock.yaml', '..'); readLocalFile.mockResolvedValueOnce(plocktest1Lock); const res = await getPnpmLock('package.json'); - expect(Object.keys(res.lockedVersions)).toHaveLength(8); + expect(Object.keys(res.lockedVersionsWithPath!)).toHaveLength(11); }); - it('logs when packagePath is invalid', async () => { + it('extracts version from normal repo', async () => { const plocktest1Lock = Fixtures.get( 'lockfile-parsing/pnpm-lock.yaml', '..' ); readLocalFile.mockResolvedValueOnce(plocktest1Lock); const res = await getPnpmLock('package.json'); - expect(Object.keys(res.lockedVersions)).toHaveLength(2); - expect(logger.logger.trace).toHaveBeenLastCalledWith( - 'Invalid package path /sux-1.2.4' - ); + expect(Object.keys(res.lockedVersionsWithPath!)).toHaveLength(1); }); it('returns empty if no deps', async () => { readLocalFile.mockResolvedValueOnce('{}'); const res = await getPnpmLock('package.json'); - expect(Object.keys(res.lockedVersions)).toHaveLength(0); + expect(res.lockedVersionsWithPath).toBeUndefined(); }); }); }); diff --git a/lib/modules/manager/npm/extract/pnpm.ts b/lib/modules/manager/npm/extract/pnpm.ts index 3ceeabc30bb001ac8cd780d6f198cf02d2881ef8..49c0da8a8b8987fad366510328295a477f1bf827 100644 --- a/lib/modules/manager/npm/extract/pnpm.ts +++ b/lib/modules/manager/npm/extract/pnpm.ts @@ -10,9 +10,8 @@ import { localPathExists, readLocalFile, } from '../../../../util/fs'; -import { regEx } from '../../../../util/regex'; import type { PackageFile } from '../../types'; -import type { PnpmLockFile } from '../post-update/types'; +import type { PnpmDependencySchema, PnpmLockFile } from '../post-update/types'; import type { NpmManagerData } from '../types'; import type { LockFile, PnpmWorkspaceFile } from './types'; @@ -160,29 +159,10 @@ export async function getPnpmLock(filePath: string): Promise<LockFile> { ? lockParsed.lockfileVersion : parseFloat(lockParsed.lockfileVersion); - const lockedVersions: Record<string, string> = {}; - const packagePathRegex = regEx( - /^\/(?<packageName>.+)(?:@|\/)(?<version>[^/@]+)$/ - ); // eg. "/<packageName>(@|/)<version>" + const lockedVersions = getLockedVersions(lockParsed); - for (const packagePath of Object.keys(lockParsed.packages ?? {})) { - const result = packagePath.match(packagePathRegex); - if (!result?.groups) { - logger.trace(`Invalid package path ${packagePath}`); - continue; - } - - const packageName = result.groups.packageName; - const version = result.groups.version; - logger.trace({ - packagePath, - packageName, - version, - }); - lockedVersions[packageName] = version; - } return { - lockedVersions, + lockedVersionsWithPath: lockedVersions, lockfileVersion, }; } catch (err) { @@ -190,3 +170,57 @@ export async function getPnpmLock(filePath: string): Promise<LockFile> { return { lockedVersions: {} }; } } + +function getLockedVersions( + lockParsed: PnpmLockFile +): Record<string, Record<string, Record<string, string>>> { + const lockedVersions: Record< + string, + Record<string, Record<string, string>> + > = {}; + + // monorepo + if (is.nonEmptyObject(lockParsed.importers)) { + for (const [importer, imports] of Object.entries(lockParsed.importers)) { + // eslint-disable-next-line + console.log(imports); + lockedVersions[importer] = getLockedDependencyVersions(imports); + } + } + // normal repo + else { + lockedVersions['.'] = getLockedDependencyVersions(lockParsed); + } + + return lockedVersions; +} + +function getLockedDependencyVersions( + obj: PnpmLockFile | Record<string, PnpmDependencySchema> +): Record<string, Record<string, string>> { + const dependencyTypes = [ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + ] as const; + + const res: Record<string, Record<string, string>> = {}; + for (const depType of dependencyTypes) { + res[depType] = {}; + for (const [pkgName, versionCarrier] of Object.entries( + obj[depType] ?? {} + )) { + let version: string; + if (is.object(versionCarrier)) { + version = versionCarrier['version']; + } else { + version = versionCarrier; + } + + const pkgVersion = version.split('(')[0].trim(); + res[depType][pkgName] = pkgVersion; + } + } + + return res; +} diff --git a/lib/modules/manager/npm/extract/types.ts b/lib/modules/manager/npm/extract/types.ts index babf9b658fa0677d9b60188d18db74beb5a852ac..fbfbb6546960fe866a7998286c4599d0098cfefe 100644 --- a/lib/modules/manager/npm/extract/types.ts +++ b/lib/modules/manager/npm/extract/types.ts @@ -22,7 +22,11 @@ export type LockFileEntry = Record< >; export interface LockFile { - lockedVersions: Record<string, string>; + lockedVersions?: Record<string, string>; + lockedVersionsWithPath?: Record< + string, + Record<string, Record<string, string>> + >; lockfileVersion?: number; // cache version for Yarn isYarn1?: boolean; } diff --git a/lib/modules/manager/npm/extract/yarn.spec.ts b/lib/modules/manager/npm/extract/yarn.spec.ts index 4e3bed358a723824af67022421c392a67f4c4a3d..594dddce55ee9bab9c5094a0f4f21f2d72b2cbb3 100644 --- a/lib/modules/manager/npm/extract/yarn.spec.ts +++ b/lib/modules/manager/npm/extract/yarn.spec.ts @@ -10,7 +10,7 @@ describe('modules/manager/npm/extract/yarn', () => { fs.readLocalFile.mockResolvedValueOnce('abcd'); const res = await getYarnLock('package.json'); expect(res.isYarn1).toBeTrue(); - expect(Object.keys(res.lockedVersions)).toHaveLength(0); + expect(Object.keys(res.lockedVersions!)).toHaveLength(0); }); it('extracts yarn 1', async () => { @@ -20,7 +20,7 @@ describe('modules/manager/npm/extract/yarn', () => { expect(res.isYarn1).toBeTrue(); expect(res.lockfileVersion).toBeUndefined(); expect(res.lockedVersions).toMatchSnapshot(); - expect(Object.keys(res.lockedVersions)).toHaveLength(7); + expect(Object.keys(res.lockedVersions!)).toHaveLength(7); }); it('extracts yarn 2', async () => { @@ -30,7 +30,7 @@ describe('modules/manager/npm/extract/yarn', () => { expect(res.isYarn1).toBeFalse(); expect(res.lockfileVersion).toBeNaN(); expect(res.lockedVersions).toMatchSnapshot(); - expect(Object.keys(res.lockedVersions)).toHaveLength(8); + expect(Object.keys(res.lockedVersions!)).toHaveLength(8); }); it('extracts yarn 2 cache version', async () => { @@ -40,7 +40,7 @@ describe('modules/manager/npm/extract/yarn', () => { expect(res.isYarn1).toBeFalse(); expect(res.lockfileVersion).toBe(6); expect(res.lockedVersions).toMatchSnapshot(); - expect(Object.keys(res.lockedVersions)).toHaveLength(10); + expect(Object.keys(res.lockedVersions!)).toHaveLength(10); }); it('ignores individual invalid entries', async () => { @@ -52,7 +52,7 @@ describe('modules/manager/npm/extract/yarn', () => { const res = await getYarnLock('package.json'); expect(res.isYarn1).toBeTrue(); expect(res.lockfileVersion).toBeUndefined(); - expect(Object.keys(res.lockedVersions)).toHaveLength(14); + expect(Object.keys(res.lockedVersions!)).toHaveLength(14); }); }); }); diff --git a/lib/modules/manager/npm/post-update/types.ts b/lib/modules/manager/npm/post-update/types.ts index a134a951e791348cbe52c1315259198cdb00bf72..58e687d48c9dc247257a8ddf3d7181f155217cda 100644 --- a/lib/modules/manager/npm/post-update/types.ts +++ b/lib/modules/manager/npm/post-update/types.ts @@ -30,9 +30,16 @@ export interface GenerateLockFileResult { stdout?: string; } +// the dependencies schema is different for v6 and other lockfile versions +// Ref: https://github.com/pnpm/spec/issues/4#issuecomment-1524059392 +export type PnpmDependencySchema = Record<string, { version: string } | string>; + export interface PnpmLockFile { lockfileVersion: number | string; - packages?: Record<string, unknown>; + importers?: Record<string, Record<string, PnpmDependencySchema>>; + dependencies: PnpmDependencySchema; + devDependencies: PnpmDependencySchema; + optionalDependencies: PnpmDependencySchema; } export interface YarnRcYmlFile {