mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-01 12:41:16 -04:00
refactor: replace enquirer with @inquirer/prompts (#11942)
Replaces the unmaintained `enquirer` package with `@inquirer/prompts` for all interactive CLI prompts. Fixes the `update -i` scrolling overflow bug where long choice lists were clipped in the terminal. Fixes #6643 ## User-facing changes - **`pnpm update -i` / `pnpm update -i --latest`**: Scrolling now works correctly when many packages are available; the new library uses visual-line-aware pagination via `usePagination` - **`pnpm audit --fix -i`**: Same scrolling fix for vulnerability selection - **`pnpm approve-builds`**: Interactive build approval prompts updated - **`pnpm patch`**: Version selection and "apply to all" prompts updated - **`pnpm patch-remove`**: Patch removal selection updated - **`pnpm publish`**: Branch confirmation prompt updated - **`pnpm login`**: Credential prompts updated - **`pnpm run` / `pnpm exec`** (with `verifyDepsBeforeRun=prompt`): Confirmation prompt updated ## Internal changes - `OtpEnquirer` DI interface changed from `{ prompt }` to `{ input }` - `LoginEnquirer` DI interface changed from `{ prompt }` to `{ input, password }` - `enquirer` removed from catalog and all 8 package.json files - `@inquirer/prompts` v8.4.3 added to catalog and all 8 package.json files - Removed `OtpPromptOptions` and `OtpPromptResponse` exports from `@pnpm/network.web-auth` (no longer needed) --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
29
.changeset/migrate-enquirer-to-inquirer.md
Normal file
29
.changeset/migrate-enquirer-to-inquirer.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
"@pnpm/auth.commands": minor
|
||||
"@pnpm/building.commands": minor
|
||||
"@pnpm/deps.compliance.commands": minor
|
||||
"@pnpm/exec.commands": minor
|
||||
"@pnpm/installing.commands": minor
|
||||
"@pnpm/installing.deps-installer": minor
|
||||
"@pnpm/network.web-auth": minor
|
||||
"@pnpm/patching.commands": minor
|
||||
"@pnpm/registry-access.commands": minor
|
||||
"@pnpm/releasing.commands": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Replaced `enquirer` with `@inquirer/prompts` for all interactive prompts. Fixes the `update -i` scrolling overflow bug where long choice lists were clipped in the terminal [#6643](https://github.com/pnpm/pnpm/issues/6643).
|
||||
|
||||
**User-facing changes:**
|
||||
- `pnpm update -i` / `pnpm update -i --latest`: Scrolling now works correctly when many packages are available; the new library uses visual-line-aware pagination via `usePagination`
|
||||
- `pnpm audit --fix -i`: Same scrolling fix for vulnerability selection
|
||||
- `pnpm approve-builds`: Interactive build approval prompts updated
|
||||
- `pnpm patch`: Version selection and "apply to all" prompts updated
|
||||
- `pnpm patch-remove`: Patch removal selection updated
|
||||
- `pnpm publish`: Branch confirmation prompt updated
|
||||
- `pnpm login`: Credential prompts updated
|
||||
- `pnpm run` / `pnpm exec` (with `verifyDepsBeforeRun=prompt`): Confirmation prompt updated
|
||||
|
||||
Vim-style `j`/`k` keys still work for up/down navigation in all interactive prompts.
|
||||
|
||||
**Internal:** The `OtpEnquirer` and `LoginEnquirer` DI interfaces changed from `{ prompt }` to `{ input }` / `{ input, password }` respectively. Plugins or custom builds that inject their own enquirer mock will need to update.
|
||||
@@ -32,13 +32,13 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/cli.utils": "workspace:*",
|
||||
"@pnpm/config.reader": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/network.fetch": "workspace:*",
|
||||
"@pnpm/network.web-auth": "workspace:*",
|
||||
"@pnpm/registry-access.client": "workspace:*",
|
||||
"enquirer": "catalog:",
|
||||
"normalize-registry-url": "catalog:",
|
||||
"read-ini-file": "catalog:",
|
||||
"render-help": "catalog:",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from 'node:path'
|
||||
import readline from 'node:readline'
|
||||
|
||||
import { input, password as passwordPrompt } from '@inquirer/prompts'
|
||||
import { docsUrl } from '@pnpm/cli.utils'
|
||||
import { type Config, types as allTypes } from '@pnpm/config.reader'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
withOtpHandling,
|
||||
} from '@pnpm/network.web-auth'
|
||||
import { addUser, AddUserHttpError, AddUserNoTokenError } from '@pnpm/registry-access.client'
|
||||
import enquirer from 'enquirer'
|
||||
import normalizeRegistryUrl from 'normalize-registry-url'
|
||||
import { readIniFile } from 'read-ini-file'
|
||||
import { renderHelp } from 'render-help'
|
||||
@@ -87,13 +87,8 @@ export interface LoginDate {
|
||||
}
|
||||
|
||||
export interface LoginEnquirer {
|
||||
prompt: (options: LoginEnquirerOptions) => Promise<Record<string, string>>
|
||||
}
|
||||
|
||||
export interface LoginEnquirerOptions {
|
||||
message: string
|
||||
name: string
|
||||
type: string
|
||||
input: (options: { message: string }) => Promise<string>
|
||||
password: (options: { message: string }) => Promise<string>
|
||||
}
|
||||
|
||||
export interface LoginFetchResponse {
|
||||
@@ -145,7 +140,7 @@ export const DEFAULT_CONTEXT: LoginContext = {
|
||||
Date,
|
||||
setTimeout,
|
||||
createReadlineInterface: readline.createInterface.bind(null, { input: process.stdin }),
|
||||
enquirer,
|
||||
enquirer: { input, password: passwordPrompt },
|
||||
fetch,
|
||||
globalInfo,
|
||||
globalWarn,
|
||||
@@ -285,21 +280,19 @@ async function classicLogin ({
|
||||
}: ClassicLoginParams): Promise<string> {
|
||||
const { enquirer, fetch, globalInfo, globalWarn } = context
|
||||
|
||||
const { username } = await enquirer.prompt({
|
||||
message: 'Username:',
|
||||
name: 'username',
|
||||
type: 'input',
|
||||
})
|
||||
const { password } = await enquirer.prompt({
|
||||
message: 'Password:',
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
})
|
||||
const { email } = await enquirer.prompt({
|
||||
message: 'Email (this IS public):',
|
||||
name: 'email',
|
||||
type: 'input',
|
||||
})
|
||||
let username: string
|
||||
let password: string
|
||||
let email: string
|
||||
try {
|
||||
username = await enquirer.input({ message: 'Username:' })
|
||||
password = await enquirer.password({ message: 'Password:' })
|
||||
email = await enquirer.input({ message: 'Email (this IS public):' })
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
throw new PnpmError('LOGIN_CANCELED', 'Login canceled')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
if (!username || !password || !email) {
|
||||
throw new LoginMissingCredentialsError()
|
||||
|
||||
@@ -13,8 +13,10 @@ const TEST_CONTEXT: LoginContext = {
|
||||
once: () => {},
|
||||
close: () => {},
|
||||
}),
|
||||
enquirer: { prompt: async () => {
|
||||
throw new Error('Unexpected call to enquirer.prompt')
|
||||
enquirer: { input: async () => {
|
||||
throw new Error('Unexpected call to enquirer.input')
|
||||
}, password: async () => {
|
||||
throw new Error('Unexpected call to enquirer.password')
|
||||
} },
|
||||
fetch: async url => {
|
||||
throw new Error(`Unexpected call to fetch: ${url}`)
|
||||
@@ -269,11 +271,14 @@ describe('login', () => {
|
||||
throw new Error(`Unexpected call to fetch: ${url}`)
|
||||
},
|
||||
enquirer: {
|
||||
prompt: async (opts: { message: string, name: string, type: string }): Promise<Record<string, string>> => {
|
||||
if (opts.name === 'username') return { username: 'john' }
|
||||
if (opts.name === 'password') return { password: 'secret' }
|
||||
if (opts.name === 'email') return { email: 'john@example.com' }
|
||||
throw new Error(`Unexpected call to enquirer.prompt: ${opts.name}`)
|
||||
input: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Username:') return 'john'
|
||||
if (opts.message === 'Email (this IS public):') return 'john@example.com'
|
||||
throw new Error(`Unexpected call to enquirer.input: ${opts.message}`)
|
||||
},
|
||||
password: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Password:') return 'secret'
|
||||
throw new Error(`Unexpected call to enquirer.password: ${opts.message}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -325,12 +330,15 @@ describe('login', () => {
|
||||
throw new Error(`Unexpected call to fetch: ${url}`)
|
||||
},
|
||||
enquirer: {
|
||||
prompt: async (opts: { message: string, name: string, type: string }): Promise<Record<string, string>> => {
|
||||
if (opts.name === 'username') return { username: 'alice' }
|
||||
if (opts.name === 'password') return { password: 'pass' }
|
||||
if (opts.name === 'email') return { email: 'alice@example.com' }
|
||||
if (opts.name === 'otp') return { otp: '999999' }
|
||||
throw new Error(`Unexpected call to enquirer.prompt: ${opts.name}`)
|
||||
input: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Username:') return 'alice'
|
||||
if (opts.message === 'Email (this IS public):') return 'alice@example.com'
|
||||
if (opts.message === 'This operation requires a one-time password.\nEnter OTP:') return '999999'
|
||||
throw new Error(`Unexpected call to enquirer.input: ${opts.message}`)
|
||||
},
|
||||
password: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Password:') return 'pass'
|
||||
throw new Error(`Unexpected call to enquirer.password: ${opts.message}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -388,11 +396,14 @@ describe('login', () => {
|
||||
throw new Error(`Unexpected call to fetch: ${url}`)
|
||||
},
|
||||
enquirer: {
|
||||
prompt: async (opts: { message: string, name: string, type: string }): Promise<Record<string, string>> => {
|
||||
if (opts.name === 'username') return { username: 'bob' }
|
||||
if (opts.name === 'password') return { password: 'pass' }
|
||||
if (opts.name === 'email') return { email: 'bob@example.com' }
|
||||
throw new Error(`Unexpected call to enquirer.prompt: ${opts.name}`)
|
||||
input: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Username:') return 'bob'
|
||||
if (opts.message === 'Email (this IS public):') return 'bob@example.com'
|
||||
throw new Error(`Unexpected call to enquirer.input: ${opts.message}`)
|
||||
},
|
||||
password: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Password:') return 'pass'
|
||||
throw new Error(`Unexpected call to enquirer.password: ${opts.message}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -424,15 +435,18 @@ describe('login', () => {
|
||||
})
|
||||
},
|
||||
enquirer: {
|
||||
prompt: async (opts: { message: string, name: string, type: string }): Promise<Record<string, string>> => {
|
||||
if (opts.name === 'username') return { username: 'alice' }
|
||||
if (opts.name === 'password') return { password: 'pass' }
|
||||
if (opts.name === 'email') return { email: 'alice@example.com' }
|
||||
throw new Error(`Unexpected call to enquirer.prompt: ${opts.name}`)
|
||||
input: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Username:') return 'alice'
|
||||
if (opts.message === 'Email (this IS public):') return 'alice@example.com'
|
||||
throw new Error(`Unexpected call to enquirer.input: ${opts.message}`)
|
||||
},
|
||||
password: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Password:') return 'pass'
|
||||
throw new Error(`Unexpected call to enquirer.password: ${opts.message}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
const opts = { configDir: '/otp/config', dir: '/mock', authConfig: {}, registry: 'https://example.org' }
|
||||
const opts = { configDir: '/mock/config', dir: '/mock', authConfig: {}, registry: 'https://example.org' }
|
||||
const promise = login({ context, opts })
|
||||
await expect(promise).rejects.toHaveProperty(['code'], 'ERR_PNPM_LOGIN_FAILED')
|
||||
await expect(promise).rejects.toHaveProperty(['message'], 'Login failed (HTTP 403): Forbidden')
|
||||
@@ -453,11 +467,14 @@ describe('login', () => {
|
||||
throw new Error(`Unexpected call to fetch: ${url}`)
|
||||
},
|
||||
enquirer: {
|
||||
prompt: async (opts: { message: string, name: string, type: string }): Promise<Record<string, string>> => {
|
||||
if (opts.name === 'username') return { username: '' }
|
||||
if (opts.name === 'password') return { password: 'pass' }
|
||||
if (opts.name === 'email') return { email: 'a@b.com' }
|
||||
throw new Error(`Unexpected call to enquirer.prompt: ${opts.name}`)
|
||||
input: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Username:') return ''
|
||||
if (opts.message === 'Email (this IS public):') return 'a@b.com'
|
||||
throw new Error(`Unexpected call to enquirer.input: ${opts.message}`)
|
||||
},
|
||||
password: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Password:') return 'pass'
|
||||
throw new Error(`Unexpected call to enquirer.password: ${opts.message}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -489,11 +506,14 @@ describe('login', () => {
|
||||
throw new Error(`Unexpected call to fetch: ${url}`)
|
||||
},
|
||||
enquirer: {
|
||||
prompt: async (opts: { message: string, name: string, type: string }): Promise<Record<string, string>> => {
|
||||
if (opts.name === 'username') return { username: 'alice' }
|
||||
if (opts.name === 'password') return { password: 'pass' }
|
||||
if (opts.name === 'email') return { email: 'alice@example.com' }
|
||||
throw new Error(`Unexpected call to enquirer.prompt: ${opts.name}`)
|
||||
input: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Username:') return 'alice'
|
||||
if (opts.message === 'Email (this IS public):') return 'alice@example.com'
|
||||
throw new Error(`Unexpected call to enquirer.input: ${opts.message}`)
|
||||
},
|
||||
password: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Password:') return 'pass'
|
||||
throw new Error(`Unexpected call to enquirer.password: ${opts.message}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -551,11 +571,14 @@ describe('login', () => {
|
||||
throw new Error(`Unexpected call to fetch: ${url}`)
|
||||
},
|
||||
enquirer: {
|
||||
prompt: async (opts: { message: string, name: string, type: string }): Promise<Record<string, string>> => {
|
||||
if (opts.name === 'username') return { username: 'jane' }
|
||||
if (opts.name === 'password') return { password: 'pass' }
|
||||
if (opts.name === 'email') return { email: 'jane@example.com' }
|
||||
throw new Error(`Unexpected call to enquirer.prompt: ${opts.name}`)
|
||||
input: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Username:') return 'jane'
|
||||
if (opts.message === 'Email (this IS public):') return 'jane@example.com'
|
||||
throw new Error(`Unexpected call to enquirer.input: ${opts.message}`)
|
||||
},
|
||||
password: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Password:') return 'pass'
|
||||
throw new Error(`Unexpected call to enquirer.password: ${opts.message}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -589,11 +612,14 @@ describe('login', () => {
|
||||
})
|
||||
},
|
||||
enquirer: {
|
||||
prompt: async (opts: { message: string, name: string, type: string }): Promise<Record<string, string>> => {
|
||||
if (opts.name === 'username') return { username: 'alice' }
|
||||
if (opts.name === 'password') return { password: 'pass' }
|
||||
if (opts.name === 'email') return { email: 'alice@example.com' }
|
||||
throw new Error(`Unexpected call to enquirer.prompt: ${opts.name}`)
|
||||
input: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Username:') return 'alice'
|
||||
if (opts.message === 'Email (this IS public):') return 'alice@example.com'
|
||||
throw new Error(`Unexpected call to enquirer.input: ${opts.message}`)
|
||||
},
|
||||
password: async (opts: { message: string }): Promise<string> => {
|
||||
if (opts.message === 'Password:') return 'pass'
|
||||
throw new Error(`Unexpected call to enquirer.password: ${opts.message}`)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/building.after-install": "workspace:*",
|
||||
"@pnpm/cli.command": "workspace:*",
|
||||
"@pnpm/cli.common-cli-options-help": "workspace:*",
|
||||
@@ -48,7 +49,6 @@
|
||||
"@pnpm/util.lex-comparator": "catalog:",
|
||||
"@pnpm/workspace.projects-sorter": "workspace:*",
|
||||
"chalk": "catalog:",
|
||||
"enquirer": "catalog:",
|
||||
"p-limit": "catalog:",
|
||||
"ramda": "catalog:",
|
||||
"render-help": "catalog:"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { checkbox, confirm } from '@inquirer/prompts'
|
||||
import type { CommandHandlerMap } from '@pnpm/cli.command'
|
||||
import type { Config, ConfigContext } from '@pnpm/config.reader'
|
||||
import { writeSettings } from '@pnpm/config.writer'
|
||||
@@ -8,7 +9,6 @@ import { type StrictModules, writeModulesManifest } from '@pnpm/installing.modul
|
||||
import { globalInfo } from '@pnpm/logger'
|
||||
import { lexCompare } from '@pnpm/util.lex-comparator'
|
||||
import chalk from 'chalk'
|
||||
import enquirer from 'enquirer'
|
||||
import { renderHelp } from 'render-help'
|
||||
|
||||
import { rebuild, type RebuildCommandOpts } from '../build/index.js'
|
||||
@@ -121,43 +121,32 @@ export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOp
|
||||
} else if (opts.all) {
|
||||
buildPackages = sortUniqueStrings([...automaticallyIgnoredBuilds])
|
||||
} else {
|
||||
const { result } = await enquirer.prompt({
|
||||
choices: sortUniqueStrings([...automaticallyIgnoredBuilds]),
|
||||
indicator (state: any, choice: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
return ` ${choice.enabled ? '●' : '○'}`
|
||||
},
|
||||
message: 'Choose which packages to build ' +
|
||||
`(Press ${chalk.cyan('<space>')} to select, ` +
|
||||
`${chalk.cyan('<a>')} to toggle all, ` +
|
||||
`${chalk.cyan('<i>')} to invert selection)`,
|
||||
name: 'result',
|
||||
pointer: '❯',
|
||||
result () {
|
||||
return this.selected
|
||||
},
|
||||
styles: {
|
||||
dark: chalk.reset,
|
||||
em: chalk.bgBlack.whiteBright,
|
||||
success: chalk.reset,
|
||||
},
|
||||
type: 'multiselect',
|
||||
|
||||
// For Vim users (related: https://github.com/enquirer/enquirer/pull/163)
|
||||
j () {
|
||||
return this.down()
|
||||
},
|
||||
k () {
|
||||
return this.up()
|
||||
},
|
||||
cancel () {
|
||||
// By default, canceling the prompt via Ctrl+c throws an empty string.
|
||||
// The custom cancel function prevents that behavior.
|
||||
// Otherwise, pnpm CLI would print an error and confuse users.
|
||||
// See related issue: https://github.com/enquirer/enquirer/issues/225
|
||||
try {
|
||||
const buildPackagesValues = await checkbox({
|
||||
choices: sortUniqueStrings([...automaticallyIgnoredBuilds]).map((name) => ({
|
||||
name,
|
||||
value: name,
|
||||
})),
|
||||
message: 'Choose which packages to build ' +
|
||||
`(Press ${chalk.cyan('<space>')} to select, ` +
|
||||
`${chalk.cyan('<a>')} to toggle all, ` +
|
||||
`${chalk.cyan('<i>')} to invert selection)`,
|
||||
required: false,
|
||||
theme: {
|
||||
icon: { checked: '●', unchecked: '○', cursor: '❯' },
|
||||
style: {
|
||||
highlight: chalk.bgBlack.whiteBright,
|
||||
},
|
||||
keybindings: ['vim'],
|
||||
},
|
||||
})
|
||||
buildPackages = buildPackagesValues
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
process.exit(0)
|
||||
},
|
||||
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
buildPackages = result.map(({ value }: { value: string }) => value)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
const allowBuilds: Record<string, boolean | string> = { ...opts.allowBuilds }
|
||||
if (params.length) {
|
||||
@@ -178,14 +167,19 @@ export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOp
|
||||
}
|
||||
if (!opts.all && !params.length) {
|
||||
if (buildPackages.length) {
|
||||
const confirmed = await enquirer.prompt<{ build: boolean }>({
|
||||
type: 'confirm',
|
||||
name: 'build',
|
||||
message: `The next packages will now be built: ${buildPackages.join(', ')}.
|
||||
Do you approve?`,
|
||||
initial: false,
|
||||
})
|
||||
if (!confirmed.build) {
|
||||
let isConfirmed: boolean
|
||||
try {
|
||||
isConfirmed = await confirm({
|
||||
message: `The next packages will now be built: ${buildPackages.join(', ')}.\nDo you approve?`,
|
||||
default: false,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
process.exit(0)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
if (!isConfirmed) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -14,11 +14,28 @@ import { readYamlFileSync } from 'read-yaml-file'
|
||||
import { writePackageSync } from 'write-package'
|
||||
import { writeYamlFileSync } from 'write-yaml-file'
|
||||
|
||||
jest.unstable_mockModule('enquirer', () => ({ default: { prompt: jest.fn() } }))
|
||||
const { default: enquirer } = await import('enquirer')
|
||||
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, confirm } = await import('@inquirer/prompts')
|
||||
const { approveBuilds } = await import('@pnpm/building.commands')
|
||||
|
||||
const prompt = jest.mocked(enquirer.prompt)
|
||||
const mockCheckbox = jest.mocked(checkbox)
|
||||
const mockConfirm = jest.mocked(confirm)
|
||||
|
||||
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}/`
|
||||
const pnpmBin = path.join(import.meta.dirname, '../../../../pnpm/bin/pnpm.mjs')
|
||||
@@ -61,16 +78,8 @@ async function approveSomeBuilds (opts?: ApproveBuildsOptions) {
|
||||
await execPnpmInstall()
|
||||
const config = await getApproveBuildsConfig()
|
||||
|
||||
prompt.mockResolvedValueOnce({
|
||||
result: [
|
||||
{
|
||||
value: '@pnpm.e2e/pre-and-postinstall-scripts-example',
|
||||
},
|
||||
],
|
||||
})
|
||||
prompt.mockResolvedValueOnce({
|
||||
build: true,
|
||||
})
|
||||
mockCheckbox.mockResolvedValueOnce(['@pnpm.e2e/pre-and-postinstall-scripts-example'])
|
||||
mockConfirm.mockResolvedValueOnce(true)
|
||||
|
||||
await approveBuilds.handler({ ...config, ...opts }, [], {})
|
||||
}
|
||||
@@ -79,9 +88,7 @@ async function approveNoBuilds (opts?: ApproveBuildsOptions) {
|
||||
await execPnpmInstall()
|
||||
const config = await getApproveBuildsConfig()
|
||||
|
||||
prompt.mockResolvedValueOnce({
|
||||
result: [],
|
||||
})
|
||||
mockCheckbox.mockResolvedValueOnce([])
|
||||
|
||||
await approveBuilds.handler({ ...config, ...opts }, [], {})
|
||||
}
|
||||
@@ -153,10 +160,8 @@ test("works when root project manifest doesn't exist in a workspace", async () =
|
||||
writeYamlFileSync(workspaceManifestFile, { packages: ['packages/*'] })
|
||||
|
||||
const config = await getApproveBuildsConfig()
|
||||
prompt.mockResolvedValueOnce({
|
||||
result: [{ value: '@pnpm.e2e/pre-and-postinstall-scripts-example' }],
|
||||
})
|
||||
prompt.mockResolvedValueOnce({ build: true })
|
||||
mockCheckbox.mockResolvedValueOnce(['@pnpm.e2e/pre-and-postinstall-scripts-example'])
|
||||
mockConfirm.mockResolvedValueOnce(true)
|
||||
await approveBuilds.handler({ ...config, workspaceDir, rootProjectManifestDir: workspaceDir }, [], {})
|
||||
|
||||
expect(readYamlFileSync(workspaceManifestFile)).toStrictEqual({
|
||||
@@ -204,10 +209,12 @@ test('approve all builds with --all flag', async () => {
|
||||
await execPnpmInstall()
|
||||
const config = await getApproveBuildsConfig()
|
||||
|
||||
prompt.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
await approveBuilds.handler({ ...config, all: true }, [], {})
|
||||
|
||||
expect(prompt).not.toHaveBeenCalled()
|
||||
expect(mockCheckbox).not.toHaveBeenCalled()
|
||||
expect(mockConfirm).not.toHaveBeenCalled()
|
||||
|
||||
const workspaceManifest = readYamlFileSync<any>(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line
|
||||
expect(workspaceManifest.allowBuilds).toStrictEqual({
|
||||
@@ -231,10 +238,12 @@ test('approve builds via positional arguments', async () => {
|
||||
await execPnpmInstall()
|
||||
const config = await getApproveBuildsConfig()
|
||||
|
||||
prompt.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
await approveBuilds.handler(config, ['@pnpm.e2e/pre-and-postinstall-scripts-example'], {})
|
||||
|
||||
expect(prompt).not.toHaveBeenCalled()
|
||||
expect(mockCheckbox).not.toHaveBeenCalled()
|
||||
expect(mockConfirm).not.toHaveBeenCalled()
|
||||
|
||||
const workspaceManifest = readYamlFileSync<any>(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line
|
||||
expect(workspaceManifest.allowBuilds).toStrictEqual({
|
||||
@@ -261,13 +270,15 @@ test('deny builds via !pkg positional arguments', async () => {
|
||||
await execPnpmInstall()
|
||||
const config = await getApproveBuildsConfig()
|
||||
|
||||
prompt.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
await approveBuilds.handler(config, [
|
||||
'@pnpm.e2e/pre-and-postinstall-scripts-example',
|
||||
'!@pnpm.e2e/install-script-example',
|
||||
], {})
|
||||
|
||||
expect(prompt).not.toHaveBeenCalled()
|
||||
expect(mockCheckbox).not.toHaveBeenCalled()
|
||||
expect(mockConfirm).not.toHaveBeenCalled()
|
||||
|
||||
const workspaceManifest = readYamlFileSync<any>(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line
|
||||
expect(workspaceManifest.allowBuilds).toStrictEqual({
|
||||
@@ -290,12 +301,14 @@ test('deny-only via !pkg keeps other builds pending', async () => {
|
||||
await execPnpmInstall()
|
||||
const config = await getApproveBuildsConfig()
|
||||
|
||||
prompt.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
await approveBuilds.handler(config, [
|
||||
'!@pnpm.e2e/install-script-example',
|
||||
], {})
|
||||
|
||||
expect(prompt).not.toHaveBeenCalled()
|
||||
expect(mockCheckbox).not.toHaveBeenCalled()
|
||||
expect(mockConfirm).not.toHaveBeenCalled()
|
||||
|
||||
const workspaceManifest = readYamlFileSync<any>(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line
|
||||
expect(workspaceManifest.allowBuilds).toStrictEqual({
|
||||
@@ -439,10 +452,8 @@ test('should retain existing allowBuilds entries when approving builds', async (
|
||||
})
|
||||
|
||||
const config = await getApproveBuildsConfig()
|
||||
prompt.mockResolvedValueOnce({
|
||||
result: [{ value: '@pnpm.e2e/pre-and-postinstall-scripts-example' }],
|
||||
})
|
||||
prompt.mockResolvedValueOnce({ build: true })
|
||||
mockCheckbox.mockResolvedValueOnce(['@pnpm.e2e/pre-and-postinstall-scripts-example'])
|
||||
mockConfirm.mockResolvedValueOnce(true)
|
||||
await approveBuilds.handler({
|
||||
...config,
|
||||
workspaceDir: temp,
|
||||
@@ -492,10 +503,8 @@ test('GVS approve-builds writes settings to globalPkgDir without scanning siblin
|
||||
await execPnpmInstall({ enableGlobalVirtualStore: true })
|
||||
|
||||
const config = await getApproveBuildsConfig()
|
||||
prompt.mockResolvedValueOnce({
|
||||
result: [{ value: '@pnpm.e2e/pre-and-postinstall-scripts-example' }],
|
||||
})
|
||||
prompt.mockResolvedValueOnce({ build: true })
|
||||
mockCheckbox.mockResolvedValueOnce(['@pnpm.e2e/pre-and-postinstall-scripts-example'])
|
||||
mockConfirm.mockResolvedValueOnce(true)
|
||||
|
||||
// Match the global-install call site: settingsDir points at the global
|
||||
// packages dir (for writeSettings) but workspaceDir is not set, so install
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { packageManager } from '@pnpm/cli.meta'
|
||||
|
||||
export * from './packageIsInstallable.js'
|
||||
export * from './promptPageSize.js'
|
||||
export * from './readDepNameCompletions.js'
|
||||
export * from './readProjectManifest.js'
|
||||
export * from './recursiveSummary.js'
|
||||
|
||||
4
cli/utils/src/promptPageSize.ts
Normal file
4
cli/utils/src/promptPageSize.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function interactivePromptPageSize (): number {
|
||||
const availableRows = process.stdout.rows
|
||||
return availableRows == null ? 7 : Math.max(7, availableRows - 6)
|
||||
}
|
||||
2
deps/compliance/commands/package.json
vendored
2
deps/compliance/commands/package.json
vendored
@@ -35,6 +35,7 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/cli.command": "workspace:*",
|
||||
"@pnpm/cli.common-cli-options-help": "workspace:*",
|
||||
"@pnpm/cli.meta": "workspace:*",
|
||||
@@ -61,7 +62,6 @@
|
||||
"@pnpm/workspace.project-manifest-reader": "workspace:*",
|
||||
"@zkochan/table": "catalog:",
|
||||
"chalk": "catalog:",
|
||||
"enquirer": "catalog:",
|
||||
"memoize": "catalog:",
|
||||
"ramda": "catalog:",
|
||||
"render-help": "catalog:",
|
||||
|
||||
110
deps/compliance/commands/src/audit/audit.ts
vendored
110
deps/compliance/commands/src/audit/audit.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { docsUrl, TABLE_OPTIONS } from '@pnpm/cli.utils'
|
||||
import { checkbox, Separator } from '@inquirer/prompts'
|
||||
import { docsUrl, interactivePromptPageSize, TABLE_OPTIONS } from '@pnpm/cli.utils'
|
||||
import { type Config, type ConfigContext, types as allTypes, type UniversalOptions } from '@pnpm/config.reader'
|
||||
import { audit, type AuditAdvisory, type AuditLevelNumber, type AuditLevelString, type AuditReport, type AuditVulnerabilityCounts, type IgnoredAuditVulnerabilityCounts, normalizeGhsaId } from '@pnpm/deps.compliance.audit'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
@@ -8,14 +9,13 @@ import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
|
||||
import type { Registries } from '@pnpm/types'
|
||||
import { table } from '@zkochan/table'
|
||||
import chalk, { type ChalkInstance } from 'chalk'
|
||||
import enquirer from 'enquirer'
|
||||
import { pick, pickBy } from 'ramda'
|
||||
import { renderHelp } from 'render-help'
|
||||
|
||||
import { createAuditNetworkOptions, loadAuditContext } from './auditContext.js'
|
||||
import { fix } from './fix.js'
|
||||
import { fixWithUpdate, type FixWithUpdateResult } from './fixWithUpdate.js'
|
||||
import { type AuditChoiceRow, getAuditFixChoices } from './getAuditFixChoices.js'
|
||||
import { getAuditFixChoices } from './getAuditFixChoices.js'
|
||||
import { ignore } from './ignore.js'
|
||||
import { auditSignatures } from './signatures.js'
|
||||
|
||||
@@ -463,71 +463,67 @@ function filterAdvisoriesForFix (
|
||||
}
|
||||
|
||||
async function interactiveAuditFix (auditReport: AuditReport): Promise<AuditReport> {
|
||||
const choices = getAuditFixChoices(Object.values(auditReport.advisories))
|
||||
if (choices.length === 0) {
|
||||
const choiceGroups = getAuditFixChoices(Object.values(auditReport.advisories))
|
||||
if (choiceGroups.length === 0) {
|
||||
return auditReport
|
||||
}
|
||||
const { selectedVulnerabilities } = await enquirer.prompt({
|
||||
choices,
|
||||
footer: '\nEnter to start fixing. Ctrl-c to cancel.',
|
||||
indicator (state: any, choice: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
return ` ${choice.enabled ? '●' : '○'}`
|
||||
},
|
||||
message: 'Choose which vulnerabilities to fix ' +
|
||||
`(Press ${chalk.cyan('<space>')} to select, ` +
|
||||
`${chalk.cyan('<a>')} to toggle all, ` +
|
||||
`${chalk.cyan('<i>')} to invert selection)`,
|
||||
name: 'selectedVulnerabilities',
|
||||
pointer: '❯',
|
||||
result () {
|
||||
return this.selected
|
||||
},
|
||||
format () {
|
||||
if (!this.state.submitted || this.state.cancelled) return ''
|
||||
|
||||
if (Array.isArray(this.selected)) {
|
||||
return this.selected
|
||||
.filter((choice: AuditChoiceRow) => !/^\[.+\]$/.test(choice.name))
|
||||
.map((choice: AuditChoiceRow) => this.styles.primary(choice.name)).join(', ')
|
||||
const flatChoices: Array<Separator | { name: string; value: string; disabled?: boolean | string }> = []
|
||||
for (const group of choiceGroups) {
|
||||
flatChoices.push(new Separator(chalk.bold(`── ${group.message} ──`)))
|
||||
for (const choice of group.choices) {
|
||||
if (choice.disabled) {
|
||||
flatChoices.push(new Separator(` ${choice.message ?? choice.name}`))
|
||||
} else {
|
||||
flatChoices.push({
|
||||
name: choice.message,
|
||||
value: choice.value,
|
||||
})
|
||||
}
|
||||
return this.styles.primary(this.selected.name)
|
||||
},
|
||||
styles: {
|
||||
dark: chalk.reset,
|
||||
em: chalk.bgBlack.whiteBright,
|
||||
success: chalk.reset,
|
||||
},
|
||||
type: 'multiselect',
|
||||
validate (value: string[]) {
|
||||
if (value.length === 0) {
|
||||
return 'You must choose at least one vulnerability.'
|
||||
}
|
||||
return true
|
||||
},
|
||||
j () {
|
||||
return this.down()
|
||||
},
|
||||
k () {
|
||||
return this.up()
|
||||
},
|
||||
cancel () {
|
||||
// By default, canceling the prompt via Ctrl+c throws an empty string.
|
||||
// The custom cancel function prevents that behavior.
|
||||
// Otherwise, pnpm CLI would print an error and confuse users.
|
||||
// See related issue: https://github.com/enquirer/enquirer/issues/225
|
||||
}
|
||||
}
|
||||
|
||||
const message = 'Choose which vulnerabilities to fix ' +
|
||||
`(Press ${chalk.cyan('<space>')} to select, ` +
|
||||
`${chalk.cyan('<a>')} to toggle all, ` +
|
||||
`${chalk.cyan('<i>')} to invert selection)\n\nEnter to start fixing. Ctrl-c to cancel.`
|
||||
let selectedKeys: string[]
|
||||
try {
|
||||
selectedKeys = await checkbox({
|
||||
choices: flatChoices,
|
||||
pageSize: interactivePromptPageSize(),
|
||||
message,
|
||||
required: true,
|
||||
validate: (values) => {
|
||||
if (values.length === 0) {
|
||||
return 'You must choose at least one vulnerability.'
|
||||
}
|
||||
return true
|
||||
},
|
||||
theme: {
|
||||
icon: { checked: '●', unchecked: '○', cursor: '❯' },
|
||||
style: {
|
||||
highlight: (text: string) => text,
|
||||
},
|
||||
keybindings: ['vim'],
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
globalInfo('Audit fix canceled')
|
||||
process.exit(0)
|
||||
},
|
||||
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const selectedKeys = new Set(
|
||||
(selectedVulnerabilities as AuditChoiceRow[]).map((c) => c.value)
|
||||
)
|
||||
const selectedKeySet = new Set(selectedKeys)
|
||||
const selectedAdvisories = Object.fromEntries(
|
||||
Object.entries(auditReport.advisories)
|
||||
.filter(([, advisory]) =>
|
||||
selectedKeys.has(`${advisory.module_name}@${advisory.vulnerable_versions}`)
|
||||
selectedKeySet.has(`${advisory.module_name}@${advisory.vulnerable_versions}`)
|
||||
)
|
||||
)
|
||||
return { ...auditReport, advisories: selectedAdvisories }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ const COLUMN_HEADER = ['Package', 'Vulnerable', 'Patched', 'Advisories']
|
||||
export interface AuditChoiceRow {
|
||||
name: string
|
||||
value: string
|
||||
message: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
@@ -89,6 +90,7 @@ export function getAuditFixChoices (advisories: AuditAdvisory[]): AuditChoiceGro
|
||||
if (i === 0) {
|
||||
return {
|
||||
name: rendered[i],
|
||||
message: rendered[i],
|
||||
value: '',
|
||||
disabled: true,
|
||||
hint: '',
|
||||
|
||||
@@ -9,11 +9,27 @@ import { readYamlFileSync } from 'read-yaml-file'
|
||||
import { AUDIT_REGISTRY, AUDIT_REGISTRY_OPTS } from './utils/options.js'
|
||||
import * as responses from './utils/responses/index.js'
|
||||
|
||||
jest.unstable_mockModule('enquirer', () => ({ default: { prompt: jest.fn() } }))
|
||||
const { default: enquirer } = await import('enquirer')
|
||||
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 { audit } = await import('@pnpm/deps.compliance.commands')
|
||||
|
||||
const prompt = jest.mocked(enquirer.prompt)
|
||||
const mockCheckbox = jest.mocked(checkbox)
|
||||
|
||||
const f = fixtures(import.meta.dirname)
|
||||
|
||||
@@ -23,7 +39,7 @@ beforeEach(async () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await teardownMockAgent()
|
||||
prompt.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
})
|
||||
|
||||
test('audit --fix -i shows interactive prompt and only fixes selected vulnerabilities', async () => {
|
||||
@@ -33,15 +49,7 @@ test('audit --fix -i shows interactive prompt and only fixes selected vulnerabil
|
||||
.intercept({ path: '/-/npm/v1/security/advisories/bulk', method: 'POST' })
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
// Mock the user selecting only the xmlhttprequest-ssl critical advisory
|
||||
prompt.mockResolvedValue({
|
||||
selectedVulnerabilities: [
|
||||
{
|
||||
value: 'xmlhttprequest-ssl@<1.6.1',
|
||||
name: 'xmlhttprequest-ssl@<1.6.1',
|
||||
},
|
||||
],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue(['xmlhttprequest-ssl@<1.6.1'])
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
@@ -74,14 +82,7 @@ test('audit --fix -i prompt is called with correct structure', async () => {
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
// Mock selecting one advisory so the fix proceeds
|
||||
prompt.mockResolvedValue({
|
||||
selectedVulnerabilities: [
|
||||
{
|
||||
value: 'xmlhttprequest-ssl@<1.6.1',
|
||||
name: 'xmlhttprequest-ssl@<1.6.1',
|
||||
},
|
||||
],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue(['xmlhttprequest-ssl@<1.6.1'])
|
||||
|
||||
await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
@@ -91,26 +92,29 @@ test('audit --fix -i prompt is called with correct structure', async () => {
|
||||
interactive: true,
|
||||
})
|
||||
|
||||
expect(prompt).toHaveBeenCalledWith(
|
||||
expect(mockCheckbox).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
footer: '\nEnter to start fixing. Ctrl-c to cancel.',
|
||||
message:
|
||||
'Choose which vulnerabilities to fix ' +
|
||||
`(Press ${chalk.cyan('<space>')} to select, ` +
|
||||
`${chalk.cyan('<a>')} to toggle all, ` +
|
||||
`${chalk.cyan('<i>')} to invert selection)`,
|
||||
name: 'selectedVulnerabilities',
|
||||
type: 'multiselect',
|
||||
`${chalk.cyan('<i>')} to invert selection)` +
|
||||
'\n\nEnter to start fixing. Ctrl-c to cancel.',
|
||||
pageSize: process.stdout.rows == null ? 7 : Math.max(7, process.stdout.rows - 6),
|
||||
})
|
||||
)
|
||||
|
||||
// Verify choices are grouped by severity
|
||||
const choices = (prompt.mock.calls[0][0] as unknown as Record<string, unknown>).choices as Array<{ name: string }>
|
||||
const groupNames = choices.map((g) => g.name)
|
||||
// Should have severity groups (order: critical, high, moderate, low)
|
||||
expect(groupNames[0]).toBe('[critical]')
|
||||
expect(groupNames[1]).toBe('[high]')
|
||||
expect(groupNames[2]).toBe('[moderate]')
|
||||
const callArgs = mockCheckbox.mock.calls[0][0]
|
||||
expect((callArgs.theme?.style?.highlight as (str: string) => string)?.('focused row')).toBe('focused row')
|
||||
const choices = callArgs.choices as Array<{ type?: string; name?: string; value?: string }>
|
||||
|
||||
const separatorNames = choices
|
||||
.filter((c) => c instanceof Separator || c.type === 'separator')
|
||||
.map((c) => c instanceof Separator ? c.separator : String(c))
|
||||
|
||||
expect(separatorNames.some((s: string) => s.includes('critical'))).toBe(true)
|
||||
expect(separatorNames.some((s: string) => s.includes('high'))).toBe(true)
|
||||
expect(separatorNames.some((s: string) => s.includes('moderate'))).toBe(true)
|
||||
})
|
||||
|
||||
test('audit --fix -i collapses advisories that share module_name@vulnerable_versions', async () => {
|
||||
@@ -120,11 +124,7 @@ test('audit --fix -i collapses advisories that share module_name@vulnerable_vers
|
||||
.intercept({ path: '/-/npm/v1/security/advisories/bulk', method: 'POST' })
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
prompt.mockResolvedValue({
|
||||
selectedVulnerabilities: [
|
||||
{ value: 'minimatch@<3.1.3', name: 'minimatch@<3.1.3' },
|
||||
],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue(['minimatch@<3.1.3'])
|
||||
|
||||
await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
@@ -134,14 +134,12 @@ test('audit --fix -i collapses advisories that share module_name@vulnerable_vers
|
||||
interactive: true,
|
||||
})
|
||||
|
||||
// The mock fixture has 2 distinct advisories for minimatch@<3.1.3 with
|
||||
// different GHSA IDs; they must render as a single interactive choice
|
||||
// whose rendered row lists both GHSA IDs.
|
||||
const choices = (prompt.mock.calls[0][0] as unknown as Record<string, unknown>).choices as Array<{ choices: Array<{ value: string, message?: string }> }>
|
||||
const allRows = choices.flatMap((g) => g.choices)
|
||||
const minimatchRows = allRows.filter((c) => c.value === 'minimatch@<3.1.3')
|
||||
const callArgs = mockCheckbox.mock.calls[0][0]
|
||||
const choices = callArgs.choices as Array<Record<string, unknown>>
|
||||
const valueChoices = choices.filter((c) => 'value' in c)
|
||||
const minimatchRows = valueChoices.filter((c) => c.value === 'minimatch@<3.1.3')
|
||||
expect(minimatchRows).toHaveLength(1)
|
||||
expect(minimatchRows[0].message).toMatch(/GHSA-3ppc-4f35-3m26.*GHSA-7r86-[a-z0-9-]+/)
|
||||
expect(String(minimatchRows[0].name)).toMatch(/GHSA-3ppc-4f35-3m26.*GHSA-7r86-[a-z0-9-]+/)
|
||||
})
|
||||
|
||||
test('audit --fix -i with auditLevel filters before showing prompt', async () => {
|
||||
@@ -151,14 +149,7 @@ test('audit --fix -i with auditLevel filters before showing prompt', async () =>
|
||||
.intercept({ path: '/-/npm/v1/security/advisories/bulk', method: 'POST' })
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
prompt.mockResolvedValue({
|
||||
selectedVulnerabilities: [
|
||||
{
|
||||
value: 'xmlhttprequest-ssl@<1.6.1',
|
||||
name: 'xmlhttprequest-ssl@<1.6.1',
|
||||
},
|
||||
],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue(['xmlhttprequest-ssl@<1.6.1'])
|
||||
|
||||
await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
@@ -169,8 +160,11 @@ test('audit --fix -i with auditLevel filters before showing prompt', async () =>
|
||||
interactive: true,
|
||||
})
|
||||
|
||||
// Verify only critical severity group is shown
|
||||
const choices = (prompt.mock.calls[0][0] as unknown as Record<string, unknown>).choices as Array<{ name: string }>
|
||||
const groupNames = choices.map((g) => g.name)
|
||||
expect(groupNames).toStrictEqual(['[critical]'])
|
||||
const callArgs = mockCheckbox.mock.calls[0][0]
|
||||
const choices = callArgs.choices as Array<Record<string, unknown>>
|
||||
const separatorNames = choices
|
||||
.filter((c) => c instanceof Separator || c.type === 'separator')
|
||||
.map((c) => c instanceof Separator ? c.separator : String(c))
|
||||
expect(separatorNames.filter((s: string) => s.includes('critical') || s.includes('high') || s.includes('moderate') || s.includes('low'))).toHaveLength(1)
|
||||
expect(separatorNames.some((s: string) => s.includes('critical'))).toBe(true)
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/bins.resolver": "workspace:*",
|
||||
"@pnpm/building.commands": "workspace:*",
|
||||
"@pnpm/catalogs.resolver": "workspace:*",
|
||||
@@ -62,7 +63,6 @@
|
||||
"@pnpm/workspace.projects-sorter": "workspace:*",
|
||||
"@zkochan/rimraf": "catalog:",
|
||||
"didyoumean2": "catalog:",
|
||||
"enquirer": "catalog:",
|
||||
"execa": "catalog:",
|
||||
"p-limit": "catalog:",
|
||||
"ramda": "catalog:",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { confirm } from '@inquirer/prompts'
|
||||
import type { Config, VerifyDepsBeforeRun } from '@pnpm/config.reader'
|
||||
import { checkDepsStatus, type CheckDepsStatusOptions, type WorkspaceStateSettings } from '@pnpm/deps.status'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { runPnpmCli } from '@pnpm/exec.pnpm-cli-runner'
|
||||
import { globalWarn } from '@pnpm/logger'
|
||||
import enquirer from 'enquirer'
|
||||
|
||||
export interface RunDepsStatusCheckOptions extends CheckDepsStatusOptions {
|
||||
dir: string
|
||||
@@ -35,22 +35,21 @@ export async function runDepsStatusCheck (opts: RunDepsStatusCheckOptions): Prom
|
||||
hint: 'Run "pnpm install" before running scripts. The "verifyDepsBeforeRun: prompt" setting cannot prompt for confirmation in non-interactive environments.',
|
||||
})
|
||||
}
|
||||
let confirmed: { runInstall: boolean }
|
||||
let confirmed: boolean
|
||||
try {
|
||||
confirmed = await enquirer.prompt<{ runInstall: boolean }>({
|
||||
type: 'confirm',
|
||||
name: 'runInstall',
|
||||
confirmed = await confirm({
|
||||
message: `Your "node_modules" directory is out of sync with the "pnpm-lock.yaml" file. This can lead to issues during scripts execution.
|
||||
|
||||
Would you like to run "pnpm ${command.join(' ')}" to update your "node_modules"?`,
|
||||
initial: true,
|
||||
default: true,
|
||||
})
|
||||
} catch {
|
||||
// User cancelled the prompt (e.g. Ctrl+C) — exit immediately
|
||||
// so the caller doesn't proceed to run the script.
|
||||
process.exit(1)
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
process.exit(1)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
if (confirmed.runInstall) {
|
||||
if (confirmed) {
|
||||
install()
|
||||
}
|
||||
break
|
||||
|
||||
@@ -15,16 +15,30 @@ jest.unstable_mockModule('@pnpm/logger', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.unstable_mockModule('enquirer', () => ({
|
||||
default: {
|
||||
prompt: jest.fn(),
|
||||
},
|
||||
}))
|
||||
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 { run } = await import('@pnpm/exec.commands')
|
||||
const { default: enquirer } = await import('enquirer')
|
||||
const { confirm } = await import('@inquirer/prompts')
|
||||
const { globalWarn } = await import('@pnpm/logger')
|
||||
|
||||
const mockConfirm = jest.mocked(confirm)
|
||||
|
||||
const rootProjectManifest = {
|
||||
name: 'root',
|
||||
private: true,
|
||||
@@ -84,8 +98,7 @@ test('log a warning if verifyDepsBeforeRun is set to warn', async () => {
|
||||
test('prompt the user if verifyDepsBeforeRun is set to prompt', async () => {
|
||||
prepare(rootProjectManifest)
|
||||
|
||||
// Mock the user confirming the prompt
|
||||
jest.mocked(enquirer.prompt).mockResolvedValue({ runInstall: true })
|
||||
mockConfirm.mockResolvedValue(true)
|
||||
|
||||
const originalIsTTY = process.stdin.isTTY
|
||||
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true })
|
||||
@@ -96,14 +109,14 @@ test('prompt the user if verifyDepsBeforeRun is set to prompt', async () => {
|
||||
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true })
|
||||
}
|
||||
|
||||
expect(enquirer.prompt).toHaveBeenCalledWith({
|
||||
type: 'confirm',
|
||||
name: 'runInstall',
|
||||
message: expect.stringContaining(
|
||||
'Your "node_modules" directory is out of sync with the "pnpm-lock.yaml" file'
|
||||
),
|
||||
initial: true,
|
||||
})
|
||||
expect(mockConfirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining(
|
||||
'Your "node_modules" directory is out of sync with the "pnpm-lock.yaml" file'
|
||||
),
|
||||
default: true,
|
||||
})
|
||||
)
|
||||
|
||||
expect(fs.existsSync(path.resolve('node_modules'))).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/building.after-install": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/cli.command": "workspace:*",
|
||||
@@ -82,7 +83,6 @@
|
||||
"@zkochan/table": "catalog:",
|
||||
"chalk": "catalog:",
|
||||
"ci-info": "catalog:",
|
||||
"enquirer": "catalog:",
|
||||
"get-npm-tarball-url": "catalog:",
|
||||
"is-subdir": "catalog:",
|
||||
"load-json-file": "catalog:",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { confirm } from '@inquirer/prompts'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { globalInfo } from '@pnpm/logger'
|
||||
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from '@pnpm/resolving.npm-resolver'
|
||||
import { isCI } from 'ci-info'
|
||||
import enquirer from 'enquirer'
|
||||
|
||||
/**
|
||||
* Shape returned by `installing/deps-installer`'s
|
||||
@@ -241,13 +241,17 @@ async function promptForApproval (immature: readonly PolicyViolation[]): Promise
|
||||
`${sorted.length} ${sorted.length === 1 ? 'version does' : 'versions do'} not meet the minimumReleaseAge constraint:\n` +
|
||||
sorted.map((v) => ` ${v.name}@${v.version}`).join('\n') + '\n' +
|
||||
'Add to minimumReleaseAgeExclude in pnpm-workspace.yaml and proceed with the install?'
|
||||
const answer = await enquirer.prompt<{ confirmed: boolean }>({
|
||||
type: 'confirm',
|
||||
name: 'confirmed',
|
||||
message,
|
||||
initial: false,
|
||||
})
|
||||
if (!answer.confirmed) {
|
||||
let confirmed: boolean
|
||||
try {
|
||||
confirmed = await confirm({ message, default: false })
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
confirmed = false
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
if (!confirmed) {
|
||||
throw new PnpmError(
|
||||
'MINIMUM_RELEASE_AGE_DENIED',
|
||||
'Aborted: the immature versions were not approved.',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { and, groupBy, isEmpty, pickBy, pipe, pluck, uniqBy } from 'ramda'
|
||||
export interface ChoiceRow {
|
||||
name: string
|
||||
value: string
|
||||
message: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
@@ -71,6 +72,7 @@ export function getUpdateChoices (outdatedPkgsOfProjects: OutdatedPackage[], wor
|
||||
if (i === 0) {
|
||||
return {
|
||||
name: renderedTable[i],
|
||||
message: renderedTable[i],
|
||||
value: '',
|
||||
disabled: true,
|
||||
hint: '',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { checkbox, Separator } from '@inquirer/prompts'
|
||||
import type { CommandHandler, CommandHandlerMap, CompletionFunc } from '@pnpm/cli.command'
|
||||
import { FILTERING, OPTIONS, UNIVERSAL_OPTIONS } from '@pnpm/cli.common-cli-options-help'
|
||||
import {
|
||||
docsUrl,
|
||||
interactivePromptPageSize,
|
||||
readDepNameCompletions,
|
||||
readProjectManifestOnly,
|
||||
} from '@pnpm/cli.utils'
|
||||
@@ -14,15 +16,14 @@ import type { UpdateMatchingFunction } from '@pnpm/installing.deps-installer'
|
||||
import { globalInfo } from '@pnpm/logger'
|
||||
import type { IncludedDependencies, PackageVulnerabilityAudit, ProjectRootDir } from '@pnpm/types'
|
||||
import chalk from 'chalk'
|
||||
import enquirer from 'enquirer'
|
||||
import { pick, pluck, unnest } from 'ramda'
|
||||
import { pick, unnest } from 'ramda'
|
||||
import { renderHelp } from 'render-help'
|
||||
|
||||
import type { InstallCommandOptions } from '../install.js'
|
||||
import { installDeps } from '../installDeps.js'
|
||||
import { parseUpdateParam } from '../recursive.js'
|
||||
import { createGlobalPolicyCallbacks } from '../resolutionPolicyManifest.js'
|
||||
import { type ChoiceRow, getUpdateChoices } from './getUpdateChoices.js'
|
||||
import { getUpdateChoices } from './getUpdateChoices.js'
|
||||
export function rcOptionsTypes (): Record<string, unknown> {
|
||||
return pick([
|
||||
'cache-dir',
|
||||
@@ -219,71 +220,62 @@ async function interactiveUpdate (
|
||||
timeout: opts.fetchTimeout,
|
||||
})
|
||||
const workspacesEnabled = !!opts.workspaceDir
|
||||
const choices = getUpdateChoices(unnest(outdatedPkgsOfProjects), workspacesEnabled)
|
||||
if (choices.length === 0) {
|
||||
const choiceGroups = getUpdateChoices(unnest(outdatedPkgsOfProjects), workspacesEnabled)
|
||||
if (choiceGroups.length === 0) {
|
||||
if (opts.latest) {
|
||||
return 'All of your dependencies are already up to date'
|
||||
}
|
||||
return 'All of your dependencies are already up to date inside the specified ranges. Use the --latest option to update the ranges in package.json'
|
||||
}
|
||||
const { updateDependencies } = await enquirer.prompt({
|
||||
choices,
|
||||
footer: '\nEnter to start updating. Ctrl-c to cancel.',
|
||||
indicator (state: any, choice: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
return ` ${choice.enabled ? '●' : '○'}`
|
||||
},
|
||||
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',
|
||||
pointer: '❯',
|
||||
result () {
|
||||
return this.selected
|
||||
},
|
||||
format () {
|
||||
if (!this.state.submitted || this.state.cancelled) return ''
|
||||
|
||||
if (Array.isArray(this.selected)) {
|
||||
return this.selected
|
||||
// The custom format function is used to filter out "[dependencies]" or "[devDependencies]" from the output.
|
||||
// https://github.com/enquirer/enquirer/blob/master/lib/prompts/select.js#L98
|
||||
.filter((choice: ChoiceRow) => !/^\[.+\]$/.test(choice.name))
|
||||
.map((choice: ChoiceRow) => this.styles.primary(choice.name)).join(', ')
|
||||
const flatChoices: Array<Separator | { name: string; value: string; disabled?: boolean | string }> = []
|
||||
for (const group of choiceGroups) {
|
||||
flatChoices.push(new Separator(chalk.bold(`── ${group.message} ──`)))
|
||||
for (const choice of group.choices) {
|
||||
if (choice.disabled) {
|
||||
flatChoices.push(new Separator(` ${choice.message ?? choice.name}`))
|
||||
} else {
|
||||
flatChoices.push({
|
||||
name: choice.message,
|
||||
value: choice.value,
|
||||
})
|
||||
}
|
||||
return this.styles.primary(this.selected.name)
|
||||
},
|
||||
styles: {
|
||||
dark: chalk.reset,
|
||||
em: chalk.bgBlack.whiteBright,
|
||||
success: chalk.reset,
|
||||
},
|
||||
type: 'multiselect',
|
||||
validate (value: string[]) {
|
||||
if (value.length === 0) {
|
||||
return 'You must choose at least one package.'
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// For Vim users (related: https://github.com/enquirer/enquirer/pull/163)
|
||||
j () {
|
||||
return this.down()
|
||||
},
|
||||
k () {
|
||||
return this.up()
|
||||
},
|
||||
cancel () {
|
||||
// By default, canceling the prompt via Ctrl+c throws an empty string.
|
||||
// The custom cancel function prevents that behavior.
|
||||
// Otherwise, pnpm CLI would print an error and confuse users.
|
||||
// See related issue: https://github.com/enquirer/enquirer/issues/225
|
||||
const 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.`
|
||||
let updatePkgNames: string[]
|
||||
try {
|
||||
updatePkgNames = await checkbox({
|
||||
choices: flatChoices,
|
||||
pageSize: interactivePromptPageSize(),
|
||||
message,
|
||||
required: true,
|
||||
validate: (values) => {
|
||||
if (values.length === 0) {
|
||||
return 'You must choose at least one package.'
|
||||
}
|
||||
return true
|
||||
},
|
||||
theme: {
|
||||
icon: { checked: '●', unchecked: '○', cursor: '❯' },
|
||||
style: {
|
||||
highlight: (text: string) => text,
|
||||
},
|
||||
keybindings: ['vim'],
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
globalInfo('Update canceled')
|
||||
process.exit(0)
|
||||
},
|
||||
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const updatePkgNames = pluck('value', updateDependencies as ChoiceRow[])
|
||||
return update(updatePkgNames, opts, rebuildHandler) as Promise<undefined>
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ test('getUpdateChoices()', () => {
|
||||
choices: [
|
||||
{
|
||||
name: 'Package Current Target URL ',
|
||||
message: 'Package Current Target URL ',
|
||||
disabled: true,
|
||||
hint: '',
|
||||
value: '',
|
||||
@@ -103,6 +104,7 @@ test('getUpdateChoices()', () => {
|
||||
choices: [
|
||||
{
|
||||
name: 'Package Current Target URL ',
|
||||
message: 'Package Current Target URL ',
|
||||
disabled: true,
|
||||
hint: '',
|
||||
value: '',
|
||||
@@ -130,6 +132,7 @@ test('getUpdateChoices()', () => {
|
||||
choices: [
|
||||
{
|
||||
name: 'Package Current Target URL ',
|
||||
message: 'Package Current Target URL ',
|
||||
disabled: true,
|
||||
hint: '',
|
||||
value: '',
|
||||
|
||||
@@ -9,11 +9,27 @@ import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects
|
||||
import chalk from 'chalk'
|
||||
import { readYamlFileSync } from 'read-yaml-file'
|
||||
|
||||
jest.unstable_mockModule('enquirer', () => ({ default: { prompt: jest.fn() } }))
|
||||
const { default: enquirer } = await import('enquirer')
|
||||
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 prompt = jest.mocked(enquirer.prompt)
|
||||
const mockCheckbox = jest.mocked(checkbox)
|
||||
|
||||
const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`
|
||||
|
||||
@@ -61,13 +77,6 @@ test('interactively update', async () => {
|
||||
|
||||
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' }),
|
||||
@@ -85,16 +94,9 @@ test('interactively update', async () => {
|
||||
['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/ `,
|
||||
},
|
||||
],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue(['is-negative'])
|
||||
|
||||
prompt.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
// Update to compatible versions
|
||||
await update.handler({
|
||||
...DEFAULT_OPTIONS,
|
||||
@@ -106,37 +108,32 @@ test('interactively update', async () => {
|
||||
})
|
||||
|
||||
// eslint-disable-next-line
|
||||
expect((prompt.mock.calls[0][0] as any).choices).toStrictEqual([
|
||||
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 '),
|
||||
{
|
||||
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',
|
||||
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(prompt).toHaveBeenCalledWith(
|
||||
expect(mockCheckbox).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',
|
||||
`${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()
|
||||
@@ -147,7 +144,8 @@ test('interactively update', async () => {
|
||||
}
|
||||
|
||||
// Update to latest versions
|
||||
prompt.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
mockCheckbox.mockResolvedValue(['is-negative'])
|
||||
await update.handler({
|
||||
...DEFAULT_OPTIONS,
|
||||
cacheDir: path.resolve('cache'),
|
||||
@@ -159,40 +157,32 @@ test('interactively update', async () => {
|
||||
})
|
||||
|
||||
// eslint-disable-next-line
|
||||
expect((prompt.mock.calls[0][0] as any).choices).toStrictEqual([
|
||||
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 '),
|
||||
{
|
||||
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',
|
||||
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(prompt).toHaveBeenCalledWith(
|
||||
expect(mockCheckbox).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',
|
||||
`${chalk.cyan('<i>')} to invert selection)\n\nEnter to start updating. Ctrl-c to cancel.`,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -224,14 +214,7 @@ test('interactive update of dev dependencies only', async () => {
|
||||
])
|
||||
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/ `,
|
||||
},
|
||||
],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue(['is-negative'])
|
||||
|
||||
const { allProjects, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(
|
||||
process.cwd(),
|
||||
@@ -303,11 +286,9 @@ test('interactively update should ignore dependencies from the ignoreDependencie
|
||||
['is-negative@1.0.0', 'is-positive@2.0.0', 'micromatch@3.0.0']
|
||||
)
|
||||
|
||||
prompt.mockResolvedValue({
|
||||
updateDependencies: [{ value: 'micromatch', name: 'anything' }],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue(['micromatch'])
|
||||
|
||||
prompt.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
await update.handler({
|
||||
...DEFAULT_OPTIONS,
|
||||
cacheDir: path.resolve('cache'),
|
||||
@@ -321,38 +302,27 @@ test('interactively update should ignore dependencies from the ignoreDependencie
|
||||
})
|
||||
|
||||
// eslint-disable-next-line
|
||||
expect((prompt.mock.calls[0][0] as any).choices as any).toStrictEqual(
|
||||
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 '),
|
||||
{
|
||||
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',
|
||||
name: `micromatch 3.0.0 ❯ 3.${chalk.yellowBright.bold('1.10')} `,
|
||||
value: 'micromatch',
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
expect(prompt).toHaveBeenCalledWith(
|
||||
expect(mockCheckbox).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',
|
||||
`${chalk.cyan('<i>')} to invert selection)\n\nEnter to start updating. Ctrl-c to cancel.`,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -5,12 +5,28 @@ import { preparePackages } from '@pnpm/prepare'
|
||||
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
|
||||
import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter'
|
||||
|
||||
jest.unstable_mockModule('enquirer', () => ({ default: { prompt: jest.fn() } }))
|
||||
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 { default: enquirer } = await import('enquirer')
|
||||
const { checkbox } = await import('@inquirer/prompts')
|
||||
const { update, install } = await import('@pnpm/installing.commands')
|
||||
|
||||
const prompt = jest.mocked(enquirer.prompt)
|
||||
const mockCheckbox = jest.mocked(checkbox)
|
||||
|
||||
const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`
|
||||
|
||||
@@ -60,9 +76,7 @@ test('interactive recursive should not error on git specifier override', async (
|
||||
},
|
||||
])
|
||||
|
||||
prompt.mockResolvedValue({
|
||||
updateDependencies: [],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue([])
|
||||
|
||||
const { allProjects, selectedProjectsGraph } = await filterProjectsBySelectorObjectsFromDir(process.cwd(), [])
|
||||
const sharedOptions = {
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
".test": "cross-env PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/agent.client": "workspace:*",
|
||||
"@pnpm/bins.linker": "workspace:*",
|
||||
"@pnpm/bins.remover": "workspace:*",
|
||||
@@ -109,7 +110,6 @@
|
||||
"@pnpm/util.lex-comparator": "catalog:",
|
||||
"@pnpm/workspace.project-manifest-reader": "workspace:*",
|
||||
"@zkochan/rimraf": "catalog:",
|
||||
"enquirer": "catalog:",
|
||||
"is-inner-link": "catalog:",
|
||||
"is-subdir": "catalog:",
|
||||
"load-json-file": "catalog:",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { confirm } from '@inquirer/prompts'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import type {
|
||||
IncludedDependencies,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
type Registries,
|
||||
} from '@pnpm/types'
|
||||
import { rimraf } from '@zkochan/rimraf'
|
||||
import enquirer from 'enquirer'
|
||||
import { pathAbsolute } from 'path-absolute'
|
||||
import { equals } from 'ramda'
|
||||
|
||||
@@ -155,15 +155,21 @@ async function purgeModulesDirsOfImporters (
|
||||
hint: 'If you are running pnpm in CI, set the CI environment variable to "true", or set "confirmModulesPurge" to "false".',
|
||||
})
|
||||
}
|
||||
const confirmed = await enquirer.prompt<{ question: boolean }>({
|
||||
type: 'confirm',
|
||||
name: 'question',
|
||||
message: importers.length === 1
|
||||
? `The modules directory at "${importers[0].modulesDir}" will be removed and reinstalled from scratch. Proceed?`
|
||||
: 'The modules directories will be removed and reinstalled from scratch. Proceed?',
|
||||
initial: true,
|
||||
})
|
||||
if (!confirmed.question) {
|
||||
let confirmed: boolean
|
||||
try {
|
||||
confirmed = await confirm({
|
||||
message: importers.length === 1
|
||||
? `The modules directory at "${importers[0].modulesDir}" will be removed and reinstalled from scratch. Proceed?`
|
||||
: 'The modules directories will be removed and reinstalled from scratch. Proceed?',
|
||||
default: true,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
throw new PnpmError('ABORTED_REMOVE_MODULES_DIR', 'Aborted removal of modules directory')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
if (!confirmed) {
|
||||
throw new PnpmError('ABORTED_REMOVE_MODULES_DIR', 'Aborted removal of modules directory')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ export {
|
||||
type OtpHandlingParams,
|
||||
OtpNonInteractiveError,
|
||||
type OtpProcess,
|
||||
type OtpPromptOptions,
|
||||
type OtpPromptResponse,
|
||||
OtpSecondChallengeError,
|
||||
SyntheticOtpError,
|
||||
withOtpHandling,
|
||||
|
||||
@@ -7,17 +7,7 @@ import type { PromptBrowserOpenReadlineInterface } from './promptBrowserOpen.js'
|
||||
import { promptBrowserOpen } from './promptBrowserOpen.js'
|
||||
|
||||
export interface OtpEnquirer {
|
||||
prompt: (options: OtpPromptOptions) => Promise<OtpPromptResponse | undefined>
|
||||
}
|
||||
|
||||
export interface OtpPromptOptions {
|
||||
message: string
|
||||
name: 'otp'
|
||||
type: 'input'
|
||||
}
|
||||
|
||||
export interface OtpPromptResponse {
|
||||
otp?: string
|
||||
input: (options: { message: string }) => Promise<string | undefined>
|
||||
}
|
||||
|
||||
interface OtpDate {
|
||||
@@ -113,14 +103,19 @@ export async function withOtpHandling<T> ({
|
||||
pollPromise,
|
||||
})
|
||||
} else {
|
||||
const enquirerResponse = await enquirer.prompt({
|
||||
message: 'This operation requires a one-time password.\nEnter OTP:',
|
||||
name: 'otp',
|
||||
type: 'input',
|
||||
})
|
||||
let otpValue: string | undefined
|
||||
try {
|
||||
otpValue = await enquirer.input({
|
||||
message: 'This operation requires a one-time password.\nEnter OTP:',
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
throw error
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
// Use || (not ??) so that empty-string input is treated as "no OTP provided"
|
||||
otp = enquirerResponse?.otp || undefined
|
||||
otp = otpValue || undefined
|
||||
}
|
||||
|
||||
if (otp != null) {
|
||||
|
||||
@@ -40,7 +40,7 @@ type MockContextOverrides = Omit<Partial<OtpContext>, 'process'> & {
|
||||
const createOtpMockContext = (overrides?: MockContextOverrides): OtpContext => ({
|
||||
Date: { now: () => 0 },
|
||||
setTimeout: (cb: () => void) => cb(),
|
||||
enquirer: { prompt: async () => ({ otp: '123456' }) },
|
||||
enquirer: { input: async () => '123456' },
|
||||
fetch: async () => createMockResponse({
|
||||
ok: false,
|
||||
status: 404,
|
||||
@@ -103,7 +103,7 @@ describe('withOtpHandling', () => {
|
||||
it('prompts for OTP and retries operation', async () => {
|
||||
let callCount = 0
|
||||
const context = createOtpMockContext({
|
||||
enquirer: { prompt: async () => ({ otp: '654321' }) },
|
||||
enquirer: { input: async () => '654321' },
|
||||
})
|
||||
const result = await withOtpHandling({
|
||||
context,
|
||||
@@ -149,7 +149,7 @@ describe('withOtpHandling', () => {
|
||||
|
||||
it('re-throws the original OTP error when enquirer returns no OTP', async () => {
|
||||
const context = createOtpMockContext({
|
||||
enquirer: { prompt: async () => ({ otp: '' }) },
|
||||
enquirer: { input: async () => '' },
|
||||
})
|
||||
await expect(withOtpHandling({
|
||||
context,
|
||||
@@ -162,7 +162,7 @@ describe('withOtpHandling', () => {
|
||||
|
||||
it('re-throws the original OTP error when enquirer returns undefined', async () => {
|
||||
const context = createOtpMockContext({
|
||||
enquirer: { prompt: async () => undefined },
|
||||
enquirer: { input: async () => undefined },
|
||||
})
|
||||
await expect(withOtpHandling({
|
||||
context,
|
||||
@@ -224,7 +224,7 @@ describe('withOtpHandling', () => {
|
||||
it('falls back to classic prompt when only authUrl is present (no doneUrl)', async () => {
|
||||
let callCount = 0
|
||||
const context = createOtpMockContext({
|
||||
enquirer: { prompt: async () => ({ otp: 'manual-code' }) },
|
||||
enquirer: { input: async () => 'manual-code' },
|
||||
})
|
||||
const result = await withOtpHandling({
|
||||
context,
|
||||
@@ -247,7 +247,7 @@ describe('withOtpHandling', () => {
|
||||
it('falls back to classic prompt when only doneUrl is present (no authUrl)', async () => {
|
||||
let callCount = 0
|
||||
const context = createOtpMockContext({
|
||||
enquirer: { prompt: async () => ({ otp: 'manual-code' }) },
|
||||
enquirer: { input: async () => 'manual-code' },
|
||||
})
|
||||
const result = await withOtpHandling({
|
||||
context,
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/cli.utils": "workspace:*",
|
||||
"@pnpm/config.reader": "workspace:*",
|
||||
"@pnpm/config.writer": "workspace:*",
|
||||
@@ -54,7 +55,6 @@
|
||||
"@pnpm/workspace.project-manifest-reader": "workspace:*",
|
||||
"@pnpm/workspace.workspace-manifest-reader": "workspace:*",
|
||||
"chalk": "catalog:",
|
||||
"enquirer": "catalog:",
|
||||
"escape-string-regexp": "catalog:",
|
||||
"is-windows": "catalog:",
|
||||
"make-empty-dir": "catalog:",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { confirm, select } from '@inquirer/prompts'
|
||||
import type { Config } from '@pnpm/config.reader'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { isGitHostedPkgUrl } from '@pnpm/fetching.pick-fetcher'
|
||||
import { readCurrentLockfile, type TarballResolution } from '@pnpm/lockfile.fs'
|
||||
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils'
|
||||
import { parseWantedDependency, type ParseWantedDependencyResult } from '@pnpm/resolving.parse-wanted-dependency'
|
||||
import enquirer from 'enquirer'
|
||||
import { realpathMissing } from 'realpath-missing'
|
||||
import semver from 'semver'
|
||||
|
||||
@@ -30,28 +30,27 @@ export async function getPatchedDependency (rawDependency: string, opts: GetPatc
|
||||
|
||||
dep.alias = dep.alias ?? rawDependency
|
||||
if (preferredVersions.length > 1) {
|
||||
const { version, applyToAll } = await enquirer.prompt<{
|
||||
version: string
|
||||
applyToAll: boolean
|
||||
}>([{
|
||||
type: 'select',
|
||||
name: 'version',
|
||||
message: 'Choose which version to patch',
|
||||
choices: preferredVersions.map(preferred => ({
|
||||
name: preferred.version,
|
||||
message: preferred.version,
|
||||
value: preferred.gitTarballUrl ?? preferred.version,
|
||||
hint: preferred.gitTarballUrl ? 'Git Hosted' : undefined,
|
||||
})),
|
||||
result (selected) {
|
||||
const selectedVersion = preferredVersions.find(preferred => preferred.version === selected)!
|
||||
return selectedVersion.gitTarballUrl ?? selected
|
||||
},
|
||||
}, {
|
||||
type: 'confirm',
|
||||
name: 'applyToAll',
|
||||
message: 'Apply this patch to all versions?',
|
||||
}])
|
||||
let version: string
|
||||
let applyToAll: boolean
|
||||
try {
|
||||
version = await select({
|
||||
message: 'Choose which version to patch',
|
||||
choices: preferredVersions.map(preferred => ({
|
||||
name: preferred.version,
|
||||
value: preferred.gitTarballUrl ?? preferred.version,
|
||||
description: preferred.gitTarballUrl ? 'Git Hosted' : undefined,
|
||||
})),
|
||||
theme: { keybindings: ['vim'] },
|
||||
})
|
||||
applyToAll = await confirm({
|
||||
message: 'Apply this patch to all versions?',
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
throw new PnpmError('PATCH_CANCELED', 'Canceled')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
return {
|
||||
...dep,
|
||||
applyToAll,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
import { checkbox } from '@inquirer/prompts'
|
||||
import { docsUrl } from '@pnpm/cli.utils'
|
||||
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { install } from '@pnpm/installing.commands'
|
||||
import enquirer from 'enquirer'
|
||||
import { pick } from 'ramda'
|
||||
import { renderHelp } from 'render-help'
|
||||
|
||||
@@ -40,17 +40,22 @@ export async function handler (opts: PatchRemoveCommandOptions, params: string[]
|
||||
if (!params.length) {
|
||||
const allPatches = Object.keys(patchedDependencies)
|
||||
if (allPatches.length) {
|
||||
({ patches: patchesToRemove } = await enquirer.prompt<{
|
||||
patches: string[]
|
||||
}>({
|
||||
type: 'multiselect',
|
||||
name: 'patches',
|
||||
message: 'Select the patch to be removed',
|
||||
choices: allPatches,
|
||||
validate (value) {
|
||||
return value.length === 0 ? 'Select at least one option.' : true
|
||||
},
|
||||
}))
|
||||
try {
|
||||
patchesToRemove = await checkbox({
|
||||
choices: allPatches.map((name) => ({ name, value: name })),
|
||||
message: 'Select the patch to be removed',
|
||||
required: true,
|
||||
validate: (values) => {
|
||||
return values.length === 0 ? 'Select at least one option.' : true
|
||||
},
|
||||
theme: { keybindings: ['vim'] },
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
throw new PnpmError('PATCH_REMOVE_CANCELED', 'Canceled')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,30 @@ import { writeYamlFileSync } from 'write-yaml-file'
|
||||
|
||||
import { DEFAULT_OPTS } from './utils/index.js'
|
||||
|
||||
jest.unstable_mockModule('enquirer', () => ({ default: { prompt: jest.fn() } }))
|
||||
const { default: enquirer } = await import('enquirer')
|
||||
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: mockCheckboxFn, select, confirm: confirmPrompt } = await import('@inquirer/prompts')
|
||||
const { patch, patchCommit, patchRemove } = await import('@pnpm/patching.commands')
|
||||
|
||||
const prompt = jest.mocked(enquirer.prompt)
|
||||
const mockSelect = jest.mocked(select)
|
||||
const mockConfirm = jest.mocked(confirmPrompt)
|
||||
const mockCheckbox = jest.mocked(mockCheckboxFn)
|
||||
const f = fixtures(import.meta.dirname)
|
||||
|
||||
const basePatchOption = {
|
||||
@@ -642,24 +661,23 @@ describe('multiple versions', () => {
|
||||
dir: process.cwd(),
|
||||
saveLockfile: true,
|
||||
})
|
||||
prompt.mockResolvedValue({
|
||||
version: '1.0.0',
|
||||
applyToAll: true,
|
||||
})
|
||||
prompt.mockClear()
|
||||
mockSelect.mockResolvedValue('1.0.0')
|
||||
mockConfirm.mockResolvedValue(true)
|
||||
mockSelect.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
const output = await patch.handler(defaultPatchOption, ['@pnpm.e2e/console-log'])
|
||||
|
||||
expect(prompt.mock.calls).toMatchObject([[[
|
||||
{
|
||||
type: 'select',
|
||||
name: 'version',
|
||||
choices: ['1.0.0', '2.0.0', '3.0.0'].map(x => ({ name: x, message: x, value: x })),
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'applyToAll',
|
||||
},
|
||||
]]] as unknown as Record<string, unknown>[])
|
||||
expect(mockSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Choose which version to patch',
|
||||
choices: ['1.0.0', '2.0.0', '3.0.0'].map(x => expect.objectContaining({ name: x, value: x })),
|
||||
})
|
||||
)
|
||||
expect(mockConfirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Apply this patch to all versions?',
|
||||
})
|
||||
)
|
||||
|
||||
const patchDir = getPatchDirFromPatchOutput(output)
|
||||
const fileToPatch = path.join(patchDir, 'index.js')
|
||||
@@ -737,35 +755,26 @@ describe('prompt to choose version', () => {
|
||||
dir: process.cwd(),
|
||||
saveLockfile: true,
|
||||
})
|
||||
prompt.mockResolvedValue({
|
||||
version: '5.3.0',
|
||||
applyToAll: false,
|
||||
})
|
||||
prompt.mockClear()
|
||||
mockSelect.mockResolvedValue('5.3.0')
|
||||
mockConfirm.mockResolvedValue(false)
|
||||
mockSelect.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
const output = await patch.handler(defaultPatchOption, ['chalk'])
|
||||
|
||||
expect(prompt.mock.calls).toMatchObject([[[
|
||||
{
|
||||
type: 'select',
|
||||
name: 'version',
|
||||
expect(mockSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Choose which version to patch',
|
||||
choices: [
|
||||
{
|
||||
name: '4.1.2',
|
||||
message: '4.1.2',
|
||||
value: '4.1.2',
|
||||
},
|
||||
{
|
||||
name: '5.3.0',
|
||||
message: '5.3.0',
|
||||
value: '5.3.0',
|
||||
},
|
||||
expect.objectContaining({ name: '4.1.2', value: '4.1.2' }),
|
||||
expect.objectContaining({ name: '5.3.0', value: '5.3.0' }),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'applyToAll',
|
||||
},
|
||||
]]] as unknown as Record<string, unknown>[])
|
||||
})
|
||||
)
|
||||
expect(mockConfirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Apply this patch to all versions?',
|
||||
})
|
||||
)
|
||||
|
||||
const patchDir = getPatchDirFromPatchOutput(output)
|
||||
|
||||
@@ -804,35 +813,26 @@ describe('prompt to choose version', () => {
|
||||
dir: process.cwd(),
|
||||
saveLockfile: true,
|
||||
})
|
||||
prompt.mockResolvedValue({
|
||||
version: '5.3.0',
|
||||
applyToAll: true,
|
||||
})
|
||||
prompt.mockClear()
|
||||
mockSelect.mockResolvedValue('5.3.0')
|
||||
mockConfirm.mockResolvedValue(true)
|
||||
mockSelect.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
const output = await patch.handler(defaultPatchOption, ['chalk'])
|
||||
|
||||
expect(prompt.mock.calls).toMatchObject([[[
|
||||
{
|
||||
type: 'select',
|
||||
name: 'version',
|
||||
expect(mockSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Choose which version to patch',
|
||||
choices: [
|
||||
{
|
||||
name: '4.1.2',
|
||||
message: '4.1.2',
|
||||
value: '4.1.2',
|
||||
},
|
||||
{
|
||||
name: '5.3.0',
|
||||
message: '5.3.0',
|
||||
value: '5.3.0',
|
||||
},
|
||||
expect.objectContaining({ name: '4.1.2', value: '4.1.2' }),
|
||||
expect.objectContaining({ name: '5.3.0', value: '5.3.0' }),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'applyToAll',
|
||||
},
|
||||
]]] as unknown as Record<string, unknown>[])
|
||||
})
|
||||
)
|
||||
expect(mockConfirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Apply this patch to all versions?',
|
||||
})
|
||||
)
|
||||
|
||||
const patchDir = getPatchDirFromPatchOutput(output)
|
||||
|
||||
@@ -1198,35 +1198,28 @@ describe('patch and commit in workspaces', () => {
|
||||
saveLockfile: true,
|
||||
})
|
||||
|
||||
prompt.mockResolvedValue({
|
||||
version: 'https://codeload.github.com/zkochan/hi/tar.gz/4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd',
|
||||
applyToAll: false,
|
||||
})
|
||||
prompt.mockClear()
|
||||
mockSelect.mockResolvedValue('https://codeload.github.com/zkochan/hi/tar.gz/4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd')
|
||||
mockConfirm.mockResolvedValue(false)
|
||||
mockSelect.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
const output = await patch.handler(defaultPatchOption, ['hi'])
|
||||
expect(prompt.mock.calls).toMatchObject([[[
|
||||
{
|
||||
type: 'select',
|
||||
name: 'version',
|
||||
expect(mockSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Choose which version to patch',
|
||||
choices: [
|
||||
{
|
||||
name: '0.0.0',
|
||||
message: '0.0.0',
|
||||
value: '0.0.0',
|
||||
},
|
||||
{
|
||||
expect.objectContaining({ name: '0.0.0', value: '0.0.0' }),
|
||||
expect.objectContaining({
|
||||
name: '1.0.0',
|
||||
message: '1.0.0',
|
||||
value: 'https://codeload.github.com/zkochan/hi/tar.gz/4cdebec76b7b9d1f6e219e06c42d92a6b8ea60cd',
|
||||
hint: 'Git Hosted',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'applyToAll',
|
||||
},
|
||||
]]] as unknown as Record<string, unknown>[])
|
||||
})
|
||||
)
|
||||
expect(mockConfirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Apply this patch to all versions?',
|
||||
})
|
||||
)
|
||||
const patchDir = getPatchDirFromPatchOutput(output)
|
||||
expect(fs.existsSync(patchDir)).toBe(true)
|
||||
expect(fs.readFileSync(path.join(patchDir, 'index.js'), 'utf8')).toContain('module.exports = \'Hi\'')
|
||||
@@ -1326,7 +1319,9 @@ describe('patch-remove', () => {
|
||||
let storeDir: string
|
||||
|
||||
beforeEach(async () => {
|
||||
prompt.mockClear()
|
||||
mockSelect.mockClear()
|
||||
mockConfirm.mockClear()
|
||||
mockCheckbox.mockClear()
|
||||
prepare({
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
@@ -1384,9 +1379,7 @@ describe('patch-remove', () => {
|
||||
packages: ['.'],
|
||||
patchedDependencies,
|
||||
})
|
||||
prompt.mockResolvedValue({
|
||||
patches: ['is-positive@1.0.0', 'chalk@4.1.2'],
|
||||
})
|
||||
mockCheckbox.mockResolvedValue(['is-positive@1.0.0', 'chalk@4.1.2'])
|
||||
const { manifest } = await readProjectManifest(process.cwd())
|
||||
await patchRemove.handler({
|
||||
...defaultPatchRemoveOption,
|
||||
@@ -1394,8 +1387,8 @@ describe('patch-remove', () => {
|
||||
patchedDependencies,
|
||||
}, [])
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect((prompt.mock.calls[0][0] as any).choices).toEqual(expect.arrayContaining(['is-positive@1.0.0', 'chalk@4.1.2']))
|
||||
prompt.mockClear()
|
||||
expect((mockCheckbox.mock.calls[0][0] as any).choices.map((c: any) => c.value)).toEqual(expect.arrayContaining(['is-positive@1.0.0', 'chalk@4.1.2']))
|
||||
mockCheckbox.mockClear()
|
||||
|
||||
const workspaceManifest = await readWorkspaceManifest(process.cwd())
|
||||
expect(workspaceManifest!.patchedDependencies).toBeUndefined()
|
||||
|
||||
338
pnpm-lock.yaml
generated
338
pnpm-lock.yaml
generated
@@ -292,6 +292,9 @@ catalogs:
|
||||
'@eslint/js':
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
'@inquirer/prompts':
|
||||
specifier: ^8.4.3
|
||||
version: 8.4.3
|
||||
'@jest/globals':
|
||||
specifier: 30.3.0
|
||||
version: 30.3.0
|
||||
@@ -601,9 +604,6 @@ catalogs:
|
||||
encode-registry:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
enquirer:
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1
|
||||
esbuild:
|
||||
specifier: ^0.27.7
|
||||
version: 0.27.7
|
||||
@@ -1598,6 +1598,9 @@ importers:
|
||||
|
||||
auth/commands:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/cli.utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../cli/utils
|
||||
@@ -1616,9 +1619,6 @@ importers:
|
||||
'@pnpm/registry-access.client':
|
||||
specifier: workspace:*
|
||||
version: link:../../registry-access/client
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
normalize-registry-url:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.1
|
||||
@@ -1891,6 +1891,9 @@ importers:
|
||||
|
||||
building/commands:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/building.after-install':
|
||||
specifier: workspace:*
|
||||
version: link:../after-install
|
||||
@@ -1939,9 +1942,6 @@ importers:
|
||||
chalk:
|
||||
specifier: 'catalog:'
|
||||
version: 5.6.2
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
p-limit:
|
||||
specifier: 'catalog:'
|
||||
version: 7.3.0
|
||||
@@ -3026,6 +3026,9 @@ importers:
|
||||
|
||||
deps/compliance/commands:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/cli.command':
|
||||
specifier: workspace:*
|
||||
version: link:../../../cli/command
|
||||
@@ -3104,9 +3107,6 @@ importers:
|
||||
chalk:
|
||||
specifier: 'catalog:'
|
||||
version: 5.6.2
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
memoize:
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.0
|
||||
@@ -4262,6 +4262,9 @@ importers:
|
||||
|
||||
exec/commands:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/bins.resolver':
|
||||
specifier: workspace:*
|
||||
version: link:../../bins/resolver
|
||||
@@ -4349,9 +4352,6 @@ importers:
|
||||
didyoumean2:
|
||||
specifier: 'catalog:'
|
||||
version: 7.0.4
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
execa:
|
||||
specifier: 'catalog:'
|
||||
version: safe-execa@0.3.0
|
||||
@@ -5281,6 +5281,9 @@ importers:
|
||||
|
||||
installing/commands:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/building.after-install':
|
||||
specifier: workspace:*
|
||||
version: link:../../building/after-install
|
||||
@@ -5431,9 +5434,6 @@ importers:
|
||||
ci-info:
|
||||
specifier: 'catalog:'
|
||||
version: 4.4.0
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
get-npm-tarball-url:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.0
|
||||
@@ -5645,6 +5645,9 @@ importers:
|
||||
|
||||
installing/deps-installer:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/agent.client':
|
||||
specifier: workspace:*
|
||||
version: link:../../agent/client
|
||||
@@ -5807,9 +5810,6 @@ importers:
|
||||
'@zkochan/rimraf':
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
is-inner-link:
|
||||
specifier: 'catalog:'
|
||||
version: 5.0.0
|
||||
@@ -7457,6 +7457,9 @@ importers:
|
||||
|
||||
patching/commands:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/cli.utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../cli/utils
|
||||
@@ -7520,9 +7523,6 @@ importers:
|
||||
chalk:
|
||||
specifier: 'catalog:'
|
||||
version: 5.6.2
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
escape-string-regexp:
|
||||
specifier: 'catalog:'
|
||||
version: 5.0.0
|
||||
@@ -8184,6 +8184,9 @@ importers:
|
||||
|
||||
registry-access/commands:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/cli.utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../cli/utils
|
||||
@@ -8220,9 +8223,6 @@ importers:
|
||||
chalk:
|
||||
specifier: 'catalog:'
|
||||
version: 5.6.2
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
ramda:
|
||||
specifier: 'catalog:'
|
||||
version: '@pnpm/ramda@0.28.1'
|
||||
@@ -8269,6 +8269,9 @@ importers:
|
||||
|
||||
releasing/commands:
|
||||
dependencies:
|
||||
'@inquirer/prompts':
|
||||
specifier: 'catalog:'
|
||||
version: 8.4.3(@types/node@25.9.1)
|
||||
'@pnpm/bins.resolver':
|
||||
specifier: workspace:*
|
||||
version: link:../../bins/resolver
|
||||
@@ -8377,9 +8380,6 @@ importers:
|
||||
detect-libc:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.2
|
||||
enquirer:
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.1
|
||||
execa:
|
||||
specifier: 'catalog:'
|
||||
version: safe-execa@0.3.0
|
||||
@@ -10939,6 +10939,55 @@ packages:
|
||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@inquirer/ansi@2.0.5':
|
||||
resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
|
||||
'@inquirer/checkbox@5.1.5':
|
||||
resolution: {integrity: sha512-Jmf9tgBHIEK5SAOB7swYfStqmtkZb00xOTpSQmkoGEpdxOTpJi9RS0A8bkfDPHTTItZRJrRdZrEMu25wyj0VfQ==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/confirm@6.0.13':
|
||||
resolution: {integrity: sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@11.1.10':
|
||||
resolution: {integrity: sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/editor@5.1.2':
|
||||
resolution: {integrity: sha512-Y3Nor7S/DhIPo+8Ym/dSY4efwKI4BsflKDwXh0jNeXJsSF3dteS/3Yf+z4wkibVZDvYMyCgknSTQlNahfunGHg==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/expand@5.0.14':
|
||||
resolution: {integrity: sha512-qyY9zcIX2eKYwaAUiQo9zORd61Lc3sXeM72fVbeHkYnDkqfr8/armcRbmVAIrExeJhI2puk+uomeKtWrpUVUmQ==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/external-editor@1.0.3':
|
||||
resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10948,10 +10997,95 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/external-editor@3.0.0':
|
||||
resolution: {integrity: sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/figures@1.0.15':
|
||||
resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@inquirer/figures@2.0.5':
|
||||
resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
|
||||
'@inquirer/input@5.0.13':
|
||||
resolution: {integrity: sha512-0l0jCHlJnXIV8CTxwQC0C+5Ziq8WP22edWgmciW2xYvoeoSck4v5FvCS1ctKdqLLR0dUo93uAHgWHywgBSoRyw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/number@4.0.13':
|
||||
resolution: {integrity: sha512-WHmkYnnJAou5gx7RgcvAfUggnHNM1zWfoh0dFPl3dxVssuqt+dK5rIbaOYQXNyOegvFnopbKupjnhw2O8gANNg==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/password@5.0.13':
|
||||
resolution: {integrity: sha512-XDGu64ROHZjOOXLAANvJN7iIxWKhOSCG5VakrZ5kaScVR+snVJCFglD/hL3/677awtWcu4pXoWa280CDIYcBeg==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/prompts@8.4.3':
|
||||
resolution: {integrity: sha512-ai5LseTw9HhegupIgmo4cn7RpnCGznjjXu4OI+7jMR8vu7T1ZCCNMzFFAovUCjL1fl0cceksIN1++yQE59SmZw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/rawlist@5.2.9':
|
||||
resolution: {integrity: sha512-a1ErXEfgjfPYpyQ89dp+7n2IISjH9oQg3ygvF5adz8B7aHn4n2PjEgu1wpVTp69K3bj3lVLxP0qJ2b1clk1Whw==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/search@4.1.9':
|
||||
resolution: {integrity: sha512-ZlbM28Q9lmLkFPNAIv+ZuY530n5Km8U1WW48oYEvDhe9yc2uL3m3t+JSdRUkQlk5fuIuskgiIVjcb7czFzQpuA==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/select@5.1.5':
|
||||
resolution: {integrity: sha512-6SRg6kHfK/sjLXOsuqNebuir+sjwrf/iWuRUnXgB2slzEewppI1WfzeS16XxDcOQmXBruMmmB9Cgrz7wsAxqMg==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@4.0.5':
|
||||
resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==}
|
||||
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/cliui@9.0.0':
|
||||
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -13778,9 +13912,18 @@ packages:
|
||||
resolution: {integrity: sha512-PY66/8HelapGo5nqMN17ZTKqJj1nppuS1OoC9Y0aI2jsUDlZDEYhMODTpb68wVCq+xMbaEbPGXRd7qutHzkRXA==}
|
||||
engines: {node: ^14.13.1 || >=16.0.0}
|
||||
|
||||
fast-string-truncated-width@3.0.3:
|
||||
resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
|
||||
|
||||
fast-string-width@3.0.2:
|
||||
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
|
||||
|
||||
fast-uri@3.1.2:
|
||||
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||
|
||||
fast-wrap-ansi@0.2.2:
|
||||
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
|
||||
|
||||
fastest-levenshtein@1.0.16:
|
||||
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
|
||||
engines: {node: '>= 4.9.1'}
|
||||
@@ -15315,6 +15458,10 @@ packages:
|
||||
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
mute-stream@3.0.0:
|
||||
resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
||||
nanoresource@1.3.0:
|
||||
resolution: {integrity: sha512-OI5dswqipmlYfyL3k/YMm7mbERlh4Bd1KuKdMHpeoVD1iVxqxaTMKleB4qaA2mbQZ6/zMNSxCXv9M9P/YbqTuQ==}
|
||||
|
||||
@@ -18266,6 +18413,51 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@inquirer/ansi@2.0.5': {}
|
||||
|
||||
'@inquirer/checkbox@5.1.5(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.5
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/figures': 2.0.5
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/confirm@6.0.13(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/core@11.1.10(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.5
|
||||
'@inquirer/figures': 2.0.5
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
cli-width: 4.1.0
|
||||
fast-wrap-ansi: 0.2.2
|
||||
mute-stream: 3.0.0
|
||||
signal-exit: 4.1.0
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/editor@5.1.2(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/external-editor': 3.0.0(@types/node@25.9.1)
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/expand@5.0.14(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/external-editor@1.0.3(@types/node@22.19.19)':
|
||||
dependencies:
|
||||
chardet: 2.1.1
|
||||
@@ -18273,8 +18465,82 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.19
|
||||
|
||||
'@inquirer/external-editor@3.0.0(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
chardet: 2.1.1
|
||||
iconv-lite: 0.7.2
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/figures@1.0.15': {}
|
||||
|
||||
'@inquirer/figures@2.0.5': {}
|
||||
|
||||
'@inquirer/input@5.0.13(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/number@4.0.13(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/password@5.0.13(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.5
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/prompts@8.4.3(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/checkbox': 5.1.5(@types/node@25.9.1)
|
||||
'@inquirer/confirm': 6.0.13(@types/node@25.9.1)
|
||||
'@inquirer/editor': 5.1.2(@types/node@25.9.1)
|
||||
'@inquirer/expand': 5.0.14(@types/node@25.9.1)
|
||||
'@inquirer/input': 5.0.13(@types/node@25.9.1)
|
||||
'@inquirer/number': 4.0.13(@types/node@25.9.1)
|
||||
'@inquirer/password': 5.0.13(@types/node@25.9.1)
|
||||
'@inquirer/rawlist': 5.2.9(@types/node@25.9.1)
|
||||
'@inquirer/search': 4.1.9(@types/node@25.9.1)
|
||||
'@inquirer/select': 5.1.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/rawlist@5.2.9(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/search@4.1.9(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/figures': 2.0.5
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/select@5.1.5(@types/node@25.9.1)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 2.0.5
|
||||
'@inquirer/core': 11.1.10(@types/node@25.9.1)
|
||||
'@inquirer/figures': 2.0.5
|
||||
'@inquirer/type': 4.0.5(@types/node@25.9.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@inquirer/type@4.0.5(@types/node@25.9.1)':
|
||||
optionalDependencies:
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@isaacs/cliui@9.0.0': {}
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
@@ -22073,8 +22339,18 @@ snapshots:
|
||||
|
||||
fast-string-compare@3.0.0: {}
|
||||
|
||||
fast-string-truncated-width@3.0.3: {}
|
||||
|
||||
fast-string-width@3.0.2:
|
||||
dependencies:
|
||||
fast-string-truncated-width: 3.0.3
|
||||
|
||||
fast-uri@3.1.2: {}
|
||||
|
||||
fast-wrap-ansi@0.2.2:
|
||||
dependencies:
|
||||
fast-string-width: 3.0.2
|
||||
|
||||
fastest-levenshtein@1.0.16: {}
|
||||
|
||||
fastq@1.20.1:
|
||||
@@ -24020,6 +24296,8 @@ snapshots:
|
||||
|
||||
mute-stream@1.0.0: {}
|
||||
|
||||
mute-stream@3.0.0: {}
|
||||
|
||||
nanoresource@1.3.0:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
|
||||
@@ -79,6 +79,7 @@ catalog:
|
||||
'@commitlint/prompt-cli': ^20.5.3
|
||||
'@cyclonedx/cyclonedx-library': 10.0.0
|
||||
'@eslint/js': ^10.0.1
|
||||
'@inquirer/prompts': ^8.4.3
|
||||
'@jest/globals': 30.3.0
|
||||
'@npm/types': ^2.1.0
|
||||
'@pnpm/byline': ^1.0.0
|
||||
@@ -182,7 +183,6 @@ catalog:
|
||||
dint: ^5.1.0
|
||||
dir-is-case-sensitive: ^3.0.0
|
||||
encode-registry: ^3.0.1
|
||||
enquirer: ^2.4.1
|
||||
esbuild: ^0.27.7
|
||||
escape-string-regexp: ^5.0.0
|
||||
eslint: ^10.4.0
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/cli.utils": "workspace:*",
|
||||
"@pnpm/config.pick-registry-for-package": "workspace:*",
|
||||
"@pnpm/config.reader": "workspace:*",
|
||||
@@ -44,7 +45,6 @@
|
||||
"@pnpm/resolving.registry.types": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"chalk": "catalog:",
|
||||
"enquirer": "catalog:",
|
||||
"ramda": "catalog:",
|
||||
"render-help": "catalog:",
|
||||
"semver": "catalog:"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import readline from 'node:readline'
|
||||
|
||||
import { input } from '@inquirer/prompts'
|
||||
import { docsUrl } from '@pnpm/cli.utils'
|
||||
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
import npa from '@pnpm/npm-package-arg'
|
||||
import { setDistTag } from '@pnpm/registry-access.client'
|
||||
import type { Registries, RegistryConfig } from '@pnpm/types'
|
||||
import enquirer from 'enquirer'
|
||||
import { renderHelp } from 'render-help'
|
||||
import semver from 'semver'
|
||||
|
||||
@@ -319,7 +319,7 @@ function createOtpContext (opts: CreateFetchFromRegistryOptions): OtpContext {
|
||||
return {
|
||||
Date,
|
||||
createReadlineInterface: readline.createInterface.bind(null, { input: process.stdin }),
|
||||
enquirer,
|
||||
enquirer: { input },
|
||||
fetch: createFetchFromRegistry(opts),
|
||||
globalInfo,
|
||||
globalWarn,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "catalog:",
|
||||
"@pnpm/bins.resolver": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/cli.common-cli-options-help": "workspace:*",
|
||||
@@ -68,7 +69,6 @@
|
||||
"chalk": "catalog:",
|
||||
"ci-info": "catalog:",
|
||||
"detect-libc": "catalog:",
|
||||
"enquirer": "catalog:",
|
||||
"execa": "catalog:",
|
||||
"libnpmpublish": "catalog:",
|
||||
"normalize-path": "catalog:",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { confirm } from '@inquirer/prompts'
|
||||
import { FILTERING } from '@pnpm/cli.common-cli-options-help'
|
||||
import { docsUrl, readProjectManifest } from '@pnpm/cli.utils'
|
||||
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
|
||||
@@ -9,7 +10,6 @@ import { getCurrentBranch, isGitRepo, isRemoteHistoryClean, isWorkingTreeClean }
|
||||
import type { ExportedManifest } from '@pnpm/releasing.exportable-manifest'
|
||||
import type { ProjectManifest } from '@pnpm/types'
|
||||
import { rimraf } from '@zkochan/rimraf'
|
||||
import enquirer from 'enquirer'
|
||||
import { pick } from 'ramda'
|
||||
import { realpathMissing } from 'realpath-missing'
|
||||
import { renderHelp } from 'render-help'
|
||||
@@ -188,14 +188,20 @@ export async function publish (
|
||||
)
|
||||
}
|
||||
if (!branches.includes(currentBranch)) {
|
||||
const { confirm } = await enquirer.prompt({
|
||||
message: `You're on branch "${currentBranch}" but your "publish-branch" is set to "${branches.join('|')}". \
|
||||
Do you want to continue?`,
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
let isConfirmed: boolean
|
||||
try {
|
||||
isConfirmed = await confirm({
|
||||
message: `You're on branch "${currentBranch}" but your "publish-branch" is set to "${branches.join('|')}". Do you want to continue?`,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'ExitPromptError') {
|
||||
isConfirmed = false
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
if (!confirm) {
|
||||
if (!isConfirmed) {
|
||||
throw new PnpmError('GIT_NOT_CORRECT_BRANCH', `Branch is not on '${branches.join('|')}'.`, {
|
||||
hint: GIT_CHECKS_HINT,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import readline from 'node:readline'
|
||||
|
||||
import { input } from '@inquirer/prompts'
|
||||
import { globalInfo, globalWarn } from '@pnpm/logger'
|
||||
import { fetch } from '@pnpm/network.fetch'
|
||||
import type { ExportedManifest } from '@pnpm/releasing.exportable-manifest'
|
||||
import ciInfo from 'ci-info'
|
||||
import enquirer from 'enquirer'
|
||||
import { publish as _publish } from 'libnpmpublish'
|
||||
|
||||
import type { AuthTokenContext } from '../oidc/authToken.js'
|
||||
@@ -32,7 +32,7 @@ export const SHARED_CONTEXT: SharedContext = {
|
||||
Date,
|
||||
createReadlineInterface: readline.createInterface.bind(null, { input: process.stdin }),
|
||||
ciInfo,
|
||||
enquirer,
|
||||
enquirer: { input },
|
||||
fetch,
|
||||
globalInfo,
|
||||
globalWarn,
|
||||
|
||||
@@ -8,11 +8,27 @@ import { temporaryDirectory } from 'tempy'
|
||||
|
||||
import { DEFAULT_OPTS } from './utils/index.js'
|
||||
|
||||
jest.unstable_mockModule('enquirer', () => ({ default: { prompt: jest.fn() } }))
|
||||
const { default: enquirer } = await import('enquirer')
|
||||
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 { confirm } = await import('@inquirer/prompts')
|
||||
const { publish } = await import('@pnpm/releasing.commands')
|
||||
|
||||
const prompt = jest.mocked(enquirer.prompt)
|
||||
const mockConfirm = jest.mocked(confirm)
|
||||
|
||||
test('publish: fails git check if branch is not on master or main', async () => {
|
||||
prepare({
|
||||
@@ -26,9 +42,7 @@ test('publish: fails git check if branch is not on master or main', async () =>
|
||||
await execa('git', ['add', '*'])
|
||||
await execa('git', ['commit', '-m', 'init', '--no-gpg-sign'])
|
||||
|
||||
prompt.mockResolvedValue({
|
||||
confirm: false,
|
||||
})
|
||||
mockConfirm.mockResolvedValue(false)
|
||||
|
||||
await expect(
|
||||
publish.handler({
|
||||
@@ -54,9 +68,7 @@ test('publish: fails git check if branch is not on specified branch', async () =
|
||||
await execa('git', ['add', '*'])
|
||||
await execa('git', ['commit', '-m', 'init', '--no-gpg-sign'])
|
||||
|
||||
prompt.mockResolvedValue({
|
||||
confirm: false,
|
||||
})
|
||||
mockConfirm.mockResolvedValue(false)
|
||||
|
||||
await expect(
|
||||
publish.handler({
|
||||
|
||||
@@ -24,7 +24,7 @@ function createMockContext (overrides?: MockContextOverrides): OtpContext {
|
||||
return {
|
||||
Date: { now: () => 0 },
|
||||
setTimeout: (cb: () => void) => cb(),
|
||||
enquirer: { prompt: async () => ({ otp: '123456' }) },
|
||||
enquirer: { input: async () => '123456' },
|
||||
fetch: async () => ({
|
||||
headers: { get: () => null },
|
||||
json: async () => ({}),
|
||||
@@ -95,7 +95,7 @@ describe('publishWithOtpHandling', () => {
|
||||
expect(opts.otp).toBe('654321')
|
||||
return createOkResponse()
|
||||
},
|
||||
enquirer: { prompt: async () => ({ otp: '654321' }) },
|
||||
enquirer: { input: async () => '654321' },
|
||||
})
|
||||
const result = await publishWithOtpHandling({ context, manifest, publishOptions, tarballData })
|
||||
expect(result.ok).toBe(true)
|
||||
@@ -133,7 +133,7 @@ describe('publishWithOtpHandling', () => {
|
||||
publish: async () => {
|
||||
throw Object.assign(new Error('otp'), { code: 'EOTP' })
|
||||
},
|
||||
enquirer: { prompt: async () => ({ otp: '' }) },
|
||||
enquirer: { input: async () => '' },
|
||||
})
|
||||
await expect(publishWithOtpHandling({ context, manifest, publishOptions, tarballData }))
|
||||
.rejects.toMatchObject({ code: 'EOTP' })
|
||||
@@ -144,7 +144,7 @@ describe('publishWithOtpHandling', () => {
|
||||
publish: async () => {
|
||||
throw Object.assign(new Error('otp'), { code: 'EOTP' })
|
||||
},
|
||||
enquirer: { prompt: async () => undefined },
|
||||
enquirer: { input: async () => undefined },
|
||||
})
|
||||
await expect(publishWithOtpHandling({ context, manifest, publishOptions, tarballData }))
|
||||
.rejects.toMatchObject({ code: 'EOTP' })
|
||||
|
||||
Reference in New Issue
Block a user