From ca821eb0e7a26f96d0d40971d9badece90751fdc Mon Sep 17 00:00:00 2001 From: Nathan Kleyn Date: Wed, 18 Dec 2024 14:39:51 +0000 Subject: feat(bun): Add support for updating text-format lockfile when package.json changes (#33189) Co-authored-by: Rhys Arkins --- lib/modules/manager/bun/artifacts.spec.ts | 315 ++++++++++++++++++++---------- lib/modules/manager/bun/extract.spec.ts | 156 ++++++++++----- lib/modules/manager/bun/extract.ts | 7 +- lib/modules/manager/bun/index.ts | 2 +- 4 files changed, 322 insertions(+), 158 deletions(-) diff --git a/lib/modules/manager/bun/artifacts.spec.ts b/lib/modules/manager/bun/artifacts.spec.ts index 75a7797a4d4..99bd3b0e734 100644 --- a/lib/modules/manager/bun/artifacts.spec.ts +++ b/lib/modules/manager/bun/artifacts.spec.ts @@ -41,101 +41,198 @@ describe('modules/manager/bun/artifacts', () => { expect(await updateArtifacts(updateArtifact)).toBeNull(); }); - it('skips if cannot read lock file', async () => { - updateArtifact.updatedDeps = [ - { manager: 'bun', lockFiles: ['bun.lockb'] }, - ]; - expect(await updateArtifacts(updateArtifact)).toBeNull(); - }); + describe('when using .lockb lockfile format', () => { + it('skips if cannot read lock file', async () => { + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lockb'] }, + ]; + expect(await updateArtifacts(updateArtifact)).toBeNull(); + }); - it('returns null if lock content unchanged', async () => { - updateArtifact.updatedDeps = [ - { manager: 'bun', lockFiles: ['bun.lockb'] }, - ]; - const oldLock = Buffer.from('old'); - fs.readFile.mockResolvedValueOnce(oldLock as never); - fs.readFile.mockResolvedValueOnce(oldLock as never); - expect(await updateArtifacts(updateArtifact)).toBeNull(); - }); + it('returns null if lock content unchanged', async () => { + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lockb'] }, + ]; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + fs.readFile.mockResolvedValueOnce(oldLock as never); + expect(await updateArtifacts(updateArtifact)).toBeNull(); + }); - it('returns updated lock content', async () => { - updateArtifact.updatedDeps = [ - { manager: 'bun', lockFiles: ['bun.lockb'] }, - ]; - const oldLock = Buffer.from('old'); - fs.readFile.mockResolvedValueOnce(oldLock as never); - const newLock = Buffer.from('new'); - fs.readFile.mockResolvedValueOnce(newLock as never); - expect(await updateArtifacts(updateArtifact)).toEqual([ - { - file: { - path: 'bun.lockb', - type: 'addition', - contents: newLock, + it('returns updated lock content', async () => { + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lockb'] }, + ]; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + const newLock = Buffer.from('new'); + fs.readFile.mockResolvedValueOnce(newLock as never); + expect(await updateArtifacts(updateArtifact)).toEqual([ + { + file: { + path: 'bun.lockb', + type: 'addition', + contents: newLock, + }, }, - }, - ]); - }); + ]); + }); - it('supports lockFileMaintenance', async () => { - updateArtifact.updatedDeps = [ - { manager: 'bun', lockFiles: ['bun.lockb'] }, - ]; - updateArtifact.config.updateType = 'lockFileMaintenance'; - const oldLock = Buffer.from('old'); - fs.readFile.mockResolvedValueOnce(oldLock as never); - const newLock = Buffer.from('new'); - fs.readFile.mockResolvedValueOnce(newLock as never); - expect(await updateArtifacts(updateArtifact)).toEqual([ - { - file: { - path: 'bun.lockb', - type: 'addition', - contents: newLock, + it('supports lockFileMaintenance', async () => { + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lockb'] }, + ]; + updateArtifact.config.updateType = 'lockFileMaintenance'; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + const newLock = Buffer.from('new'); + fs.readFile.mockResolvedValueOnce(newLock as never); + expect(await updateArtifacts(updateArtifact)).toEqual([ + { + file: { + path: 'bun.lockb', + type: 'addition', + contents: newLock, + }, }, - }, - ]); - }); + ]); + }); - it('handles temporary error', async () => { - const execError = new ExecError(TEMPORARY_ERROR, { - cmd: '', - stdout: '', - stderr: '', - options: { encoding: 'utf8' }, + it('handles temporary error', async () => { + const execError = new ExecError(TEMPORARY_ERROR, { + cmd: '', + stdout: '', + stderr: '', + options: { encoding: 'utf8' }, + }); + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lockb'] }, + ]; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + exec.mockRejectedValueOnce(execError); + await expect(updateArtifacts(updateArtifact)).rejects.toThrow( + TEMPORARY_ERROR, + ); + }); + + it('handles full error', async () => { + const execError = new ExecError('nope', { + cmd: '', + stdout: '', + stderr: '', + options: { encoding: 'utf8' }, + }); + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lockb'] }, + ]; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + exec.mockRejectedValueOnce(execError); + expect(await updateArtifacts(updateArtifact)).toEqual([ + { artifactError: { lockFile: 'bun.lockb', stderr: 'nope' } }, + ]); }); - updateArtifact.updatedDeps = [ - { manager: 'bun', lockFiles: ['bun.lockb'] }, - ]; - const oldLock = Buffer.from('old'); - fs.readFile.mockResolvedValueOnce(oldLock as never); - exec.mockRejectedValueOnce(execError); - await expect(updateArtifacts(updateArtifact)).rejects.toThrow( - TEMPORARY_ERROR, - ); }); - it('handles full error', async () => { - const execError = new ExecError('nope', { - cmd: '', - stdout: '', - stderr: '', - options: { encoding: 'utf8' }, + describe('when using .lock lockfile format', () => { + it('skips if cannot read lock file', async () => { + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lock'] }, + ]; + expect(await updateArtifacts(updateArtifact)).toBeNull(); + }); + + it('returns null if lock content unchanged', async () => { + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lock'] }, + ]; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + fs.readFile.mockResolvedValueOnce(oldLock as never); + expect(await updateArtifacts(updateArtifact)).toBeNull(); + }); + + it('returns updated lock content', async () => { + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lock'] }, + ]; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + const newLock = Buffer.from('new'); + fs.readFile.mockResolvedValueOnce(newLock as never); + expect(await updateArtifacts(updateArtifact)).toEqual([ + { + file: { + path: 'bun.lock', + type: 'addition', + contents: newLock, + }, + }, + ]); + }); + + it('supports lockFileMaintenance', async () => { + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lock'] }, + ]; + updateArtifact.config.updateType = 'lockFileMaintenance'; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + const newLock = Buffer.from('new'); + fs.readFile.mockResolvedValueOnce(newLock as never); + expect(await updateArtifacts(updateArtifact)).toEqual([ + { + file: { + path: 'bun.lock', + type: 'addition', + contents: newLock, + }, + }, + ]); + }); + + it('handles temporary error', async () => { + const execError = new ExecError(TEMPORARY_ERROR, { + cmd: '', + stdout: '', + stderr: '', + options: { encoding: 'utf8' }, + }); + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lock'] }, + ]; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + exec.mockRejectedValueOnce(execError); + await expect(updateArtifacts(updateArtifact)).rejects.toThrow( + TEMPORARY_ERROR, + ); + }); + + it('handles full error', async () => { + const execError = new ExecError('nope', { + cmd: '', + stdout: '', + stderr: '', + options: { encoding: 'utf8' }, + }); + updateArtifact.updatedDeps = [ + { manager: 'bun', lockFiles: ['bun.lock'] }, + ]; + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + exec.mockRejectedValueOnce(execError); + expect(await updateArtifacts(updateArtifact)).toEqual([ + { artifactError: { lockFile: 'bun.lock', stderr: 'nope' } }, + ]); }); - updateArtifact.updatedDeps = [ - { manager: 'bun', lockFiles: ['bun.lockb'] }, - ]; - const oldLock = Buffer.from('old'); - fs.readFile.mockResolvedValueOnce(oldLock as never); - exec.mockRejectedValueOnce(execError); - expect(await updateArtifacts(updateArtifact)).toEqual([ - { artifactError: { lockFile: 'bun.lockb', stderr: 'nope' } }, - ]); }); }); describe('bun command execution', () => { it('check install options with configs', async () => { + const lockfileFormats = ['bun.lockb', 'bun.lock']; const testCases = [ { allowScripts: undefined, @@ -184,38 +281,40 @@ describe('modules/manager/bun/artifacts', () => { }, ]; - for (const testCase of testCases) { - GlobalConfig.set({ - ...globalConfig, - allowScripts: testCase.allowScripts, - }); - const updateArtifact: UpdateArtifact = { - config: { ignoreScripts: testCase.ignoreScripts }, - newPackageFileContent: '', - packageFileName: '', - updatedDeps: [{ manager: 'bun', lockFiles: ['bun.lockb'] }], - }; + for (const lockFile of lockfileFormats) { + for (const testCase of testCases) { + GlobalConfig.set({ + ...globalConfig, + allowScripts: testCase.allowScripts, + }); + const updateArtifact: UpdateArtifact = { + config: { ignoreScripts: testCase.ignoreScripts }, + newPackageFileContent: '', + packageFileName: '', + updatedDeps: [{ manager: 'bun', lockFiles: [lockFile] }], + }; - const oldLock = Buffer.from('old'); - fs.readFile.mockResolvedValueOnce(oldLock as never); - const newLock = Buffer.from('new'); - fs.readFile.mockResolvedValueOnce(newLock as never); + const oldLock = Buffer.from('old'); + fs.readFile.mockResolvedValueOnce(oldLock as never); + const newLock = Buffer.from('new'); + fs.readFile.mockResolvedValueOnce(newLock as never); - await updateArtifacts(updateArtifact); + await updateArtifacts(updateArtifact); - expect(exec).toHaveBeenCalledWith(testCase.expectedCmd, { - cwdFile: '', - docker: {}, - toolConstraints: [ - { - toolName: 'bun', - }, - ], - userConfiguredEnv: undefined, - }); + expect(exec).toHaveBeenCalledWith(testCase.expectedCmd, { + cwdFile: '', + docker: {}, + toolConstraints: [ + { + toolName: 'bun', + }, + ], + userConfiguredEnv: undefined, + }); - exec.mockClear(); - GlobalConfig.reset(); + exec.mockClear(); + GlobalConfig.reset(); + } } }); }); diff --git a/lib/modules/manager/bun/extract.spec.ts b/lib/modules/manager/bun/extract.spec.ts index 8ca869f9c3a..b5135fa3eab 100644 --- a/lib/modules/manager/bun/extract.spec.ts +++ b/lib/modules/manager/bun/extract.spec.ts @@ -9,60 +9,120 @@ describe('modules/manager/bun/extract', () => { expect(await extractAllPackageFiles({}, ['package.json'])).toEqual([]); }); - it('ignores missing package.json file', async () => { - expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]); - }); + describe('when using the .lockb lockfile format', () => { + it('ignores missing package.json file', async () => { + expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]); + }); - it('ignores invalid package.json file', async () => { - (fs.readLocalFile as jest.Mock).mockResolvedValueOnce('invalid'); - expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]); - }); + it('ignores invalid package.json file', async () => { + (fs.readLocalFile as jest.Mock).mockResolvedValueOnce('invalid'); + expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]); + }); - it('handles null response', async () => { - fs.getSiblingFileName.mockReturnValueOnce('package.json'); - fs.readLocalFile.mockResolvedValueOnce( - // This package.json returns null from the extractor - JSON.stringify({ - _id: 1, - _args: 1, - _from: 1, - }), - ); - expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]); - }); + it('handles null response', async () => { + fs.getSiblingFileName.mockReturnValueOnce('package.json'); + fs.readLocalFile.mockResolvedValueOnce( + // This package.json returns null from the extractor + JSON.stringify({ + _id: 1, + _args: 1, + _from: 1, + }), + ); + expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]); + }); - it('parses valid package.json file', async () => { - fs.getSiblingFileName.mockReturnValueOnce('package.json'); - fs.readLocalFile.mockResolvedValueOnce( - JSON.stringify({ - name: 'test', - version: '0.0.1', - dependencies: { - dep1: '1.0.0', + it('parses valid package.json file', async () => { + fs.getSiblingFileName.mockReturnValueOnce('package.json'); + fs.readLocalFile.mockResolvedValueOnce( + JSON.stringify({ + name: 'test', + version: '0.0.1', + dependencies: { + dep1: '1.0.0', + }, + }), + ); + expect(await extractAllPackageFiles({}, ['bun.lockb'])).toMatchObject([ + { + deps: [ + { + currentValue: '1.0.0', + datasource: 'npm', + depName: 'dep1', + depType: 'dependencies', + prettyDepType: 'dependency', + }, + ], + extractedConstraints: {}, + lockFiles: ['bun.lockb'], + managerData: { + hasPackageManager: false, + packageJsonName: 'test', + }, + packageFile: 'package.json', + packageFileVersion: '0.0.1', }, - }), - ); - expect(await extractAllPackageFiles({}, ['bun.lockb'])).toMatchObject([ - { - deps: [ - { - currentValue: '1.0.0', - datasource: 'npm', - depName: 'dep1', - depType: 'dependencies', - prettyDepType: 'dependency', + ]); + }); + }); + + describe('when using the .lock lockfile format', () => { + it('ignores missing package.json file', async () => { + expect(await extractAllPackageFiles({}, ['bun.lock'])).toEqual([]); + }); + + it('ignores invalid package.json file', async () => { + (fs.readLocalFile as jest.Mock).mockResolvedValueOnce('invalid'); + expect(await extractAllPackageFiles({}, ['bun.lock'])).toEqual([]); + }); + + it('handles null response', async () => { + fs.getSiblingFileName.mockReturnValueOnce('package.json'); + fs.readLocalFile.mockResolvedValueOnce( + // This package.json returns null from the extractor + JSON.stringify({ + _id: 1, + _args: 1, + _from: 1, + }), + ); + expect(await extractAllPackageFiles({}, ['bun.lock'])).toEqual([]); + }); + + it('parses valid package.json file', async () => { + fs.getSiblingFileName.mockReturnValueOnce('package.json'); + fs.readLocalFile.mockResolvedValueOnce( + JSON.stringify({ + name: 'test', + version: '0.0.1', + dependencies: { + dep1: '1.0.0', + }, + }), + ); + expect(await extractAllPackageFiles({}, ['bun.lock'])).toMatchObject([ + { + deps: [ + { + currentValue: '1.0.0', + datasource: 'npm', + depName: 'dep1', + depType: 'dependencies', + prettyDepType: 'dependency', + }, + ], + extractedConstraints: {}, + lockFiles: ['bun.lock'], + managerData: { + hasPackageManager: false, + packageJsonName: 'test', }, - ], - extractedConstraints: {}, - lockFiles: ['bun.lockb'], - managerData: { - hasPackageManager: false, - packageJsonName: 'test', + packageFile: 'package.json', + packageFileVersion: '0.0.1', }, - packageFile: 'package.json', - packageFileVersion: '0.0.1', - }, - ]); + ]); + }); }); }); }); diff --git a/lib/modules/manager/bun/extract.ts b/lib/modules/manager/bun/extract.ts index 9f41b23cb9a..6a8d8e8cdb6 100644 --- a/lib/modules/manager/bun/extract.ts +++ b/lib/modules/manager/bun/extract.ts @@ -18,7 +18,12 @@ export async function extractAllPackageFiles( ): Promise { const packageFiles: PackageFile[] = []; for (const matchedFile of matchedFiles) { - if (!matchesFileName(matchedFile, 'bun.lockb')) { + if ( + !( + matchesFileName(matchedFile, 'bun.lockb') || + matchesFileName(matchedFile, 'bun.lock') + ) + ) { logger.warn({ matchedFile }, 'Invalid bun lockfile match'); continue; } diff --git a/lib/modules/manager/bun/index.ts b/lib/modules/manager/bun/index.ts index b3130e04b37..f885639ad89 100644 --- a/lib/modules/manager/bun/index.ts +++ b/lib/modules/manager/bun/index.ts @@ -13,7 +13,7 @@ export const supersedesManagers = ['npm']; export const supportsLockFileMaintenance = true; export const defaultConfig = { - fileMatch: ['(^|/)bun\\.lockb$'], + fileMatch: ['(^|/)bun\\.lockb?$'], digest: { prBodyDefinitions: { Change: -- cgit v1.2.3