Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-commands-patching): add patch-remove command #6521

Merged
merged 1 commit into from May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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