Skip to content

Commit

Permalink
feat(manager/npm): handle pnpm lockfile updates (#26770)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
guillaumeduboc and viceice committed Feb 22, 2024
1 parent a1fddc4 commit 11658df
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 25 deletions.
2 changes: 1 addition & 1 deletion docs/usage/configuration-options.md
Expand Up @@ -3424,7 +3424,7 @@ Behavior:
- `bump` = e.g. bump the range even if the new version satisfies the existing range, e.g. `^1.0.0` -> `^1.1.0`
- `replace` = Replace the range with a newer one if the new version falls outside it, and update nothing otherwise
- `widen` = Widen the range with newer one, e.g. `^1.0.0` -> `^1.0.0 || ^2.0.0`
- `update-lockfile` = Update the lock file when in-range updates are available, otherwise `replace` for updates out of range. Works for `bundler`, `cargo`, `composer`, `npm`, `yarn`, `terraform` and `poetry` so far
- `update-lockfile` = Update the lock file when in-range updates are available, otherwise `replace` for updates out of range. Works for `bundler`, `cargo`, `composer`, `npm`, `yarn`, `pnpm`, `terraform` and `poetry` so far
- `in-range-only` = Update the lock file when in-range updates are available, ignore package file updates

Renovate's `"auto"` strategy works like this for npm:
Expand Down
11 changes: 5 additions & 6 deletions lib/modules/manager/npm/post-update/index.ts
Expand Up @@ -49,15 +49,14 @@ export function determineLockFileDirs(
const pnpmShrinkwrapDirs: (string | undefined)[] = [];

for (const upgrade of config.upgrades) {
if (upgrade.updateType === 'lockFileMaintenance' || upgrade.isRemediation) {
if (
upgrade.updateType === 'lockFileMaintenance' ||
upgrade.isRemediation === true ||
upgrade.isLockfileUpdate === true
) {
yarnLockDirs.push(upgrade.managerData?.yarnLock);
npmLockDirs.push(upgrade.managerData?.npmLock);
pnpmShrinkwrapDirs.push(upgrade.managerData?.pnpmShrinkwrap);
continue;
}
if (upgrade.isLockfileUpdate) {
yarnLockDirs.push(upgrade.managerData?.yarnLock);
npmLockDirs.push(upgrade.managerData?.npmLock);
}
}

Expand Down
83 changes: 79 additions & 4 deletions lib/modules/manager/npm/post-update/pnpm.spec.ts
Expand Up @@ -2,7 +2,7 @@ import { envMock, mockExecAll } from '../../../../../test/exec-util';
import { Fixtures } from '../../../../../test/fixtures';
import { env, fs, mockedFunction, partial } from '../../../../../test/util';
import { GlobalConfig } from '../../../../config/global';
import type { PostUpdateConfig } from '../../types';
import type { PostUpdateConfig, Upgrade } from '../../types';
import { getNodeToolConstraint } from './node-version';
import * as pnpmHelper from './pnpm';

Expand All @@ -15,6 +15,7 @@ process.env.CONTAINERBASE = 'true';

describe('modules/manager/npm/post-update/pnpm', () => {
let config: PostUpdateConfig;
const upgrades: Upgrade[] = [{}];

beforeEach(() => {
config = partial<PostUpdateConfig>({ constraints: { pnpm: '^2.0.0' } });
Expand All @@ -26,10 +27,22 @@ describe('modules/manager/npm/post-update/pnpm', () => {
});
});

it('does nothing when no upgrades', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
await pnpmHelper.generateLockFile('some-dir', {}, config);
expect(execSnapshots).toMatchObject([]);
});

it('generates lock files', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await pnpmHelper.generateLockFile('some-dir', {}, config);
const res = await pnpmHelper.generateLockFile(
'some-dir',
{},
config,
upgrades,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toMatchSnapshot();
Expand All @@ -40,7 +53,12 @@ describe('modules/manager/npm/post-update/pnpm', () => {
fs.readLocalFile.mockImplementation(() => {
throw new Error('not found');
});
const res = await pnpmHelper.generateLockFile('some-dir', {}, config);
const res = await pnpmHelper.generateLockFile(
'some-dir',
{},
config,
upgrades,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.error).toBeTrue();
expect(res.lockFile).toBeUndefined();
Expand All @@ -50,12 +68,66 @@ describe('modules/manager/npm/post-update/pnpm', () => {
it('finds pnpm globally', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await pnpmHelper.generateLockFile('some-dir', {}, config);
const res = await pnpmHelper.generateLockFile(
'some-dir',
{},
config,
upgrades,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toMatchSnapshot();
});

it('performs lock file updates', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await pnpmHelper.generateLockFile('some-folder', {}, config, [
{ packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: true },
{
packageName: 'some-other-dep',
newVersion: '1.1.0',
isLockfileUpdate: true,
},
]);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toMatchObject([
{
cmd: 'pnpm update --no-save some-dep@1.0.1 some-other-dep@1.1.0 --recursive --lockfile-only --ignore-scripts --ignore-pnpmfile',
},
]);
});

it('performs lock file updates and install when lock file updates mixed with regular updates', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await pnpmHelper.generateLockFile('some-folder', {}, config, [
{
groupName: 'some-group',
packageName: 'some-dep',
newVersion: '1.1.0',
isLockfileUpdate: true,
},
{
groupName: 'some-group',
packageName: 'some-other-dep',
newVersion: '1.1.0',
isLockfileUpdate: false,
},
]);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toMatchObject([
{
cmd: 'pnpm install --recursive --lockfile-only --ignore-scripts --ignore-pnpmfile',
},
{
cmd: 'pnpm update --no-save some-dep@1.1.0 --recursive --lockfile-only --ignore-scripts --ignore-pnpmfile',
},
]);
});

it('performs lock file maintenance', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
Expand All @@ -76,6 +148,7 @@ describe('modules/manager/npm/post-update/pnpm', () => {
'some-dir',
{},
{ ...config, postUpdateOptions },
upgrades,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
Expand Down Expand Up @@ -220,6 +293,7 @@ describe('modules/manager/npm/post-update/pnpm', () => {
'some-dir',
{},
{ ...config, constraints: { pnpm: '6.0.0' } },
upgrades,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
Expand Down Expand Up @@ -254,6 +328,7 @@ describe('modules/manager/npm/post-update/pnpm', () => {
'some-dir',
{},
{ ...config, constraints: { pnpm: '6.0.0' } },
upgrades,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
Expand Down
33 changes: 26 additions & 7 deletions lib/modules/manager/npm/post-update/pnpm.ts
@@ -1,4 +1,5 @@
import is from '@sindresorhus/is';
import { quote } from 'shlex';
import upath from 'upath';
import { GlobalConfig } from '../../../../config/global';
import { TEMPORARY_ERROR } from '../../../../constants/error-messages';
Expand All @@ -10,6 +11,7 @@ import type {
ToolConstraint,
} from '../../../../util/exec/types';
import { deleteLocalFile, readLocalFile } from '../../../../util/fs';
import { uniqueStrings } from '../../../../util/string';
import { parseSingleYaml } from '../../../../util/yaml';
import type { PostUpdateConfig, Upgrade } from '../../types';
import { getNodeToolConstraint } from './node-version';
Expand All @@ -36,7 +38,7 @@ export async function generateLockFile(
let lockFile: string | null = null;
let stdout: string | undefined;
let stderr: string | undefined;
let cmd = 'pnpm';
const commands: string[] = [];
try {
const lazyPgkJson = lazyLoadPackageJson(lockFileDir);
const pnpmToolConstraint: ToolConstraint = {
Expand Down Expand Up @@ -67,16 +69,33 @@ export async function generateLockFile(
extraEnv.NPM_AUTH = env.NPM_AUTH;
extraEnv.NPM_EMAIL = env.NPM_EMAIL;
}
const commands: string[] = [];

cmd = 'pnpm';
let args = 'install --recursive --lockfile-only';
let args = '--recursive --lockfile-only';
if (!GlobalConfig.get('allowScripts') || config.ignoreScripts) {
args += ' --ignore-scripts';
args += ' --ignore-pnpmfile';
}
logger.trace({ cmd, args }, 'pnpm command');
commands.push(`${cmd} ${args}`);
logger.trace({ args }, 'pnpm command options');

const lockUpdates = upgrades.filter((upgrade) => upgrade.isLockfileUpdate);

if (lockUpdates.length !== upgrades.length) {
// This command updates the lock file based on package.json
commands.push(`pnpm install ${args}`);
}

// rangeStrategy = update-lockfile
if (lockUpdates.length) {
logger.debug('Performing lockfileUpdate (pnpm)');
commands.push(
`pnpm update --no-save ${lockUpdates
// TODO: types (#22198)
.map((update) => `${update.packageName!}@${update.newVersion!}`)
.filter(uniqueStrings)
.map(quote)
.join(' ')} ${args}`,
);
}

// postUpdateOptions
if (config.postUpdateOptions?.includes('pnpmDedupe')) {
Expand Down Expand Up @@ -105,7 +124,7 @@ export async function generateLockFile(
}
logger.debug(
{
cmd,
commands,
err,
stdout,
stderr,
Expand Down
Expand Up @@ -246,10 +246,10 @@ describe('modules/manager/npm/update/locked-dependency/index', () => {
expect(res.status).toBe('update-failed');
});

it('fails if pnpm', async () => {
it('rejects in-range remediation if pnpm', async () => {
config.lockFile = 'pnpm-lock.yaml';
const res = await updateLockedDependency(config);
expect(res.status).toBe('update-failed');
expect(res.status).toBe('unsupported');
});
});
});
10 changes: 5 additions & 5 deletions lib/modules/manager/npm/update/locked-dependency/index.ts
Expand Up @@ -21,12 +21,12 @@ export async function updateLockedDependency(
}
if (lockFile.endsWith('pnpm-lock.yaml')) {
logger.debug(
'updateLockedDependency(): pnpm is not supported yet. See https://github.com/renovatebot/renovate/issues/21438',
);
} else {
logger.debug(
`updateLockedDependency(): unsupported lock file: ${lockFile}`,
'Cannot patch pnpm lock file directly - falling back to using pnpm',
);
return { status: 'unsupported' };
}

logger.debug(`updateLockedDependency(): unsupported lock file: ${lockFile}`);

return { status: 'update-failed' };
}

0 comments on commit 11658df

Please sign in to comment.