Skip to content

Commit

Permalink
fix(pnpm): modify locked version extraction (#22415)
Browse files Browse the repository at this point in the history
Co-authored-by: Rhys Arkins <rhys@arkins.net>
  • Loading branch information
RahulGautamSingh and rarkins committed Jun 1, 2023
1 parent cbff6cd commit 97d2bf4
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 56 deletions.
32 changes: 24 additions & 8 deletions lib/modules/manager/npm/extract/locked-versions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand All @@ -510,27 +514,39 @@ 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);
expect(packageFiles).toEqual([
{
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',
},
]);
});
Expand Down
20 changes: 15 additions & 5 deletions lib/modules/manager/npm/extract/locked-versions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import is from '@sindresorhus/is';
import semver from 'semver';
import { logger } from '../../../../logger';
import type { PackageFile } from '../../types';
Expand Down Expand Up @@ -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}`
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
8 changes: 4 additions & 4 deletions lib/modules/manager/npm/extract/npm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,30 @@ 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 () => {
const plocktest1Lock = Fixtures.get('plocktest1/package-lock.json', '..');
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 () => {
const npm7Lock = Fixtures.get('npm7/package-lock.json', '..');
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);
});
});
});
15 changes: 6 additions & 9 deletions lib/modules/manager/npm/extract/pnpm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
80 changes: 57 additions & 23 deletions lib/modules/manager/npm/extract/pnpm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -160,33 +159,68 @@ 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) {
logger.debug({ filePath, err }, 'Warning: Exception parsing pnpm 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;
}
6 changes: 5 additions & 1 deletion lib/modules/manager/npm/extract/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
10 changes: 5 additions & 5 deletions lib/modules/manager/npm/extract/yarn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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);
});
});
});
9 changes: 8 additions & 1 deletion lib/modules/manager/npm/post-update/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 97d2bf4

Please sign in to comment.