@pnpm/global.commands 1100.0.30 → 1100.0.32
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/lib/binOwnership.d.ts +10 -0
- package/lib/binOwnership.js +21 -0
- package/lib/checkGlobalBinConflicts.js +11 -1
- package/lib/globalAdd.d.ts +3 -0
- package/lib/globalAdd.js +31 -6
- package/lib/globalRemove.js +7 -1
- package/lib/globalUpdate.js +7 -2
- package/lib/readInstalledPackages.js +2 -1
- package/package.json +12 -12
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The set of bin names provided by global package groups *other* than those
|
|
3
|
+
* in `excludeHashes`.
|
|
4
|
+
*
|
|
5
|
+
* Used before unlinking a group's bins (on remove / update / replace) so
|
|
6
|
+
* that a bin name shared with — and owned by — another still-installed
|
|
7
|
+
* group is never removed. Without this, removing one group could delete a
|
|
8
|
+
* bin that actually belongs to a different global package.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getBinNamesOfOtherGroups(globalDir: string, excludeHashes: Set<string>): Promise<Set<string>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getInstalledBinNames, scanGlobalPackages } from '@pnpm/global.packages';
|
|
2
|
+
/**
|
|
3
|
+
* The set of bin names provided by global package groups *other* than those
|
|
4
|
+
* in `excludeHashes`.
|
|
5
|
+
*
|
|
6
|
+
* Used before unlinking a group's bins (on remove / update / replace) so
|
|
7
|
+
* that a bin name shared with — and owned by — another still-installed
|
|
8
|
+
* group is never removed. Without this, removing one group could delete a
|
|
9
|
+
* bin that actually belongs to a different global package.
|
|
10
|
+
*/
|
|
11
|
+
export async function getBinNamesOfOtherGroups(globalDir, excludeHashes) {
|
|
12
|
+
const others = scanGlobalPackages(globalDir).filter((pkg) => !excludeHashes.has(pkg.hash));
|
|
13
|
+
const names = new Set();
|
|
14
|
+
await Promise.all(others.map(async (pkg) => {
|
|
15
|
+
for (const name of await getInstalledBinNames(pkg)) {
|
|
16
|
+
names.add(name);
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
return names;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=binOwnership.js.map
|
|
@@ -28,7 +28,7 @@ export async function checkGlobalBinConflicts(opts) {
|
|
|
28
28
|
if (newBinOwners.size === 0)
|
|
29
29
|
return binsToSkip;
|
|
30
30
|
// Quick check: only investigate if a bin with the same name already exists
|
|
31
|
-
const conflicting = new Set([...newBinOwners.keys()].filter((name) =>
|
|
31
|
+
const conflicting = new Set([...newBinOwners.keys()].filter((name) => binSlotExists(opts.globalBinDir, name)));
|
|
32
32
|
if (conflicting.size === 0)
|
|
33
33
|
return binsToSkip;
|
|
34
34
|
// Some bins already exist — find out if they belong to packages being replaced
|
|
@@ -72,4 +72,14 @@ export async function checkGlobalBinConflicts(opts) {
|
|
|
72
72
|
}
|
|
73
73
|
return binsToSkip;
|
|
74
74
|
}
|
|
75
|
+
// Whether a bin named `name` already occupies a slot in `globalBinDir`. On
|
|
76
|
+
// Windows the `node` runtime bin is linked as `<name>.exe` (with no bare
|
|
77
|
+
// `<name>` file), so that flavor is checked too — otherwise an existing
|
|
78
|
+
// `node.exe` would not be detected as a conflict and could be silently
|
|
79
|
+
// overwritten.
|
|
80
|
+
function binSlotExists(globalBinDir, name) {
|
|
81
|
+
if (fs.existsSync(path.join(globalBinDir, name)))
|
|
82
|
+
return true;
|
|
83
|
+
return process.platform === 'win32' && fs.existsSync(path.join(globalBinDir, `${name}.exe`));
|
|
84
|
+
}
|
|
75
85
|
//# sourceMappingURL=checkGlobalBinConflicts.js.map
|
package/lib/globalAdd.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommandHandlerMap } from '@pnpm/cli.command';
|
|
2
|
+
import { type GlobalPackageInfo } from '@pnpm/global.packages';
|
|
2
3
|
import type { CreateStoreControllerOptions } from '@pnpm/store.connection-manager';
|
|
3
4
|
import { type ResolutionPolicyViolation } from './installGlobalPackages.js';
|
|
4
5
|
export type GlobalAddOptions = CreateStoreControllerOptions & {
|
|
@@ -17,3 +18,5 @@ export type GlobalAddOptions = CreateStoreControllerOptions & {
|
|
|
17
18
|
updateResolutionPolicyManifest?: (violations: readonly ResolutionPolicyViolation[], dir: string) => Promise<void>;
|
|
18
19
|
};
|
|
19
20
|
export declare function handleGlobalAdd(opts: GlobalAddOptions, params: string[], commands: CommandHandlerMap): Promise<void>;
|
|
21
|
+
export declare function getReplacementAliases(aliases: string[]): string[];
|
|
22
|
+
export declare function shouldReplaceExistingGlobalInstall(pkg: GlobalPackageInfo, aliases: string[], replacementAliases: string[]): boolean;
|
package/lib/globalAdd.js
CHANGED
|
@@ -7,6 +7,7 @@ import { cleanOrphanedInstallDirs, createGlobalCacheKey, createInstallDir, findG
|
|
|
7
7
|
import { readPackageJsonFromDirRawSync } from '@pnpm/pkg-manifest.reader';
|
|
8
8
|
import { isSubdir } from 'is-subdir';
|
|
9
9
|
import { symlinkDir } from 'symlink-dir';
|
|
10
|
+
import { getBinNamesOfOtherGroups } from './binOwnership.js';
|
|
10
11
|
import { checkGlobalBinConflicts } from './checkGlobalBinConflicts.js';
|
|
11
12
|
import { installGlobalPackages } from './installGlobalPackages.js';
|
|
12
13
|
import { promptApproveGlobalBuilds } from './promptApproveGlobalBuilds.js';
|
|
@@ -87,6 +88,7 @@ async function installGroup(ctx, commands) {
|
|
|
87
88
|
// Read resolved aliases from the installed package.json
|
|
88
89
|
const pkgJson = readPackageJsonFromDirRawSync(installDir);
|
|
89
90
|
const aliases = Object.keys(pkgJson.dependencies ?? {});
|
|
91
|
+
const replacementAliases = getReplacementAliases(aliases);
|
|
90
92
|
// Check for bin name conflicts with other global packages
|
|
91
93
|
// (must happen before removeExistingGlobalInstalls so we don't lose existing packages on failure)
|
|
92
94
|
const pkgs = await readInstalledPackages(installDir);
|
|
@@ -96,7 +98,7 @@ async function installGroup(ctx, commands) {
|
|
|
96
98
|
globalDir,
|
|
97
99
|
globalBinDir,
|
|
98
100
|
newPkgs: pkgs,
|
|
99
|
-
shouldSkip: (pkg) =>
|
|
101
|
+
shouldSkip: (pkg) => shouldReplaceExistingGlobalInstall(pkg, aliases, replacementAliases),
|
|
100
102
|
});
|
|
101
103
|
}
|
|
102
104
|
catch (err) {
|
|
@@ -104,7 +106,7 @@ async function installGroup(ctx, commands) {
|
|
|
104
106
|
throw err;
|
|
105
107
|
}
|
|
106
108
|
// Remove any existing global installations of these aliases
|
|
107
|
-
await removeExistingGlobalInstalls(globalDir, globalBinDir, aliases);
|
|
109
|
+
await removeExistingGlobalInstalls({ globalDir, globalBinDir, aliases, replacementAliases });
|
|
108
110
|
// Compute cache key and create hash symlink pointing to install dir
|
|
109
111
|
const cacheHash = createGlobalCacheKey({
|
|
110
112
|
aliases,
|
|
@@ -116,6 +118,21 @@ async function installGroup(ctx, commands) {
|
|
|
116
118
|
await linkBinsOfPackages(pkgs, globalBinDir, { excludeBins: binsToSkip });
|
|
117
119
|
await opts.updateResolutionPolicyManifest?.(resolutionPolicyViolations, globalDir);
|
|
118
120
|
}
|
|
121
|
+
const PNPM_CLI_PACKAGE_ALIASES = ['pnpm', '@pnpm/exe'];
|
|
122
|
+
export function getReplacementAliases(aliases) {
|
|
123
|
+
if (!aliases.some((alias) => PNPM_CLI_PACKAGE_ALIASES.includes(alias)))
|
|
124
|
+
return aliases;
|
|
125
|
+
return [...new Set([...aliases, ...PNPM_CLI_PACKAGE_ALIASES])];
|
|
126
|
+
}
|
|
127
|
+
export function shouldReplaceExistingGlobalInstall(pkg, aliases, replacementAliases) {
|
|
128
|
+
if (aliases.some((alias) => alias in pkg.dependencies))
|
|
129
|
+
return true;
|
|
130
|
+
return isPnpmCliOnlyGroup(pkg) && replacementAliases.some((alias) => alias in pkg.dependencies);
|
|
131
|
+
}
|
|
132
|
+
function isPnpmCliOnlyGroup(pkg) {
|
|
133
|
+
const aliases = Object.keys(pkg.dependencies);
|
|
134
|
+
return aliases.length > 0 && aliases.every((alias) => PNPM_CLI_PACKAGE_ALIASES.includes(alias));
|
|
135
|
+
}
|
|
119
136
|
function splitCommaSeparated(param, baseDir) {
|
|
120
137
|
if (!param.includes(','))
|
|
121
138
|
return [param];
|
|
@@ -172,19 +189,27 @@ function resolveLocalParam(param, baseDir) {
|
|
|
172
189
|
}
|
|
173
190
|
return param;
|
|
174
191
|
}
|
|
175
|
-
async function removeExistingGlobalInstalls(
|
|
192
|
+
async function removeExistingGlobalInstalls(opts) {
|
|
193
|
+
const { globalDir, globalBinDir, aliases, replacementAliases } = opts;
|
|
176
194
|
// Collect unique groups to remove (dedup by hash)
|
|
177
195
|
const groupsToRemove = new Map();
|
|
178
|
-
for (const alias of
|
|
196
|
+
for (const alias of replacementAliases) {
|
|
179
197
|
const existing = findGlobalPackage(globalDir, alias);
|
|
180
|
-
if (existing &&
|
|
198
|
+
if (existing &&
|
|
199
|
+
shouldReplaceExistingGlobalInstall(existing, aliases, replacementAliases) &&
|
|
200
|
+
!groupsToRemove.has(existing.hash)) {
|
|
181
201
|
groupsToRemove.set(existing.hash, getInstalledBinNames(existing));
|
|
182
202
|
}
|
|
183
203
|
}
|
|
204
|
+
// Bins owned by groups that survive this replacement must not be
|
|
205
|
+
// unlinked, or we'd delete a different global package's bin.
|
|
206
|
+
const protectedBins = await getBinNamesOfOtherGroups(globalDir, new Set(groupsToRemove.keys()));
|
|
184
207
|
// Remove all groups in parallel
|
|
185
208
|
await Promise.all([...groupsToRemove.entries()].map(async ([hash, binNamesPromise]) => {
|
|
186
209
|
const binNames = await binNamesPromise;
|
|
187
|
-
await Promise.all(binNames
|
|
210
|
+
await Promise.all(binNames
|
|
211
|
+
.filter((binName) => !protectedBins.has(binName))
|
|
212
|
+
.map((binName) => removeBin(path.join(globalBinDir, binName))));
|
|
188
213
|
// Remove both the hash symlink and the install dir it points to
|
|
189
214
|
const hashLink = getHashLink(globalDir, hash);
|
|
190
215
|
let installDir = null;
|
package/lib/globalRemove.js
CHANGED
|
@@ -4,6 +4,7 @@ import { removeBin } from '@pnpm/bins.remover';
|
|
|
4
4
|
import { PnpmError } from '@pnpm/error';
|
|
5
5
|
import { findGlobalPackage, getHashLink, getInstalledBinNames, } from '@pnpm/global.packages';
|
|
6
6
|
import { isSubdir } from 'is-subdir';
|
|
7
|
+
import { getBinNamesOfOtherGroups } from './binOwnership.js';
|
|
7
8
|
export async function handleGlobalRemove(opts, params) {
|
|
8
9
|
const globalDir = opts.globalPkgDir;
|
|
9
10
|
const globalBinDir = opts.bin;
|
|
@@ -16,10 +17,15 @@ export async function handleGlobalRemove(opts, params) {
|
|
|
16
17
|
}
|
|
17
18
|
groupsToRemove.set(pkg.hash, pkg);
|
|
18
19
|
}
|
|
20
|
+
// Bins shared with (and owned by) groups that survive this removal must
|
|
21
|
+
// not be unlinked, or we'd delete another global package's bin.
|
|
22
|
+
const protectedBins = await getBinNamesOfOtherGroups(globalDir, new Set(groupsToRemove.keys()));
|
|
19
23
|
// Remove bins, hash symlinks, and install dirs for all affected groups in parallel
|
|
20
24
|
await Promise.all([...groupsToRemove.entries()].map(async ([hash, pkg]) => {
|
|
21
25
|
const binNames = await getInstalledBinNames(pkg);
|
|
22
|
-
await Promise.all(binNames
|
|
26
|
+
await Promise.all(binNames
|
|
27
|
+
.filter((binName) => !protectedBins.has(binName))
|
|
28
|
+
.map((binName) => removeBin(path.join(globalBinDir, binName))));
|
|
23
29
|
await fs.promises.rm(getHashLink(globalDir, hash), { force: true });
|
|
24
30
|
if (isSubdir(globalDir, pkg.installDir)) {
|
|
25
31
|
await fs.promises.rm(pkg.installDir, { recursive: true, force: true });
|
package/lib/globalUpdate.js
CHANGED
|
@@ -6,6 +6,7 @@ import { summaryLogger } from '@pnpm/core-loggers';
|
|
|
6
6
|
import { cleanOrphanedInstallDirs, createInstallDir, getHashLink, getInstalledBinNames, scanGlobalPackages, } from '@pnpm/global.packages';
|
|
7
7
|
import { isSubdir } from 'is-subdir';
|
|
8
8
|
import { symlinkDir } from 'symlink-dir';
|
|
9
|
+
import { getBinNamesOfOtherGroups } from './binOwnership.js';
|
|
9
10
|
import { checkGlobalBinConflicts } from './checkGlobalBinConflicts.js';
|
|
10
11
|
import { installGlobalPackages } from './installGlobalPackages.js';
|
|
11
12
|
import { promptApproveGlobalBuilds } from './promptApproveGlobalBuilds.js';
|
|
@@ -91,9 +92,13 @@ async function updateGlobalPackageGroup(opts, globalDir, globalBinDir, pkg, comm
|
|
|
91
92
|
await fs.promises.rm(installDir, { recursive: true, force: true });
|
|
92
93
|
throw err;
|
|
93
94
|
}
|
|
94
|
-
// Remove stale bins from old installation before swapping
|
|
95
|
+
// Remove stale bins from old installation before swapping, but keep any
|
|
96
|
+
// bin owned by a different global group (don't delete another package's bin).
|
|
97
|
+
const protectedBins = await getBinNamesOfOtherGroups(globalDir, new Set([pkg.hash]));
|
|
95
98
|
const oldBinNames = await getInstalledBinNames(pkg);
|
|
96
|
-
await Promise.all(oldBinNames
|
|
99
|
+
await Promise.all(oldBinNames
|
|
100
|
+
.filter((binName) => !protectedBins.has(binName))
|
|
101
|
+
.map((binName) => removeBin(path.join(globalBinDir, binName))));
|
|
97
102
|
// Swap hash symlink to new install dir, then clean up old one
|
|
98
103
|
const hashLink = getHashLink(globalDir, pkg.hash);
|
|
99
104
|
const oldInstallDir = pkg.installDir;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { isValidGlobalDependencyAlias } from '@pnpm/global.packages';
|
|
2
3
|
import { readPackageJsonFromDir, readPackageJsonFromDirRawSync } from '@pnpm/pkg-manifest.reader';
|
|
3
4
|
export async function readInstalledPackages(installDir) {
|
|
4
5
|
const pkgJson = readPackageJsonFromDirRawSync(installDir);
|
|
5
|
-
const depNames = Object.keys(pkgJson.dependencies ?? {});
|
|
6
|
+
const depNames = Object.keys(pkgJson.dependencies ?? {}).filter(isValidGlobalDependencyAlias);
|
|
6
7
|
const manifests = await Promise.all(depNames.map((depName) => readPackageJsonFromDir(path.join(installDir, 'node_modules', depName))));
|
|
7
8
|
return depNames.map((depName, i) => ({
|
|
8
9
|
manifest: manifests[i],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/global.commands",
|
|
3
|
-
"version": "1100.0.
|
|
3
|
+
"version": "1100.0.32",
|
|
4
4
|
"description": "Global package command handlers for pnpm",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -32,24 +32,24 @@
|
|
|
32
32
|
"is-subdir": "^2.0.0",
|
|
33
33
|
"symlink-dir": "^10.0.1",
|
|
34
34
|
"@pnpm/bins.linker": "1100.0.16",
|
|
35
|
+
"@pnpm/bins.remover": "1100.0.12",
|
|
35
36
|
"@pnpm/cli.command": "1100.0.1",
|
|
36
|
-
"@pnpm/
|
|
37
|
-
"@pnpm/cli.utils": "1101.0.13",
|
|
38
|
-
"@pnpm/config.matcher": "1100.0.1",
|
|
39
|
-
"@pnpm/bins.remover": "1100.0.11",
|
|
37
|
+
"@pnpm/config.reader": "1101.11.1",
|
|
40
38
|
"@pnpm/core-loggers": "1100.2.1",
|
|
41
|
-
"@pnpm/
|
|
39
|
+
"@pnpm/deps.inspection.list": "1100.0.22",
|
|
40
|
+
"@pnpm/config.matcher": "1100.0.1",
|
|
41
|
+
"@pnpm/global.packages": "1100.0.10",
|
|
42
42
|
"@pnpm/error": "1100.0.1",
|
|
43
|
-
"@pnpm/global.packages": "1100.0.9",
|
|
44
|
-
"@pnpm/installing.deps-installer": "1102.1.1",
|
|
45
|
-
"@pnpm/deps.inspection.list": "1100.0.20",
|
|
46
43
|
"@pnpm/pkg-manifest.reader": "1100.0.9",
|
|
47
|
-
"@pnpm/
|
|
48
|
-
"@pnpm/
|
|
44
|
+
"@pnpm/cli.utils": "1101.0.13",
|
|
45
|
+
"@pnpm/installing.deps-installer": "1102.2.1",
|
|
46
|
+
"@pnpm/types": "1101.3.2",
|
|
47
|
+
"@pnpm/store.connection-manager": "1100.3.4",
|
|
48
|
+
"@pnpm/bins.resolver": "1100.0.8"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@jest/globals": "30.4.1",
|
|
52
|
-
"@pnpm/global.commands": "1100.0.
|
|
52
|
+
"@pnpm/global.commands": "1100.0.32"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=22.13"
|