Files
pnpm/installing/commands/test/update/interactive.ts
Marvin Hagemeister 49e6074644 test: replace @pnpm/registry-mock with an in-repo in-process registry (#11927)
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.
2026-05-29 14:35:45 +02:00

336 lines
9.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import path from 'node:path'
import { expect, jest, test } from '@jest/globals'
import type { LockfileObject } from '@pnpm/lockfile.types'
import { prepare, preparePackages } from '@pnpm/prepare'
import { addDistTag, REGISTRY_MOCK_PORT } from '@pnpm/testing.registry-mock'
import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter'
import chalk from 'chalk'
import { readYamlFileSync } from 'read-yaml-file'
jest.unstable_mockModule('@inquirer/prompts', () => {
class Separator {
separator: string
readonly type = 'separator' as const
constructor (separator: string) {
this.separator = separator
}
}
return {
Separator,
checkbox: jest.fn(),
confirm: jest.fn(),
input: jest.fn(),
password: jest.fn(),
select: jest.fn(),
}
})
const { checkbox, Separator } = await import('@inquirer/prompts')
const { add, install, update } = await import('@pnpm/installing.commands')
const mockCheckbox = jest.mocked(checkbox)
const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`
const DEFAULT_OPTIONS = {
argv: {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
excludeLinksFromLockfile: false,
extraEnv: {},
cliOptions: {},
deployAllFiles: false,
include: {
dependencies: true,
devDependencies: true,
optionalDependencies: true,
},
lock: true,
pnpmfile: ['.pnpmfile.cjs'],
pnpmHomeDir: '',
preferWorkspacePackages: true,
configByUri: {},
registries: {
default: REGISTRY_URL,
},
rootProjectManifestDir: '',
sort: true,
userConfig: {},
workspaceConcurrency: 1,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}
test('interactively update', async () => {
const project = prepare({
dependencies: {
// has 1.0.0 and 1.0.1 that satisfy this range
'is-negative': '^1.0.0',
// only 2.0.0 satisfies this range
'is-positive': '^2.0.0',
// has many versions that satisfy ^3.0.0
micromatch: '^3.0.0',
},
})
const storeDir = path.resolve('pnpm-store')
await Promise.all([
addDistTag({ package: 'is-negative', version: '2.1.0', distTag: 'latest' }),
addDistTag({ package: 'micromatch', version: '4.0.0', distTag: 'latest' }),
])
await add.handler(
{
...DEFAULT_OPTIONS,
cacheDir: path.resolve('cache'),
dir: process.cwd(),
linkWorkspacePackages: true,
save: false,
storeDir,
},
['is-negative@1.0.0', 'is-positive@2.0.0', 'micromatch@3.0.0']
)
mockCheckbox.mockResolvedValue(['is-negative'])
mockCheckbox.mockClear()
// Update to compatible versions
await update.handler({
...DEFAULT_OPTIONS,
cacheDir: path.resolve('cache'),
dir: process.cwd(),
interactive: true,
linkWorkspacePackages: true,
storeDir,
})
// eslint-disable-next-line
const callArgs = mockCheckbox.mock.calls[0][0] as any
const flatChoices = callArgs.choices
expect(flatChoices).toStrictEqual([
new Separator(chalk.bold('── dependencies ──')),
new Separator(' Package Current Target URL '),
{
name: `is-negative 1.0.0 1.0.${chalk.greenBright.bold('1')} `,
value: 'is-negative',
},
{
name: `micromatch 3.0.0 3.${chalk.yellowBright.bold('1.10')} `,
value: 'micromatch',
},
])
expect(mockCheckbox).toHaveBeenCalledWith(
expect.objectContaining({
message:
'Choose which packages to update ' +
`(Press ${chalk.cyan('<space>')} to select, ` +
`${chalk.cyan('<a>')} to toggle all, ` +
`${chalk.cyan('<i>')} to invert selection)\n\nEnter to start updating. Ctrl-c to cancel.`,
pageSize: process.stdout.rows == null ? 7 : Math.max(7, process.stdout.rows - 6),
})
)
expect(callArgs.theme.style.highlight('focused row')).toBe('focused row')
{
const lockfile = project.readLockfile()
expect(lockfile.packages['micromatch@3.0.0']).toBeTruthy()
expect(lockfile.packages['is-negative@1.0.1']).toBeTruthy()
expect(lockfile.packages['is-positive@2.0.0']).toBeTruthy()
}
// Update to latest versions
mockCheckbox.mockClear()
mockCheckbox.mockResolvedValue(['is-negative'])
await update.handler({
...DEFAULT_OPTIONS,
cacheDir: path.resolve('cache'),
dir: process.cwd(),
interactive: true,
latest: true,
linkWorkspacePackages: true,
storeDir,
})
// eslint-disable-next-line
const callArgs2 = mockCheckbox.mock.calls[0][0] as any
const flatChoices2 = callArgs2.choices
expect(flatChoices2).toStrictEqual([
new Separator(chalk.bold('── dependencies ──')),
new Separator(' Package Current Target URL '),
{
name: `is-negative 1.0.1 ${chalk.redBright.bold('2.1.0')} `,
value: 'is-negative',
},
{
name: `is-positive 2.0.0 ${chalk.redBright.bold('3.1.0')} `,
value: 'is-positive',
},
{
name: `micromatch 3.0.0 ${chalk.redBright.bold('4.0.0')} `,
value: 'micromatch',
},
])
expect(mockCheckbox).toHaveBeenCalledWith(
expect.objectContaining({
message:
'Choose which packages to update ' +
`(Press ${chalk.cyan('<space>')} to select, ` +
`${chalk.cyan('<a>')} to toggle all, ` +
`${chalk.cyan('<i>')} to invert selection)\n\nEnter to start updating. Ctrl-c to cancel.`,
})
)
{
const lockfile = project.readLockfile()
expect(lockfile.packages['micromatch@3.0.0']).toBeTruthy()
expect(lockfile.packages['is-negative@2.1.0']).toBeTruthy()
expect(lockfile.packages['is-positive@2.0.0']).toBeTruthy()
}
})
test('interactive update of dev dependencies only', async () => {
preparePackages([
{
name: 'project1',
dependencies: {
'is-negative': '^1.0.1',
},
},
{
name: 'project2',
devDependencies: {
'is-negative': '^1.0.0',
},
},
])
const storeDir = path.resolve('store')
mockCheckbox.mockResolvedValue(['is-negative'])
const { allProjects, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(
process.cwd(),
[]
)
await install.handler({
...DEFAULT_OPTIONS,
cacheDir: path.resolve('cache'),
allProjects,
dir: process.cwd(),
linkWorkspacePackages: true,
lockfileDir: process.cwd(),
recursive: true,
selectedProjectsGraph,
storeDir,
workspaceDir: process.cwd(),
})
await update.handler({
...DEFAULT_OPTIONS,
cacheDir: path.resolve('cache'),
allProjects,
cliOptions: {
dev: true,
optional: false,
production: false,
},
dir: process.cwd(),
interactive: true,
latest: true,
linkWorkspacePackages: true,
lockfileDir: process.cwd(),
recursive: true,
selectedProjectsGraph,
storeDir,
workspaceDir: process.cwd(),
})
const lockfile = readYamlFileSync<LockfileObject>('pnpm-lock.yaml')
expect(Object.keys(lockfile.packages ?? {})).toStrictEqual([
'is-negative@1.0.1',
'is-negative@2.1.0',
])
})
test('interactively update should ignore dependencies from the ignoreDependencies field', async () => {
const project = prepare({
dependencies: {
// has 1.0.0 and 1.0.1 that satisfy this range
'is-negative': '^1.0.0',
// only 2.0.0 satisfies this range
'is-positive': '^2.0.0',
// has many versions that satisfy ^3.0.0
micromatch: '^3.0.0',
},
})
const storeDir = path.resolve('pnpm-store')
await add.handler(
{
...DEFAULT_OPTIONS,
cacheDir: path.resolve('cache'),
dir: process.cwd(),
linkWorkspacePackages: true,
save: false,
storeDir,
},
['is-negative@1.0.0', 'is-positive@2.0.0', 'micromatch@3.0.0']
)
mockCheckbox.mockResolvedValue(['micromatch'])
mockCheckbox.mockClear()
await update.handler({
...DEFAULT_OPTIONS,
cacheDir: path.resolve('cache'),
dir: process.cwd(),
interactive: true,
linkWorkspacePackages: true,
storeDir,
updateConfig: {
ignoreDependencies: ['is-negative'],
},
})
// eslint-disable-next-line
const callArgs3 = mockCheckbox.mock.calls[0][0] as any
const flatChoices3 = callArgs3.choices
expect(flatChoices3).toStrictEqual(
[
new Separator(chalk.bold('── dependencies ──')),
new Separator(' Package Current Target URL '),
{
name: `micromatch 3.0.0 3.${chalk.yellowBright.bold('1.10')} `,
value: 'micromatch',
},
]
)
expect(mockCheckbox).toHaveBeenCalledWith(
expect.objectContaining({
message:
'Choose which packages to update ' +
`(Press ${chalk.cyan('<space>')} to select, ` +
`${chalk.cyan('<a>')} to toggle all, ` +
`${chalk.cyan('<i>')} to invert selection)\n\nEnter to start updating. Ctrl-c to cancel.`,
})
)
{
const lockfile = project.readLockfile()
expect(lockfile.packages['micromatch@3.1.10']).toBeTruthy()
expect(lockfile.packages['is-negative@1.0.0']).toBeTruthy()
expect(lockfile.packages['is-positive@2.0.0']).toBeTruthy()
}
})