Files
renovate/lib/modules/manager/npm/post-update/index.spec.ts

1156 lines
34 KiB
TypeScript

import upath from 'upath';
import { Fixtures } from '~test/fixtures.ts';
import { fs, git, logger, partial, scm } from '~test/util.ts';
import { GlobalConfig } from '../../../../config/global.ts';
import type { FileChange } from '../../../../util/git/types.ts';
import type { PostUpdateConfig } from '../../types.ts';
import {
determineLockFileDirs,
getAdditionalFiles,
updateYarnBinary,
writeExistingFiles,
writeUpdatedPackageFiles,
} from './index.ts';
import * as npm from './npm.ts';
import * as pnpm from './pnpm.ts';
import * as rules from './rules.ts';
import type { AdditionalPackageFiles } from './types.ts';
import * as yarn from './yarn.ts';
vi.mock('../../../../util/fs/index.ts');
vi.mock('./npm.ts');
vi.mock('./yarn.ts');
vi.mock('./pnpm.ts');
describe('modules/manager/npm/post-update/index', () => {
let baseConfig: PostUpdateConfig;
let updateConfig: PostUpdateConfig;
const additionalFiles: AdditionalPackageFiles = {
npm: [
{ packageFile: 'dummy.txt' },
{
packageFile: 'packages/core/package.json',
managerData: {
npmLock: 'package-lock.json',
},
npmrc: '#dummy',
},
{
packageFile: 'packages/cli/package.json',
managerData: {
yarnLock: 'yarn.lock',
},
},
{
packageFile: 'packages/test/package.json',
managerData: {
yarnLock: 'yarn.lock',
},
},
{
packageFile: 'packages/pnpm/package.json',
managerData: {
pnpmLockFile: 'packages/pnpm/pnpm-lock.yaml',
},
},
],
};
beforeEach(() => {
GlobalConfig.set({ localDir: '' });
baseConfig = partial<PostUpdateConfig>({
upgrades: [],
});
updateConfig = {
...baseConfig,
upgrades: [
{
isRemediation: true,
},
{
depName: 'postcss',
isRemediation: true,
managerData: {
npmLock: 'package-lock.json',
},
rangeStrategy: 'widen',
},
{
depName: 'core-js',
isRemediation: true,
managerData: {
npmLock: 'randomFolder/package-lock.json',
},
lockFiles: ['randomFolder/package-lock.json'],
rangeStrategy: 'pin',
},
{
isLockfileUpdate: true,
managerData: {
npmLock: 'package-lock.json',
},
},
{
managerData: {
yarnLock: 'yarn.lock',
},
isLockfileUpdate: true,
},
],
updatedPackageFiles: [
{
type: 'addition',
path: 'dummy.txt',
contents: '',
},
{
type: 'deletion',
path: 'some.txt',
},
{
type: 'addition',
path: 'package-lock.json',
contents: '{}',
},
{
type: 'addition',
path: 'yarn.lock',
contents: '{}',
},
{
type: 'addition',
path: 'packages/pnpm/pnpm-lock.yaml',
contents: '',
},
{
type: 'addition',
path: 'packages/core/package.json',
contents: '{}',
},
{
type: 'addition',
path: 'packages/cli/package.json',
contents: '{}',
},
{
type: 'addition',
path: 'packages/pnpm/package.json',
contents: '{}',
},
{
type: 'addition',
path: 'package.json',
contents: '{}',
},
],
};
// reset mocked version
fs.getParentDir.mockImplementation((p) => upath.parse(p).dir);
});
describe('determineLockFileDirs()', () => {
it('works', () => {
expect(
determineLockFileDirs(
updateConfig,
additionalFiles,
),
).toStrictEqual({
npmLockDirs: ['package-lock.json', 'randomFolder/package-lock.json'],
pnpmLockFileDirs: ['packages/pnpm/pnpm-lock.yaml'],
yarnLockDirs: ['yarn.lock'],
});
});
it('lockfile maintenance', () => {
expect(
determineLockFileDirs(
{
...baseConfig,
upgrades: [
{
isLockfileUpdate: true,
managerData: {
yarnLock: 'yarn.lock',
},
},
],
},
{},
),
).toStrictEqual({
npmLockDirs: [],
pnpmLockFileDirs: [],
yarnLockDirs: ['yarn.lock'],
});
});
});
describe('writeExistingFiles()', () => {
it('works', async () => {
git.getFile.mockResolvedValueOnce(
Fixtures.get('update-lockfile-massage-1/package-lock.json'),
);
await expect(
writeExistingFiles(updateConfig, additionalFiles),
).resolves.toBeUndefined();
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
expect(fs.deleteLocalFile).not.toHaveBeenCalled();
expect(git.getFile).toHaveBeenCalledExactlyOnceWith('package-lock.json');
});
it('massages out lockstep siblings pinning the old version', async () => {
git.getFile.mockResolvedValueOnce(
JSON.stringify({
name: 'update-lockfile-massage-2',
version: '1.0.0',
lockfileVersion: 3,
requires: true,
packages: {
'': {
name: 'update-lockfile-massage-2',
version: '1.0.0',
dependencies: { vue: '^3.3.10' },
devDependencies: { '@vue/test-utils': '^2.4.3' },
},
'node_modules/vue': {
version: '3.5.39',
dependencies: {
'@vue/server-renderer': '3.5.39',
'@vue/shared': '3.5.39',
},
},
// lockstep sibling pinning vue via peerDependencies
'node_modules/@vue/server-renderer': {
version: '3.5.39',
dependencies: { '@vue/shared': '3.5.39' },
peerDependencies: { vue: '3.5.39' },
},
// lockstep sibling pinning vue via dependencies
'node_modules/@vue/compiler-sfc': {
version: '3.5.39',
dependencies: { vue: '3.5.39' },
},
// lockstep sibling pinning vue via optionalDependencies
'node_modules/@vue/optional-consumer': {
version: '3.5.39',
optionalDependencies: { vue: '3.5.39' },
},
'node_modules/@vue/shared': {
version: '3.5.39',
},
// range peer pin on vue, must stay
'node_modules/@vue/test-utils': {
version: '2.4.11',
dev: true,
peerDependencies: { vue: '3.x' },
},
// workspace pinning vue exactly, must stay: it is the link target
// of node_modules/@vue-repro/app
'packages/app': {
name: '@vue-repro/app',
version: '1.0.0',
dependencies: { vue: '3.5.39' },
},
'node_modules/@vue-repro/app': {
resolved: 'packages/app',
link: true,
},
},
}),
);
const config = {
...baseConfig,
upgrades: [
{
depName: 'vue',
lockedVersion: '3.5.39',
newVersion: '3.5.40',
managerData: { npmLock: 'package-lock.json' },
},
],
};
await writeExistingFiles(config, {
npm: [
{
packageFile: 'package.json',
managerData: { npmLock: 'package-lock.json' },
},
],
});
const lockWrite = fs.writeLocalFile.mock.calls.find(
(call) => call[0] === 'package-lock.json',
);
expect(lockWrite).toBeDefined();
const written = JSON.parse(lockWrite![1] as string);
// the updated dep itself and its exact-pinned lockstep siblings go
expect(written.packages['node_modules/vue']).toBeUndefined();
expect(
written.packages['node_modules/@vue/server-renderer'],
).toBeUndefined();
expect(
written.packages['node_modules/@vue/compiler-sfc'],
).toBeUndefined();
expect(
written.packages['node_modules/@vue/optional-consumer'],
).toBeUndefined();
// packages without an exact pin on the updated dep stay
expect(written.packages['node_modules/@vue/test-utils']).toBeDefined();
expect(written.packages['node_modules/@vue/shared']).toBeDefined();
// workspaces stay even when they pin exactly, else npm fails with
// EMISSINGTARGET on the link entry referencing them
expect(written.packages['packages/app']).toBeDefined();
expect(written.packages['node_modules/@vue-repro/app']).toBeDefined();
});
it('writes .npmrc files', async () => {
await writeExistingFiles(updateConfig, {
npm: [
// This package's npmrc should be written verbatim.
{
packageFile: 'packages/core/package.json',
npmrc: '#dummy',
managerData: {},
},
// No npmrc content should be written for this package.
{ packageFile: 'packages/core/package.json', managerData: {} },
],
});
expect(fs.writeLocalFile).toHaveBeenCalledExactlyOnceWith(
'packages/core/.npmrc',
'#dummy\n',
);
});
it('only sources npmrc content from package config', async () => {
await writeExistingFiles(
{ ...updateConfig, npmrc: '#foobar' },
{
npm: [
// This package's npmrc should be written verbatim.
{
packageFile: 'packages/core/package.json',
npmrc: '#dummy',
managerData: {},
},
// No npmrc content should be written for this package.
{ packageFile: 'packages/core/package.json', managerData: {} },
],
},
);
expect(fs.writeLocalFile).toHaveBeenCalledExactlyOnceWith(
'packages/core/.npmrc',
'#dummy\n',
);
});
it('snapshots a shared .npmrc only once', async () => {
fs.readLocalFile.mockResolvedValue('original');
const originalNpmrcFiles = new Map<string, string>();
await writeExistingFiles(
updateConfig,
{
npm: [
{
packageFile: 'packages/core/package.json',
npmrc: 'first',
managerData: {},
},
{
packageFile: 'packages/core/package.json',
npmrc: 'second',
managerData: {},
},
],
},
originalNpmrcFiles,
);
expect(fs.readLocalFile).toHaveBeenCalledExactlyOnceWith(
'packages/core/.npmrc',
'utf8',
);
expect(originalNpmrcFiles).toEqual(
new Map([['packages/core/.npmrc', 'original']]),
);
});
it('logs and continues if a temporary .npmrc cannot be prepared', async () => {
const err = new Error('read failed');
fs.readLocalFile.mockRejectedValueOnce(err);
await expect(
writeExistingFiles(updateConfig, {
npm: [
{
packageFile: 'packages/core/package.json',
npmrc: 'sanitized',
managerData: {},
},
],
}),
).toResolve();
expect(fs.writeLocalFile).not.toHaveBeenCalled();
expect(logger.logger.warn).toHaveBeenCalledWith(
{ npmrcFilename: 'packages/core/.npmrc', err },
'Error writing .npmrc',
);
});
it('works only on relevant folders', async () => {
git.getFile.mockResolvedValueOnce(
Fixtures.get('update-lockfile-massage-1/package-lock.json'),
);
await expect(
writeExistingFiles(updateConfig, additionalFiles),
).resolves.toBeUndefined();
expect(fs.writeLocalFile).toHaveBeenCalledTimes(2);
expect(fs.deleteLocalFile).not.toHaveBeenCalled();
expect(git.getFile).toHaveBeenCalledExactlyOnceWith('package-lock.json');
});
it('has no npm files', async () => {
await expect(writeExistingFiles(baseConfig, {})).toResolve();
});
});
describe('writeUpdatedPackageFiles()', () => {
it('works', async () => {
await writeUpdatedPackageFiles({
...updateConfig,
upgrades: [{ gitRef: true }],
});
expect(fs.writeLocalFile).toHaveBeenCalledTimes(6);
});
it('missing updated packages files', async () => {
await expect(
writeUpdatedPackageFiles(baseConfig),
).resolves.toBeUndefined();
expect(fs.writeLocalFile).not.toHaveBeenCalled();
});
it('prefers artifact content over package file content for the same path', async () => {
await writeUpdatedPackageFiles({
...baseConfig,
updatedPackageFiles: [
{
type: 'addition',
path: 'pnpm-workspace.yaml',
contents: 'catalog:\n effect: ^3.20.0\nminimumReleaseAge: 10080\n',
},
],
updatedArtifacts: [
{
type: 'deletion',
path: 'some-deleted-file.yaml',
},
{
type: 'addition',
path: 'pnpm-workspace.yaml',
contents:
'catalog:\n effect: ^3.20.0\nminimumReleaseAge: 10080\nminimumReleaseAgeExclude:\n - effect@3.20.0\n',
},
],
});
expect(fs.writeLocalFile).toHaveBeenCalledOnce();
expect(fs.writeLocalFile).toHaveBeenCalledWith(
'pnpm-workspace.yaml',
'catalog:\n effect: ^3.20.0\nminimumReleaseAge: 10080\nminimumReleaseAgeExclude:\n - effect@3.20.0\n',
);
});
});
describe('updateYarnBinary()', () => {
const lockFileDir = `path/to/lockfile`;
const oldYarnrcYml = `yarnPath: .yarn/releases/yarn-3.0.1.cjs\na: b\n`;
const newYarnrcYml = `yarnPath: .yarn/releases/yarn-3.0.2.cjs\nc: d\n`;
const newYarn = `new yarn\n`;
it('should update the Yarn binary', async () => {
git.getFile.mockResolvedValueOnce(oldYarnrcYml);
fs.readLocalFile.mockResolvedValueOnce(newYarnrcYml);
fs.readLocalFile.mockResolvedValueOnce(newYarn);
const updatedArtifacts: FileChange[] = [];
const yarnrcYmlContent = await updateYarnBinary(
lockFileDir,
updatedArtifacts,
undefined,
);
expect(yarnrcYmlContent).toBeUndefined();
expect(updatedArtifacts).toEqual([
{
type: 'addition',
path: 'path/to/lockfile/.yarnrc.yml',
contents: 'yarnPath: .yarn/releases/yarn-3.0.2.cjs\na: b\n',
},
{
type: 'deletion',
path: 'path/to/lockfile/.yarn/releases/yarn-3.0.1.cjs',
},
{
type: 'addition',
path: 'path/to/lockfile/.yarn/releases/yarn-3.0.2.cjs',
contents: 'new yarn\n',
isExecutable: true,
},
]);
});
it('should return .yarnrc.yml content if it has been overwritten', async () => {
fs.readLocalFile.mockResolvedValueOnce(newYarnrcYml);
fs.readLocalFile.mockResolvedValueOnce(newYarn);
const updatedArtifacts: FileChange[] = [];
const existingYarnrcYmlContent = await updateYarnBinary(
lockFileDir,
updatedArtifacts,
oldYarnrcYml,
);
expect(git.getFile).not.toHaveBeenCalled();
expect(existingYarnrcYmlContent).toBe(
'yarnPath: .yarn/releases/yarn-3.0.2.cjs\na: b\n',
);
expect(updatedArtifacts).toEqual([
{
type: 'addition',
path: 'path/to/lockfile/.yarnrc.yml',
contents: 'yarnPath: .yarn/releases/yarn-3.0.2.cjs\na: b\n',
},
{
type: 'deletion',
path: 'path/to/lockfile/.yarn/releases/yarn-3.0.1.cjs',
},
{
type: 'addition',
path: 'path/to/lockfile/.yarn/releases/yarn-3.0.2.cjs',
contents: 'new yarn\n',
isExecutable: true,
},
]);
});
it("should not update the Yarn binary if the old .yarnrc.yml doesn't exist", async () => {
git.getFile.mockResolvedValueOnce(null);
fs.readLocalFile.mockResolvedValueOnce(newYarnrcYml);
const updatedArtifacts: FileChange[] = [];
const yarnrcYmlContent = await updateYarnBinary(
lockFileDir,
updatedArtifacts,
undefined,
);
expect(yarnrcYmlContent).toBeUndefined();
expect(updatedArtifacts).toBeEmpty();
});
it("should not update the Yarn binary if the new .yarnrc.yml doesn't exist", async () => {
git.getFile.mockResolvedValueOnce(oldYarnrcYml);
fs.readLocalFile.mockResolvedValueOnce(null);
const updatedArtifacts: FileChange[] = [];
const yarnrcYmlContent = await updateYarnBinary(
lockFileDir,
updatedArtifacts,
undefined,
);
expect(yarnrcYmlContent).toBeUndefined();
expect(updatedArtifacts).toBeEmpty();
});
it("should return existing .yarnrc.yml if the new one doesn't exist", async () => {
fs.readLocalFile.mockResolvedValueOnce(null);
const updatedArtifacts: FileChange[] = [];
const existingYarnrcYmlContent = await updateYarnBinary(
lockFileDir,
updatedArtifacts,
oldYarnrcYml,
);
expect(existingYarnrcYmlContent).toMatch(oldYarnrcYml);
expect(updatedArtifacts).toBeEmpty();
});
it('should support Yarn with corepack', async () => {
git.getFile.mockResolvedValueOnce('');
fs.readLocalFile.mockResolvedValueOnce('');
fs.readLocalFile.mockResolvedValueOnce('');
const updatedArtifacts: FileChange[] = [];
const yarnrcYmlContent = await updateYarnBinary(
lockFileDir,
updatedArtifacts,
'',
);
expect(yarnrcYmlContent).toBe('');
expect(updatedArtifacts).toEqual([]);
expect(logger.logger.debug).not.toHaveBeenCalled();
expect(logger.logger.error).not.toHaveBeenCalled();
});
});
describe('getAdditionalFiles()', () => {
const spyNpm = vi.spyOn(npm, 'generateLockFile');
const spyYarn = vi.spyOn(yarn, 'generateLockFile');
const spyPnpm = vi.spyOn(pnpm, 'generateLockFile');
const spyProcessHostRules = vi.spyOn(rules, 'processHostRules');
beforeEach(() => {
spyNpm.mockResolvedValue({});
spyPnpm.mockResolvedValue({});
spyYarn.mockResolvedValue({});
spyProcessHostRules.mockReturnValue({
additionalNpmrcContent: [],
additionalYarnRcYml: undefined,
});
});
it('works', async () => {
await expect(
getAdditionalFiles({ ...updateConfig }, additionalFiles),
).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [],
});
});
it('works for npm', async () => {
spyNpm.mockResolvedValueOnce({ error: false, lockFile: '{}' });
fs.readLocalFile.mockImplementation((f): Promise<string> => {
if (f === '.npmrc') {
return Promise.resolve('# dummy');
}
return Promise.resolve('');
});
await expect(
getAdditionalFiles(
{ ...updateConfig, reuseExistingBranch: true },
additionalFiles,
),
).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [
{
type: 'addition',
path: 'package-lock.json',
contents: '{}',
},
],
});
expect(fs.readLocalFile).toHaveBeenCalledWith('.npmrc', 'utf8');
expect(fs.writeLocalFile).toHaveBeenCalledWith('.npmrc', '# dummy');
expect(fs.deleteLocalFile.mock.calls).toMatchObject([
['randomFolder/.npmrc'],
['packages/pnpm/.npmrc'],
]);
});
it('restores a nested repository .npmrc after generating artifacts', async () => {
const npmrcFilename = 'packages/core/.npmrc';
const originalNpmrc = 'package-lock=false\r\nkeep = true';
const files = new Map<string, string>([[npmrcFilename, originalNpmrc]]);
fs.readLocalFile.mockImplementation((fileName): Promise<string | null> =>
Promise.resolve(files.get(fileName) ?? null),
);
fs.writeLocalFile.mockImplementation((fileName, content) => {
files.set(fileName, content.toString());
return Promise.resolve();
});
fs.deleteLocalFile.mockImplementation((fileName) => {
files.delete(fileName);
return Promise.resolve();
});
const packageFiles: AdditionalPackageFiles = {
npm: [
{
packageFile: 'packages/core/package.json',
npmrc: 'keep = true',
managerData: {
npmLock: 'packages/core/package-lock.json',
npmrcFileName: npmrcFilename,
},
},
],
};
const config = partial<PostUpdateConfig>({
upgrades: [{}],
updatedPackageFiles: [
{
type: 'addition',
path: 'packages/core/package.json',
contents: '{}',
},
],
});
await expect(getAdditionalFiles(config, packageFiles)).toResolve();
expect(files.get(npmrcFilename)).toBe(originalNpmrc);
expect(fs.writeLocalFile).toHaveBeenCalledWith(
npmrcFilename,
'keep = true\n',
);
expect(fs.writeLocalFile).toHaveBeenLastCalledWith(
npmrcFilename,
originalNpmrc,
);
});
it('restores a repository .npmrc when artifact generation throws', async () => {
const npmrcFilename = '.npmrc';
const originalNpmrc = 'package-lock=false';
let npmrcOnDisk = originalNpmrc;
fs.readLocalFile.mockImplementation((fileName): Promise<string | null> =>
Promise.resolve(fileName === npmrcFilename ? npmrcOnDisk : null),
);
fs.writeLocalFile.mockImplementation((fileName, content) => {
if (fileName === npmrcFilename) {
npmrcOnDisk = content.toString();
}
return Promise.resolve();
});
spyNpm.mockImplementationOnce(() => {
expect(npmrcOnDisk).toBe('\n');
return Promise.reject(new Error('artifact failure'));
});
const packageFiles: AdditionalPackageFiles = {
npm: [
{
packageFile: 'package.json',
npmrc: '',
managerData: {
npmLock: 'package-lock.json',
npmrcFileName: npmrcFilename,
},
},
],
};
const config = partial<PostUpdateConfig>({
upgrades: [{}],
updatedPackageFiles: [
{
type: 'addition',
path: 'package.json',
contents: '{}',
},
],
});
await expect(getAdditionalFiles(config, packageFiles)).rejects.toThrow(
'artifact failure',
);
expect(npmrcOnDisk).toBe(originalNpmrc);
expect(fs.writeLocalFile).toHaveBeenLastCalledWith(
npmrcFilename,
originalNpmrc,
);
});
it('logs and continues when restoring a repository .npmrc fails', async () => {
const npmrcFilename = '.npmrc';
const originalNpmrc = 'package-lock=false';
const err = new Error('restore failed');
fs.readLocalFile.mockResolvedValue(originalNpmrc);
fs.writeLocalFile.mockResolvedValueOnce().mockRejectedValueOnce(err);
await expect(
getAdditionalFiles(partial<PostUpdateConfig>({ upgrades: [] }), {
npm: [
{
packageFile: 'package.json',
npmrc: '',
managerData: { npmrcFileName: npmrcFilename },
},
],
}),
).toResolve();
expect(fs.writeLocalFile).toHaveBeenNthCalledWith(1, npmrcFilename, '\n');
expect(fs.writeLocalFile).toHaveBeenNthCalledWith(
2,
npmrcFilename,
originalNpmrc,
);
expect(logger.logger.warn).toHaveBeenCalledWith(
{ npmrcFilename, err },
'Error restoring .npmrc',
);
});
it('adds artifact notice on beforeFallback', async () => {
spyNpm.mockResolvedValueOnce({
error: false,
lockFile: '{}',
beforeFallback: true,
});
fs.readLocalFile.mockImplementation((f): Promise<string> => {
if (f === '.npmrc') {
return Promise.resolve('# dummy');
}
return Promise.resolve('');
});
const res = await getAdditionalFiles(
{ ...updateConfig, reuseExistingBranch: true },
additionalFiles,
);
expect(res.artifactNotices).toEqual([
{
file: 'package-lock.json',
message:
'npm `--before` could not be enforced because existing locked packages were published after the `minimumReleaseAge` cutoff. This will resolve after the next lock file maintenance run.',
},
]);
expect(logger.logger.warn).toHaveBeenCalledWith(
{ npmLock: 'package-lock.json' },
'npm `--before` could not be enforced because existing locked packages were published after the `minimumReleaseAge` cutoff. This will resolve after the next lock file maintenance run.',
);
});
it('detects if lock file contents are unchanged(reuseExistingBranch=true)', async () => {
spyNpm.mockResolvedValueOnce({ error: false, lockFile: '{}' });
fs.readLocalFile.mockImplementation((f): Promise<any> => {
if (f === 'package-lock.json') {
return Promise.resolve('{}');
}
return Promise.resolve(null);
});
git.getFile.mockImplementation((f) => {
if (f === 'package-lock.json') {
return Promise.resolve('{}');
}
return Promise.resolve(null);
});
expect(
(
await getAdditionalFiles(
{
...updateConfig,
reuseExistingBranch: true,
},
additionalFiles,
)
).updatedArtifacts.find((a) => a.path === 'package-lock.json'),
).toBeUndefined();
});
// for coverage run once when not reusing the branch
it('detects if lock file contents are unchanged(reuseExistingBranch=false)', async () => {
spyNpm.mockResolvedValueOnce({ error: false, lockFile: '{}' });
fs.readLocalFile.mockImplementation((f): Promise<any> => {
if (f === 'package-lock.json') {
return Promise.resolve('{}');
}
return Promise.resolve(null);
});
git.getFile.mockImplementation((f) => {
if (f === 'package-lock.json') {
return Promise.resolve('{}');
}
return Promise.resolve(null);
});
expect(
(
await getAdditionalFiles(
{
...updateConfig,
reuseExistingBranch: false,
baseBranch: 'base',
},
additionalFiles,
)
).updatedArtifacts.find((a) => a.path === 'package-lock.json'),
).toBeUndefined();
});
it('works for yarn', async () => {
spyYarn.mockResolvedValueOnce({ error: false, lockFile: '{}' });
await expect(
getAdditionalFiles(
{ ...updateConfig, reuseExistingBranch: true },
additionalFiles,
),
).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [
{
type: 'addition',
path: 'yarn.lock',
contents: '{}',
},
],
});
expect(fs.deleteLocalFile).toHaveBeenCalled();
});
it('works for pnpm', async () => {
spyPnpm.mockResolvedValueOnce({
error: false,
lockFile: 'some-contents:',
});
await expect(
getAdditionalFiles(
{
...updateConfig,
reuseExistingBranch: true,
upgrades: [
{
isRemediation: true,
packageFile: 'packages/pnpm/package.json',
},
],
},
additionalFiles,
),
).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [
{
type: 'addition',
path: 'packages/pnpm/pnpm-lock.yaml',
contents: 'some-contents:',
},
],
});
expect(fs.deleteLocalFile).toHaveBeenCalled();
});
it('no npm files', async () => {
await expect(getAdditionalFiles(baseConfig, {})).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [],
});
});
it('no lockfiles updates', async () => {
await expect(
getAdditionalFiles(baseConfig, additionalFiles),
).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [],
});
});
it('skip lock file updating', async () => {
await expect(
getAdditionalFiles(
{
...updateConfig,
skipArtifactsUpdate: true,
reuseExistingBranch: true,
upgrades: [
{
depName: 'postcss',
isRemediation: true,
managerData: {
npmLock: 'package-lock.json',
},
rangeStrategy: 'widen',
},
],
},
additionalFiles,
),
).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [],
});
expect(spyNpm).not.toHaveBeenCalled();
expect(logger.logger.debug).toHaveBeenCalledWith(
'Skipping lock file generation',
);
});
it('reuse existing up-to-date', async () => {
await expect(
getAdditionalFiles(
{
...baseConfig,
reuseExistingBranch: true,
upgrades: [{ isLockfileUpdate: true }],
},
additionalFiles,
),
).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [],
});
});
it('lockfile maintenance branch exists', async () => {
// TODO: can this really happen?
scm.branchExists.mockResolvedValueOnce(true);
await expect(
getAdditionalFiles(
{
...baseConfig,
upgrades: [{ isLockfileUpdate: false }],
reuseExistingBranch: true,
isLockFileMaintenance: true,
},
additionalFiles,
),
).resolves.toStrictEqual({
artifactErrors: [],
artifactNotices: [],
updatedArtifacts: [],
});
});
it('fails for npm', async () => {
spyNpm.mockResolvedValueOnce({ error: true, stderr: 'some-error' });
await expect(
getAdditionalFiles({ ...updateConfig }, additionalFiles),
).resolves.toStrictEqual({
artifactErrors: [
{ fileName: 'package-lock.json', stderr: 'some-error' },
],
artifactNotices: [],
updatedArtifacts: [],
});
});
it('fails for yarn', async () => {
spyYarn.mockResolvedValueOnce({ error: true, stdout: 'some-error' });
await expect(
getAdditionalFiles(
{ ...updateConfig, reuseExistingBranch: true },
additionalFiles,
),
).resolves.toStrictEqual({
artifactErrors: [{ fileName: 'yarn.lock', stderr: 'some-error' }],
artifactNotices: [],
updatedArtifacts: [],
});
});
it('fails for pnpm', async () => {
spyPnpm.mockResolvedValueOnce({ error: true, stdout: 'some-error' });
await expect(
getAdditionalFiles(
{
...updateConfig,
upgrades: [
{
isRemediation: true,
packageFile: 'packages/pnpm/package.json',
},
],
},
additionalFiles,
),
).resolves.toStrictEqual({
artifactErrors: [
{ fileName: 'packages/pnpm/pnpm-lock.yaml', stderr: 'some-error' },
],
artifactNotices: [],
updatedArtifacts: [],
});
});
describe('should fuzzy merge yarn npmRegistries', () => {
beforeEach(() => {
spyProcessHostRules.mockReturnValue({
additionalNpmrcContent: [],
additionalYarnRcYml: {
npmRegistries: {
'//my-private-registry': {
npmAuthToken: 'xxxxxx',
},
},
},
});
fs.getSiblingFileName.mockReturnValue('.yarnrc.yml');
});
it('should fuzzy merge the yarnrc Files', async () => {
vi.mocked(yarn.fuzzyMatchAdditionalYarnrcYml).mockReturnValue({
npmRegistries: {
'https://my-private-registry': { npmAuthToken: 'xxxxxx' },
},
});
fs.readLocalFile.mockImplementation((f): Promise<any> => {
if (f === '.yarnrc.yml') {
return Promise.resolve(
'npmRegistries:\n' +
' https://my-private-registry:\n' +
' npmAlwaysAuth: true\n',
);
}
return Promise.resolve(null);
});
spyYarn.mockResolvedValueOnce({ error: false, lockFile: '{}' });
await getAdditionalFiles(
{
...updateConfig,
reuseExistingBranch: true,
},
additionalFiles,
);
expect(fs.writeLocalFile).toHaveBeenCalledWith(
'.yarnrc.yml',
'npmRegistries:\n' +
' https://my-private-registry:\n' +
' npmAlwaysAuth: true\n' +
' npmAuthToken: xxxxxx\n',
);
});
it('should warn if there is an error writing the yarnrc.yml', async () => {
fs.readLocalFile.mockImplementation((f): Promise<any> => {
if (f === '.yarnrc.yml') {
return Promise.resolve(
`yarnPath: .yarn/releases/yarn-3.0.1.cjs\na: b\n`,
);
}
return Promise.resolve(null);
});
fs.writeLocalFile.mockImplementation((f): Promise<any> => {
if (f === '.yarnrc.yml') {
throw new Error();
}
return Promise.resolve(null);
});
spyYarn.mockResolvedValueOnce({ error: false, lockFile: '{}' });
await expect(
getAdditionalFiles(
{
...updateConfig,
reuseExistingBranch: true,
},
additionalFiles,
),
).rejects.toThrow(Error);
expect(logger.logger.warn).toHaveBeenCalledWith(
expect.anything(),
'Error appending .yarnrc.yml content',
);
});
});
});
});