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-command-patching): add path option to patch command #5304

Merged
merged 10 commits into from Sep 4, 2022
6 changes: 6 additions & 0 deletions .changeset/funny-buses-own.md
@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-patching": minor
"pnpm": minor
---

`pnpm patch`: edit the patched package in a directory specified by the `--edit-dir` option. E.g., `pnpm patch express@3.1.0 --edit-dir=/home/xxx/src/patched-express`
1 change: 1 addition & 0 deletions packages/plugin-commands-patching/package.json
Expand Up @@ -42,6 +42,7 @@
"dependencies": {
"@pnpm/cli-utils": "workspace:*",
"@pnpm/config": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/parse-wanted-dependency": "workspace:*",
"@pnpm/pick-registry-for-package": "workspace:*",
"@pnpm/plugin-commands-installation": "workspace:*",
Expand Down
40 changes: 34 additions & 6 deletions packages/plugin-commands-patching/src/patch.ts
@@ -1,32 +1,52 @@
import fs from 'fs'
import path from 'path'
import { docsUrl } from '@pnpm/cli-utils'
import { Config, types as allTypes } from '@pnpm/config'
import { LogBase } from '@pnpm/logger'
import { createOrConnectStoreController, CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
import {
createOrConnectStoreController,
CreateStoreControllerOptions,
} from '@pnpm/store-connection-manager'
import parseWantedDependency from '@pnpm/parse-wanted-dependency'
import pick from 'ramda/src/pick'
import pickRegistryForPackage from '@pnpm/pick-registry-for-package'
import renderHelp from 'render-help'
import tempy from 'tempy'
import PnpmError from '@pnpm/error'

export const rcOptionsTypes = cliOptionsTypes
export function rcOptionsTypes () {
return pick([], allTypes)
}

export function cliOptionsTypes () {
return pick([], allTypes)
return { ...rcOptionsTypes(), 'edit-dir': String }
}

export const shorthands = {
d: '--edit-dir',
}

export const commandNames = ['patch']

export function help () {
return renderHelp({
description: 'Prepare a package for patching',
descriptionLists: [],
descriptionLists: [{
title: 'Options',
list: [
{
description: 'The package that needs to be modified will be extracted to this directory',
name: '--edit-dir',
},
],
}],
url: docsUrl('patch'),
usages: ['pnpm patch'],
usages: ['pnpm patch <pkg name>@<version>'],
})
}

export type PatchCommandOptions = Pick<Config, 'dir' | 'registries' | 'tag' | 'storeDir'> & CreateStoreControllerOptions & {
editDir?: string
reporter?: (logObj: LogBase) => void
}

Expand All @@ -45,7 +65,7 @@ export async function handler (opts: PatchCommandOptions, params: string[]) {
})
const filesResponse = await pkgResponse.files!()
const tempDir = tempy.directory()
const userChangesDir = path.join(tempDir, 'user')
const userChangesDir = opts.editDir ? createPackageDirectory(opts.editDir) : path.join(tempDir, 'user')
await Promise.all([
store.ctrl.importPackage(path.join(tempDir, 'source'), {
filesResponse,
Expand All @@ -58,3 +78,11 @@ export async function handler (opts: PatchCommandOptions, params: string[]) {
])
return `You can now edit the following folder: ${userChangesDir}`
}

function createPackageDirectory (editDir: string) {
if (fs.existsSync(editDir)) {
throw new PnpmError('PATCH_EDIT_DIR_EXISTS', `The target directory already exists: '${editDir}'`)
}
fs.mkdirSync(editDir, { recursive: true })
return editDir
}
116 changes: 74 additions & 42 deletions packages/plugin-commands-patching/test/patch.test.ts
@@ -1,54 +1,86 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
import prepare from '@pnpm/prepare'
import tempy from 'tempy'
import { patch, patchCommit } 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'

test('patch and commit', async () => {
prepare({
dependencies: {
'is-positive': '1.0.0',
},
describe('patch and commit', () => {
let defaultPatchOption: patch.PatchCommandOptions
const tempySpy = jest.spyOn(tempy, 'directory')

beforeEach(() => {
prepare({
dependencies: {
'is-positive': '1.0.0',
},
})

const cacheDir = path.resolve('cache')
const storeDir = path.resolve('store')

defaultPatchOption = {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
rawConfig: {
registry: `http://localhost:${REGISTRY_MOCK_PORT}/`,
},
registries: { default: `http://localhost:${REGISTRY_MOCK_PORT}/` },
storeDir,
userConfig: {},
}
})
const cacheDir = path.resolve('cache')
const storeDir = path.resolve('store')

const output = await patch.handler({
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
rawConfig: {
registry: `http://localhost:${REGISTRY_MOCK_PORT}/`,
},
registries: { default: `http://localhost:${REGISTRY_MOCK_PORT}/` },
storeDir,
userConfig: {},
}, ['is-positive@1.0.0'])

const userPatchDir = output.substring(output.indexOf(':') + 1).trim()

// sanity check to ensure that the license file contains the expected string
expect(fs.readFileSync(path.join(userPatchDir, 'license'), 'utf8')).toContain('The MIT License (MIT)')

fs.appendFileSync(path.join(userPatchDir, 'index.js'), '// test patching', 'utf8')
fs.unlinkSync(path.join(userPatchDir, 'license'))

await patchCommit.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, [userPatchDir])

const { manifest } = await readProjectManifest(process.cwd())
expect(manifest.pnpm?.patchedDependencies).toStrictEqual({
'is-positive@1.0.0': 'patches/is-positive@1.0.0.patch',

test('patch and commit', async () => {
const output = await patch.handler(defaultPatchOption, ['is-positive@1.0.0'])
const userPatchDir = output.substring(output.indexOf(':') + 1).trim()
const tempDir = os.tmpdir() // temp dir depends on the operating system (@see tempy)

// store patch files(user, source) in temporary directory when not given editDir option
expect(userPatchDir).toContain(tempDir)
expect(fs.existsSync(userPatchDir)).toBe(true)
expect(fs.existsSync(userPatchDir.replace('/user', '/source'))).toBe(true)

// sanity check to ensure that the license file contains the expected string
expect(fs.readFileSync(path.join(userPatchDir, 'license'), 'utf8')).toContain('The MIT License (MIT)')

fs.appendFileSync(path.join(userPatchDir, 'index.js'), '// test patching', 'utf8')
fs.unlinkSync(path.join(userPatchDir, 'license'))

await patchCommit.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
}, [userPatchDir])

const { manifest } = await readProjectManifest(process.cwd())
expect(manifest.pnpm?.patchedDependencies).toStrictEqual({
'is-positive@1.0.0': 'patches/is-positive@1.0.0.patch',
})
const patchContent = fs.readFileSync('patches/is-positive@1.0.0.patch', 'utf8')
expect(patchContent).toContain('diff --git')
expect(patchContent).toContain('// test patching')
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).toContain('// test patching')

expect(patchContent).not.toContain('The MIT License (MIT)')
expect(fs.existsSync('node_modules/is-positive/license')).toBe(false)
})
const patchContent = fs.readFileSync('patches/is-positive@1.0.0.patch', 'utf8')
expect(patchContent).toContain('diff --git')
expect(patchContent).toContain('// test patching')
expect(fs.readFileSync('node_modules/is-positive/index.js', 'utf8')).toContain('// test patching')

expect(patchContent).not.toContain('The MIT License (MIT)')
expect(fs.existsSync('node_modules/is-positive/license')).toBe(false)
test('store source files in temporary directory and user files in user directory, when given editDir option', async () => {
const editDir = 'test/user/is-positive'

const patchFn = async () => patch.handler({ ...defaultPatchOption, editDir }, ['is-positive@1.0.0'])
const output = await patchFn()
const userPatchDir = output.substring(output.indexOf(':') + 1).trim()

expect(userPatchDir).toBe(editDir)
expect(fs.existsSync(userPatchDir)).toBe(true)
expect(fs.existsSync(path.join(tempySpy.mock.results[0].value, '/source'))).toBe(true)

// If editDir already exists, it should throw an error
await expect(patchFn()).rejects.toThrow(`The target directory already exists: '${editDir}'`)
})
})
3 changes: 3 additions & 0 deletions packages/plugin-commands-patching/tsconfig.json
Expand Up @@ -18,6 +18,9 @@
{
"path": "../config"
},
{
"path": "../error"
},
{
"path": "../parse-wanted-dependency"
},
Expand Down
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.