feat: store prune displays the total size of removed files (#11047)

* feat: store prune displays the total size of removed files

* test: update
This commit is contained in:
btea
2026-03-22 03:01:58 +08:00
committed by GitHub
parent cd2dc7d481
commit 2f98ec84f4
5 changed files with 65 additions and 3 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/store.controller": patch
"pnpm": patch
---
`pnpm store prune` now displays the total size of removed files.

3
pnpm-lock.yaml generated
View File

@@ -8373,6 +8373,9 @@ importers:
is-subdir:
specifier: 'catalog:'
version: 2.0.0
pretty-bytes:
specifier: 'catalog:'
version: 7.1.0
ramda:
specifier: 'catalog:'
version: '@pnpm/ramda@0.28.1'

View File

@@ -92,6 +92,55 @@ test('remove unreferenced packages', async () => {
expect(fs.readdirSync(cacheDir)).toStrictEqual([])
})
test('prune outputs total size of removed files', 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}`,
`--cache-dir=${cacheDir}`,
`--registry=${REGISTRY}`])
await execa('node', [
pnpmBin,
'remove',
'is-negative',
`--store-dir=${storeDir}`,
`--cache-dir=${cacheDir}`,
'--config.modules-cache-max-age=0',
], { env: { npm_config_registry: REGISTRY } })
project.storeHas('is-negative', '2.1.0')
const reporter = jest.fn()
await store.handler({
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
rawConfig: {
registry: REGISTRY,
},
registries: { default: REGISTRY },
reporter,
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
// Check that the message includes file count and size information
// The message should match pattern like "Removed X files (Y B)" or "Removed 1 file (Y B)"
expect(reporter).toHaveBeenCalledWith(
expect.objectContaining({
level: 'info',
message: expect.stringMatching(/Removed \d+ files? \(\d+(\.\d+)?\s+\S+\)/),
})
)
})
test('remove packages that are used by project that no longer exist', async () => {
prepare()
const cacheDir = path.resolve('cache')
@@ -130,7 +179,7 @@ test('remove packages that are used by project that no longer exist', async () =
expect(reporter).toHaveBeenCalledWith(
expect.objectContaining({
level: 'info',
message: 'Removed 4 files',
message: expect.stringMatching(/Removed 4 files \(\d+(\.\d+)?\s+\S+\)/),
})
)
@@ -318,7 +367,7 @@ describe('prune when store directory is not properly configured', () => {
expect(reporter).toHaveBeenCalledWith(
expect.objectContaining({
level: 'info',
message: 'Removed 0 files',
message: expect.stringMatching(/Removed 0 files \(\d+(\.\d+)?\s+\S+\)/),
})
)

View File

@@ -57,6 +57,7 @@
"@pnpm/types": "workspace:*",
"@zkochan/rimraf": "catalog:",
"is-subdir": "catalog:",
"pretty-bytes": "catalog:",
"ramda": "catalog:",
"ssri": "catalog:",
"symlink-dir": "catalog:"

View File

@@ -6,6 +6,7 @@ import { globalInfo, globalWarn } from '@pnpm/logger'
import type { PackageFilesIndex } from '@pnpm/store.cafs'
import type { StoreIndex } from '@pnpm/store.index'
import { rimraf } from '@zkochan/rimraf'
import prettyBytes from 'pretty-bytes'
import { pruneGlobalVirtualStore } from './pruneGlobalVirtualStore.js'
@@ -43,6 +44,7 @@ export async function prune ({ cacheDir, storeDir, storeIndex }: PruneOptions, r
const removedHashes = new Set<string>()
const dirs = await getSubdirsSafely(cafsDir)
let fileCounter = 0
let totalSize = 0
await Promise.all(dirs.map(async (dir) => {
const subdir = path.join(cafsDir, dir)
await Promise.all((await fs.readdir(subdir)).map(async (fileName) => {
@@ -60,6 +62,7 @@ export async function prune ({ cacheDir, storeDir, storeIndex }: PruneOptions, r
}
}
if (stat.nlink === 1 || stat.nlink === BIG_ONE) {
totalSize += stat.size
await fs.unlink(filePath)
fileCounter++
// Store the hex digest, which matches the format stored in PackageFileInfo.digest
@@ -68,7 +71,7 @@ export async function prune ({ cacheDir, storeDir, storeIndex }: PruneOptions, r
}
}))
}))
globalInfo(`Removed ${fileCounter} file${fileCounter === 1 ? '' : 's'}`)
globalInfo(`Removed ${fileCounter} file${fileCounter === 1 ? '' : 's'} (${prettyBytes(totalSize)})`)
// 4. Clean up orphaned package index entries
let pkgCounter = 0