mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-02 13:13:17 -04:00
* feat(registry-access): extract setDistTag and dogfood from tests
Add `@pnpm/registry-access.commands#setDistTag` — the low-level PUT to
`/-/package/:pkg/dist-tags/:tag`. The CLI `dist-tag add` handler now
calls it instead of issuing the fetch inline.
Tests in this monorepo now use a thin new package
`@pnpm/testing.registry-mock` (REGISTRY_MOCK_PORT + REGISTRY_MOCK_CREDENTIALS
baked in) that delegates to `setDistTag`, replacing `addDistTag` from
`@pnpm/registry-mock`. That dropped helper relied on
`anonymous-npm-registry-client` and a verdaccio-era
fetch-then-DELETE-then-PUT dance that is no longer needed against
pnpm-registry.
39 test files swapped from `@pnpm/registry-mock` to
`@pnpm/testing.registry-mock`.
* fix: move setDistTag to its own package to break tsconfig project-reference cycle
testing/registry-mock → registry-access.commands → releasing/commands
→ installing/commands → installing/deps-installer → testing/registry-mock.
Extract setDistTag into @pnpm/registry-access.set-dist-tag (only depends
on @pnpm/error, @pnpm/network.fetch, @pnpm/npm-package-arg). Both
@pnpm/registry-access.commands and @pnpm/testing.registry-mock import
from it. Cycle gone.
* feat(registry-access): extract addUser helper, dogfood from login + tests
Add @pnpm/registry-access.add-user — a small helper that PUTs to
/-/user/org.couchdb.user:<name> and returns { token }. The CLI's
classicLogin (pnpm login fallback path) now calls it, and tests
use it via @pnpm/testing.registry-mock instead of the legacy
addUser from @pnpm/registry-mock.
Swapped 3 call sites: globalSetup.js, installing/deps-installer's
auth.ts, and pnpm/test/dlx.ts. AddUserHttpError exposes status +
text + parsed-json-if-applicable + headers so the CLI can still
do its OTP detection. One webauth-OTP login test mock had to be
adjusted to provide its body via `text` (JSON-stringified) rather
than `json` only, since the helper consumes the body via `text()`.
* refactor: consolidate set-dist-tag + add-user helpers into one @pnpm/registry-access.client package
One shared package is better than splitting per endpoint. Future endpoints
(publish, deprecate, etc.) can land here without another wrapper.
No behavioral change — same setDistTag and addUser exports as before,
just under one roof. Callers updated: registry-access.commands,
auth.commands, testing.registry-mock.
* fix(registry-access): sort imports
367 lines
9.9 KiB
TypeScript
367 lines
9.9 KiB
TypeScript
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 { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
|
||
import { addDistTag } 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('enquirer', () => ({ default: { prompt: jest.fn() } }))
|
||
const { default: enquirer } = await import('enquirer')
|
||
const { add, install, update } = await import('@pnpm/installing.commands')
|
||
|
||
const prompt = jest.mocked(enquirer.prompt)
|
||
|
||
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')
|
||
|
||
const headerChoice = {
|
||
name: 'Package Current Target URL ',
|
||
disabled: true,
|
||
hint: '',
|
||
value: '',
|
||
}
|
||
|
||
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']
|
||
)
|
||
|
||
prompt.mockResolvedValue({
|
||
updateDependencies: [
|
||
{
|
||
value: 'is-negative',
|
||
name: `is-negative 1.0.0 ❯ 1.0.${chalk.greenBright.bold('1')} https://pnpm.io/ `,
|
||
},
|
||
],
|
||
})
|
||
|
||
prompt.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
|
||
expect((prompt.mock.calls[0][0] as any).choices).toStrictEqual([
|
||
{
|
||
choices: [
|
||
headerChoice,
|
||
{
|
||
message: `is-negative 1.0.0 ❯ 1.0.${chalk.greenBright.bold('1')} `,
|
||
value: 'is-negative',
|
||
name: 'is-negative',
|
||
},
|
||
{
|
||
message: `micromatch 3.0.0 ❯ 3.${chalk.yellowBright.bold('1.10')} `,
|
||
value: 'micromatch',
|
||
name: 'micromatch',
|
||
},
|
||
],
|
||
name: '[dependencies]',
|
||
message: 'dependencies',
|
||
},
|
||
])
|
||
expect(prompt).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
footer: '\nEnter to start updating. Ctrl-c to cancel.',
|
||
message:
|
||
'Choose which packages to update ' +
|
||
`(Press ${chalk.cyan('<space>')} to select, ` +
|
||
`${chalk.cyan('<a>')} to toggle all, ` +
|
||
`${chalk.cyan('<i>')} to invert selection)`,
|
||
name: 'updateDependencies',
|
||
type: 'multiselect',
|
||
})
|
||
)
|
||
|
||
{
|
||
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
|
||
prompt.mockClear()
|
||
await update.handler({
|
||
...DEFAULT_OPTIONS,
|
||
cacheDir: path.resolve('cache'),
|
||
dir: process.cwd(),
|
||
interactive: true,
|
||
latest: true,
|
||
linkWorkspacePackages: true,
|
||
storeDir,
|
||
})
|
||
|
||
// eslint-disable-next-line
|
||
expect((prompt.mock.calls[0][0] as any).choices).toStrictEqual([
|
||
{
|
||
choices: [
|
||
headerChoice,
|
||
{
|
||
message: `is-negative 1.0.1 ❯ ${chalk.redBright.bold('2.1.0')} `,
|
||
value: 'is-negative',
|
||
name: 'is-negative',
|
||
},
|
||
{
|
||
message: `is-positive 2.0.0 ❯ ${chalk.redBright.bold('3.1.0')} `,
|
||
value: 'is-positive',
|
||
name: 'is-positive',
|
||
},
|
||
{
|
||
message: `micromatch 3.0.0 ❯ ${chalk.redBright.bold('4.0.0')} `,
|
||
value: 'micromatch',
|
||
name: 'micromatch',
|
||
},
|
||
],
|
||
name: '[dependencies]',
|
||
message: 'dependencies',
|
||
},
|
||
])
|
||
expect(prompt).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
footer: '\nEnter to start updating. Ctrl-c to cancel.',
|
||
message:
|
||
'Choose which packages to update ' +
|
||
`(Press ${chalk.cyan('<space>')} to select, ` +
|
||
`${chalk.cyan('<a>')} to toggle all, ` +
|
||
`${chalk.cyan('<i>')} to invert selection)`,
|
||
name: 'updateDependencies',
|
||
type: 'multiselect',
|
||
})
|
||
)
|
||
|
||
{
|
||
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')
|
||
|
||
prompt.mockResolvedValue({
|
||
updateDependencies: [
|
||
{
|
||
value: 'is-negative',
|
||
name: `is-negative 1.0.0 ❯ 1.0.${chalk.greenBright.bold('1')} https://pnpm.io/ `,
|
||
},
|
||
],
|
||
})
|
||
|
||
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']
|
||
)
|
||
|
||
prompt.mockResolvedValue({
|
||
updateDependencies: [{ value: 'micromatch', name: 'anything' }],
|
||
})
|
||
|
||
prompt.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
|
||
expect((prompt.mock.calls[0][0] as any).choices as any).toStrictEqual(
|
||
[
|
||
{
|
||
choices: [
|
||
{
|
||
disabled: true,
|
||
hint: '',
|
||
name: 'Package Current Target URL ',
|
||
value: '',
|
||
},
|
||
{
|
||
message: `micromatch 3.0.0 ❯ 3.${chalk.yellowBright.bold('1.10')} `,
|
||
value: 'micromatch',
|
||
name: 'micromatch',
|
||
},
|
||
],
|
||
name: '[dependencies]',
|
||
message: 'dependencies',
|
||
},
|
||
]
|
||
)
|
||
|
||
expect(prompt).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
footer: '\nEnter to start updating. Ctrl-c to cancel.',
|
||
message:
|
||
'Choose which packages to update ' +
|
||
`(Press ${chalk.cyan('<space>')} to select, ` +
|
||
`${chalk.cyan('<a>')} to toggle all, ` +
|
||
`${chalk.cyan('<i>')} to invert selection)`,
|
||
name: 'updateDependencies',
|
||
type: 'multiselect',
|
||
})
|
||
)
|
||
|
||
{
|
||
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()
|
||
}
|
||
})
|