@pnpm/global.commands 1100.0.13 → 1100.0.15

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/globalAdd.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { linkBinsOfPackages } from '@pnpm/bins.linker';
4
4
  import { removeBin } from '@pnpm/bins.remover';
5
+ import { summaryLogger } from '@pnpm/core-loggers';
5
6
  import { cleanOrphanedInstallDirs, createGlobalCacheKey, createInstallDir, findGlobalPackage, getHashLink, getInstalledBinNames, } from '@pnpm/global.packages';
6
7
  import { readPackageJsonFromDirRawSync } from '@pnpm/pkg-manifest.reader';
7
8
  import { isSubdir } from 'is-subdir';
@@ -11,17 +12,9 @@ import { installGlobalPackages } from './installGlobalPackages.js';
11
12
  import { promptApproveGlobalBuilds } from './promptApproveGlobalBuilds.js';
12
13
  import { readInstalledPackages } from './readInstalledPackages.js';
13
14
  export async function handleGlobalAdd(opts, params, commands) {
14
- // Resolve relative path selectors to absolute paths before the working
15
- // directory is changed to the global install dir, otherwise "." or
16
- // "./foo" would resolve against the temp install directory.
17
- params = params.map((param) => resolveLocalParam(param, opts.dir));
18
15
  const globalDir = opts.globalPkgDir;
19
16
  const globalBinDir = opts.bin;
20
17
  cleanOrphanedInstallDirs(globalDir);
21
- // Install into a new directory first, then read the resolved aliases
22
- // from the resulting package.json. This is more reliable than parsing
23
- // aliases from CLI params (which may be tarballs, git URLs, etc.).
24
- const installDir = createInstallDir(globalDir);
25
18
  // Convert allowBuild array to allowBuilds Record (same conversion as add.handler)
26
19
  let allowBuilds = opts.allowBuilds ?? {};
27
20
  if (opts.allowBuild?.length) {
@@ -30,19 +23,45 @@ export async function handleGlobalAdd(opts, params, commands) {
30
23
  allowBuilds[pkg] = true;
31
24
  }
32
25
  }
26
+ // Each space-separated CLI param becomes its own isolated install group.
27
+ // A param containing commas is split into multiple selectors that share a
28
+ // single group, so `pnpm add -g foo,bar qar` installs foo+bar together
29
+ // and qar separately. Local paths and URLs that legitimately contain
30
+ // commas are detected and kept whole.
31
+ const groups = params
32
+ .map((param) => splitCommaSeparated(param, opts.dir).map((token) => resolveLocalParam(token, opts.dir)))
33
+ .filter((group) => group.length > 0);
34
+ for (const group of groups) {
35
+ // eslint-disable-next-line no-await-in-loop
36
+ await installGroup({ opts, globalDir, globalBinDir, allowBuilds, params: group }, commands);
37
+ }
38
+ // The per-group `mutateModulesInSingleProject` calls run with
39
+ // `omitSummaryLog: true` so the default-reporter's summary block only
40
+ // appears once at the end, with every installed package listed under a
41
+ // single "global:" heading. Without this, the reporter would print
42
+ // group 1's summary and then ignore later groups, because its summary
43
+ // pipeline takes only the first `summary` log event.
44
+ summaryLogger.debug({ prefix: globalDir });
45
+ }
46
+ async function installGroup(ctx, commands) {
47
+ const { opts, globalDir, globalBinDir, allowBuilds, params } = ctx;
48
+ // Install into a new directory first, then read the resolved aliases
49
+ // from the resulting package.json. This is more reliable than parsing
50
+ // aliases from CLI params (which may be tarballs, git URLs, etc.).
51
+ const installDir = createInstallDir(globalDir);
33
52
  const include = {
34
53
  dependencies: true,
35
54
  devDependencies: false,
36
55
  optionalDependencies: true,
37
56
  };
38
57
  const fetchFullMetadata = opts.supportedArchitectures?.libc != null && true;
39
- const makeInstallOpts = (dir, builds) => ({
58
+ const installOpts = {
40
59
  ...opts,
41
60
  global: false,
42
- bin: path.join(dir, 'node_modules/.bin'),
43
- dir,
44
- lockfileDir: dir,
45
- rootProjectManifestDir: dir,
61
+ bin: path.join(installDir, 'node_modules/.bin'),
62
+ dir: installDir,
63
+ lockfileDir: installDir,
64
+ rootProjectManifestDir: installDir,
46
65
  rootProjectManifest: undefined,
47
66
  saveProd: true,
48
67
  saveDev: false,
@@ -54,9 +73,10 @@ export async function handleGlobalAdd(opts, params, commands) {
54
73
  fetchFullMetadata,
55
74
  include,
56
75
  includeDirect: include,
57
- allowBuilds: builds,
58
- });
59
- const ignoredBuilds = await installGlobalPackages(makeInstallOpts(installDir, allowBuilds), params);
76
+ allowBuilds,
77
+ omitSummaryLog: true,
78
+ };
79
+ const ignoredBuilds = await installGlobalPackages(installOpts, params);
60
80
  await promptApproveGlobalBuilds({
61
81
  globalPkgDir: globalDir,
62
82
  installDir,
@@ -95,6 +115,47 @@ export async function handleGlobalAdd(opts, params, commands) {
95
115
  // Link bins from installed packages into global bin dir
96
116
  await linkBinsOfPackages(pkgs, globalBinDir, { excludeBins: binsToSkip });
97
117
  }
118
+ function splitCommaSeparated(param, baseDir) {
119
+ if (!param.includes(','))
120
+ return [param];
121
+ // URLs may contain commas and are never a group of selectors.
122
+ if (param.includes('://'))
123
+ return [param];
124
+ // For path-like specs (relative/absolute paths, file:, link:), the
125
+ // commas could either be part of a single path that legitimately
126
+ // contains commas, or be separators between multiple distinct paths.
127
+ // Resolve the ambiguity by checking whether the whole param actually
128
+ // refers to an existing local path on disk.
129
+ if (refersToExistingLocalPath(param, baseDir))
130
+ return [param];
131
+ return param.split(',').map((token) => token.trim()).filter(Boolean);
132
+ }
133
+ function refersToExistingLocalPath(param, baseDir) {
134
+ let pathPart;
135
+ if (param.startsWith('file:')) {
136
+ pathPart = param.slice('file:'.length);
137
+ }
138
+ else if (param.startsWith('link:')) {
139
+ pathPart = param.slice('link:'.length);
140
+ }
141
+ else if (param[0] === '.' || param[0] === '/' || param[0] === '~') {
142
+ pathPart = param;
143
+ }
144
+ else if (/^[a-z]:[/\\]/i.test(param)) {
145
+ pathPart = param;
146
+ }
147
+ else {
148
+ return false;
149
+ }
150
+ const resolved = path.isAbsolute(pathPart) ? pathPart : path.resolve(baseDir, pathPart);
151
+ try {
152
+ fs.statSync(resolved);
153
+ return true;
154
+ }
155
+ catch {
156
+ return false;
157
+ }
158
+ }
98
159
  function resolveLocalParam(param, baseDir) {
99
160
  for (const prefix of ['file:', 'link:']) {
100
161
  if (param.startsWith(prefix)) {
@@ -10,6 +10,7 @@ export interface InstallGlobalPackagesOptions extends CreateStoreControllerOptio
10
10
  include: IncludedDependencies;
11
11
  includeDirect?: IncludedDependencies;
12
12
  fetchFullMetadata?: boolean;
13
+ omitSummaryLog?: boolean;
13
14
  rootProjectManifest?: unknown;
14
15
  rootProjectManifestDir?: string;
15
16
  saveDev?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/global.commands",
3
- "version": "1100.0.13",
3
+ "version": "1100.0.15",
4
4
  "description": "Global package command handlers for pnpm",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -28,24 +28,25 @@
28
28
  "@pnpm/util.lex-comparator": "^3.0.2",
29
29
  "is-subdir": "^2.0.0",
30
30
  "symlink-dir": "^10.0.1",
31
- "@pnpm/cli.command": "1100.0.1",
32
- "@pnpm/bins.remover": "1100.0.2",
33
- "@pnpm/bins.linker": "1100.0.3",
34
- "@pnpm/cli.utils": "1101.0.2",
35
- "@pnpm/bins.resolver": "1100.0.2",
36
- "@pnpm/deps.inspection.list": "1100.0.7",
31
+ "@pnpm/bins.remover": "1100.0.3",
32
+ "@pnpm/bins.resolver": "1100.0.3",
33
+ "@pnpm/cli.utils": "1101.0.3",
34
+ "@pnpm/bins.linker": "1100.0.4",
35
+ "@pnpm/config.matcher": "1100.0.1",
36
+ "@pnpm/config.reader": "1101.3.0",
37
+ "@pnpm/core-loggers": "1100.0.2",
37
38
  "@pnpm/error": "1100.0.0",
38
- "@pnpm/config.reader": "1101.2.1",
39
- "@pnpm/installing.deps-installer": "1101.0.8",
40
- "@pnpm/store.connection-manager": "1100.0.12",
41
- "@pnpm/global.packages": "1100.0.2",
42
- "@pnpm/types": "1101.0.0",
43
- "@pnpm/pkg-manifest.reader": "1100.0.2",
44
- "@pnpm/config.matcher": "1100.0.1"
39
+ "@pnpm/global.packages": "1100.0.3",
40
+ "@pnpm/installing.deps-installer": "1101.1.0",
41
+ "@pnpm/pkg-manifest.reader": "1100.0.3",
42
+ "@pnpm/store.connection-manager": "1100.1.0",
43
+ "@pnpm/types": "1101.1.0",
44
+ "@pnpm/cli.command": "1100.0.1",
45
+ "@pnpm/deps.inspection.list": "1100.0.9"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@jest/globals": "30.3.0",
48
- "@pnpm/global.commands": "1100.0.13"
49
+ "@pnpm/global.commands": "1100.0.15"
49
50
  },
50
51
  "engines": {
51
52
  "node": ">=22.13"