diff --git a/.changeset/neat-rocks-know.md b/.changeset/neat-rocks-know.md new file mode 100644 index 0000000000..6eca33c1cf --- /dev/null +++ b/.changeset/neat-rocks-know.md @@ -0,0 +1,6 @@ +--- +"@pnpm/package-store": patch +"pnpm": patch +--- + +`pnpm store prune` should not fail if there are unexpected subdirectories in the content-addressable store. diff --git a/packages/package-store/src/storeController/prune.ts b/packages/package-store/src/storeController/prune.ts index bc2288ac72..7b0daa30d4 100644 --- a/packages/package-store/src/storeController/prune.ts +++ b/packages/package-store/src/storeController/prune.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs' import path from 'path' import { PackageFilesIndex } from '@pnpm/cafs' -import { globalInfo } from '@pnpm/logger' +import { globalInfo, globalWarn } from '@pnpm/logger' import rimraf from '@zkochan/rimraf' import loadJsonFile from 'load-json-file' import ssri from 'ssri' @@ -29,6 +29,10 @@ export default async function prune (storeDir: string) { continue } const stat = await fs.stat(filePath) + if (stat.isDirectory()) { + globalWarn(`An alien directory is present in the store: ${filePath}`) + continue + } if (stat.nlink === 1 || stat.nlink === BIG_ONE) { await fs.unlink(filePath) fileCounter++ diff --git a/packages/plugin-commands-store/package.json b/packages/plugin-commands-store/package.json index 7b7ced2e19..33aa2c9f21 100644 --- a/packages/plugin-commands-store/package.json +++ b/packages/plugin-commands-store/package.json @@ -39,13 +39,11 @@ "@pnpm/prepare": "workspace:0.0.28", "@types/archy": "0.0.31", "@types/ramda": "0.27.39", - "@types/sinon": "^9.0.11", "@types/ssri": "^7.1.0", "@zkochan/rimraf": "^2.1.1", "execa": "npm:safe-execa@^0.1.1", "load-json-file": "^6.2.0", "path-exists": "^4.0.0", - "sinon": "^11.1.1", "ssri": "^8.0.1", "tempy": "^1.0.0" }, diff --git a/packages/plugin-commands-store/test/storePrune.ts b/packages/plugin-commands-store/test/storePrune.ts index 18218a306c..5632646c8c 100644 --- a/packages/plugin-commands-store/test/storePrune.ts +++ b/packages/plugin-commands-store/test/storePrune.ts @@ -8,7 +8,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import rimraf from '@zkochan/rimraf' import execa from 'execa' import isEmpty from 'ramda/src/isEmpty' -import sinon from 'sinon' import ssri from 'ssri' const STORE_VERSION = 'v3' @@ -25,7 +24,7 @@ test('remove unreferenced packages', async () => { await project.storeHas('is-negative', '2.1.0') - const reporter = sinon.spy() + const reporter = jest.fn() await store.handler({ cacheDir, dir: process.cwd(), @@ -37,14 +36,16 @@ test('remove unreferenced packages', async () => { storeDir, }, ['prune']) - expect(reporter.calledWithMatch({ - level: 'info', - message: 'Removed 1 package', - })).toBeTruthy() + expect(reporter).toBeCalledWith( + expect.objectContaining({ + level: 'info', + message: 'Removed 1 package', + }) + ) await project.storeHasNot('is-negative', '2.1.0') - reporter.resetHistory() + reporter.mockClear() await store.handler({ cacheDir, dir: process.cwd(), @@ -56,10 +57,12 @@ test('remove unreferenced packages', async () => { storeDir, }, ['prune']) - expect(reporter.calledWithMatch({ - level: 'info', - message: 'Removed 1 package', - })).toBeFalsy() + expect(reporter).not.toBeCalledWith( + expect.objectContaining({ + level: 'info', + message: 'Removed 1 package', + }) + ) }) test.skip('remove packages that are used by project that no longer exist', async () => { @@ -74,7 +77,7 @@ test.skip('remove packages that are used by project that no longer exist', async await cafsHas(ssri.fromHex('f0d86377aa15a64c34961f38ac2a9be2b40a1187', 'sha1').toString()) - const reporter = sinon.spy() + const reporter = jest.fn() await store.handler({ cacheDir, dir: process.cwd(), @@ -86,10 +89,12 @@ test.skip('remove packages that are used by project that no longer exist', async storeDir, }, ['prune']) - expect(reporter.calledWithMatch({ - level: 'info', - message: `- localhost+${REGISTRY_MOCK_PORT}/is-negative/2.1.0`, - })).toBeTruthy() + expect(reporter).toBeCalledWith( + expect.objectContaining({ + level: 'info', + message: `- localhost+${REGISTRY_MOCK_PORT}/is-negative/2.1.0`, + }) + ) await cafsHasNot(ssri.fromHex('f0d86377aa15a64c34961f38ac2a9be2b40a1187', 'sha1').toString()) }) @@ -168,3 +173,34 @@ test('prune will skip scanning non-directory in storeDir', async () => { storeDir, }, ['prune']) }) + +test('prune does not fail if the store contains an unexpected directory', async () => { + const project = prepare() + const cacheDir = path.resolve('cache') + const storeDir = path.resolve('store') + + await execa('node', [pnpmBin, 'add', 'is-negative@2.1.0', '--store-dir', storeDir, '--registry', REGISTRY]) + + await project.storeHas('is-negative', '2.1.0') + const alienDir = path.join(storeDir, 'v3/files/44/directory') + fs.mkdirSync(alienDir) + + const reporter = jest.fn() + await store.handler({ + cacheDir, + dir: process.cwd(), + rawConfig: { + registry: REGISTRY, + }, + registries: { default: REGISTRY }, + reporter, + storeDir, + }, ['prune']) + + expect(reporter).toBeCalledWith( + expect.objectContaining({ + level: 'warn', + message: `An alien directory is present in the store: ${alienDir}`, + }) + ) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abad9d32c3..106b941d06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2635,7 +2635,6 @@ importers: '@pnpm/types': workspace:7.6.0 '@types/archy': 0.0.31 '@types/ramda': 0.27.39 - '@types/sinon': ^9.0.11 '@types/ssri': ^7.1.0 '@zkochan/rimraf': ^2.1.1 archy: ^1.0.0 @@ -2647,7 +2646,6 @@ importers: path-exists: ^4.0.0 ramda: ^0.27.1 render-help: ^1.0.1 - sinon: ^11.1.1 ssri: ^8.0.1 tempy: ^1.0.0 dependencies: @@ -2678,13 +2676,11 @@ importers: '@pnpm/prepare': link:../../privatePackages/prepare '@types/archy': 0.0.31 '@types/ramda': 0.27.39 - '@types/sinon': 9.0.11 '@types/ssri': 7.1.1 '@zkochan/rimraf': 2.1.1 execa: /safe-execa/0.1.1 load-json-file: 6.2.0 path-exists: 4.0.0 - sinon: 11.1.2 ssri: 8.0.1 tempy: 1.0.1