Registry metadata mirrors were keyed on host[:port], so several registries served from one host under different path prefixes shared one directory and could answer with each other's versions, integrity hashes and tarball URLs. Both stacks now key the mirror on <scheme>%3A+<host>[+<port>][%2F<path>][%5F<sha256>]. The host and every path segment are percent-escaped down to [A-Za-z0-9._-], so a `%` or `+` in a key is always one the encoder wrote and the key can carry no path separator, no character Windows rejects in a filename, and no glob metacharacter — the cache commands feed the key to a glob whose matches `pnpm cache delete` removes. The scheme is included because http and https at one host and path are two different trust domains: metadata served over http can be rewritten in transit and must never reach a resolution configured for https. The separators are percent-escapes because every key pnpm wrote before this change was a URL host, which can never hold a `%`; no new key can therefore land on the stale directory of an unrelated one whose hostname held a separator, which would otherwise let `https://nexus/npm/` read what was cached for `https://nexus_npm/`. A path that is not all lowercase gets a sha256 suffix, the guard encodePkgName already applies to package names, so HFS+ and NTFS cannot merge two registries; a trailing `.` is escaped because Win32 strips one; and a key too long for a 255-byte filename is replaced by its own hash. Only the one trailing slash the resolver itself appends is normalized away; a repeated slash reaches the registry as a distinct request path and stays in the key. Every cache directory is renamed, so the first install after upgrading refetches registry metadata once. The package store is untouched. `pnpm cache view` decodes the key back to the registry rather than replacing `+` with `:`, the registry URL is redacted in both stacks' errors, and the Rust diagnostic codes now match pnpm's. Closes pnpm/pnpm#13558. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
import { beforeEach, describe, expect, test } from '@jest/globals'
|
|
import { cache } from '@pnpm/cache.commands'
|
|
import { ABBREVIATED_META_DIR, FULL_FILTERED_META_DIR, FULL_META_DIR } from '@pnpm/constants'
|
|
import { prepare } from '@pnpm/prepare'
|
|
import { REGISTRY_MOCK_PORT } from '@pnpm/testing.registry-mock'
|
|
import { rimrafSync } from '@zkochan/rimraf'
|
|
import { safeExeca as execa } from 'execa'
|
|
|
|
const pnpmBin = path.join(import.meta.dirname, '../../../pnpm/bin/pnpm.mjs')
|
|
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}/`
|
|
|
|
describe('cache delete', () => {
|
|
let cacheDir: string
|
|
let storeDir: string
|
|
beforeEach(async () => {
|
|
prepare()
|
|
cacheDir = path.resolve('cache')
|
|
storeDir = path.resolve('store')
|
|
|
|
await execa('node', [
|
|
pnpmBin,
|
|
'add',
|
|
'is-negative@2.1.0',
|
|
`--store-dir=${storeDir}`,
|
|
`--cache-dir=${cacheDir}`,
|
|
'--config.resolution-mode=highest',
|
|
`--registry=${REGISTRY}`,
|
|
])
|
|
rimrafSync('node_modules')
|
|
rimrafSync('pnpm-lock.yaml')
|
|
await execa('node', [
|
|
pnpmBin,
|
|
'add',
|
|
'is-negative@2.1.0',
|
|
'is-positive@1.0.0',
|
|
`--store-dir=${storeDir}`,
|
|
`--cache-dir=${cacheDir}`,
|
|
'--config.resolution-mode=highest',
|
|
])
|
|
})
|
|
test('delete all metadata from the cache that matches a pattern', async () => {
|
|
await cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: storeDir,
|
|
}, ['delete', '*-positive'])
|
|
const result = await cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: storeDir,
|
|
}, ['list'])
|
|
|
|
expect(result).toBe(`http%3A+localhost+${REGISTRY_MOCK_PORT}/is-negative.jsonl
|
|
https%3A+registry.npmjs.org/is-negative.jsonl`)
|
|
})
|
|
})
|
|
|
|
describe('cache delete across metadata directories', () => {
|
|
test('deletes a package from every metadata cache directory, not only the one the current mode reads', async () => {
|
|
prepare()
|
|
const cacheDir = path.resolve('cache')
|
|
const registryName = 'https%3A+registry.npmjs.org'
|
|
const metaDirs = [ABBREVIATED_META_DIR, FULL_META_DIR, FULL_FILTERED_META_DIR]
|
|
const sentinel = (metaDir: string) => path.join(cacheDir, metaDir, registryName, '@vue', 'compiler-core.jsonl')
|
|
for (const metaDir of metaDirs) {
|
|
fs.mkdirSync(path.dirname(sentinel(metaDir)), { recursive: true })
|
|
fs.writeFileSync(sentinel(metaDir), '')
|
|
}
|
|
|
|
const result = await cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: cacheDir,
|
|
}, ['delete', '@vue/compiler-core'])
|
|
|
|
for (const metaDir of metaDirs) {
|
|
expect(fs.existsSync(sentinel(metaDir))).toBe(false)
|
|
}
|
|
expect(result).toBe(`${registryName}/@vue/compiler-core.jsonl`)
|
|
})
|
|
|
|
test('deletes metadata cached under a non-current mode even when the other directories are absent', async () => {
|
|
prepare()
|
|
const cacheDir = path.resolve('cache')
|
|
// The default mode reads `metadata`, but the package is only cached under
|
|
// `metadata-full-filtered` and the other directories don't exist.
|
|
const sentinel = path.join(cacheDir, FULL_FILTERED_META_DIR, 'https%3A+registry.npmjs.org', '@vue', 'compiler-core.jsonl')
|
|
fs.mkdirSync(path.dirname(sentinel), { recursive: true })
|
|
fs.writeFileSync(sentinel, '')
|
|
|
|
await cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: cacheDir,
|
|
}, ['delete', '@vue/compiler-core'])
|
|
|
|
expect(fs.existsSync(sentinel)).toBe(false)
|
|
})
|
|
})
|