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({ 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(); 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 => { 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([[npmrcFilename, originalNpmrc]]); fs.readLocalFile.mockImplementation((fileName): Promise => 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({ 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 => 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({ 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({ 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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', ); }); }); }); });