From 793eedb0b2cf79cac2016e1277f41eabe7444d41 Mon Sep 17 00:00:00 2001 From: await-ovo <13152410380@163.com> Date: Wed, 10 May 2023 03:38:30 +0800 Subject: [PATCH] feat(plugin-commands-patching): add patch-remove command (#6521) --- .../plugin-commands-patching/src/index.ts | 3 +- .../src/patchRemove.ts | 79 +++++++++++++++++++ .../test/patch.test.ts | 73 ++++++++++++++++- pnpm/src/cmd/index.ts | 3 +- pnpm/src/main.ts | 2 +- 5 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 patching/plugin-commands-patching/src/patchRemove.ts diff --git a/patching/plugin-commands-patching/src/index.ts b/patching/plugin-commands-patching/src/index.ts index 0752d4557e..304958e0b3 100644 --- a/patching/plugin-commands-patching/src/index.ts +++ b/patching/plugin-commands-patching/src/index.ts @@ -1,4 +1,5 @@ import * as patch from './patch' import * as patchCommit from './patchCommit' +import * as patchRemove from './patchRemove' -export { patch, patchCommit } +export { patch, patchCommit, patchRemove } diff --git a/patching/plugin-commands-patching/src/patchRemove.ts b/patching/plugin-commands-patching/src/patchRemove.ts new file mode 100644 index 0000000000..09313cbd39 --- /dev/null +++ b/patching/plugin-commands-patching/src/patchRemove.ts @@ -0,0 +1,79 @@ +import path from 'path' +import fs from 'fs/promises' +import { docsUrl } from '@pnpm/cli-utils' +import { install } from '@pnpm/plugin-commands-installation' +import { type Config, types as allTypes } from '@pnpm/config' +import { tryReadProjectManifest } from '@pnpm/read-project-manifest' +import { PnpmError } from '@pnpm/error' +import renderHelp from 'render-help' +import { prompt } from 'enquirer' +import pick from 'ramda/src/pick' + +export function rcOptionsTypes () { + return pick([], allTypes) +} + +export function cliOptionsTypes () { + return { ...rcOptionsTypes() } +} + +export const commandNames = ['patch-remove'] + +export function help () { + return renderHelp({ + description: 'Remove existing patch files', + url: docsUrl('patch-remove'), + usages: ['pnpm patch-remove [pkg...]'], + }) +} + +export type PatchRemoveCommandOptions = install.InstallCommandOptions & Pick + +export async function handler (opts: PatchRemoveCommandOptions, params: string[]) { + let patchesToRemove = params + const lockfileDir = opts.lockfileDir ?? opts.dir ?? process.cwd() + const { writeProjectManifest, manifest } = await tryReadProjectManifest(lockfileDir) + const rootProjectManifest = opts.rootProjectManifest ?? manifest ?? {} + const patchedDependencies = rootProjectManifest.pnpm?.patchedDependencies ?? {} + + if (!params.length) { + const allPatches = Object.keys(patchedDependencies) + if (allPatches.length) { + ({ patches: patchesToRemove } = await 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 + }, + })) + } + } + + if (!patchesToRemove.length) { + throw new PnpmError('NO_PATCHES_TO_REMOVE', 'There are no patches that need to be removed') + } + + for (const patch of patchesToRemove) { + if (Object.prototype.hasOwnProperty.call(patchedDependencies, patch)) { + const patchFile = path.join(lockfileDir, patchedDependencies[patch]) + await fs.rm(patchFile, { force: true }) + delete rootProjectManifest.pnpm!.patchedDependencies![patch] + } + } + + await writeProjectManifest(rootProjectManifest) + + if (opts?.selectedProjectsGraph?.[lockfileDir]) { + opts.selectedProjectsGraph[lockfileDir].package.manifest = rootProjectManifest + } + + if (opts?.allProjectsGraph?.[lockfileDir].package.manifest) { + opts.allProjectsGraph[lockfileDir].package.manifest = rootProjectManifest + } + + return install.handler(opts) +} diff --git a/patching/plugin-commands-patching/test/patch.test.ts b/patching/plugin-commands-patching/test/patch.test.ts index 9f13e0d27f..2202af7fc0 100644 --- a/patching/plugin-commands-patching/test/patch.test.ts +++ b/patching/plugin-commands-patching/test/patch.test.ts @@ -6,7 +6,7 @@ import { install } from '@pnpm/plugin-commands-installation' import { readProjects } from '@pnpm/filter-workspace-packages' import writeYamlFile from 'write-yaml-file' import tempy from 'tempy' -import { patch, patchCommit } from '@pnpm/plugin-commands-patching' +import { patch, patchCommit, patchRemove } from '@pnpm/plugin-commands-patching' import { readProjectManifest } from '@pnpm/read-project-manifest' import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock' import { DEFAULT_OPTS } from './utils/index' @@ -593,6 +593,77 @@ describe('patch with custom modules-dir and virtual-store-dir', () => { }) }) +describe('patch-remove', () => { + let defaultPatchRemoveOption: patchRemove.PatchRemoveCommandOptions + let cacheDir: string + let storeDir: string + + beforeEach(async () => { + prompt.mockClear() + prepare({ + dependencies: { + 'is-positive': '1.0.0', + }, + }) + cacheDir = path.resolve('cache') + storeDir = path.resolve('store') + defaultPatchRemoveOption = { + ...DEFAULT_OPTS, + dir: process.cwd(), + } + + await install.handler({ + ...DEFAULT_OPTS, + cacheDir, + storeDir, + dir: process.cwd(), + saveLockfile: true, + }) + }) + test('patch-remove should work as expected', async () => { + const { manifest, writeProjectManifest } = await readProjectManifest(process.cwd()) + manifest.pnpm = { + patchedDependencies: { + 'is-positive@1.0.0': 'patches/is-positive@1.0.0.patch', + }, + } + await writeProjectManifest(manifest) + fs.mkdirSync(path.join(process.cwd(), 'patches')) + fs.writeFileSync(path.join(process.cwd(), 'patches/is-positive@1.0.0.patch'), 'test patch content', 'utf8') + + await patchRemove.handler(defaultPatchRemoveOption, ['is-positive@1.0.0']) + + const { manifest: newManifest } = await readProjectManifest(process.cwd()) + expect(newManifest!.pnpm!.patchedDependencies).toEqual({}) + expect(fs.existsSync(path.join(process.cwd(), 'patches/is-positive@1.0.0.patch'))).toBe(false) + }) + + test('prompt to select patches that to be removed', async () => { + const { manifest, writeProjectManifest } = await readProjectManifest(process.cwd()) + manifest.pnpm = { + patchedDependencies: { + 'is-positive@1.0.0': 'patches/is-positive@1.0.0.patch', + 'chalk@4.1.2': 'patches/chalk@4.1.2.patch', + }, + } + await writeProjectManifest(manifest) + prompt.mockResolvedValue({ + patches: ['is-positive@1.0.0', 'chalk@4.1.2'], + }) + await patchRemove.handler(defaultPatchRemoveOption, []) + expect(prompt.mock.calls[0][0].choices).toEqual(expect.arrayContaining(['is-positive@1.0.0', 'chalk@4.1.2'])) + prompt.mockClear() + + const { manifest: newManifest } = await readProjectManifest(process.cwd()) + expect(newManifest!.pnpm!.patchedDependencies).toEqual({}) + }) + + test('should throw error when there is no patch to remove', async () => { + await expect(() => patchRemove.handler(defaultPatchRemoveOption, [])) + .rejects.toThrow('There are no patches that need to be removed') + }) +}) + function getPatchDirFromPatchOutput (output: string) { const [firstLine] = output.split('\n') return firstLine.substring(firstLine.indexOf(':') + 1).trim() diff --git a/pnpm/src/cmd/index.ts b/pnpm/src/cmd/index.ts index cf3231c54b..3298ba50df 100644 --- a/pnpm/src/cmd/index.ts +++ b/pnpm/src/cmd/index.ts @@ -10,7 +10,7 @@ import { list, ll, why } from '@pnpm/plugin-commands-listing' import { licenses } from '@pnpm/plugin-commands-licenses' import { outdated } from '@pnpm/plugin-commands-outdated' import { pack, publish } from '@pnpm/plugin-commands-publishing' -import { patch, patchCommit } from '@pnpm/plugin-commands-patching' +import { patch, patchCommit, patchRemove } from '@pnpm/plugin-commands-patching' import { rebuild } from '@pnpm/plugin-commands-rebuild' import { create, @@ -127,6 +127,7 @@ const commands: CommandDefinition[] = [ pack, patch, patchCommit, + patchRemove, prune, publish, rebuild, diff --git a/pnpm/src/main.ts b/pnpm/src/main.ts index 3ef06421f1..e7e2cdbf28 100644 --- a/pnpm/src/main.ts +++ b/pnpm/src/main.ts @@ -168,7 +168,7 @@ export async function main (inputArgv: string[]) { } if ( - (cmd === 'install' || cmd === 'import' || cmd === 'dedupe' || cmd === 'patch-commit' || cmd === 'patch') && + (cmd === 'install' || cmd === 'import' || cmd === 'dedupe' || cmd === 'patch-commit' || cmd === 'patch' || cmd === 'patch-remove') && typeof workspaceDir === 'string' ) { cliOptions['recursive'] = true