mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 18:46:18 -04:00
feat: skip confirm modules purge prompt if --yes is passed (#10383)
* feat: add --yes command line option * feat: skip confirm modules purge prompt if --yes is passed * refactor: factor out `ExecPnpmSyncOpts` * test: add end-to-end test for --yes flag
This commit is contained in:
8
.changeset/tasty-eyes-retire.md
Normal file
8
.changeset/tasty-eyes-retire.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@pnpm/common-cli-options-help": minor
|
||||
"@pnpm/core": minor
|
||||
"@pnpm/config": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
A new `--yes` flag can be passed to pnpm to automatically confirm prompts. This is useful when running pnpm in non-interactive script.
|
||||
@@ -35,6 +35,11 @@ export const UNIVERSAL_OPTIONS = [
|
||||
name: '--help',
|
||||
shortAlias: '-h',
|
||||
},
|
||||
{
|
||||
description: 'Automatically answer yes to prompts and run non-interactively. Will abort if an undesirable situation occurs and user input is strictly necessary.',
|
||||
name: '--yes',
|
||||
shortAlias: '-y',
|
||||
},
|
||||
{
|
||||
description: `Change to directory <dir> (default: ${process.cwd()})`,
|
||||
name: '--dir <dir>',
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface Config extends AuthInfo, OptionsFromRootManifest {
|
||||
allProjectsGraph?: ProjectsGraph
|
||||
|
||||
allowNew: boolean
|
||||
autoConfirmAllPrompts?: boolean
|
||||
autoInstallPeers?: boolean
|
||||
bail: boolean
|
||||
color: 'always' | 'auto' | 'never'
|
||||
|
||||
@@ -154,6 +154,7 @@ export const excludedPnpmKeys = [
|
||||
'libc',
|
||||
'os',
|
||||
'audit-level',
|
||||
'yes',
|
||||
] as const satisfies ReadonlyArray<Exclude<PnpmKey, PnpmConfigFileKey>>
|
||||
export type ExcludedPnpmKey = typeof excludedPnpmKeys[number]
|
||||
|
||||
|
||||
@@ -612,6 +612,13 @@ export async function getConfig (opts: {
|
||||
pnpmConfig.enableGlobalVirtualStore = false
|
||||
}
|
||||
|
||||
// The yes option is only meant to be a CLI option. Remove it from the
|
||||
// returned pnpm config.
|
||||
delete (pnpmConfig as { yes?: boolean }).yes
|
||||
if (cliOptions.yes) {
|
||||
pnpmConfig.autoConfirmAllPrompts = true
|
||||
}
|
||||
|
||||
transformPathKeys(pnpmConfig, os.homedir())
|
||||
|
||||
return { config: pnpmConfig, warnings }
|
||||
|
||||
@@ -129,6 +129,7 @@ export const pnpmTypes = {
|
||||
'workspace-concurrency': Number,
|
||||
'workspace-packages': [String, Array],
|
||||
'workspace-root': Boolean,
|
||||
yes: Boolean,
|
||||
'test-pattern': [String, Array],
|
||||
'changed-files-ignore-pattern': [String, Array],
|
||||
'embed-readme': Boolean,
|
||||
|
||||
@@ -1404,6 +1404,24 @@ test('no warning when directory does not contain PATH delimiter character', asyn
|
||||
}
|
||||
})
|
||||
|
||||
test.each([
|
||||
[undefined, undefined],
|
||||
[false, undefined],
|
||||
[true, true],
|
||||
])('sets autoConfirmAllPrompts when CLI is passed --yes=%s', async (cliValue?: boolean, expectedValue?: boolean) => {
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {
|
||||
'yes': cliValue,
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
expect(config.autoConfirmAllPrompts).toBe(expectedValue)
|
||||
})
|
||||
|
||||
describe('global config.yaml', () => {
|
||||
let XDG_CONFIG_HOME: string | undefined
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { pnpmPkgJson } from '../pnpmPkgJson.js'
|
||||
import { type ReporterFunction } from '../types.js'
|
||||
|
||||
export interface StrictInstallOptions {
|
||||
autoConfirmAllPrompts: boolean
|
||||
autoInstallPeers: boolean
|
||||
autoInstallPeersFromHighestMatch: boolean
|
||||
catalogs: Catalogs
|
||||
@@ -188,11 +189,12 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => {
|
||||
return {
|
||||
allowedDeprecatedVersions: {},
|
||||
allowUnusedPatches: false,
|
||||
autoConfirmAllPrompts: opts.autoConfirmAllPrompts ?? false,
|
||||
autoInstallPeers: true,
|
||||
autoInstallPeersFromHighestMatch: false,
|
||||
catalogs: {},
|
||||
childConcurrency: 5,
|
||||
confirmModulesPurge: !opts.force,
|
||||
confirmModulesPurge: !(opts.autoConfirmAllPrompts || opts.force),
|
||||
depth: 0,
|
||||
dedupeInjectedDeps: true,
|
||||
enableGlobalVirtualStore: false,
|
||||
|
||||
@@ -54,6 +54,7 @@ export const GLOBAL_OPTIONS = pick([
|
||||
'ignore-workspace',
|
||||
'workspace-packages',
|
||||
'workspace-root',
|
||||
'yes',
|
||||
'include-workspace-root',
|
||||
'fail-if-no-match',
|
||||
], allTypes)
|
||||
|
||||
@@ -31,4 +31,5 @@ export const shorthands: Record<string, string> = {
|
||||
w: '--workspace-root',
|
||||
i: '--interactive',
|
||||
F: '--filter',
|
||||
y: '--yes',
|
||||
}
|
||||
|
||||
32
pnpm/test/install/yesFlag.ts
Normal file
32
pnpm/test/install/yesFlag.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { prepare } from '@pnpm/prepare'
|
||||
import { type PackageManifest } from '@pnpm/types'
|
||||
import { loadJsonFileSync } from 'load-json-file'
|
||||
import { execPnpmSync } from '../utils/index.js'
|
||||
import { type ExecPnpmSyncOpts } from '../utils/execPnpm.js'
|
||||
|
||||
const basicPackageManifest = loadJsonFileSync<PackageManifest>(path.join(import.meta.dirname, '../utils/simple-package.json'))
|
||||
|
||||
describe('pnpm install --yes', () => {
|
||||
beforeEach(() => {
|
||||
prepare(basicPackageManifest)
|
||||
execPnpmSync(['install'])
|
||||
|
||||
// Write an incompatible layoutVersion to force a module purge prompt.
|
||||
fs.writeFileSync('node_modules/.modules.yaml', 'layoutVersion: 1')
|
||||
})
|
||||
|
||||
const execPnpmOpts: ExecPnpmSyncOpts = {
|
||||
expectSuccess: true,
|
||||
env: { CI: 'false' },
|
||||
}
|
||||
|
||||
test('prompts without --yes flag', () => {
|
||||
expect(() => execPnpmSync(['install'], execPnpmOpts)).toThrow('Aborted removal of modules directory due to no TTY')
|
||||
})
|
||||
|
||||
test('skips prompt when --yes is passed', () => {
|
||||
expect(() => execPnpmSync(['install', '--yes'], execPnpmOpts)).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -93,16 +93,18 @@ export interface ChildProcess {
|
||||
stderr: { toString: () => string }
|
||||
}
|
||||
|
||||
export interface ExecPnpmSyncOpts {
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
expectSuccess?: boolean // similar to expect(status).toBe(0), but also prints error messages, which makes it easier to debug failed tests
|
||||
stdio?: StdioOptions
|
||||
storeDir?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export function execPnpmSync (
|
||||
args: string[],
|
||||
opts?: {
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
expectSuccess?: boolean // similar to expect(status).toBe(0), but also prints error messages, which makes it easier to debug failed tests
|
||||
stdio?: StdioOptions
|
||||
storeDir?: string
|
||||
timeout?: number
|
||||
}
|
||||
opts?: ExecPnpmSyncOpts
|
||||
): ChildProcess {
|
||||
const execResult = crossSpawn.sync(process.execPath, [pnpmBinLocation, ...args], {
|
||||
cwd: opts?.cwd,
|
||||
|
||||
Reference in New Issue
Block a user