diff --git a/.changeset/fix-non-interactive-modules-purge-hint.md b/.changeset/fix-non-interactive-modules-purge-hint.md new file mode 100644 index 0000000000..30c2490b58 --- /dev/null +++ b/.changeset/fix-non-interactive-modules-purge-hint.md @@ -0,0 +1,10 @@ +--- +"@pnpm/core": patch +"pnpm": patch +--- + +Improve the non-interactive modules purge error hint to include the `confirmModulesPurge=false` workaround. + +When pnpm needs to recreate `node_modules` but no TTY is available, the error now suggests either setting `CI=true` or disabling the purge confirmation prompt via `confirmModulesPurge=false`. + +Adds a regression test for the non-TTY flow. diff --git a/pkg-manager/core/src/install/validateModules.ts b/pkg-manager/core/src/install/validateModules.ts index bc33d4913b..7f2619ddbe 100644 --- a/pkg-manager/core/src/install/validateModules.ts +++ b/pkg-manager/core/src/install/validateModules.ts @@ -151,7 +151,7 @@ async function purgeModulesDirsOfImporters ( if (opts.confirmModulesPurge ?? true) { if (!process.stdin.isTTY) { throw new PnpmError('ABORTED_REMOVE_MODULES_DIR_NO_TTY', 'Aborted removal of modules directory due to no TTY', { - hint: 'If you are running pnpm in CI, set the CI environment variable to "true".', + 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 }>({ diff --git a/pkg-manager/core/test/breakingChanges.ts b/pkg-manager/core/test/breakingChanges.ts index 00f3f899c3..5499d5bfa0 100644 --- a/pkg-manager/core/test/breakingChanges.ts +++ b/pkg-manager/core/test/breakingChanges.ts @@ -1,5 +1,6 @@ import fs from 'node:fs' import path from 'node:path' +import util from 'node:util' import { WANTED_LOCKFILE } from '@pnpm/constants' import { addDependenciesToPackage, install } from '@pnpm/core' @@ -102,6 +103,32 @@ test('do not fail on non-compatible store when forced during named installation' }) }) +test('fail fast with actionable hint on non-TTY when modules purge needs confirmation', async () => { + prepareEmpty() + const opts = testDefaults() + + await saveModulesYaml('0.50.0', opts.storeDir) + + const originalIsTTY = process.stdin.isTTY + Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }) + + let err: unknown + try { + await install({}, opts) + } catch (_err: unknown) { + err = _err + } finally { + Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true }) + } + + expect(util.types.isNativeError(err)).toBeTruthy() + if (util.types.isNativeError(err)) { + expect('code' in err && err.code).toBe('ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY') + expect(err.message).toContain('no TTY') + expect('hint' in err && typeof err.hint === 'string' && err.hint).toContain('confirmModulesPurge') + } +}) + async function saveModulesYaml (pnpmVersion: string, storeDir: string) { fs.mkdirSync('node_modules') fs.writeFileSync('node_modules/.modules.yaml', `packageManager: pnpm@${pnpmVersion}\nstoreDir: ${storeDir}`)