mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 18:05:29 -04:00
Replace the external `@pnpm/registry-mock` (Verdaccio) test dependency with an in-repo, in-process registry that serves package fixtures to **both** the pacquet Rust tests and the pnpm CLI (Jest) tests. No separately managed registry process is needed. ### How it works - **Fixtures** live at `registry/.fixtures/packages/<name>/<version>/…`, moved verbatim from [`pnpm/registry-mock`](https://github.com/pnpm/registry-mock) (keyed by each `package.json`'s `name`+`version`). - **`pnpm-registry-fixtures`** builds verdaccio-shaped storage from those fixtures; the in-tree **`pnpm-registry`** crate serves it. - Files whose names differ only by case (`@pnpm.e2e/with-same-file-in-different-cases`) and `bundleDependencies` trees are composed **in memory** by the builder, since neither can be committed to the working tree. - **pacquet**: `pacquet-testing-utils`' `TestRegistry` starts the server lazily (once per process) in proxy mode, serving `@pnpm.e2e` fixtures locally and falling through to the npm uplink for real packages (`is-positive`, `is-negative`, …) — matching how registry-mock behaved. - **pnpm CLI**: the `with-registry` Jest `globalSetup` builds storage from the fixtures via the new `pnpm-registry-prepare` binary (built from source in the Test CI job) and serves it with `pnpm-registry`. `REGISTRY_MOCK_PORT` / `REGISTRY_MOCK_CREDENTIALS` / `getIntegrity` now come from `@pnpm/testing.registry-mock`. ### Result `@pnpm/registry-mock` is removed from every manifest, the catalog, and `packageExtensions`; `cargo test` / `cargo nextest run` / `just test` and the pnpm CLI Jest suites all run registry-backed tests without launching Verdaccio.
198 lines
6.8 KiB
TypeScript
198 lines
6.8 KiB
TypeScript
import path from 'node:path'
|
|
|
|
import { expect, test } from '@jest/globals'
|
|
import { resolveConfigDeps } from '@pnpm/installing.env-installer'
|
|
import { readEnvLockfile, writeEnvLockfile } from '@pnpm/lockfile.fs'
|
|
import { prepareEmpty } from '@pnpm/prepare'
|
|
import { getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/testing.registry-mock'
|
|
import { createTempStore } from '@pnpm/testing.temp-store'
|
|
import { readYamlFileSync } from 'read-yaml-file'
|
|
|
|
const registry = `http://localhost:${REGISTRY_MOCK_PORT}/`
|
|
|
|
test('configuration dependency is resolved', async () => {
|
|
prepareEmpty()
|
|
const { storeController, storeDir } = createTempStore()
|
|
|
|
await resolveConfigDeps(['@pnpm.e2e/foo@100.0.0'], {
|
|
registries: {
|
|
default: registry,
|
|
},
|
|
rootDir: process.cwd(),
|
|
cacheDir: path.resolve('cache'),
|
|
store: storeController,
|
|
storeDir,
|
|
})
|
|
|
|
// Workspace manifest should have a clean specifier (no integrity)
|
|
const workspaceManifest = readYamlFileSync<{ configDependencies: Record<string, string> }>('pnpm-workspace.yaml')
|
|
expect(workspaceManifest.configDependencies).toStrictEqual({
|
|
'@pnpm.e2e/foo': '100.0.0',
|
|
})
|
|
|
|
// Env lockfile should contain the resolved dependency with integrity
|
|
const envLockfile = await readEnvLockfile(process.cwd())
|
|
expect(envLockfile).not.toBeNull()
|
|
expect(envLockfile!.importers['.'].configDependencies['@pnpm.e2e/foo']).toStrictEqual({
|
|
specifier: '100.0.0',
|
|
version: '100.0.0',
|
|
})
|
|
expect(envLockfile!.packages['@pnpm.e2e/foo@100.0.0']).toStrictEqual({
|
|
resolution: {
|
|
integrity: getIntegrity('@pnpm.e2e/foo', '100.0.0'),
|
|
},
|
|
})
|
|
expect(envLockfile!.snapshots['@pnpm.e2e/foo@100.0.0']).toStrictEqual({})
|
|
})
|
|
|
|
test('one level of optionalDependencies is recorded in the env lockfile with platform fields', async () => {
|
|
prepareEmpty()
|
|
const { storeController, storeDir } = createTempStore()
|
|
|
|
await resolveConfigDeps(['@pnpm.e2e/support-different-architectures@1.0.0'], {
|
|
registries: {
|
|
default: registry,
|
|
},
|
|
rootDir: process.cwd(),
|
|
cacheDir: path.resolve('cache'),
|
|
store: storeController,
|
|
storeDir,
|
|
})
|
|
|
|
const envLockfile = await readEnvLockfile(process.cwd())
|
|
expect(envLockfile).not.toBeNull()
|
|
|
|
const parentKey = '@pnpm.e2e/support-different-architectures@1.0.0'
|
|
expect(envLockfile!.snapshots[parentKey]).toStrictEqual({
|
|
optionalDependencies: {
|
|
'@pnpm.e2e/only-darwin-arm64': '1.0.0',
|
|
'@pnpm.e2e/only-darwin-x64': '1.0.0',
|
|
'@pnpm.e2e/only-linux-arm64-glibc': '1.0.0',
|
|
'@pnpm.e2e/only-linux-arm64-musl': '1.0.0',
|
|
'@pnpm.e2e/only-linux-x64-glibc': '1.0.0',
|
|
'@pnpm.e2e/only-linux-x64-musl': '1.0.0',
|
|
'@pnpm.e2e/only-win32-arm64': '1.0.0',
|
|
'@pnpm.e2e/only-win32-x64': '1.0.0',
|
|
},
|
|
})
|
|
|
|
// Each optional subdep is in `packages` with its os/cpu fields preserved for
|
|
// install-time platform filtering, and gets an `optional: true` snapshot
|
|
// to match how optional packages are recorded elsewhere in the lockfile.
|
|
expect(envLockfile!.packages['@pnpm.e2e/only-darwin-arm64@1.0.0']).toStrictEqual({
|
|
resolution: {
|
|
integrity: getIntegrity('@pnpm.e2e/only-darwin-arm64', '1.0.0'),
|
|
},
|
|
os: ['darwin'],
|
|
cpu: ['arm64'],
|
|
})
|
|
expect(envLockfile!.snapshots['@pnpm.e2e/only-darwin-arm64@1.0.0']).toStrictEqual({ optional: true })
|
|
// libc is preserved alongside os/cpu for musl/glibc variants.
|
|
expect(envLockfile!.packages['@pnpm.e2e/only-linux-x64-musl@1.0.0']).toStrictEqual({
|
|
resolution: {
|
|
integrity: getIntegrity('@pnpm.e2e/only-linux-x64-musl', '1.0.0'),
|
|
},
|
|
os: ['linux'],
|
|
cpu: ['x64'],
|
|
libc: ['musl'],
|
|
})
|
|
|
|
// The parent config dep itself is still registered as the only top-level config dep.
|
|
expect(envLockfile!.importers['.'].configDependencies).toStrictEqual({
|
|
'@pnpm.e2e/support-different-architectures': {
|
|
specifier: '1.0.0',
|
|
version: '1.0.0',
|
|
},
|
|
})
|
|
})
|
|
|
|
test('config dep with no optionalDependencies keeps an empty snapshot', async () => {
|
|
prepareEmpty()
|
|
const { storeController, storeDir } = createTempStore()
|
|
|
|
await resolveConfigDeps(['@pnpm.e2e/foo@100.0.0'], {
|
|
registries: {
|
|
default: registry,
|
|
},
|
|
rootDir: process.cwd(),
|
|
cacheDir: path.resolve('cache'),
|
|
store: storeController,
|
|
storeDir,
|
|
})
|
|
|
|
const envLockfile = await readEnvLockfile(process.cwd())
|
|
expect(envLockfile!.snapshots['@pnpm.e2e/foo@100.0.0']).toStrictEqual({})
|
|
})
|
|
|
|
test('rejects an optionalDependency declared with a non-exact version', async () => {
|
|
prepareEmpty()
|
|
const { storeController, storeDir } = createTempStore()
|
|
|
|
// @pnpm.e2e/foobar declares `@pnpm.e2e/bar: "^100.0.0"` — a range, not an exact version.
|
|
await expect(resolveConfigDeps(['@pnpm.e2e/foobar@100.0.0'], {
|
|
registries: {
|
|
default: registry,
|
|
},
|
|
rootDir: process.cwd(),
|
|
cacheDir: path.resolve('cache'),
|
|
store: storeController,
|
|
storeDir,
|
|
})).rejects.toThrow(/only exact versions are supported/)
|
|
})
|
|
|
|
test('orphan optional subdeps from a previous resolution are pruned', async () => {
|
|
prepareEmpty()
|
|
const { storeController, storeDir } = createTempStore()
|
|
|
|
// Simulate a prior resolution that left optional subdeps for a now-removed
|
|
// version of a config dependency. The stale `foo@99.0.0` and its optional
|
|
// subdep `bar@1.0.0` are not referenced from any current configDependency.
|
|
await writeEnvLockfile(process.cwd(), {
|
|
lockfileVersion: '9.0',
|
|
importers: {
|
|
'.': { configDependencies: {} },
|
|
},
|
|
packages: {
|
|
'@pnpm.e2e/foo@99.0.0': { resolution: { integrity: 'sha512-stale==' } },
|
|
'@pnpm.e2e/bar@1.0.0': { resolution: { integrity: 'sha512-stale==' } },
|
|
},
|
|
snapshots: {
|
|
'@pnpm.e2e/foo@99.0.0': { optionalDependencies: { '@pnpm.e2e/bar': '1.0.0' } },
|
|
'@pnpm.e2e/bar@1.0.0': { optional: true },
|
|
},
|
|
})
|
|
|
|
await resolveConfigDeps(['@pnpm.e2e/foo@100.0.0'], {
|
|
registries: {
|
|
default: registry,
|
|
},
|
|
rootDir: process.cwd(),
|
|
cacheDir: path.resolve('cache'),
|
|
store: storeController,
|
|
storeDir,
|
|
})
|
|
|
|
const envLockfile = await readEnvLockfile(process.cwd())
|
|
expect(envLockfile!.packages['@pnpm.e2e/foo@99.0.0']).toBeUndefined()
|
|
expect(envLockfile!.packages['@pnpm.e2e/bar@1.0.0']).toBeUndefined()
|
|
expect(envLockfile!.snapshots['@pnpm.e2e/foo@99.0.0']).toBeUndefined()
|
|
expect(envLockfile!.snapshots['@pnpm.e2e/bar@1.0.0']).toBeUndefined()
|
|
expect(envLockfile!.packages['@pnpm.e2e/foo@100.0.0']).toBeDefined()
|
|
})
|
|
|
|
test('fails with frozenLockfile', async () => {
|
|
prepareEmpty()
|
|
const { storeController, storeDir } = createTempStore()
|
|
|
|
await expect(resolveConfigDeps(['@pnpm.e2e/foo@100.0.0'], {
|
|
registries: {
|
|
default: registry,
|
|
},
|
|
rootDir: process.cwd(),
|
|
cacheDir: path.resolve('cache'),
|
|
store: storeController,
|
|
storeDir,
|
|
frozenLockfile: true,
|
|
})).rejects.toThrow('Cannot resolve configDependencies with "frozen-lockfile"')
|
|
})
|