@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.
@@ -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) => fs.existsSync(path.join(opts.globalBinDir, 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
@@ -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) => aliases.some((alias) => alias in pkg.dependencies),
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(globalDir, globalBinDir, aliases) {
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 aliases) {
196
+ for (const alias of replacementAliases) {
179
197
  const existing = findGlobalPackage(globalDir, alias);
180
- if (existing && !groupsToRemove.has(existing.hash)) {
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.map((binName) => removeBin(path.join(globalBinDir, binName))));
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;
@@ -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.map((binName) => removeBin(path.join(globalBinDir, binName))));
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 });
@@ -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.map((binName) => removeBin(path.join(globalBinDir, binName))));
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.30",
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/bins.resolver": "1100.0.8",
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/config.reader": "1101.10.1",
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/store.connection-manager": "1100.3.2",
48
- "@pnpm/types": "1101.3.2"
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.30"
52
+ "@pnpm/global.commands": "1100.0.32"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=22.13"