Skip to content

Commit

Permalink
feat(plugin-commands-patching): add patch-remove command (#6521)
Browse files Browse the repository at this point in the history
  • Loading branch information
await-ovo committed May 9, 2023
1 parent d5c40b5 commit 793eedb
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 4 deletions.
3 changes: 2 additions & 1 deletion 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 }
79 changes: 79 additions & 0 deletions 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<Config, 'dir' | 'lockfileDir' | 'patchesDir' | 'rootProjectManifest'>

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)
}
73 changes: 72 additions & 1 deletion patching/plugin-commands-patching/test/patch.test.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion pnpm/src/cmd/index.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -127,6 +127,7 @@ const commands: CommandDefinition[] = [
pack,
patch,
patchCommit,
patchRemove,
prune,
publish,
rebuild,
Expand Down
2 changes: 1 addition & 1 deletion pnpm/src/main.ts
Expand Up @@ -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
Expand Down

0 comments on commit 793eedb

Please sign in to comment.