pnpr served npm at the server root when npm was the only configured ecosystem and moved it under /npm/ as soon as a second ecosystem was added. Adding an ecosystem therefore broke every .npmrc registry URL, every tarball URL already written into a lockfile, named-registry URLs, and OIDC workload publish paths, and /npm meant the package `npm` on one server and the ecosystem prefix on another. This settles section A1 of pnpm/tasks#131: every ecosystem is always prefixed. - Ecosystem::base_path is now independent of the configuration. The per-configuration Registries::base_path and is_only_ecosystem are gone, and the routes, the resolver's tarball URLs, OIDC workload paths and pnpr's own-origin route classification all use it. - The default image registry still answers at /v2/ on the host root; an image client cannot be pointed at a prefix. The registry directory keeps its `prefixed` and `namedPrefixed` flags for the web UI. - The read-only token allowlist matched the npm audit endpoints at the root only, so read-only tokens were already refused for audits on multi-ecosystem servers. It now matches them under the npm prefix. - /metrics no longer switches between Prometheus metrics and the npm package `metrics` on the Accept header; the package is /npm/metrics. - The pnpm v11 and v12 test harnesses, the registry-mock crate and the integrated benchmark address the fixture registry at /npm/ and keep the server origin for the pnpr protocol endpoints. The benchmark asks each revision mock's registry directory whether npm is prefixed, so it can still compare against a pnpr revision from before this change.
168 lines
5.4 KiB
TypeScript
168 lines
5.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 } 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}/npm/`
|
|
|
|
describe('cache view', () => {
|
|
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',
|
|
`--store-dir=${storeDir}`,
|
|
`--cache-dir=${cacheDir}`,
|
|
'--config.resolution-mode=highest',
|
|
])
|
|
})
|
|
test('lists all metadata for requested package', async () => {
|
|
const result = await cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: process.cwd(),
|
|
storeDir,
|
|
}, ['view', 'is-negative'])
|
|
|
|
expect(JSON.parse(result!)).toMatchObject({
|
|
[`http://localhost:${REGISTRY_MOCK_PORT}/npm/`]: {
|
|
cachedVersions: ['2.1.0'],
|
|
nonCachedVersions: [
|
|
'1.0.0',
|
|
'1.0.1',
|
|
'2.0.0',
|
|
'2.0.1',
|
|
'2.0.2',
|
|
],
|
|
},
|
|
'https://registry.npmjs.org/': {
|
|
cachedVersions: ['2.1.0'],
|
|
nonCachedVersions: [
|
|
'1.0.0',
|
|
'1.0.1',
|
|
'2.0.0',
|
|
'2.0.1',
|
|
'2.0.2',
|
|
],
|
|
},
|
|
})
|
|
})
|
|
test('lists metadata for requested package from specified registry', async () => {
|
|
const result = await cache.handler({
|
|
cacheDir,
|
|
cliOptions: {
|
|
registry: 'https://registry.npmjs.org/',
|
|
},
|
|
pnpmHomeDir: process.cwd(),
|
|
storeDir,
|
|
}, ['view', 'is-negative'])
|
|
|
|
expect(JSON.parse(result!)).toMatchObject({
|
|
'https://registry.npmjs.org/': {
|
|
cachedVersions: ['2.1.0'],
|
|
nonCachedVersions: [
|
|
'1.0.0',
|
|
'1.0.1',
|
|
'2.0.0',
|
|
'2.0.1',
|
|
'2.0.2',
|
|
],
|
|
},
|
|
})
|
|
})
|
|
|
|
// A damaged file is what the resolver refuses to read, so listing the
|
|
// versions around the damage would report a cache no install will use.
|
|
test('omits a package whose cache file is damaged', async () => {
|
|
const mirror = path.join(cacheDir, ABBREVIATED_META_DIR, 'https%3A+registry.npmjs.org', 'is-negative.jsonl')
|
|
const bytes = fs.readFileSync(mirror)
|
|
const at = bytes.indexOf(Buffer.from('"integrity"'))
|
|
expect(at).toBeGreaterThan(-1)
|
|
// Same byte count, so the index spans still address the fragment, but
|
|
// its own bytes no longer parse.
|
|
bytes.fill('x', at, at + 11)
|
|
fs.writeFileSync(mirror, bytes)
|
|
|
|
const result = await cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: process.cwd(),
|
|
storeDir,
|
|
}, ['view', 'is-negative'])
|
|
|
|
expect(JSON.parse(result!)['https://registry.npmjs.org/']).toBeUndefined()
|
|
})
|
|
|
|
// The registry served that version in the wrong shape, which the resolver
|
|
// reads as a version without a manifest rather than as a damaged file.
|
|
test('skips a version whose cached manifest has the wrong shape', async () => {
|
|
const mirror = path.join(cacheDir, ABBREVIATED_META_DIR, 'https%3A+registry.npmjs.org', 'is-negative.jsonl')
|
|
const bytes = fs.readFileSync(mirror)
|
|
const lineEnd = bytes.indexOf(10)
|
|
const [, headersLen, indexLen] = bytes.toString('utf8', 0, lineEnd).split(' ').map(Number)
|
|
const indexStart = lineEnd + 1 + headersLen
|
|
const index = JSON.parse(bytes.toString('utf8', indexStart, indexStart + indexLen)) as { versions: Array<[string, number, number]> }
|
|
const [version, offset, length] = index.versions.find(([candidate]) => candidate === '2.0.2')!
|
|
bytes.write(`[${' '.repeat(length - 2)}]`, indexStart + indexLen + offset)
|
|
fs.writeFileSync(mirror, bytes)
|
|
|
|
const result = await cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: process.cwd(),
|
|
storeDir,
|
|
}, ['view', 'is-negative'])
|
|
|
|
const view = JSON.parse(result!)['https://registry.npmjs.org/']
|
|
expect(view.cachedVersions).not.toContain(version)
|
|
expect(view.nonCachedVersions).not.toContain(version)
|
|
expect(view.cachedVersions.length + view.nonCachedVersions.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('lists all metadata for requested package should specify a package name', async () => {
|
|
await expect(
|
|
cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: process.cwd(),
|
|
storeDir,
|
|
}, ['view'])
|
|
).rejects.toThrow('`pnpm cache view` requires the package name')
|
|
})
|
|
|
|
test('lists all metadata for requested package should not accept more than one package name', async () => {
|
|
await expect(
|
|
cache.handler({
|
|
cacheDir,
|
|
cliOptions: {},
|
|
pnpmHomeDir: process.cwd(),
|
|
storeDir,
|
|
}, ['view', 'is-negative', 'is-positive'])
|
|
).rejects.toThrow('`pnpm cache view` only accepts one package name')
|
|
})
|
|
})
|