@pnpm/patching.commands 1000.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2016 Rico Sta. Cruz and other contributors
4
+ Copyright (c) 2016-2026 Zoltan Kochan and other contributors
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @pnpm/plugin-commands-patches
2
+
3
+ > Commands for creating patches
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ pnpm add @pnpm/plugin-commands-patching
9
+ ```
10
+
11
+ ## License
12
+
13
+ MIT
@@ -0,0 +1,5 @@
1
+ import type { ParseWantedDependencyResult } from '@pnpm/resolving.parse-wanted-dependency';
2
+ export interface GetEditDirOptions {
3
+ modulesDir: string;
4
+ }
5
+ export declare function getEditDirPath(param: string, patchedDep: ParseWantedDependencyResult, opts: GetEditDirOptions): string;
@@ -0,0 +1,16 @@
1
+ import path from 'node:path';
2
+ export function getEditDirPath(param, patchedDep, opts) {
3
+ const editDirName = getEditDirNameFromParsedDep(patchedDep) ?? param;
4
+ return path.join(opts.modulesDir, '.pnpm_patches', editDirName);
5
+ }
6
+ function getEditDirNameFromParsedDep(patchedDep) {
7
+ if (patchedDep.alias && patchedDep.bareSpecifier) {
8
+ const bareSpecifier = patchedDep.bareSpecifier.replace(/[\\/:*?"<>|]+/g, '+');
9
+ return `${patchedDep.alias}@${bareSpecifier}`;
10
+ }
11
+ if (patchedDep.alias) {
12
+ return patchedDep.alias;
13
+ }
14
+ return undefined;
15
+ }
16
+ //# sourceMappingURL=getEditDirPath.js.map
@@ -0,0 +1,21 @@
1
+ import type { Config } from '@pnpm/config.reader';
2
+ import { type ParseWantedDependencyResult } from '@pnpm/resolving.parse-wanted-dependency';
3
+ export type GetPatchedDependencyOptions = {
4
+ lockfileDir: string;
5
+ } & Pick<Config, 'virtualStoreDir' | 'modulesDir'>;
6
+ export type GetPatchedDependencyResult = ParseWantedDependencyResult & {
7
+ applyToAll: boolean;
8
+ };
9
+ export declare function getPatchedDependency(rawDependency: string, opts: GetPatchedDependencyOptions): Promise<GetPatchedDependencyResult>;
10
+ export declare function isPkgPrNewUrl(url: string): boolean;
11
+ export interface LockfileVersion {
12
+ gitTarballUrl?: string;
13
+ name: string;
14
+ peerDepGraphHash?: string;
15
+ version: string;
16
+ }
17
+ export interface LockfileVersionsList {
18
+ versions: LockfileVersion[];
19
+ preferredVersions: LockfileVersion[];
20
+ }
21
+ export declare function getVersionsFromLockfile(dep: ParseWantedDependencyResult, opts: GetPatchedDependencyOptions): Promise<LockfileVersionsList>;
@@ -0,0 +1,93 @@
1
+ import path from 'node:path';
2
+ import { PnpmError } from '@pnpm/error';
3
+ import { isGitHostedPkgUrl } from '@pnpm/fetching.pick-fetcher';
4
+ import { readCurrentLockfile } from '@pnpm/lockfile.fs';
5
+ import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
6
+ import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency';
7
+ import enquirer from 'enquirer';
8
+ import { realpathMissing } from 'realpath-missing';
9
+ import semver from 'semver';
10
+ export async function getPatchedDependency(rawDependency, opts) {
11
+ const dep = parseWantedDependency(rawDependency);
12
+ const { versions, preferredVersions } = await getVersionsFromLockfile(dep, opts);
13
+ if (!preferredVersions.length) {
14
+ throw new PnpmError('PATCH_VERSION_NOT_FOUND', `Can not find ${rawDependency} in project ${opts.lockfileDir}, ${versions.length ? `you can specify currently installed version: ${versions.map(({ version }) => version).join(', ')}.` : `did you forget to install ${rawDependency}?`}`);
15
+ }
16
+ dep.alias = dep.alias ?? rawDependency;
17
+ if (preferredVersions.length > 1) {
18
+ const { version, applyToAll } = await enquirer.prompt([{
19
+ type: 'select',
20
+ name: 'version',
21
+ message: 'Choose which version to patch',
22
+ choices: preferredVersions.map(preferred => ({
23
+ name: preferred.version,
24
+ message: preferred.version,
25
+ value: preferred.gitTarballUrl ?? preferred.version,
26
+ hint: preferred.gitTarballUrl ? 'Git Hosted' : undefined,
27
+ })),
28
+ result(selected) {
29
+ const selectedVersion = preferredVersions.find(preferred => preferred.version === selected);
30
+ return selectedVersion.gitTarballUrl ?? selected;
31
+ },
32
+ }, {
33
+ type: 'confirm',
34
+ name: 'applyToAll',
35
+ message: 'Apply this patch to all versions?',
36
+ }]);
37
+ return {
38
+ ...dep,
39
+ applyToAll,
40
+ bareSpecifier: version,
41
+ };
42
+ }
43
+ else {
44
+ const preferred = preferredVersions[0];
45
+ if (preferred.gitTarballUrl) {
46
+ return {
47
+ ...opts,
48
+ applyToAll: false,
49
+ bareSpecifier: preferred.gitTarballUrl,
50
+ };
51
+ }
52
+ return {
53
+ ...dep,
54
+ applyToAll: !dep.bareSpecifier,
55
+ bareSpecifier: preferred.version,
56
+ };
57
+ }
58
+ }
59
+ // https://github.com/stackblitz-labs/pkg.pr.new
60
+ // With pkg.pr.new, each of your commits and pull requests will trigger an instant preview release without publishing anything to NPM.
61
+ // This enables users to access features and bug-fixes without the need to wait for release cycles using npm or pull request merges.
62
+ // When a package is installed via pkg.pr.new and has never been published to npm,
63
+ // the version or name obtained is incorrect, and an error will occur when patching. We can treat it as a tarball url.
64
+ export function isPkgPrNewUrl(url) {
65
+ return url.startsWith('https://pkg.pr.new/');
66
+ }
67
+ export async function getVersionsFromLockfile(dep, opts) {
68
+ const modulesDir = await realpathMissing(path.join(opts.lockfileDir, opts.modulesDir ?? 'node_modules'));
69
+ const lockfile = await readCurrentLockfile(path.join(modulesDir, '.pnpm'), {
70
+ ignoreIncompatible: true,
71
+ }) ?? null;
72
+ if (!lockfile) {
73
+ throw new PnpmError('PATCH_NO_LOCKFILE', 'The modules directory is not ready for patching', {
74
+ hint: 'Run pnpm install first',
75
+ });
76
+ }
77
+ const pkgName = dep.alias && dep.bareSpecifier ? dep.alias : (dep.bareSpecifier ?? dep.alias);
78
+ const versions = Object.entries(lockfile.packages ?? {})
79
+ .map(([depPath, pkgSnapshot]) => {
80
+ const tarball = pkgSnapshot.resolution?.tarball ?? '';
81
+ return {
82
+ ...nameVerFromPkgSnapshot(depPath, pkgSnapshot),
83
+ gitTarballUrl: (isGitHostedPkgUrl(tarball) || isPkgPrNewUrl(tarball)) ? tarball : undefined,
84
+ };
85
+ })
86
+ .filter(({ name }) => name === pkgName)
87
+ .sort((v1, v2) => semver.compare(v1.version, v2.version));
88
+ return {
89
+ versions,
90
+ preferredVersions: versions.filter(({ version }) => dep.alias && dep.bareSpecifier ? semver.satisfies(version, dep.bareSpecifier) : true),
91
+ };
92
+ }
93
+ //# sourceMappingURL=getPatchedDependency.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import * as patch from './patch.js';
2
+ import * as patchCommit from './patchCommit.js';
3
+ import * as patchRemove from './patchRemove.js';
4
+ export { type PatchCommandOptions } from './patch.js';
5
+ export { type PatchRemoveCommandOptions } from './patchRemove.js';
6
+ export { patch, patchCommit, patchRemove };
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import * as patch from './patch.js';
2
+ import * as patchCommit from './patchCommit.js';
3
+ import * as patchRemove from './patchRemove.js';
4
+ export {} from './patch.js';
5
+ export {} from './patchRemove.js';
6
+ export { patch, patchCommit, patchRemove };
7
+ //# sourceMappingURL=index.js.map
package/lib/patch.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type Config } from '@pnpm/config.reader';
2
+ import type { LogBase } from '@pnpm/logger';
3
+ import type { CreateStoreControllerOptions } from '@pnpm/store.connection-manager';
4
+ export declare function rcOptionsTypes(): Record<string, unknown>;
5
+ export declare function cliOptionsTypes(): Record<string, unknown>;
6
+ export declare const shorthands: {
7
+ d: string;
8
+ };
9
+ export declare const commandNames: string[];
10
+ export declare function help(): string;
11
+ export type PatchCommandOptions = Pick<Config, 'dir' | 'patchedDependencies' | 'registries' | 'tag' | 'storeDir' | 'rootProjectManifest' | 'lockfileDir' | 'modulesDir' | 'virtualStoreDir' | 'sharedWorkspaceLockfile'> & CreateStoreControllerOptions & {
12
+ editDir?: string;
13
+ reporter?: (logObj: LogBase) => void;
14
+ ignoreExisting?: boolean;
15
+ };
16
+ export declare function handler(opts: PatchCommandOptions, params: string[]): Promise<string>;
package/lib/patch.js ADDED
@@ -0,0 +1,113 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { docsUrl } from '@pnpm/cli.utils';
4
+ import { types as allTypes } from '@pnpm/config.reader';
5
+ import { PnpmError } from '@pnpm/error';
6
+ import { applyPatchToDir } from '@pnpm/patching.apply-patch';
7
+ import chalk from 'chalk';
8
+ import isWindows from 'is-windows';
9
+ import { pick } from 'ramda';
10
+ import { renderHelp } from 'render-help';
11
+ import terminalLink from 'terminal-link';
12
+ import { getEditDirPath } from './getEditDirPath.js';
13
+ import { getPatchedDependency } from './getPatchedDependency.js';
14
+ import { writeEditDirState } from './stateFile.js';
15
+ import { writePackage } from './writePackage.js';
16
+ export function rcOptionsTypes() {
17
+ return pick([], allTypes);
18
+ }
19
+ export function cliOptionsTypes() {
20
+ return { ...rcOptionsTypes(), 'edit-dir': String, 'ignore-existing': Boolean };
21
+ }
22
+ export const shorthands = {
23
+ d: '--edit-dir',
24
+ };
25
+ export const commandNames = ['patch'];
26
+ export function help() {
27
+ return renderHelp({
28
+ description: 'Prepare a package for patching',
29
+ descriptionLists: [{
30
+ title: 'Options',
31
+ list: [
32
+ {
33
+ description: 'The package that needs to be modified will be extracted to this directory',
34
+ name: '--edit-dir',
35
+ },
36
+ {
37
+ description: 'Ignore existing patch files when patching',
38
+ name: '--ignore-existing',
39
+ },
40
+ ],
41
+ }],
42
+ url: docsUrl('patch'),
43
+ usages: ['pnpm patch <pkg name>@<version>'],
44
+ });
45
+ }
46
+ export async function handler(opts, params) {
47
+ if (opts.editDir && fs.existsSync(opts.editDir) && fs.readdirSync(opts.editDir).length > 0) {
48
+ throw new PnpmError('PATCH_EDIT_DIR_EXISTS', `The target directory already exists: '${opts.editDir}'`);
49
+ }
50
+ if (!params[0]) {
51
+ throw new PnpmError('MISSING_PACKAGE_NAME', '`pnpm patch` requires the package name');
52
+ }
53
+ const lockfileDir = opts.lockfileDir ?? opts.dir ?? process.cwd();
54
+ const patchedDep = await getPatchedDependency(params[0], {
55
+ lockfileDir,
56
+ modulesDir: opts.modulesDir,
57
+ virtualStoreDir: opts.virtualStoreDir,
58
+ });
59
+ const quote = isWindows() ? '"' : "'";
60
+ const modulesDir = path.join(lockfileDir, opts.modulesDir ?? 'node_modules');
61
+ const editDir = opts.editDir
62
+ ? path.resolve(opts.dir, opts.editDir)
63
+ : getEditDirPath(params[0], patchedDep, { modulesDir });
64
+ if (fs.existsSync(editDir) && fs.readdirSync(editDir).length !== 0) {
65
+ throw new PnpmError('EDIT_DIR_NOT_EMPTY', `The directory ${editDir} is not empty`, {
66
+ hint: 'Either run `pnpm patch-commit ' + quote + editDir + quote + '` to commit or delete it then run `pnpm patch` to recreate it',
67
+ });
68
+ }
69
+ await writePackage(patchedDep, editDir, opts);
70
+ writeEditDirState({
71
+ editDir,
72
+ modulesDir,
73
+ patchedPkg: params[0],
74
+ applyToAll: patchedDep.applyToAll,
75
+ });
76
+ if (!opts.ignoreExisting && opts.patchedDependencies) {
77
+ tryPatchWithExistingPatchFile({
78
+ patchedDep,
79
+ patchedDir: editDir,
80
+ patchedDependencies: opts.patchedDependencies,
81
+ lockfileDir,
82
+ });
83
+ }
84
+ return `Patch: You can now edit the package at:
85
+
86
+ ${terminalLink(chalk.blue(editDir), 'file://' + editDir, { fallback: false })}
87
+
88
+ To commit your changes, run:
89
+
90
+ ${chalk.green(`pnpm patch-commit ${quote}${editDir}${quote}`)}
91
+
92
+ `;
93
+ }
94
+ function tryPatchWithExistingPatchFile({ patchedDep: { applyToAll, alias, bareSpecifier }, patchedDir, patchedDependencies, lockfileDir, }) {
95
+ if (!alias)
96
+ return;
97
+ let existingPatchFile;
98
+ if (bareSpecifier) {
99
+ existingPatchFile = patchedDependencies[`${alias}@${bareSpecifier}`];
100
+ }
101
+ if (!existingPatchFile && applyToAll) {
102
+ existingPatchFile = patchedDependencies[alias];
103
+ }
104
+ if (!existingPatchFile) {
105
+ return;
106
+ }
107
+ const existingPatchFilePath = path.resolve(lockfileDir, existingPatchFile);
108
+ if (!fs.existsSync(existingPatchFilePath)) {
109
+ throw new PnpmError('PATCH_FILE_NOT_FOUND', `Unable to find patch file ${existingPatchFilePath}`);
110
+ }
111
+ applyPatchToDir({ patchedDir, patchFilePath: existingPatchFilePath });
112
+ }
113
+ //# sourceMappingURL=patch.js.map
@@ -0,0 +1,9 @@
1
+ import { type Config } from '@pnpm/config.reader';
2
+ import { install } from '@pnpm/installing.commands';
3
+ export declare const rcOptionsTypes: typeof cliOptionsTypes;
4
+ export declare function cliOptionsTypes(): Record<string, unknown>;
5
+ export declare const commandNames: string[];
6
+ export declare function help(): string;
7
+ type PatchCommitCommandOptions = install.InstallCommandOptions & Pick<Config, 'patchesDir' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'patchedDependencies'>;
8
+ export declare function handler(opts: PatchCommitCommandOptions, params: string[]): Promise<string | undefined>;
9
+ export {};
@@ -0,0 +1,216 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { docsUrl } from '@pnpm/cli.utils';
4
+ import { types as allTypes } from '@pnpm/config.reader';
5
+ import { createShortHash } from '@pnpm/crypto.hash';
6
+ import { PnpmError } from '@pnpm/error';
7
+ import { packlist } from '@pnpm/fs.packlist';
8
+ import { install } from '@pnpm/installing.commands';
9
+ import { globalWarn } from '@pnpm/logger';
10
+ import { readPackageJsonFromDir } from '@pnpm/pkg-manifest.reader';
11
+ import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency';
12
+ import { getStorePath } from '@pnpm/store.path';
13
+ import escapeStringRegexp from 'escape-string-regexp';
14
+ import { makeEmptyDir } from 'make-empty-dir';
15
+ import normalizePath from 'normalize-path';
16
+ import { equals, pick } from 'ramda';
17
+ import { renderHelp } from 'render-help';
18
+ import { safeExeca as execa } from 'safe-execa';
19
+ import { glob } from 'tinyglobby';
20
+ import { getVersionsFromLockfile } from './getPatchedDependency.js';
21
+ import { readEditDirState } from './stateFile.js';
22
+ import { updatePatchedDependencies } from './updatePatchedDependencies.js';
23
+ import { writePackage } from './writePackage.js';
24
+ export const rcOptionsTypes = cliOptionsTypes;
25
+ export function cliOptionsTypes() {
26
+ return pick(['patches-dir'], allTypes);
27
+ }
28
+ export const commandNames = ['patch-commit'];
29
+ export function help() {
30
+ return renderHelp({
31
+ description: 'Generate a patch out of a directory',
32
+ descriptionLists: [{
33
+ title: 'Options',
34
+ list: [
35
+ {
36
+ description: 'The generated patch file will be saved to this directory',
37
+ name: '--patches-dir',
38
+ },
39
+ ],
40
+ }],
41
+ url: docsUrl('patch-commit'),
42
+ usages: ['pnpm patch-commit <patchDir>'],
43
+ });
44
+ }
45
+ export async function handler(opts, params) {
46
+ const userDir = params[0];
47
+ const lockfileDir = (opts.lockfileDir ?? opts.dir ?? process.cwd());
48
+ const patchesDirName = normalizePath(path.normalize(opts.patchesDir ?? 'patches'));
49
+ const patchesDir = path.join(lockfileDir, patchesDirName);
50
+ const patchedPkgManifest = await readPackageJsonFromDir(userDir);
51
+ const editDir = path.resolve(opts.dir, userDir);
52
+ const stateValue = readEditDirState({
53
+ editDir,
54
+ modulesDir: path.join(lockfileDir, opts.modulesDir ?? 'node_modules'),
55
+ });
56
+ if (!stateValue) {
57
+ throw new PnpmError('INVALID_PATCH_DIR', `${userDir} is not a valid patch directory`, {
58
+ hint: 'A valid patch directory should be created by `pnpm patch`',
59
+ });
60
+ }
61
+ const { applyToAll } = stateValue;
62
+ const nameAndVersion = `${patchedPkgManifest.name}@${patchedPkgManifest.version}`;
63
+ const patchKey = applyToAll ? patchedPkgManifest.name : nameAndVersion;
64
+ let gitTarballUrl;
65
+ if (!applyToAll) {
66
+ gitTarballUrl = await getGitTarballUrlFromLockfile({
67
+ alias: patchedPkgManifest.name,
68
+ bareSpecifier: patchedPkgManifest.version || undefined,
69
+ }, {
70
+ lockfileDir,
71
+ modulesDir: opts.modulesDir,
72
+ virtualStoreDir: opts.virtualStoreDir,
73
+ });
74
+ }
75
+ const patchedPkg = parseWantedDependency(gitTarballUrl ? `${patchedPkgManifest.name}@${gitTarballUrl}` : nameAndVersion);
76
+ const patchedPkgDir = await preparePkgFilesForDiff(userDir);
77
+ const patchContent = await getPatchContent({
78
+ patchedPkg,
79
+ patchedPkgDir,
80
+ tmpName: createShortHash(editDir),
81
+ }, opts);
82
+ if (patchedPkgDir !== userDir) {
83
+ fs.rmSync(patchedPkgDir, { recursive: true });
84
+ }
85
+ if (!patchContent.length) {
86
+ return `No changes were found to the following directory: ${userDir}`;
87
+ }
88
+ await fs.promises.mkdir(patchesDir, { recursive: true });
89
+ const patchFileName = patchKey.replace('/', '__');
90
+ await fs.promises.writeFile(path.join(patchesDir, `${patchFileName}.patch`), patchContent, 'utf8');
91
+ const patchedDependencies = {
92
+ ...opts.patchedDependencies,
93
+ [patchKey]: `${patchesDirName}/${patchFileName}.patch`,
94
+ };
95
+ await updatePatchedDependencies(patchedDependencies, {
96
+ ...opts,
97
+ workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
98
+ });
99
+ return install.handler({
100
+ ...opts,
101
+ patchedDependencies,
102
+ rawLocalConfig: {
103
+ ...opts.rawLocalConfig,
104
+ 'frozen-lockfile': false,
105
+ },
106
+ });
107
+ }
108
+ async function getPatchContent(ctx, opts) {
109
+ const storeDir = await getStorePath({
110
+ pkgRoot: opts.dir,
111
+ storePath: opts.storeDir,
112
+ pnpmHomeDir: opts.pnpmHomeDir,
113
+ });
114
+ const srcDir = path.join(storeDir, 'tmp', 'patch-commit', ctx.tmpName);
115
+ await writePackage(ctx.patchedPkg, srcDir, opts);
116
+ const patchContent = await diffFolders(srcDir, ctx.patchedPkgDir);
117
+ try {
118
+ fs.rmSync(srcDir, { recursive: true });
119
+ }
120
+ catch (error) {
121
+ globalWarn(`Failed to clean up temporary directory at ${srcDir} with error: ${String(error)}`);
122
+ }
123
+ return patchContent;
124
+ }
125
+ async function diffFolders(folderA, folderB) {
126
+ const folderAN = folderA.replace(/\\/g, '/');
127
+ const folderBN = folderB.replace(/\\/g, '/');
128
+ let stdout;
129
+ let stderr;
130
+ try {
131
+ const result = await execa('git', ['-c', 'core.safecrlf=false', 'diff', '--src-prefix=a/', '--dst-prefix=b/', '--ignore-cr-at-eol', '--irreversible-delete', '--full-index', '--no-index', '--text', '--no-ext-diff', '--no-color', folderAN, folderBN], {
132
+ cwd: process.cwd(),
133
+ env: {
134
+ ...process.env,
135
+ // #region Predictable output
136
+ // These variables aim to ignore the global git config so we get predictable output
137
+ // https://git-scm.com/docs/git#Documentation/git.txt-codeGITCONFIGNOSYSTEMcode
138
+ GIT_CONFIG_NOSYSTEM: '1',
139
+ // Redirect the global git config to /dev/null instead of setting
140
+ // HOME to an empty string. An empty HOME causes git to resolve '~' as
141
+ // '/' (root), which triggers a "Permission denied" warning when git
142
+ // tries to access '/.config/git/attributes', making pnpm throw an
143
+ // error because any stderr output is treated as a failure.
144
+ // We do not set XDG_CONFIG_HOME to avoid the same issue: an empty
145
+ // value would make git resolve paths like /git/config and /git/attributes.
146
+ // We use '/dev/null' literally instead of os.devNull because on Windows
147
+ // os.devNull is '\\.\nul', which git cannot open as a config file path
148
+ // (fatal: unable to access '\\.\nul': Invalid argument). Git for Windows
149
+ // translates '/dev/null' correctly via its MSYS2 layer.
150
+ GIT_CONFIG_GLOBAL: '/dev/null',
151
+ // #endregion
152
+ },
153
+ stripFinalNewline: false,
154
+ });
155
+ stdout = result.stdout;
156
+ stderr = result.stderr;
157
+ }
158
+ catch (err) { // eslint-disable-line
159
+ stdout = err.stdout;
160
+ stderr = err.stderr;
161
+ }
162
+ // we cannot rely on exit code, because --no-index implies --exit-code
163
+ // i.e. git diff will exit with 1 if there were differences
164
+ if (stderr.length > 0)
165
+ throw new Error(`Unable to diff directories. Make sure you have a recent version of 'git' available in PATH.\nThe following error was reported by 'git':\n${stderr}`);
166
+ return stdout
167
+ .replace(new RegExp(`(a|b)(${escapeStringRegexp(`/${removeTrailingAndLeadingSlash(folderAN)}/`)})`, 'g'), '$1/')
168
+ .replace(new RegExp(`(a|b)${escapeStringRegexp(`/${removeTrailingAndLeadingSlash(folderBN)}/`)}`, 'g'), '$1/')
169
+ .replace(new RegExp(escapeStringRegexp(`${folderAN}/`), 'g'), '')
170
+ .replace(new RegExp(escapeStringRegexp(`${folderBN}/`), 'g'), '')
171
+ .replace(/\n\\n$/, '\n')
172
+ .replace(/^diff --git a\/.*\.DS_Store b\/.*\.DS_Store[\s\S]+?(?=^diff --git)/gm, '')
173
+ .replace(/^diff --git a\/.*\.DS_Store b\/.*\.DS_Store[\s\S]*$/gm, '');
174
+ }
175
+ function removeTrailingAndLeadingSlash(p) {
176
+ if (p[0] === '/' || p.endsWith('/')) {
177
+ return p.replace(/^\/|\/$/g, '');
178
+ }
179
+ return p;
180
+ }
181
+ /**
182
+ * Link files from the source directory to a new temporary directory,
183
+ * but only if not all files in the source directory should be included in the package.
184
+ * If all files should be included, return the original source directory without creating any links.
185
+ * This is required in order for the diff to not include files that are not part of the package.
186
+ */
187
+ async function preparePkgFilesForDiff(src) {
188
+ const files = Array.from(new Set((await packlist(src)).map((f) => path.join(f))));
189
+ // If there are no extra files in the source directories, then there is no reason
190
+ // to copy.
191
+ if (await areAllFilesInPkg(files, src)) {
192
+ return src;
193
+ }
194
+ const dest = `${src}_tmp`;
195
+ await makeEmptyDir(dest);
196
+ await Promise.all(files.map(async (file) => {
197
+ const srcFile = path.join(src, file);
198
+ const destFile = path.join(dest, file);
199
+ const destDir = path.dirname(destFile);
200
+ await fs.promises.mkdir(destDir, { recursive: true });
201
+ await fs.promises.link(srcFile, destFile);
202
+ }));
203
+ return dest;
204
+ }
205
+ async function areAllFilesInPkg(files, basePath) {
206
+ const allFiles = await glob('**', {
207
+ cwd: basePath,
208
+ expandDirectories: false,
209
+ });
210
+ return equals(allFiles.sort(), files.sort());
211
+ }
212
+ async function getGitTarballUrlFromLockfile(dep, opts) {
213
+ const { preferredVersions } = await getVersionsFromLockfile(dep, opts);
214
+ return preferredVersions[0]?.gitTarballUrl;
215
+ }
216
+ //# sourceMappingURL=patchCommit.js.map
@@ -0,0 +1,8 @@
1
+ import { type Config } from '@pnpm/config.reader';
2
+ import { install } from '@pnpm/installing.commands';
3
+ export declare function rcOptionsTypes(): Record<string, unknown>;
4
+ export declare function cliOptionsTypes(): Record<string, unknown>;
5
+ export declare const commandNames: string[];
6
+ export declare function help(): string;
7
+ export type PatchRemoveCommandOptions = install.InstallCommandOptions & Pick<Config, 'dir' | 'lockfileDir' | 'patchesDir' | 'rootProjectManifest' | 'patchedDependencies'>;
8
+ export declare function handler(opts: PatchRemoveCommandOptions, params: string[]): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { docsUrl } from '@pnpm/cli.utils';
4
+ import { types as allTypes } from '@pnpm/config.reader';
5
+ import { PnpmError } from '@pnpm/error';
6
+ import { install } from '@pnpm/installing.commands';
7
+ import enquirer from 'enquirer';
8
+ import { pick } from 'ramda';
9
+ import { renderHelp } from 'render-help';
10
+ import { updatePatchedDependencies } from './updatePatchedDependencies.js';
11
+ export function rcOptionsTypes() {
12
+ return pick([], allTypes);
13
+ }
14
+ export function cliOptionsTypes() {
15
+ return { ...rcOptionsTypes() };
16
+ }
17
+ export const commandNames = ['patch-remove'];
18
+ export function help() {
19
+ return renderHelp({
20
+ description: 'Remove existing patch files',
21
+ url: docsUrl('patch-remove'),
22
+ usages: ['pnpm patch-remove [pkg...]'],
23
+ });
24
+ }
25
+ export async function handler(opts, params) {
26
+ let patchesToRemove = params;
27
+ const patchedDependencies = opts.patchedDependencies ?? {};
28
+ if (!params.length) {
29
+ const allPatches = Object.keys(patchedDependencies);
30
+ if (allPatches.length) {
31
+ ({ patches: patchesToRemove } = await enquirer.prompt({
32
+ type: 'multiselect',
33
+ name: 'patches',
34
+ message: 'Select the patch to be removed',
35
+ choices: allPatches,
36
+ validate(value) {
37
+ return value.length === 0 ? 'Select at least one option.' : true;
38
+ },
39
+ }));
40
+ }
41
+ }
42
+ if (!patchesToRemove.length) {
43
+ throw new PnpmError('NO_PATCHES_TO_REMOVE', 'There are no patches that need to be removed');
44
+ }
45
+ for (const patch of patchesToRemove) {
46
+ if (!Object.hasOwn(patchedDependencies, patch)) {
47
+ throw new PnpmError('PATCH_NOT_FOUND', `Patch "${patch}" not found in patched dependencies`);
48
+ }
49
+ }
50
+ const patchesDirs = new Set();
51
+ await Promise.all(patchesToRemove.map(async (patch) => {
52
+ if (Object.hasOwn(patchedDependencies, patch)) {
53
+ const patchFile = patchedDependencies[patch];
54
+ patchesDirs.add(path.dirname(patchFile));
55
+ await fs.rm(patchFile, { force: true });
56
+ delete patchedDependencies[patch];
57
+ }
58
+ }));
59
+ await Promise.all(Array.from(patchesDirs).map(async (dir) => {
60
+ try {
61
+ const files = await fs.readdir(dir);
62
+ if (!files.length) {
63
+ await fs.rmdir(dir);
64
+ }
65
+ }
66
+ catch { }
67
+ }));
68
+ await updatePatchedDependencies(patchedDependencies, {
69
+ ...opts,
70
+ workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
71
+ });
72
+ return install.handler(opts);
73
+ }
74
+ //# sourceMappingURL=patchRemove.js.map
@@ -0,0 +1,18 @@
1
+ export type EditDir = string & {
2
+ __brand: 'patch-edit-dir';
3
+ };
4
+ export interface EditDirState {
5
+ patchedPkg: string;
6
+ applyToAll: boolean;
7
+ }
8
+ export type State = Record<EditDir, EditDirState>;
9
+ export interface EditDirKeyInput {
10
+ editDir: string;
11
+ }
12
+ export interface ReadEditDirStateOptions extends EditDirKeyInput {
13
+ modulesDir: string;
14
+ }
15
+ export declare function readEditDirState(opts: ReadEditDirStateOptions): EditDirState | undefined;
16
+ export interface WriteEditDirStateOptions extends ReadEditDirStateOptions, EditDirState {
17
+ }
18
+ export declare function writeEditDirState(opts: WriteEditDirStateOptions): void;
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import util from 'node:util';
4
+ const createEditDirKey = (opts) => opts.editDir;
5
+ export function readEditDirState(opts) {
6
+ const state = readStateFile(opts.modulesDir);
7
+ if (!state)
8
+ return undefined;
9
+ const key = createEditDirKey(opts);
10
+ return state[key];
11
+ }
12
+ export function writeEditDirState(opts) {
13
+ modifyStateFile(opts.modulesDir, state => {
14
+ const key = createEditDirKey(opts);
15
+ state[key] = {
16
+ patchedPkg: opts.patchedPkg,
17
+ applyToAll: opts.applyToAll,
18
+ };
19
+ });
20
+ }
21
+ function modifyStateFile(modulesDir, modifyState) {
22
+ const filePath = getStateFilePath(modulesDir);
23
+ let state = readStateFile(modulesDir);
24
+ if (!state) {
25
+ state = {};
26
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
27
+ }
28
+ modifyState(state);
29
+ fs.writeFileSync(filePath, JSON.stringify(state, undefined, 2));
30
+ }
31
+ function readStateFile(modulesDir) {
32
+ let fileContent;
33
+ try {
34
+ fileContent = fs.readFileSync(getStateFilePath(modulesDir), 'utf-8');
35
+ }
36
+ catch (err) {
37
+ if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
38
+ return undefined;
39
+ }
40
+ throw err;
41
+ }
42
+ return JSON.parse(fileContent);
43
+ }
44
+ function getStateFilePath(modulesDir) {
45
+ return path.join(modulesDir, '.pnpm_patches', 'state.json');
46
+ }
47
+ //# sourceMappingURL=stateFile.js.map
@@ -0,0 +1,2 @@
1
+ import { type WriteSettingsOptions } from '@pnpm/config.writer';
2
+ export declare function updatePatchedDependencies(patchedDependencies: Record<string, string>, opts: Omit<WriteSettingsOptions, 'updatedSettings'>): Promise<void>;
@@ -0,0 +1,19 @@
1
+ import path from 'node:path';
2
+ import { writeSettings } from '@pnpm/config.writer';
3
+ import normalizePath from 'normalize-path';
4
+ export async function updatePatchedDependencies(patchedDependencies, opts) {
5
+ const workspaceDir = opts.workspaceDir ?? opts.rootProjectManifestDir;
6
+ for (const [patchName, patchPath] of Object.entries(patchedDependencies)) {
7
+ if (path.isAbsolute(patchPath)) {
8
+ patchedDependencies[patchName] = normalizePath(path.relative(workspaceDir, patchPath));
9
+ }
10
+ }
11
+ await writeSettings({
12
+ ...opts,
13
+ workspaceDir,
14
+ updatedSettings: {
15
+ patchedDependencies: Object.keys(patchedDependencies).length ? patchedDependencies : undefined,
16
+ },
17
+ });
18
+ }
19
+ //# sourceMappingURL=updatePatchedDependencies.js.map
@@ -0,0 +1,5 @@
1
+ import type { Config } from '@pnpm/config.reader';
2
+ import type { ParseWantedDependencyResult } from '@pnpm/resolving.parse-wanted-dependency';
3
+ import { type CreateStoreControllerOptions } from '@pnpm/store.connection-manager';
4
+ export type WritePackageOptions = CreateStoreControllerOptions & Pick<Config, 'registries'>;
5
+ export declare function writePackage(dep: ParseWantedDependencyResult, dest: string, opts: WritePackageOptions): Promise<void>;
@@ -0,0 +1,19 @@
1
+ import { createStoreController, } from '@pnpm/store.connection-manager';
2
+ export async function writePackage(dep, dest, opts) {
3
+ const store = await createStoreController({
4
+ ...opts,
5
+ packageImportMethod: 'clone-or-copy',
6
+ });
7
+ const pkgResponse = await store.ctrl.requestPackage(dep, {
8
+ downloadPriority: 1,
9
+ lockfileDir: opts.dir,
10
+ preferredVersions: {},
11
+ projectDir: opts.dir,
12
+ });
13
+ const { files } = await pkgResponse.fetching();
14
+ await store.ctrl.importPackage(dest, {
15
+ filesResponse: files,
16
+ force: true,
17
+ });
18
+ }
19
+ //# sourceMappingURL=writePackage.js.map
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@pnpm/patching.commands",
3
+ "version": "1000.3.21",
4
+ "description": "Commands for creating patches",
5
+ "keywords": [
6
+ "pnpm",
7
+ "pnpm11",
8
+ "scripts"
9
+ ],
10
+ "license": "MIT",
11
+ "funding": "https://opencollective.com/pnpm",
12
+ "repository": "https://github.com/pnpm/pnpm/tree/main/patching/commands",
13
+ "homepage": "https://github.com/pnpm/pnpm/tree/main/patching/commands#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/pnpm/pnpm/issues"
16
+ },
17
+ "type": "module",
18
+ "main": "lib/index.js",
19
+ "types": "lib/index.d.ts",
20
+ "exports": {
21
+ ".": "./lib/index.js"
22
+ },
23
+ "files": [
24
+ "lib",
25
+ "!*.map"
26
+ ],
27
+ "dependencies": {
28
+ "chalk": "^5.6.0",
29
+ "enquirer": "^2.4.1",
30
+ "escape-string-regexp": "^5.0.0",
31
+ "is-windows": "^1.0.2",
32
+ "make-empty-dir": "^4.0.0",
33
+ "normalize-path": "^3.0.0",
34
+ "ramda": "npm:@pnpm/ramda@0.28.1",
35
+ "realpath-missing": "^2.0.0",
36
+ "render-help": "^2.0.0",
37
+ "safe-execa": "^0.3.0",
38
+ "semver": "^7.7.2",
39
+ "terminal-link": "^4.0.0",
40
+ "tinyglobby": "^0.2.14",
41
+ "@pnpm/config.reader": "1004.4.2",
42
+ "@pnpm/config.writer": "1000.0.14",
43
+ "@pnpm/constants": "1001.3.1",
44
+ "@pnpm/crypto.hash": "1000.2.1",
45
+ "@pnpm/error": "1000.0.5",
46
+ "@pnpm/cli.utils": "1001.2.8",
47
+ "@pnpm/fetching.pick-fetcher": "1001.0.0",
48
+ "@pnpm/fs.packlist": "1000.0.0",
49
+ "@pnpm/installing.modules-yaml": "1000.3.6",
50
+ "@pnpm/lockfile.fs": "1001.1.21",
51
+ "@pnpm/installing.commands": "1004.6.10",
52
+ "@pnpm/lockfile.utils": "1003.0.3",
53
+ "@pnpm/patching.apply-patch": "1000.0.7",
54
+ "@pnpm/pkg-manifest.reader": "1000.1.2",
55
+ "@pnpm/resolving.parse-wanted-dependency": "1001.0.0",
56
+ "@pnpm/store.connection-manager": "1002.2.4",
57
+ "@pnpm/store.path": "1000.0.5",
58
+ "@pnpm/types": "1000.9.0",
59
+ "@pnpm/workspace.project-manifest-reader": "1001.1.4",
60
+ "@pnpm/workspace.workspace-manifest-reader": "1000.2.5"
61
+ },
62
+ "peerDependencies": {
63
+ "@pnpm/logger": ">=1001.0.0 <1002.0.0"
64
+ },
65
+ "devDependencies": {
66
+ "@jest/globals": "30.0.5",
67
+ "@pnpm/registry-mock": "5.2.4",
68
+ "@types/is-windows": "^1.0.2",
69
+ "@types/normalize-path": "^3.0.2",
70
+ "@types/ramda": "0.29.12",
71
+ "@types/semver": "7.7.1",
72
+ "tempy": "3.0.0",
73
+ "write-yaml-file": "^6.0.0",
74
+ "@pnpm/logger": "1001.0.1",
75
+ "@pnpm/patching.commands": "1000.3.21",
76
+ "@pnpm/prepare": "1000.0.4",
77
+ "@pnpm/workspace.projects-filter": "1000.0.43",
78
+ "@pnpm/test-fixtures": "1000.0.0"
79
+ },
80
+ "engines": {
81
+ "node": ">=22.13"
82
+ },
83
+ "jest": {
84
+ "preset": "@pnpm/jest-config/with-registry"
85
+ },
86
+ "scripts": {
87
+ "start": "tsgo --watch",
88
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
89
+ "_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest",
90
+ "test": "pnpm run compile && pnpm run _test",
91
+ "compile": "tsgo --build && pnpm run lint --fix"
92
+ }
93
+ }